diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj
index a6aebcef0..e345913a9 100644
--- a/BTCPayServer.Client/BTCPayServer.Client.csproj
+++ b/BTCPayServer.Client/BTCPayServer.Client.csproj
@@ -31,7 +31,7 @@
-
+
diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj
index 666249f73..02599b911 100644
--- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj
+++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs
index 1c5605114..908214e06 100644
--- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs
+++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs
@@ -176,7 +176,7 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618 // Type or member is obsolete
.OfType().First(o => o.PaymentId.IsBTCOnChain);
#pragma warning restore CS0618 // Type or member is obsolete
- DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected, out var error);
+ FastTests.GetParsers().TryParseWalletFile(content, onchainBTC.Network, out var expected, out var error);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
Assert.Null(error);
diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj
index ed4b315e3..5951f6af1 100644
--- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj
+++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj
@@ -25,8 +25,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs
index 19dbde8eb..8392b7f66 100644
--- a/BTCPayServer.Tests/FastTests.cs
+++ b/BTCPayServer.Tests/FastTests.cs
@@ -822,7 +822,8 @@ namespace BTCPayServer.Tests
// xpub
var xpub = "xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw";
- DerivationStrategyBase strategyBase = parser.Parse(xpub);
+ Assert.Throws(() => parser.Parse(xpub, false, false, true));
+ DerivationStrategyBase strategyBase = parser.Parse(xpub, false, false, false);
Assert.IsType(strategyBase);
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
Assert.Equal("tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS", strategyBase.ToString());
@@ -882,6 +883,14 @@ namespace BTCPayServer.Tests
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 "));
}
+
+ public static WalletFileParsers GetParsers()
+ {
+ var service = new ServiceCollection();
+ BTCPayServerServices.AddOnchainWalletParsers(service);
+ return service.BuildServiceProvider().GetRequiredService();
+ }
+
[Fact]
public void ParseDerivationSchemeSettings()
{
@@ -890,10 +899,10 @@ namespace BTCPayServer.Tests
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();
-
+ var parsers = GetParsers();
// xpub
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS";
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(tpub, testnet, out var settings, out var error));
+ 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());
@@ -904,7 +913,7 @@ namespace BTCPayServer.Tests
var fingerprint = "e5746fd9";
var account = "84'/1'/0'";
var str = $"[{fingerprint}/{account}]{vpub}";
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(str, testnet, out settings, out error));
+ Assert.True(parsers.TryParseWalletFile(str, testnet, out settings, out error));
Assert.Null(error);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal(vpub, settings.AccountOriginal);
@@ -913,7 +922,7 @@ namespace BTCPayServer.Tests
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
// ColdCard
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
+ Assert.True(parsers.TryParseWalletFile(
"{\"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 settings, out error));
Assert.Null(error);
@@ -929,28 +938,28 @@ namespace BTCPayServer.Tests
settings.AccountDerivation.GetDerivation().ScriptPubKey);
// Should be legacy
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
+ 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.Null(error);
// Should be segwit p2sh
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
+ Assert.True(parsers.TryParseWalletFile(
"{\"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.Null(error);
// Should be segwit
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
+ Assert.True(parsers.TryParseWalletFile(
"{\"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.Null(error);
// Specter
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
+ Assert.True(parsers.TryParseWalletFile(
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
mainnet, out var specter, out error));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint);
@@ -958,7 +967,7 @@ namespace BTCPayServer.Tests
Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("Specter", specter.Label);
Assert.Null(error);
-
+
//BSMS BIP129, Nunchuk
var bsms = @"BSMS 1.0
@@ -966,39 +975,49 @@ wsh(sortedmulti(1,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt
/0/*,/1/*
bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
";
-
- Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(bsms,
+
+ Assert.True(parsers.TryParseWalletFile(bsms,
mainnet, out var nunchuk, out error));
-
- Assert.Equal(2, nunchuk.AccountKeySettings.Length);
+
+ Assert.Equal(2, nunchuk.AccountKeySettings.Length);
//check that the account key settings match those in bsms string
Assert.Equal("5c9e228d", nunchuk.AccountKeySettings[0].RootFingerprint.ToString());
Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[0].AccountKeyPath.ToString());
-Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString());
+ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString());
Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[1].AccountKeyPath.ToString());
- var multsig = Assert.IsType < MultisigDerivationStrategy >
+ var multsig = Assert.IsType
(Assert.IsType(nunchuk.AccountDerivation).Inner);
-
+
Assert.True(multsig.LexicographicOrder);
- Assert.Equal(1, multsig.RequiredSignatures);
-
- var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
- var line =nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
-
- Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
- line.ScriptPubKey);
-
+ Assert.Equal(1, multsig.RequiredSignatures);
+
+ var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
+ var line = nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
+
+ Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
+ line.ScriptPubKey);
+
Assert.Equal("BSMS", nunchuk.Source);
Assert.Null(error);
-
+
// Failure case
- Assert.False(DerivationSchemeSettings.TryParseFromWalletFile(
+ Assert.False(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}",
testnet, out settings, out error));
Assert.Null(settings);
Assert.NotNull(error);
+
+
+ //passport
+ var passportText =
+ "{\"Source\": \"Passport\", \"Descriptor\": \"tr([5c9e228d/86'/0'/0']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/0/*)\", \"FirmwareVersion\": \"v1.0.0\"}";
+ Assert.True(parsers.TryParseWalletFile(passportText, mainnet, out var passport, out error));
+ Assert.Equal("Passport", passport.Source);
+ Assert.True(passport.AccountDerivation is TaprootDerivationStrategy);
+ Assert.Equal("5c9e228d", passport.AccountKeySettings[0].RootFingerprint.ToString());
+ Assert.Equal("86'/0'/0'", passport.AccountKeySettings[0].AccountKeyPath.ToString());
}
[Fact]
@@ -1879,7 +1898,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
// 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");
+ "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t", false, false, false);
Assert.Equal(
"tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w",
result.ToString());
@@ -1903,7 +1922,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
// if prefix not recognize, assume it is segwit
result = testnetParser.Parse(
- "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
+ "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X", false, false, false);
Assert.Equal(
"tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu",
result.ToString());
@@ -1912,13 +1931,13 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
var tpub =
"tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
- result = testnetParser.Parse(tpub);
+ result = testnetParser.Parse(tpub, false, true);
Assert.Equal(tpub, result.ToString());
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC"));
var parsed =
regtestParser.Parse(
- "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
+ "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false);
Assert.Equal(
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]",
parsed.ToString());
@@ -1926,14 +1945,14 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
// Let's make sure we can't generate segwit with dogecoin
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
parsed = regtestParser.Parse(
- "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
+ "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false);
Assert.Equal(
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
parsed.ToString());
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
parsed = regtestParser.Parse(
- "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
+ "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", false, false, false);
Assert.Equal(
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
parsed.ToString());
diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs
index 5a07f8dad..0dfce128a 100644
--- a/BTCPayServer.Tests/SeleniumTester.cs
+++ b/BTCPayServer.Tests/SeleniumTester.cs
@@ -288,7 +288,7 @@ namespace BTCPayServer.Tests
///
///
///
- public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
+ public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "tpubD6NzVbkrYhZ4XxNXjYTcRujMc8z8734diCthtFGgDMimbG5hUsKBuSTCuUyxWL7YwP7R4A5StMTRQiZnb6vE4pdHWPgy9hbiHuVJfBMumUu-[legacy]")
{
if (!Driver.PageSource.Contains($"Setup {cryptoCode} Wallet"))
{
diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj
index 22d2fb36e..abf55dfa7 100644
--- a/BTCPayServer/BTCPayServer.csproj
+++ b/BTCPayServer/BTCPayServer.csproj
@@ -1,4 +1,4 @@
-
+
@@ -46,13 +46,13 @@
-
+
-
+
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs
index 955935ab6..9b1ef5393 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainPaymentMethodsController.cs
@@ -122,10 +122,11 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
- var strategy = DerivationSchemeSettings.Parse(paymentMethod.DerivationScheme, network);
+
+ var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme, false, true);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
- var line = strategy.AccountDerivation.GetLineFor(deposit);
+ var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
{
@@ -134,8 +135,9 @@ namespace BTCPayServer.Controllers.Greenfield
new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
- Address = address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork)
- .ToString()
+ Address =
+ network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), address.ScriptPubKey)
+ .ToString()
});
}
@@ -168,10 +170,10 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
- DerivationSchemeSettings strategy;
+ DerivationStrategyBase strategy;
try
{
- strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network);
+ strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme, false, true);
}
catch
{
@@ -181,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
- var line = strategy.AccountDerivation.GetLineFor(deposit);
+ var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
{
@@ -192,9 +194,9 @@ namespace BTCPayServer.Controllers.Greenfield
OnChainPaymentMethodPreviewResultAddressItem()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
- Address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
- line.KeyPathTemplate.GetKeyPath((uint)i),
- derivation.ScriptPubKey).ToString()
+ Address =
+ network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
+ .ToString()
});
}
@@ -244,12 +246,13 @@ namespace BTCPayServer.Controllers.Greenfield
{
var store = Store;
var storeBlob = store.GetStoreBlob();
- var strategy = DerivationSchemeSettings.Parse(request.DerivationScheme, network);
+ var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme, false, true);
if (strategy != null)
- await wallet.TrackAsync(strategy.AccountDerivation);
- strategy.Label = request.Label;
- var signing = strategy.GetSigningAccountKeySettings();
- if (request.AccountKeyPath is RootedKeyPath r)
+ await wallet.TrackAsync(strategy);
+
+ var dss = new DerivationSchemeSettings(strategy, network) {Label = request.Label,};
+ var signing = dss.GetSigningAccountKeySettings();
+ if (request.AccountKeyPath is { } r)
{
signing.AccountKeyPath = r.KeyPath;
signing.RootFingerprint = r.MasterFingerprint;
@@ -260,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield
signing.RootFingerprint = null;
}
- store.SetSupportedPaymentMethod(id, strategy);
+ store.SetSupportedPaymentMethod(id, dss);
storeBlob.SetExcluded(id, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs
index 85b94650d..171326a93 100644
--- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs
+++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs
@@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
if (vm.WalletFile != null)
{
- if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
+ if (!_onChainWalletParsers.TryParseWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
{
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
return View(vm.ViewName, vm);
@@ -98,7 +98,7 @@ namespace BTCPayServer.Controllers
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
- if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy, out var error))
+ if (!_onChainWalletParsers.TryParseWalletFile(vm.WalletFileContent, network, out strategy, out var error))
{
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
return View(vm.ViewName, vm);
diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs
index 2b99fecfe..dbc3fbaab 100644
--- a/BTCPayServer/Controllers/UIStoresController.cs
+++ b/BTCPayServer/Controllers/UIStoresController.cs
@@ -71,7 +71,8 @@ namespace BTCPayServer.Controllers
IOptions externalServiceOptions,
IHtmlHelper html,
LightningClientFactoryService lightningClientFactoryService,
- EmailSenderFactory emailSenderFactory)
+ EmailSenderFactory emailSenderFactory,
+ WalletFileParsers onChainWalletParsers)
{
_RateFactory = rateFactory;
_Repo = repo;
@@ -97,6 +98,7 @@ namespace BTCPayServer.Controllers
_externalServiceOptions = externalServiceOptions;
_lightningClientFactoryService = lightningClientFactoryService;
_emailSenderFactory = emailSenderFactory;
+ _onChainWalletParsers = onChainWalletParsers;
Html = html;
}
@@ -121,6 +123,7 @@ namespace BTCPayServer.Controllers
private readonly IOptions _externalServiceOptions;
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly EmailSenderFactory _emailSenderFactory;
+ private readonly WalletFileParsers _onChainWalletParsers;
public string? GeneratedPairingCode { get; set; }
public WebhookSender WebhookNotificationManager { get; }
@@ -916,7 +919,7 @@ namespace BTCPayServer.Controllers
return derivationSchemeSettings;
}
- var strategy = parser.Parse(derivationScheme);
+ var strategy = parser.Parse(derivationScheme, false, true);
return new DerivationSchemeSettings(strategy, network);
}
diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs
index 5d7956ab7..7e2ac2a75 100644
--- a/BTCPayServer/DerivationSchemeParser.cs
+++ b/BTCPayServer/DerivationSchemeParser.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using NBitcoin;
using NBitcoin.Scripting;
@@ -33,7 +34,7 @@ namespace BTCPayServer
{
throw new FormatException("Custom change paths are not supported.");
}
- return (Parse($"{hd.Extkey}{suffix}"), null);
+ return (Parse($"{hd.Extkey}{suffix}", true, false, false), null);
case PubKeyProvider.Origin origin:
var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
return (innerResult.Item1, new[] { origin.KeyOriginInfo });
@@ -47,12 +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]")}"),
+ $"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}", true ,false, false),
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)
{
@@ -78,11 +81,13 @@ namespace BTCPayServer
sh.Inner is OutputDescriptor.WSH)
{
var ds = ParseOutputDescriptor(sh.Inner.ToString());
- return (Parse(ds.Item1 + suffix), ds.Item2);
+ return (Parse(ds.Item1 + suffix, true, false, false), ds.Item2);
};
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
+ case OutputDescriptor.Tr tr:
+ return ExtractFromPkProvider(tr.InnerPubkey, "-[taproot]");
case OutputDescriptor.WPKH wpkh:
- return ExtractFromPkProvider(wpkh.PkProvider, "");
+ return ExtractFromPkProvider(wpkh.PkProvider);
case OutputDescriptor.WSH { Inner: OutputDescriptor.Multi multi }:
return ExtractFromMulti(multi);
case OutputDescriptor.WSH:
@@ -91,36 +96,7 @@ namespace BTCPayServer
throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
}
}
-
- public 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 DerivationStrategyBase Parse(string str)
+ public DerivationStrategyBase Parse(string str, bool ignorePrefix = false, bool ignoreBasePrefix = false, bool enforceNetworkPrefix = true)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
@@ -133,14 +109,6 @@ namespace BTCPayServer
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++)
@@ -157,6 +125,7 @@ namespace BTCPayServer
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue;
}
+
try
{
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
@@ -170,11 +139,15 @@ namespace BTCPayServer
var derivationScheme = GetBitcoinExtPubKeyByNetwork(Network, data).ToString();
- if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
+ if (enforceNetworkPrefix && !BtcPayNetwork.ElectrumMapping.ContainsKey(prefix))
+ throw new FormatException(
+ $"Invalid xpub. Is this really for {BtcPayNetwork.CryptoCode} {Network.ChainName}?");
+
+ if (!ignorePrefix && !hasLabel && BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
{
switch (type)
{
- case DerivationType.Legacy:
+ case DerivationType.Legacy when !ignoreBasePrefix:
hintedLabels.Add("legacy");
break;
case DerivationType.SegwitP2SH:
@@ -182,8 +155,13 @@ namespace BTCPayServer
break;
}
}
+
parts[i] = derivationScheme;
}
+ catch (FormatException e) when (e.Message.StartsWith("Invalid xpub"))
+ {
+ throw;
+ }
catch { continue; }
}
diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs
index 02eb7b570..8fc1e8b36 100644
--- a/BTCPayServer/DerivationSchemeSettings.cs
+++ b/BTCPayServer/DerivationSchemeSettings.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -22,8 +23,8 @@ namespace BTCPayServer
ArgumentNullException.ThrowIfNull(derivationStrategy);
var result = new DerivationSchemeSettings();
result.Network = network;
- var parser = new DerivationSchemeParser(network);
- if (TryParseXpub(derivationStrategy, parser, ref result, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true))
+ var parser = network.GetDerivationSchemeParser();
+ if (parser.TryParseXpub(derivationStrategy, ref result, out error))
{
return result;
}
@@ -50,299 +51,6 @@ 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)
- {
- 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 exception)
- {
- error = exception.Message;
- 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.
- // 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
- {
- 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)
- {
- error = exception.Message;
- return false;
- }
- }
-
- public static bool TryParseBSMSFile(string filecontent, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings,
- out string error)
- {
- error = null;
- try
- {
- string[] lines = filecontent.Split(
- new[] {"\r\n", "\r", "\n"},
- StringSplitOptions.None
- );
-
- if (!lines[0].Trim().Equals("BSMS 1.0"))
- {;
- return false;
- }
-
- var descriptor = lines[1];
- var derivationPath = lines[2].Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?? "/0/*";
- if (derivationPath == "No path restrictions")
- {
- derivationPath = "/0/*";
- }
- if(derivationPath != "/0/*")
- {
- error = "BTCPay Server can only derive address to the deposit and change paths";
- return false;
- }
-
-
- descriptor = descriptor.Replace("/**", derivationPath);
- var testAddress = BitcoinAddress.Create( lines[3], derivationSchemeParser.Network);
- var result = derivationSchemeParser.ParseOutputDescriptor(descriptor);
-
- var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
- var line = result.Item1.GetLineFor(deposit).Derive(0);
-
- if (testAddress.ScriptPubKey != line.ScriptPubKey)
- {
- error = "BSMS test address did not match our generated address";
- return false;
- }
-
- derivationSchemeSettings.Source = "BSMS";
- derivationSchemeSettings.AccountDerivation = result.Item1;
- derivationSchemeSettings.AccountOriginal = descriptor.Trim();
- 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 e)
- {
- error = $"BSMS parse error: {e.Message}";
- return false;
- }
- }
- public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error)
- {
- settings = null;
- error = null;
- ArgumentNullException.ThrowIfNull(fileContents);
- ArgumentNullException.ThrowIfNull(network);
- var result = new DerivationSchemeSettings();
- var derivationSchemeParser = new DerivationSchemeParser(network);
- JObject jobj;
- try
- {
- if (HexEncoder.IsWellFormed(fileContents))
- {
- fileContents = Encoding.UTF8.GetString(Encoders.Hex.DecodeData(fileContents));
- }
- jobj = JObject.Parse(fileContents);
- }
- catch
- {
- if (TryParseBSMSFile(fileContents, derivationSchemeParser,ref result, out var bsmsError))
- {
- settings = result;
- settings.Network = network;
- return true;
- }
- if (bsmsError is not null)
- {
- error = bsmsError;
- return false;
- }
- result.Source = "GenericFile";
- if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) ||
- TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false))
- {
- settings = result;
- settings.Network = network;
- return true;
- }
-
- return false;
- }
-
- // Electrum
- if (jobj.ContainsKey("keystore"))
- {
- result.Source = "ElectrumFile";
- jobj = (JObject)jobj["keystore"];
-
- if (!jobj.ContainsKey("xpub") ||
- !TryParseXpub(jobj["xpub"].Value(), derivationSchemeParser, ref result, ref error))
- {
- return false;
- }
-
- if (jobj.ContainsKey("label"))
- {
- try
- {
- result.Label = jobj["label"].Value();
- }
- catch { return false; }
- }
-
- if (jobj.ContainsKey("ckcc_xfp"))
- {
- try
- {
- result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value());
- }
- catch { return false; }
- }
-
- if (jobj.ContainsKey("derivation"))
- {
- try
- {
- result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value());
- }
- catch { return false; }
- }
- }
- // Specter
- else if (jobj.ContainsKey("descriptor") && jobj.ContainsKey("blockheight"))
- {
- result.Source = "SpecterFile";
-
- if (!TryParseXpub(jobj["descriptor"].Value(), derivationSchemeParser, ref result, ref error, false))
- {
- return false;
- }
-
- if (jobj.ContainsKey("label"))
- {
- try
- {
- result.Label = jobj["label"].Value();
- }
- catch { return false; }
- }
- }
- // Wasabi
- else
- {
- result.Source = "WasabiFile";
- if (!jobj.ContainsKey("ExtPubKey") ||
- !TryParseXpub(jobj["ExtPubKey"].Value(), derivationSchemeParser, ref result, ref error, false))
- {
- return false;
- }
- if (jobj.ContainsKey("MasterFingerprint"))
- {
- try
- {
- var mfpString = jobj["MasterFingerprint"].ToString().Trim();
- // https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
-
- if (uint.TryParse(mfpString, out var fingerprint))
- {
- result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint);
- }
- else
- {
- var shouldReverseMfp = jobj.ContainsKey("ColdCardFirmwareVersion") &&
- jobj["ColdCardFirmwareVersion"].ToString() == "2.1.0";
- var bytes = Encoders.Hex.DecodeData(mfpString);
- result.AccountKeySettings[0].RootFingerprint = shouldReverseMfp ? new HDFingerprint(bytes.Reverse().ToArray()) : new HDFingerprint(bytes);
- }
- }
-
- catch { return false; }
- }
- if (jobj.ContainsKey("AccountKeyPath"))
- {
- try
- {
- result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["AccountKeyPath"].Value());
- }
- catch { return false; }
- }
- if (jobj.ContainsKey("DerivationPath"))
- {
- try
- {
- result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["DerivationPath"].Value().ToLowerInvariant());
- }
- catch { return false; }
- }
-
- if (jobj.ContainsKey("ColdCardFirmwareVersion"))
- {
- result.Source = "ColdCard";
- }
-
- if (jobj.ContainsKey("CoboVaultFirmwareVersion"))
- {
- result.Source = "CoboVault";
- }
- }
- settings = result;
- settings.Network = network;
- return true;
- }
-
public DerivationSchemeSettings()
{
diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs
index de00c308c..51a94e404 100644
--- a/BTCPayServer/Extensions.cs
+++ b/BTCPayServer/Extensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
@@ -9,6 +10,7 @@ using System.Net.WebSockets;
using System.Reflection;
using System.Security.Claims;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.BIP78.Sender;
@@ -16,7 +18,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
-using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.NTag424;
@@ -30,10 +31,11 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
using NBitcoin;
+using NBitcoin.DataEncoders;
using NBitcoin.Payment;
-using NBitpayClient;
+using NBitcoin.Scripting;
+using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
@@ -42,6 +44,66 @@ namespace BTCPayServer
{
public static class Extensions
{
+
+ private static readonly ConcurrentDictionary _derivationSchemeParsers =
+ new();
+
+ public static DerivationSchemeParser GetDerivationSchemeParser(this BTCPayNetwork network)
+ {
+ return _derivationSchemeParsers.GetOrAdd(network, n => new DerivationSchemeParser(n));
+ }
+
+ public static bool TryParseXpub(this DerivationSchemeParser derivationSchemeParser, string xpub,
+ ref DerivationSchemeSettings derivationSchemeSettings, out string error)
+ {
+ 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 =
+ derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal, false, false, false);
+ derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys()
+ .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;
+ }
+
+ error = null;
+ return true;
+ }
+ catch (Exception 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/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs
index eda5472e5..f68f6d030 100644
--- a/BTCPayServer/Hosting/BTCPayServerServices.cs
+++ b/BTCPayServer/Hosting/BTCPayServerServices.cs
@@ -73,6 +73,8 @@ using Newtonsoft.Json;
using NicolasDorier.RateLimits;
using Serilog;
using BTCPayServer.Services.Reporting;
+using BTCPayServer.Services.WalletFileParsing;
+
#if ALTCOINS
using BTCPayServer.Services.Altcoins.Monero;
using BTCPayServer.Services.Altcoins.Zcash;
@@ -129,7 +131,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton(provider => provider.GetRequiredService());
services.AddSingleton();
services.TryAddSingleton();
-
+
services.AddSingleton>(client =>
new ChargeLightningConnectionStringHandler(client));
services.AddSingleton>(_ =>
@@ -145,8 +147,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton();
services.AddHttpClient(LightningClientFactoryService.OnionNamedClient)
.ConfigurePrimaryHttpMessageHandler();
-
-
+
+
services.TryAddSingleton();
services.TryAddSingleton(o =>
o.GetRequiredService>().Value);
@@ -159,6 +161,9 @@ namespace BTCPayServer.Hosting
AddSettingsAccessor(services);
AddSettingsAccessor(services);
//
+
+ AddOnchainWalletParsers(services);
+
services.AddStartupTask();
services.TryAddSingleton();
services.AddSingleton();
@@ -246,7 +251,7 @@ namespace BTCPayServer.Hosting
{
error = e.Message;
}
-
+
if (error is not null)
{
logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " +
@@ -363,7 +368,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton();
services.AddSingleton(provider => provider.GetService());
services.TryAddSingleton(CurrencyNameTable.Instance);
- services.TryAddSingleton();
+ services.TryAddSingleton();
services.Configure((o) =>
{
@@ -412,7 +417,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton();
services.AddScoped();
-
+
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -513,6 +518,19 @@ namespace BTCPayServer.Hosting
return services;
}
+ public static void AddOnchainWalletParsers(IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(provider => provider.GetService());
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
internal static void RegisterRateSources(IServiceCollection services)
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
diff --git a/BTCPayServer/Services/WalletFileParsers.cs b/BTCPayServer/Services/WalletFileParsers.cs
new file mode 100644
index 000000000..a5b09dc1d
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsers.cs
@@ -0,0 +1,47 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using NBitcoin.DataEncoders;
+using System.Text;
+using System.Diagnostics.CodeAnalysis;
+using BTCPayServer.Services.WalletFileParsing;
+
+namespace BTCPayServer.Services;
+
+public class WalletFileParsers
+{
+ public WalletFileParsers(IEnumerable parsers)
+ {
+ Parsers = parsers;
+ }
+ public IEnumerable Parsers { get; }
+
+ public bool TryParseWalletFile(string fileContents, BTCPayNetwork network, [MaybeNullWhen(false)] out DerivationSchemeSettings settings, [MaybeNullWhen(true)] out string error)
+ {
+ settings = null;
+ error = string.Empty;
+ ArgumentNullException.ThrowIfNull(fileContents);
+ ArgumentNullException.ThrowIfNull(network);
+ if (HexEncoder.IsWellFormed(fileContents))
+ {
+ fileContents = Encoding.UTF8.GetString(Encoders.Hex.DecodeData(fileContents));
+ }
+
+ foreach (IWalletFileParser onChainWalletParser in Parsers)
+ {
+ var result = onChainWalletParser.TryParse(network, fileContents);
+ if (result.DerivationSchemeSettings is not null)
+ {
+ settings = result.DerivationSchemeSettings;
+ error = null;
+ return true;
+ }
+
+ if (result.Error is not null)
+ {
+ error = result.Error;
+ }
+ }
+ return false;
+ }
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/BSMSWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/BSMSWalletFileParser.cs
new file mode 100644
index 000000000..36803ce74
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/BSMSWalletFileParser.cs
@@ -0,0 +1,76 @@
+#nullable enable
+using System;
+using System.Linq;
+using BTCPayServer;
+using NBitcoin;
+using NBXplorer.DerivationStrategy;
+using AccountKeySettings = BTCPayServer.AccountKeySettings;
+using BTCPayNetwork = BTCPayServer.BTCPayNetwork;
+
+namespace BTCPayServer.Services.WalletFileParsing;
+public class BSMSWalletFileParser : IWalletFileParser
+{
+ public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(
+ BTCPayNetwork network,
+ string data)
+ {
+ try
+ {
+ string[] lines = data.Split(
+ new[] {"\r\n", "\r", "\n"},
+ StringSplitOptions.None
+ );
+
+ if (!lines[0].Trim().Equals("BSMS 1.0"))
+ {
+ return (null, null);
+ }
+
+ var descriptor = lines[1];
+ var derivationPath = lines[2].Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ??
+ "/0/*";
+ if (derivationPath == "No path restrictions")
+ {
+ derivationPath = "/0/*";
+ }
+
+ if (derivationPath != "/0/*")
+ {
+ return (null, "BTCPay Server can only derive address to the deposit and change paths");
+ }
+
+
+ descriptor = descriptor.Replace("/**", derivationPath);
+ var testAddress = BitcoinAddress.Create(lines[3], network.NBitcoinNetwork);
+
+ var result = network.GetDerivationSchemeParser().ParseOutputDescriptor(descriptor);
+
+ var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
+ var line = result.Item1.GetLineFor(deposit).Derive(0);
+
+ if (testAddress.ScriptPubKey != line.ScriptPubKey)
+ {
+ return (null, "BSMS test address did not match our generated address");
+ }
+
+ var derivationSchemeSettings = new BTCPayServer.DerivationSchemeSettings()
+ {
+ Network = network,
+ Source = "BSMS",
+ AccountDerivation = result.Item1,
+ AccountOriginal = descriptor.Trim(),
+ AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
+ {
+ RootFingerprint = path?.MasterFingerprint,
+ AccountKeyPath = path?.KeyPath,
+ AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(network.NBitcoinNetwork)
+ }).ToArray()
+ };
+ return (derivationSchemeSettings, null);
+ }
+ catch (Exception e)
+ {
+ return (null, $"BSMS parse error: {e.Message}");
+ }
+ }
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs
new file mode 100644
index 000000000..65c53190b
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/ElectrumWalletFileParser.cs
@@ -0,0 +1,88 @@
+#nullable enable
+using System;
+using BTCPayServer;
+using NBitcoin;
+using Newtonsoft.Json.Linq;
+namespace BTCPayServer.Services.WalletFileParsing;
+public class ElectrumWalletFileParser : IWalletFileParser
+{
+ public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
+ string data)
+ {
+ try
+ {
+ var derivationSchemeParser = network.GetDerivationSchemeParser();
+ var jobj = JObject.Parse(data);
+ var result = new BTCPayServer.DerivationSchemeSettings() {Network = network};
+
+ if (jobj["keystore"] is JObject keyStore)
+ {
+ result.Source = "ElectrumFile";
+ jobj = keyStore;
+
+ if (!jobj.TryGetValue("xpub", StringComparison.InvariantCultureIgnoreCase, out var xpubToken))
+ {
+ return (null, "no xpub");
+ }
+ var strategy = derivationSchemeParser.Parse(xpubToken.Value(), false, false, true);
+ result.AccountDerivation = strategy;
+ result.AccountOriginal = xpubToken.Value();
+ result.GetSigningAccountKeySettings();
+
+ if (jobj["label"]?.Value() is string label)
+ {
+ try
+ {
+ result.Label = label;
+ }
+ catch
+ {
+ return (null, "Label was not a string");
+ }
+ }
+
+ if (jobj["ckcc_xfp"]?.Value() is uint xfp)
+ {
+ try
+ {
+ result.AccountKeySettings[0].RootFingerprint =
+ new HDFingerprint(xfp);
+ }
+ catch
+ {
+ return (null, "fingerprint was not a uint");
+ }
+ }
+
+ if (jobj["derivation"]?.Value() is string derivation)
+ {
+ try
+ {
+ result.AccountKeySettings[0].AccountKeyPath = new KeyPath(derivation);
+ }
+ catch
+ {
+ return (null, "derivation keypath was not valid");
+ }
+ }
+
+
+ if (jobj.ContainsKey("ColdCardFirmwareVersion"))
+ {
+ result.Source = "ColdCard";
+ }
+ else if (jobj.ContainsKey("CoboVaultFirmwareVersion"))
+ {
+ result.Source = "CoboVault";
+ }
+ return (result, null);
+ }
+
+ }
+ catch (FormatException)
+ {
+ return (null, "invalid xpub");
+ }
+ return (null, null);
+ }
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/IWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/IWalletFileParser.cs
new file mode 100644
index 000000000..1276ddead
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/IWalletFileParser.cs
@@ -0,0 +1,7 @@
+#nullable enable
+using BTCPayServer;
+namespace BTCPayServer.Services.WalletFileParsing;
+public interface IWalletFileParser
+{
+ (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network, string data);
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs
new file mode 100644
index 000000000..723daef54
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/NBXDerivGenericWalletFileParser.cs
@@ -0,0 +1,21 @@
+#nullable enable
+using System;
+using BTCPayServer;
+namespace BTCPayServer.Services.WalletFileParsing;
+public class NBXDerivGenericWalletFileParser : IWalletFileParser
+{
+ public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
+ string data)
+ {
+ try
+ {
+ var result = BTCPayServer.DerivationSchemeSettings.Parse(data, network);
+ result.Source = "Generic";
+ return (result, null);
+ }
+ catch (Exception)
+ {
+ return (null, null);
+ }
+ }
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/OutputDescriptorJsonWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/OutputDescriptorJsonWalletFileParser.cs
new file mode 100644
index 000000000..01471c456
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/OutputDescriptorJsonWalletFileParser.cs
@@ -0,0 +1,36 @@
+#nullable enable
+using System;
+using BTCPayServer;
+using Newtonsoft.Json.Linq;
+namespace BTCPayServer.Services.WalletFileParsing;
+public class OutputDescriptorJsonWalletFileParser : IWalletFileParser
+{
+ private readonly OutputDescriptorWalletFileParser _outputDescriptorOnChainWalletParser;
+
+ public OutputDescriptorJsonWalletFileParser(OutputDescriptorWalletFileParser outputDescriptorOnChainWalletParser)
+ {
+ _outputDescriptorOnChainWalletParser = outputDescriptorOnChainWalletParser;
+ }
+ public (DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
+ string data)
+ {
+ try
+ {
+ var jobj = JObject.Parse(data);
+ if (!jobj.TryGetValue("Descriptor", StringComparison.InvariantCultureIgnoreCase, out var descriptorToken) ||
+ descriptorToken?.Value() is not string desc)
+ return (null, null);
+
+
+ var result = _outputDescriptorOnChainWalletParser.TryParse(network, desc);
+ if (result.DerivationSchemeSettings is not null && jobj.TryGetValue("Source", StringComparison.InvariantCultureIgnoreCase, out var sourceToken))
+ result.DerivationSchemeSettings.Source = sourceToken.Value();
+ return result;
+
+ }
+ catch (Exception)
+ {
+ return (null, null);
+ }
+ }
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/OutputDescriptorWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/OutputDescriptorWalletFileParser.cs
new file mode 100644
index 000000000..90c7f079d
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/OutputDescriptorWalletFileParser.cs
@@ -0,0 +1,42 @@
+#nullable enable
+using System;
+using System.Linq;
+using BTCPayServer;
+namespace BTCPayServer.Services.WalletFileParsing;
+public class OutputDescriptorWalletFileParser : IWalletFileParser
+{
+ public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
+ string data)
+ {
+ try
+ {
+ var maybeOutputDesc = !data.Trim().StartsWith("{", StringComparison.OrdinalIgnoreCase);
+ if (!maybeOutputDesc)
+ return (null, null);
+
+ var derivationSchemeParser = network.GetDerivationSchemeParser();
+
+ var descriptor = derivationSchemeParser.ParseOutputDescriptor(data);
+
+ var derivationSchemeSettings = new DerivationSchemeSettings()
+ {
+ Network = network,
+ Source = "OutputDescriptor",
+ AccountOriginal = data.Trim(),
+ AccountDerivation = descriptor.Item1,
+ AccountKeySettings = descriptor.Item2.Select((path, i) => new AccountKeySettings()
+ {
+ RootFingerprint = path?.MasterFingerprint,
+ AccountKeyPath = path?.KeyPath,
+ AccountKey =
+ descriptor.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
+ }).ToArray()
+ };
+ return (derivationSchemeSettings, null);
+ }
+ catch (Exception exception)
+ {
+ return (null, exception.Message);
+ }
+ }
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/SpecterWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/SpecterWalletFileParser.cs
new file mode 100644
index 000000000..3c232974c
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/SpecterWalletFileParser.cs
@@ -0,0 +1,41 @@
+#nullable enable
+using System;
+using BTCPayServer;
+using Newtonsoft.Json.Linq;
+namespace BTCPayServer.Services.WalletFileParsing;
+public class SpecterWalletFileParser : IWalletFileParser
+{
+ private readonly OutputDescriptorWalletFileParser _outputDescriptorOnChainWalletParser;
+
+ public SpecterWalletFileParser(OutputDescriptorWalletFileParser outputDescriptorOnChainWalletParser)
+ {
+ _outputDescriptorOnChainWalletParser = outputDescriptorOnChainWalletParser;
+ }
+ public (DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
+ string data)
+ {
+ try
+ {
+ var jobj = JObject.Parse(data);
+ if (!jobj.TryGetValue("descriptor", StringComparison.InvariantCultureIgnoreCase, out var descriptorObj)
+ || !jobj.ContainsKey("blockheight")
+ || descriptorObj?.Value() is not string desc)
+ return (null, null);
+
+
+ var result = _outputDescriptorOnChainWalletParser.TryParse(network, desc);
+ if (result.DerivationSchemeSettings is not null)
+ result.DerivationSchemeSettings.Source = "Specter";
+
+ if (result.DerivationSchemeSettings is not null && jobj.TryGetValue("label",
+ StringComparison.InvariantCultureIgnoreCase, out var label) && label?.Value() is string labelValue)
+ result.DerivationSchemeSettings.Label = labelValue;
+ return result;
+
+ }
+ catch (Exception)
+ {
+ return (null, null);
+ }
+ }
+}
diff --git a/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs b/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs
new file mode 100644
index 000000000..246cb1655
--- /dev/null
+++ b/BTCPayServer/Services/WalletFileParsing/WasabiWalletFileParser.cs
@@ -0,0 +1,105 @@
+#nullable enable
+using System;
+using System.Linq;
+using BTCPayServer;
+using NBitcoin;
+using NBitcoin.DataEncoders;
+using Newtonsoft.Json.Linq;
+namespace BTCPayServer.Services.WalletFileParsing;
+public class WasabiWalletFileParser : IWalletFileParser
+{
+
+ public (DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
+ string data)
+ {
+ try
+ {
+ var jobj = JObject.Parse(data);
+ if (jobj["ExtPubKey"]?.Value() is not string extPubKey)
+ return (null, null);
+
+ var derivationSchemeParser = network.GetDerivationSchemeParser();
+ var result = new DerivationSchemeSettings()
+ {
+ Network = network
+ };
+
+ if (!derivationSchemeParser.TryParseXpub(extPubKey, ref result, out var error))
+ {
+ return (null, error);
+ }
+
+ if (jobj["MasterFingerprint"]?.ToString()?.Trim() is string mfpString)
+ {
+ try
+ {
+ // https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
+ if (uint.TryParse(mfpString, out var fingerprint))
+ {
+ result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint);
+ }
+ else
+ {
+ var bytes = Encoders.Hex.DecodeData(mfpString);
+ var shouldReverseMfp = jobj["ColdCardFirmwareVersion"]?.Value() == "2.1.0";
+ if (shouldReverseMfp) // Bug in previous version of coldcard
+ bytes = bytes.Reverse().ToArray();
+ result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(bytes);
+ }
+ }
+
+ catch
+ {
+ return (null, "MasterFingerprint was not valid");
+ }
+ }
+
+ if (jobj["AccountKeyPath"]?.Value() is string accountKeyPath)
+ {
+ try
+ {
+ result.AccountKeySettings[0].AccountKeyPath = new KeyPath(accountKeyPath);
+ }
+ catch
+ {
+ return (null, "AccountKeyPath was not valid");
+ }
+ }
+
+ if (jobj["DerivationPath"]?.Value()?.ToLowerInvariant() is string derivationPath)
+ {
+ try
+ {
+ result.AccountKeySettings[0].AccountKeyPath = new KeyPath(derivationPath);
+ }
+ catch
+ {
+ return (null, "Derivation path was not valid");
+ }
+ }
+
+ if (jobj.ContainsKey("ColdCardFirmwareVersion"))
+ {
+ result.Source = "ColdCard";
+ }
+ else if (jobj.ContainsKey("CoboVaultFirmwareVersion"))
+ {
+ result.Source = "CoboVault";
+ }
+ else if (jobj.TryGetValue("Source", StringComparison.InvariantCultureIgnoreCase, out var source))
+ {
+ result.Source = source.Value();
+ }
+ else
+ result.Source = "WasabiFile";
+
+
+ return (result, null);
+ }
+ catch (Exception)
+ {
+ return (null, null);
+ }
+
+ }
+}