diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 85e052bbd..340e7d5ea 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -47,6 +47,7 @@ using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OpenQA.Selenium.DevTools.V100.DOMSnapshot; using Xunit; using Xunit.Abstractions; using StoreData = BTCPayServer.Data.StoreData; @@ -855,8 +856,7 @@ namespace BTCPayServer.Tests // xpub var xpub = "xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"; - Assert.Throws(() => parser.Parse(xpub, false, false, true)); - DerivationStrategyBase strategyBase = parser.Parse(xpub, false, false, false); + DerivationStrategyBase strategyBase = parser.Parse(xpub); Assert.IsType(strategyBase); Assert.True(((DirectDerivationStrategy)strategyBase).Segwit); Assert.Equal("tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS", strategyBase.ToString()); @@ -938,7 +938,7 @@ namespace BTCPayServer.Tests Assert.True(parsers.TryParseWalletFile(tpub, testnet, out var settings, out var error)); Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false }); Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString()); - Assert.Equal("Generic", settings.Source); + Assert.Equal("GenericFile", settings.Source); Assert.Null(error); // xpub with fingerprint and account @@ -954,7 +954,7 @@ namespace BTCPayServer.Tests 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()); - Assert.Equal("Generic", settings.Source); + Assert.Equal("GenericFile", settings.Source); Assert.Null(error); // ColdCard @@ -1069,6 +1069,50 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku Assert.True(passport.AccountDerivation is TaprootDerivationStrategy); Assert.Equal("5c9e228d", passport.AccountKeySettings[0].RootFingerprint.ToString()); Assert.Equal("86'/0'/0'", passport.AccountKeySettings[0].AccountKeyPath.ToString()); + + //electrum + var electrumText = +""" +{ + "keystore": { + "xpub": "vpub5Z14bnDNoEQeFdwZYSpVHcpzRpH99CnvSemzqTAvhjcgBTzPUVnaA5GhjgZc9J46duUprxQRUVUuqchazanXD6bLuVyarviNHBFUu6fBZNj", + "xprv": "vprv9ENJcv8RKwqMTqyhLSuBz5bEV7hpdZjisjUBuV9K8azz1vpop6xJFEDRdfDwgWBpYgUUhEVxdvpxgV3f8NircysfebnBaPu5y2dcnSDAEEw", + "type": "bip32", + "pw_hash_version": 1 + }, + "wallet_type": "standard", + "use_encryption": false, + "seed_type": "bip39" +} +"""; + Assert.True(parsers.TryParseWalletFile(electrumText, testnet, out var electrum, out _)); + Assert.Equal("ElectrumFile", electrum.Source); + + electrumText = +""" +{ +"keystore": { + "derivation": "m/0h", + "pw_hash_version": 1, + "root_fingerprint": "fbb5b37d", + "seed": "tiger room acoustic bracket thing film umbrella rather pepper tired vault remain", + "seed_type": "segwit", + "type": "bip32", + "xprv": "zprvAaQyp6mTAX53zY4j2BbecRNtmTq2kSEKgy2y4yK3bFPKgPJLxrMmPxzZdRkWq5XvmtH2R4ko5YmJYH2MgnVkWr32pHi4Dc5627WyML32KTW", + "xpub": "zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br" +}, +"wallet_type": "standard", +"use_encryption": false, +"seed_type": "bip39" +} +"""; + Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _)); + Assert.Equal("ElectrumFile", electrum.Source); + Assert.Equal("0'", electrum.GetSigningAccountKeySettings().AccountKeyPath.ToString()); + Assert.True(electrum.AccountDerivation is DirectDerivationStrategy { Segwit: true }); + Assert.Equal("fbb5b37d", electrum.GetSigningAccountKeySettings().RootFingerprint.ToString()); + Assert.Equal("zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br", electrum.AccountOriginal); + Assert.Equal(((DirectDerivationStrategy)electrum.AccountDerivation).GetExtPubKeys().First().ParentFingerprint.ToString(), electrum.GetSigningAccountKeySettings().RootFingerprint.ToString()); } [Fact] @@ -1949,7 +1993,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku // Passing electrum stuff // Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit result = testnetParser.Parse( - "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t", false, false, false); + "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); Assert.Equal( "tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString()); @@ -1973,7 +2017,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku // if prefix not recognize, assume it is segwit result = testnetParser.Parse( - "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X", false, false, false); + "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X"); Assert.Equal( "tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu", result.ToString()); @@ -1982,13 +2026,13 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"; - result = testnetParser.Parse(tpub, false, true); + result = testnetParser.Parse(tpub); Assert.Equal(tpub, result.ToString()); var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC")); var parsed = regtestParser.Parse( - "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false); + "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); Assert.Equal( "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", parsed.ToString()); @@ -1996,14 +2040,14 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku // Let's make sure we can't generate segwit with dogecoin regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); parsed = regtestParser.Parse( - "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false); + "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); Assert.Equal( "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString()); regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); parsed = regtestParser.Parse( - "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", false, false, false); + "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]"); Assert.Equal( "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString()); @@ -2227,7 +2271,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku ["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf" }; var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", CreateNetworkProvider(ChainName.Regtest).BTC); - + Assert.True(scheme.AccountDerivation is DirectDerivationStrategy { Segwit: true }); scheme.Source = "ManualDerivationScheme"; scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"; var legacy2 = new JObject() diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs index 9b1ef5393..ffdef1945 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs @@ -123,7 +123,7 @@ namespace BTCPayServer.Controllers.Greenfield try { - var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme, false, true); + var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme); var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); var line = strategy.GetLineFor(deposit); @@ -173,7 +173,7 @@ namespace BTCPayServer.Controllers.Greenfield DerivationStrategyBase strategy; try { - strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme, false, true); + strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme); } catch { @@ -246,7 +246,7 @@ namespace BTCPayServer.Controllers.Greenfield { var store = Store; var storeBlob = store.GetStoreBlob(); - var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme, false, true); + var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme); if (strategy != null) await wallet.TrackAsync(strategy); diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs index 17829b173..934940672 100644 --- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs +++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs @@ -100,7 +100,7 @@ namespace BTCPayServer.Controllers } if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _)) { - ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed"); + ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format"); return View(vm.ViewName, vm); } } diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index 689894f7b..26a8bda39 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -919,7 +919,7 @@ namespace BTCPayServer.Controllers return derivationSchemeSettings; } - var strategy = parser.Parse(derivationScheme, false, true); + var strategy = parser.Parse(derivationScheme); return new DerivationSchemeSettings(strategy, network); } diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs index 12d8da61d..92b278b96 100644 --- a/BTCPayServer/DerivationSchemeParser.cs +++ b/BTCPayServer/DerivationSchemeParser.cs @@ -34,7 +34,7 @@ namespace BTCPayServer { throw new FormatException("Custom change paths are not supported."); } - return (Parse($"{hd.Extkey}{suffix}", true, false, false), null); + return (Parse($"{hd.Extkey}{suffix}"), null); case PubKeyProvider.Origin origin: var innerResult = ExtractFromPkProvider(origin.Inner, suffix); return (innerResult.Item1, new[] { origin.KeyOriginInfo }); @@ -48,14 +48,14 @@ namespace BTCPayServer var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider)); return ( Parse( - $"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}", true ,false, false), + $"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}"), xpubs.SelectMany(tuple => tuple.Item2).ToArray()); } ArgumentNullException.ThrowIfNull(str); str = str.Trim(); //nbitcoin output descriptor does not support taproot, so let's check if it is a taproot descriptor and fake until it is supported - + var outputDescriptor = OutputDescriptor.Parse(str, Network); switch (outputDescriptor) { @@ -81,7 +81,7 @@ namespace BTCPayServer sh.Inner is OutputDescriptor.WSH) { var ds = ParseOutputDescriptor(sh.Inner.ToString()); - return (Parse(ds.Item1 + suffix, true, false, false), ds.Item2); + return (Parse(ds.Item1 + suffix), ds.Item2); }; throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)"); case OutputDescriptor.Tr tr: @@ -96,19 +96,25 @@ namespace BTCPayServer throw new ArgumentOutOfRangeException(nameof(outputDescriptor)); } } - public DerivationStrategyBase Parse(string str, bool ignorePrefix = false, bool ignoreBasePrefix = false, bool enforceNetworkPrefix = true, bool electrum = true) + public DerivationStrategyBase Parse(string str) { ArgumentNullException.ThrowIfNull(str); str = str.Trim(); - HashSet hintedLabels = new HashSet(); - if (!Network.Consensus.SupportSegwit) { hintedLabels.Add("legacy"); str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase); } + try + { + return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str); + } + catch + { + } + var parts = str.Split('-'); bool hasLabel = false; for (int i = 0; i < parts.Length; i++) @@ -125,29 +131,23 @@ namespace BTCPayServer hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant()); continue; } - try { var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]); if (data.Length < 4) continue; var prefix = Utils.ToUInt32(data, false); - var standardPrefix = Utils.ToBytes(0x0488b21eU, false); for (int ii = 0; ii < 4; ii++) data[ii] = standardPrefix[ii]; var derivationScheme = GetBitcoinExtPubKeyByNetwork(Network, data).ToString(); - if (enforceNetworkPrefix && !BtcPayNetwork.ElectrumMapping.ContainsKey(prefix)) - throw new FormatException( - $"Invalid xpub. Is this really for {BtcPayNetwork.CryptoCode} {Network.ChainName}?"); - - if (!ignorePrefix && !hasLabel && electrum && BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) + if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) { switch (type) { - case DerivationType.Legacy when !ignoreBasePrefix: + case DerivationType.Legacy: hintedLabels.Add("legacy"); break; case DerivationType.SegwitP2SH: @@ -155,13 +155,8 @@ namespace BTCPayServer break; } } - parts[i] = derivationScheme; } - catch (FormatException e) when (e.Message.StartsWith("Invalid xpub")) - { - throw; - } catch { continue; } } @@ -170,10 +165,35 @@ namespace BTCPayServer { str = $"{str}-[{label}]"; } - return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str); } + internal DerivationStrategyBase ParseElectrum(string str) + { + ArgumentNullException.ThrowIfNull(str); + str = str.Trim(); + var data = Network.GetBase58CheckEncoder().DecodeData(str); + if (data.Length < 4) + throw new FormatException(); + var prefix = Utils.ToUInt32(data, false); + + var standardPrefix = Utils.ToBytes(0x0488b21eU, false); + for (int ii = 0; ii < 4; ii++) + data[ii] = standardPrefix[ii]; + var extPubKey = GetBitcoinExtPubKeyByNetwork(Network, data); + if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) + { + throw new FormatException(); + } + if (type == DerivationType.Segwit) + return new DirectDerivationStrategy(extPubKey, true); + if (type == DerivationType.Legacy) + return new DirectDerivationStrategy(extPubKey, false); + if (type == DerivationType.SegwitP2SH) + return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]"); + throw new FormatException(); + } + public static BitcoinExtPubKey GetBitcoinExtPubKeyByNetwork(Network network, byte[] data) { try diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index 1143f9d90..5959c9cc2 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -13,17 +13,17 @@ namespace BTCPayServer { public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network) { - string error = null; ArgumentNullException.ThrowIfNull(network); ArgumentNullException.ThrowIfNull(derivationStrategy); var result = new DerivationSchemeSettings { Network = network }; var parser = network.GetDerivationSchemeParser(); - if (parser.TryParseXpub(derivationStrategy, ref result, out error)) + if (parser.TryParseXpub(derivationStrategy, ref result) || + parser.TryParseXpub(derivationStrategy, ref result, electrum: true)) { return result; } - throw new FormatException($"Invalid Derivation Scheme: {error}"); + throw new FormatException($"Invalid Derivation Scheme"); } public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy) diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 2d23d91eb..b3f453686 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -48,8 +48,32 @@ namespace BTCPayServer } public static bool TryParseXpub(this DerivationSchemeParser derivationSchemeParser, string xpub, - ref DerivationSchemeSettings derivationSchemeSettings, out string error, bool electrum = true) + ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = false) { + if (!electrum) + { + var isOD = Regex.Match(xpub, @"\(.*?\)").Success; + try + { + var result = derivationSchemeParser.ParseOutputDescriptor(xpub); + derivationSchemeSettings.AccountOriginal = xpub.Trim(); + derivationSchemeSettings.AccountDerivation = result.Item1; + derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings() + { + RootFingerprint = path?.MasterFingerprint, + AccountKeyPath = path?.KeyPath, + AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network) + }).ToArray(); + return true; + } + catch (Exception) + { + if (isOD) + { + return false; + } // otherwise continue and try to parse input as xpub + } + } try { // Extract fingerprint and account key path from export formats that contain them. @@ -67,37 +91,32 @@ namespace BTCPayServer if (!string.IsNullOrEmpty(match.Groups[3].Value)) xpub = match.Groups[3].Value; } - derivationSchemeSettings.AccountOriginal = xpub.Trim(); - derivationSchemeSettings.AccountDerivation = - derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal, false, false, false, electrum); + derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal); derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys() - .Select(key => new AccountKeySettings {AccountKey = key.GetWif(derivationSchemeParser.Network)}) - .ToArray(); + .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 + 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; } - - error = null; return true; } - catch (Exception exception) + catch (Exception) { - error = exception.Message; return false; } } - + public static CardKey CreatePullPaymentCardKey(this IssuerKey issuerKey, byte[] uid, int version, string pullPaymentId) { var data = Encoding.UTF8.GetBytes(pullPaymentId); diff --git a/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs index 61c90165d..886fa7db3 100644 --- a/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs +++ b/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs @@ -11,6 +11,7 @@ public class ElectrumWalletFileParser : IWalletFileParser { public string? xpub { get; set; } public string? label { get; set; } + public string? root_fingerprint { get; set; } public uint? ckcc_xfp { get; set; } public string? derivation { get; set; } public string? ColdCardFirmwareVersion { get; set; } @@ -29,28 +30,25 @@ public class ElectrumWalletFileParser : IWalletFileParser var derivationSchemeParser = network.GetDerivationSchemeParser(); result.Source = "ElectrumFile"; - if (jobj.keystore.xpub is null || jobj.keystore.ckcc_xfp is null || jobj.keystore.derivation is null) + if (jobj.keystore.xpub is null) return false; - var strategy = derivationSchemeParser.Parse(jobj.keystore.xpub, false, false, true); - result.AccountDerivation = strategy; - result.AccountOriginal = jobj.keystore.xpub; - result.GetSigningAccountKeySettings(); - + if (!derivationSchemeParser.TryParseXpub(jobj.keystore.xpub, ref result, true)) + return false; + if (jobj.keystore.label is not null) result.Label = jobj.keystore.label; - result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj.keystore.ckcc_xfp.Value); - result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj.keystore.derivation); - + if (jobj.keystore.ckcc_xfp is not null) + result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj.keystore.ckcc_xfp.Value); + if (jobj.keystore.root_fingerprint is not null) + result.AccountKeySettings[0].RootFingerprint = HDFingerprint.Parse(jobj.keystore.root_fingerprint); + if (jobj.keystore.derivation is not null) + result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj.keystore.derivation); if (jobj.keystore.ColdCardFirmwareVersion is not null) - { result.Source = "ColdCard"; - } else if (jobj.keystore.CoboVaultFirmwareVersion is not null) - { result.Source = "CoboVault"; - } derivationSchemeSettings = result; return true; } diff --git a/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs index 8bb781c57..25250416c 100644 --- a/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs +++ b/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs @@ -5,8 +5,16 @@ public class NBXDerivGenericWalletFileParser : IWalletFileParser { public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings) { - derivationSchemeSettings = DerivationSchemeSettings.Parse(data, network); - derivationSchemeSettings.Source = "Generic"; - return true; + var result = new DerivationSchemeSettings { Network = network }; + var parser = network.GetDerivationSchemeParser(); + if (parser.TryParseXpub(data, ref result, electrum: true) || + parser.TryParseXpub(data, ref result)) + { + derivationSchemeSettings = result; + derivationSchemeSettings.Source = "GenericFile"; + return true; + } + derivationSchemeSettings = null; + return false; } } diff --git a/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs index 587429e16..81f8bcc74 100644 --- a/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs +++ b/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs @@ -30,7 +30,7 @@ public class WasabiWalletFileParser : IWalletFileParser Network = network }; - if (jobj is null || !derivationSchemeParser.TryParseXpub(jobj.ExtPubKey, ref result, out var error, false)) + if (jobj is null || !derivationSchemeParser.TryParseXpub(jobj.ExtPubKey, ref result)) return false; if (jobj.MasterFingerprint is not null)