diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index d28a6e11e..f8e0772c7 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -709,19 +709,41 @@ namespace BTCPayServer.Tests [Fact] public void ParseDerivationSchemeSettings() { + var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork("BTC"); var mainnet = new BTCPayNetworkProvider(ChainName.Mainnet).GetNetwork("BTC"); var root = new Mnemonic( "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage") .DeriveExtKey(); + + // xpub + var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS"; + Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(tpub, testnet, out var settings, out var error)); + Assert.Null(error); + Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false }); + Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString()); + + // xpub with fingerprint and account + tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca"; + var vpub = "vpub5YVA1ZbrqkUVq8NZTtvRDrS2a1yoeBvHbG9NbxqJ6uRtpKGFwjQT11WEqKYsgoDF6gpqrDf8ddmPZe4yXWCjzqF8ad2Cw9xHiE8DSi3X3ik"; + var fingerprint = "e5746fd9"; + var account = "84'/1'/0'"; + var str = $"[{fingerprint}/{account}]{vpub}"; + Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(str, testnet, out settings, out error)); + Assert.Null(error); + Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true }); + Assert.Equal(vpub, settings.AccountOriginal); + Assert.Equal(tpub, ((DirectDerivationStrategy)settings.AccountDerivation).ToString()); + Assert.Equal(HDFingerprint.TryParse(fingerprint, out var hd) ? hd : default, settings.AccountKeySettings[0].RootFingerprint); + Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString()); // ColdCard Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", - mainnet, out var settings, out var error)); + mainnet, out settings, out error)); Assert.Null(error); Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint); Assert.Equal(settings.AccountKeySettings[0].RootFingerprint, - HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default); + HDFingerprint.TryParse("8bafd160", out hd) ? hd : default); Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label); Assert.Equal("49'/0'/0'", settings.AccountKeySettings[0].AccountKeyPath.ToString()); Assert.Equal( @@ -729,28 +751,26 @@ namespace BTCPayServer.Tests settings.AccountOriginal); Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey, settings.AccountDerivation.GetDerivation().ScriptPubKey); - var testnet = new BTCPayNetworkProvider(ChainName.Testnet).GetNetwork("BTC"); // Should be legacy Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings, out error)); - Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit); + Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false }); Assert.Null(error); // Should be segwit p2sh Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings, out error)); - Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p && - p.Inner is DirectDerivationStrategy s2 && s2.Segwit); + Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } }); Assert.Null(error); // Should be segwit Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings, out error)); - Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit); + Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true }); Assert.Null(error); // Specter diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index f3dedac87..aaab81fc5 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -49,6 +49,7 @@ namespace BTCPayServer { return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString()); } + private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, ref string error, bool electrum = true) { if (!electrum) @@ -78,15 +79,36 @@ namespace BTCPayServer } try { + // Extract fingerprint and account key path from export formats that contain them. + // Possible formats: [fingerprint/account_key_path]xpub, [fingerprint]xpub, xpub + HDFingerprint? rootFingerprint = null; + KeyPath accountKeyPath = null; + var derivationRegex = new Regex(@"^(?:\[(\w+)(?:\/(.*?))?\])?(\w+)$", RegexOptions.IgnoreCase); + var match = derivationRegex.Match(xpub.Trim()); + if (match.Success) + { + if (!string.IsNullOrEmpty(match.Groups[1].Value)) rootFingerprint = HDFingerprint.Parse(match.Groups[1].Value); + if (!string.IsNullOrEmpty(match.Groups[2].Value)) accountKeyPath = KeyPath.Parse(match.Groups[2].Value); + if (!string.IsNullOrEmpty(match.Groups[3].Value)) xpub = match.Groups[3].Value; + } derivationSchemeSettings.AccountOriginal = xpub.Trim(); derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal); derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys() - .Select(key => new AccountKeySettings() + .Select(key => new AccountKeySettings { AccountKey = key.GetWif(derivationSchemeParser.Network) }).ToArray(); if (derivationSchemeSettings.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit) derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation + // apply initial matches if there were no results from parsing + if (rootFingerprint != null && derivationSchemeSettings.AccountKeySettings[0].RootFingerprint == null) + { + derivationSchemeSettings.AccountKeySettings[0].RootFingerprint = rootFingerprint; + } + if (accountKeyPath != null && derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath == null) + { + derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath = accountKeyPath; + } return true; } catch (Exception exception) diff --git a/BTCPayServer/Views/UIStores/ImportWallet/Scan.cshtml b/BTCPayServer/Views/UIStores/ImportWallet/Scan.cshtml index 0ca18ee25..b26867a43 100644 --- a/BTCPayServer/Views/UIStores/ImportWallet/Scan.cshtml +++ b/BTCPayServer/Views/UIStores/ImportWallet/Scan.cshtml @@ -96,8 +96,6 @@ } else if (typeof(data) === 'string') { xpub = data; } - // remove potentially leading derivation path - xpub = xpub.replace(/^\[.*?\]/, ''); // submit document.getElementById("WalletFileContent").value = xpub; document.getElementById("qr-import-form").submit();