mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Refactor Wallet import code (#5638)
* Refactor Wallet import code The code for wallet import was incredibly messy as it evolved over time from various requests. This PR: * splits up each supported format into its own file * Supports taproot descriptors (through a hack until NBitcoin supports it internally) fixes #5518 * Reduces different paths for handling electrum/non-electrum xpubs * Allows plugins to add their own import support formats for onchain wallets. * Update NBitcoin to parse tr descriptors * Fix warnings * Use dedicated type OnChainWalletParsers --------- Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
70e9ea1d5e
commit
c5a0e28420
23 changed files with 664 additions and 410 deletions
|
@ -31,7 +31,7 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.32" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.34" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.32" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.34" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -176,7 +176,7 @@ namespace BTCPayServer.Tests
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
.OfType<DerivationSchemeSettings>().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);
|
||||
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
|
||||
<PackageReference Include="xunit" Version="2.6.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -822,7 +822,8 @@ namespace BTCPayServer.Tests
|
|||
|
||||
// xpub
|
||||
var xpub = "xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw";
|
||||
DerivationStrategyBase strategyBase = parser.Parse(xpub);
|
||||
Assert.Throws<FormatException>(() => parser.Parse(xpub, false, false, true));
|
||||
DerivationStrategyBase strategyBase = parser.Parse(xpub, false, false, false);
|
||||
Assert.IsType<DirectDerivationStrategy>(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<WalletFileParsers>();
|
||||
}
|
||||
|
||||
[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<MultisigDerivationStrategy>
|
||||
(Assert.IsType<P2WSHDerivationStrategy>(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<BTCPayNetwork>("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<BTCPayNetwork>("DOGE"));
|
||||
parsed = regtestParser.Parse(
|
||||
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
|
||||
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false);
|
||||
Assert.Equal(
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
|
||||
parsed.ToString());
|
||||
|
||||
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE"));
|
||||
parsed = regtestParser.Parse(
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", false, false, false);
|
||||
Assert.Equal(
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
|
||||
parsed.ToString());
|
||||
|
|
|
@ -288,7 +288,7 @@ namespace BTCPayServer.Tests
|
|||
/// </summary>
|
||||
/// <param name="cryptoCode"></param>
|
||||
/// <param name="derivationScheme"></param>
|
||||
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"))
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
|
@ -46,13 +46,13 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.19" />
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.20" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.3" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.1.24" />
|
||||
<PackageReference Include="Dapper" Version="2.1.28" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -71,7 +71,8 @@ namespace BTCPayServer.Controllers
|
|||
IOptions<ExternalServicesOptions> 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<ExternalServicesOptions> _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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string>(), derivationSchemeParser, ref result, ref error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("label"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Label = jobj["label"].Value<string>();
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("ckcc_xfp"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("derivation"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
// Specter
|
||||
else if (jobj.ContainsKey("descriptor") && jobj.ContainsKey("blockheight"))
|
||||
{
|
||||
result.Source = "SpecterFile";
|
||||
|
||||
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("label"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Label = jobj["label"].Value<string>();
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
// Wasabi
|
||||
else
|
||||
{
|
||||
result.Source = "WasabiFile";
|
||||
if (!jobj.ContainsKey("ExtPubKey") ||
|
||||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), 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<string>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
if (jobj.ContainsKey("DerivationPath"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["DerivationPath"].Value<string>().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()
|
||||
{
|
||||
|
||||
|
|
|
@ -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<BTCPayNetwork, DerivationSchemeParser> _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);
|
||||
|
|
|
@ -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<IHostedService>(provider => provider.GetRequiredService<TorServices>());
|
||||
services.AddSingleton<ISwaggerProvider, DefaultSwaggerProvider>();
|
||||
services.TryAddSingleton<SocketFactory>();
|
||||
|
||||
|
||||
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(client =>
|
||||
new ChargeLightningConnectionStringHandler(client));
|
||||
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(_ =>
|
||||
|
@ -145,8 +147,8 @@ namespace BTCPayServer.Hosting
|
|||
services.TryAddSingleton<LightningClientFactoryService>();
|
||||
services.AddHttpClient(LightningClientFactoryService.OnionNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
|
||||
|
||||
|
||||
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o =>
|
||||
o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
|
@ -159,6 +161,9 @@ namespace BTCPayServer.Hosting
|
|||
AddSettingsAccessor<PoliciesSettings>(services);
|
||||
AddSettingsAccessor<ThemeSettings>(services);
|
||||
//
|
||||
|
||||
AddOnchainWalletParsers(services);
|
||||
|
||||
services.AddStartupTask<BlockExplorerLinkStartupTask>();
|
||||
services.TryAddSingleton<InvoiceRepository>();
|
||||
services.AddSingleton<PaymentService>();
|
||||
|
@ -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<WalletReceiveService>();
|
||||
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
|
||||
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
|
||||
services.TryAddSingleton<IFeeProviderFactory,FeeProviderFactory>();
|
||||
services.TryAddSingleton<IFeeProviderFactory, FeeProviderFactory>();
|
||||
|
||||
services.Configure<MvcOptions>((o) =>
|
||||
{
|
||||
|
@ -412,7 +417,7 @@ namespace BTCPayServer.Hosting
|
|||
|
||||
services.AddSingleton<NotificationManager>();
|
||||
services.AddScoped<NotificationSender>();
|
||||
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, InvoiceEventSaverService>();
|
||||
services.AddSingleton<IHostedService, BitpayIPNSender>();
|
||||
|
@ -513,6 +518,19 @@ namespace BTCPayServer.Hosting
|
|||
return services;
|
||||
}
|
||||
|
||||
public static void AddOnchainWalletParsers(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<WalletFileParsers>();
|
||||
services.AddSingleton<IWalletFileParser, BSMSWalletFileParser>();
|
||||
services.AddSingleton<IWalletFileParser, NBXDerivGenericWalletFileParser>();
|
||||
services.AddSingleton<IWalletFileParser, ElectrumWalletFileParser>();
|
||||
services.AddSingleton<IWalletFileParser, OutputDescriptorWalletFileParser>(provider => provider.GetService<OutputDescriptorWalletFileParser>());
|
||||
services.AddSingleton<OutputDescriptorWalletFileParser>();
|
||||
services.AddSingleton<IWalletFileParser, SpecterWalletFileParser>();
|
||||
services.AddSingleton<IWalletFileParser, OutputDescriptorJsonWalletFileParser>();
|
||||
services.AddSingleton<IWalletFileParser, WasabiWalletFileParser>();
|
||||
}
|
||||
|
||||
internal static void RegisterRateSources(IServiceCollection services)
|
||||
{
|
||||
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
|
||||
|
|
47
BTCPayServer/Services/WalletFileParsers.cs
Normal file
47
BTCPayServer/Services/WalletFileParsers.cs
Normal file
|
@ -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<IWalletFileParser> parsers)
|
||||
{
|
||||
Parsers = parsers;
|
||||
}
|
||||
public IEnumerable<IWalletFileParser> 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;
|
||||
}
|
||||
}
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string>(), false, false, true);
|
||||
result.AccountDerivation = strategy;
|
||||
result.AccountOriginal = xpubToken.Value<string>();
|
||||
result.GetSigningAccountKeySettings();
|
||||
|
||||
if (jobj["label"]?.Value<string>() is string label)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Label = label;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (null, "Label was not a string");
|
||||
}
|
||||
}
|
||||
|
||||
if (jobj["ckcc_xfp"]?.Value<uint>() is uint xfp)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeySettings[0].RootFingerprint =
|
||||
new HDFingerprint(xfp);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (null, "fingerprint was not a uint");
|
||||
}
|
||||
}
|
||||
|
||||
if (jobj["derivation"]?.Value<string>() 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string>() 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<string>();
|
||||
return result;
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string>() 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<string>() is string labelValue)
|
||||
result.DerivationSchemeSettings.Label = labelValue;
|
||||
return result;
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string>() 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<string>() == "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<string>() is string accountKeyPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(accountKeyPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (null, "AccountKeyPath was not valid");
|
||||
}
|
||||
}
|
||||
|
||||
if (jobj["DerivationPath"]?.Value<string>()?.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<string>();
|
||||
}
|
||||
else
|
||||
result.Source = "WasabiFile";
|
||||
|
||||
|
||||
return (result, null);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue