diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 74f1b4bf0..85e052bbd 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -936,9 +936,10 @@ namespace BTCPayServer.Tests // xpub var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS"; Assert.True(parsers.TryParseWalletFile(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()); + Assert.Equal("Generic", settings.Source); + Assert.Null(error); // xpub with fingerprint and account tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca"; @@ -953,6 +954,8 @@ 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.Null(error); // ColdCard Assert.True(parsers.TryParseWalletFile( @@ -969,12 +972,15 @@ 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); + Assert.Equal("ElectrumFile", settings.Source); + Assert.Null(error); // Should be legacy Assert.True(parsers.TryParseWalletFile( "{\"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 { Segwit: false }); + Assert.Equal("ElectrumFile", settings.Source); Assert.Null(error); // Should be segwit p2sh @@ -982,6 +988,7 @@ namespace BTCPayServer.Tests "{\"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 { Inner: DirectDerivationStrategy { Segwit: true } }); + Assert.Equal("ElectrumFile", settings.Source); Assert.Null(error); // Should be segwit @@ -989,6 +996,7 @@ namespace BTCPayServer.Tests "{\"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 { Segwit: true }); + Assert.Equal("ElectrumFile", settings.Source); Assert.Null(error); // Specter @@ -998,11 +1006,21 @@ namespace BTCPayServer.Tests Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint); Assert.Equal(specter.AccountKeySettings[0].RootFingerprint, hd); Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString()); + Assert.True(specter.AccountDerivation is DirectDerivationStrategy { Segwit: true }); Assert.Equal("Specter", specter.Label); Assert.Null(error); - - //BSMS BIP129, Nunchuk - + + // Wasabi + var wasabiJson = @"{""EncryptedSecret"": ""6PYNUAZZLS1ShkhHhm9ayiNwXPAPLN669fN5mY2WbGm1Hqc88tomqWXabU"",""ChainCode"": ""UoHIB+2mDbZSowo11TfDQbsYK6q1DrZ2H2yqQBxu6m8="",""MasterFingerprint"": ""0f215605"",""ExtPubKey"": ""xpub6DUXFa6fMrFpg7x4nEd8jBU6xDN3vkSXsVUrSbUB2dadbYaPE31czwVdv146JRStGsc2U6TywdKnGoVcP8Rtp2AZQyzXxQb7HrgmR9LrqLA"",""TaprootExtPubKey"": ""xpub6D2thLU5KwUk3axkJu1UT3yKFshCGU7TMuxhPgZMd91VvrcDwHdRwdzLk61cSHtZC6BkaipPgfFwjoDBY4m1WxyznxZLukYgM4dC6iRJVf8"",""SkipSynchronization"": true,""UseTurboSync"": true,""MinGapLimit"": 21,""AccountKeyPath"": ""84'/0'/0'"",""TaprootAccountKeyPath"": ""86'/0'/0'"",""BlockchainState"": {""Network"": ""Main"",""Height"": ""503723"",""TurboSyncHeight"": ""503723""},""PreferPsbtWorkflow"": false,""AutoCoinJoin"": true,""PlebStopThreshold"": ""0.01"",""AnonScoreTarget"": 5,""FeeRateMedianTimeFrameHours"": 0,""IsCoinjoinProfileSelected"": true,""RedCoinIsolation"": false,""ExcludedCoinsFromCoinJoin"": [],""HdPubKeys"": [{""PubKey"": ""03f88b9c3e16e40a5a9eaf8b36b9bcee7bbc93fd9eea640b541efb931ac55f7ff5"",""FullKeyPath"": ""84'/0'/0'/1/0"",""Label"": """",""KeyState"": 0},{""PubKey"": ""03e5241fc28aa556d7cb826b9a9f5ecee85287e7476746126263574a5e27fbf569"",""FullKeyPath"": ""84'/0'/0'/0/0"",""Label"": """",""KeyState"": 0}]}"; + Assert.True(parsers.TryParseWalletFile(wasabiJson, mainnet, out var wasabi, out error)); + Assert.Null(error); + Assert.Equal("WasabiFile", wasabi.Source); + Assert.Single(wasabi.AccountKeySettings); + Assert.Equal("84'/0'/0'", wasabi.AccountKeySettings[0].AccountKeyPath.ToString()); + Assert.Equal("0f215605", wasabi.AccountKeySettings[0].RootFingerprint.ToString()); + Assert.True(wasabi.AccountDerivation is DirectDerivationStrategy { Segwit: true }); + + // BSMS BIP129, Nunchuk var bsms = @"BSMS 1.0 wsh(sortedmulti(1,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/**,[2b0e251e/48'/0'/0'/2']xpub6DrimHB8KUSkPvmJ8Pk8RE769EdDm2VEoZ8MBz76w9QupP8Py4wexs4Pa3aRB1LUEhc9GyY6ypDWEFFRCgqeDQePcyWQfjtmintrehq3JCL/**)) /0/*,/1/* diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs index 7e2ac2a75..12d8da61d 100644 --- a/BTCPayServer/DerivationSchemeParser.cs +++ b/BTCPayServer/DerivationSchemeParser.cs @@ -96,7 +96,7 @@ namespace BTCPayServer throw new ArgumentOutOfRangeException(nameof(outputDescriptor)); } } - public DerivationStrategyBase Parse(string str, bool ignorePrefix = false, bool ignoreBasePrefix = false, bool enforceNetworkPrefix = true) + public DerivationStrategyBase Parse(string str, bool ignorePrefix = false, bool ignoreBasePrefix = false, bool enforceNetworkPrefix = true, bool electrum = true) { ArgumentNullException.ThrowIfNull(str); str = str.Trim(); @@ -143,7 +143,7 @@ namespace BTCPayServer throw new FormatException( $"Invalid xpub. Is this really for {BtcPayNetwork.CryptoCode} {Network.ChainName}?"); - if (!ignorePrefix && !hasLabel && BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) + if (!ignorePrefix && !hasLabel && electrum && BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type)) { switch (type) { diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index 8fc1e8b36..1143f9d90 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using BTCPayServer.Payments; using NBitcoin; -using NBitcoin.DataEncoders; using NBXplorer.Client; using NBXplorer.DerivationStrategy; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace BTCPayServer { @@ -21,8 +16,7 @@ namespace BTCPayServer string error = null; ArgumentNullException.ThrowIfNull(network); ArgumentNullException.ThrowIfNull(derivationStrategy); - var result = new DerivationSchemeSettings(); - result.Network = network; + var result = new DerivationSchemeSettings { Network = network }; var parser = network.GetDerivationSchemeParser(); if (parser.TryParseXpub(derivationStrategy, ref result, out error)) { diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index c52a50b4e..2d23d91eb 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -32,9 +32,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NBitcoin; -using NBitcoin.DataEncoders; using NBitcoin.Payment; -using NBitcoin.Scripting; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; @@ -50,7 +48,7 @@ namespace BTCPayServer } public static bool TryParseXpub(this DerivationSchemeParser derivationSchemeParser, string xpub, - ref DerivationSchemeSettings derivationSchemeSettings, out string error) + ref DerivationSchemeSettings derivationSchemeSettings, out string error, bool electrum = true) { try { @@ -72,7 +70,7 @@ namespace BTCPayServer derivationSchemeSettings.AccountOriginal = xpub.Trim(); derivationSchemeSettings.AccountDerivation = - derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal, false, false, false); + derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal, false, false, false, electrum); derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys() .Select(key => new AccountKeySettings {AccountKey = key.GetWif(derivationSchemeParser.Network)}) .ToArray(); diff --git a/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs index 7ff02d775..61c90165d 100644 --- a/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs +++ b/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs @@ -1,10 +1,7 @@ #nullable enable -using System; using System.Diagnostics.CodeAnalysis; -using BTCPayServer; using NBitcoin; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.WalletFileParsing; public class ElectrumWalletFileParser : IWalletFileParser { @@ -28,7 +25,7 @@ public class ElectrumWalletFileParser : IWalletFileParser if (jobj?.keystore is null) return false; - var result = new BTCPayServer.DerivationSchemeSettings() { Network = network }; + var result = new DerivationSchemeSettings { Network = network }; var derivationSchemeParser = network.GetDerivationSchemeParser(); result.Source = "ElectrumFile"; @@ -46,7 +43,6 @@ public class ElectrumWalletFileParser : IWalletFileParser result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj.keystore.ckcc_xfp.Value); result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj.keystore.derivation); - if (jobj.keystore.ColdCardFirmwareVersion is not null) { result.Source = "ColdCard"; diff --git a/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs index 80cb87e0b..8bb781c57 100644 --- a/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs +++ b/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs @@ -1,13 +1,11 @@ #nullable enable -using System; using System.Diagnostics.CodeAnalysis; -using BTCPayServer; namespace BTCPayServer.Services.WalletFileParsing; public class NBXDerivGenericWalletFileParser : IWalletFileParser { public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings) { - derivationSchemeSettings = BTCPayServer.DerivationSchemeSettings.Parse(data, network); + derivationSchemeSettings = DerivationSchemeSettings.Parse(data, network); derivationSchemeSettings.Source = "Generic"; return true; } diff --git a/BTCPayServer/Services/WalletFileParsing/SpecterWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/SpecterWalletFileParser.cs index 0a2644ce1..aea6f8cd6 100644 --- a/BTCPayServer/Services/WalletFileParsing/SpecterWalletFileParser.cs +++ b/BTCPayServer/Services/WalletFileParsing/SpecterWalletFileParser.cs @@ -27,9 +27,11 @@ public class SpecterWalletFileParser : IWalletFileParser return false; if (!_outputDescriptorOnChainWalletParser.TryParse(network, jobj.descriptor, out derivationSchemeSettings)) return false; + derivationSchemeSettings.Source = "Specter"; if (jobj.label is not null) - derivationSchemeSettings.Label = jobj.label; + derivationSchemeSettings.Label = jobj.label; + return true; } } diff --git a/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs index 4aea07463..587429e16 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)) + if (jobj is null || !derivationSchemeParser.TryParseXpub(jobj.ExtPubKey, ref result, out var error, false)) return false; if (jobj.MasterFingerprint is not null)