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:
Andrew Camilleri 2024-01-17 10:08:39 +01:00 committed by GitHub
parent 70e9ea1d5e
commit c5a0e28420
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 664 additions and 410 deletions

View file

@ -31,7 +31,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" /> <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" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" /> <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="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" /> <PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
</ItemGroup> </ItemGroup>

View file

@ -176,7 +176,7 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain); .OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
#pragma warning restore CS0618 // Type or member is obsolete #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.Equal(expected.ToJson(), onchainBTC.ToJson());
Assert.Null(error); Assert.Null(error);

View file

@ -25,8 +25,8 @@
<PackageReference Include="Selenium.Support" Version="4.1.1" /> <PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" /> <PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" /> <PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
<PackageReference Include="xunit" Version="2.6.3" /> <PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -822,7 +822,8 @@ namespace BTCPayServer.Tests
// xpub // xpub
var xpub = "xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"; 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.IsType<DirectDerivationStrategy>(strategyBase);
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit); Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
Assert.Equal("tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS", strategyBase.ToString()); Assert.Equal("tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS", strategyBase.ToString());
@ -882,6 +883,14 @@ namespace BTCPayServer.Tests
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 ")); 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] [Fact]
public void ParseDerivationSchemeSettings() public void ParseDerivationSchemeSettings()
{ {
@ -890,10 +899,10 @@ namespace BTCPayServer.Tests
var root = new Mnemonic( 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") "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(); .DeriveExtKey();
var parsers = GetParsers();
// xpub // xpub
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS"; 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.Null(error);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false }); Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString()); Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
@ -904,7 +913,7 @@ namespace BTCPayServer.Tests
var fingerprint = "e5746fd9"; var fingerprint = "e5746fd9";
var account = "84'/1'/0'"; var account = "84'/1'/0'";
var str = $"[{fingerprint}/{account}]{vpub}"; 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.Null(error);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true }); Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal(vpub, settings.AccountOriginal); Assert.Equal(vpub, settings.AccountOriginal);
@ -913,7 +922,7 @@ namespace BTCPayServer.Tests
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString()); Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
// ColdCard // 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}", "{\"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)); mainnet, out settings, out error));
Assert.Null(error); Assert.Null(error);
@ -929,28 +938,28 @@ namespace BTCPayServer.Tests
settings.AccountDerivation.GetDerivation().ScriptPubKey); settings.AccountDerivation.GetDerivation().ScriptPubKey);
// Should be legacy // 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}", "{\"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)); testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false }); Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Null(error); Assert.Null(error);
// Should be segwit p2sh // 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}", "{\"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)); testnet, out settings, out error));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } }); Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } });
Assert.Null(error); Assert.Null(error);
// Should be segwit // 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}", "{\"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)); testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true }); Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Null(error); Assert.Null(error);
// Specter // Specter
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile( Assert.True(parsers.TryParseWalletFile(
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}", "{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
mainnet, out var specter, out error)); mainnet, out var specter, out error));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint); 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("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("Specter", specter.Label); Assert.Equal("Specter", specter.Label);
Assert.Null(error); Assert.Null(error);
//BSMS BIP129, Nunchuk //BSMS BIP129, Nunchuk
var bsms = @"BSMS 1.0 var bsms = @"BSMS 1.0
@ -966,39 +975,49 @@ wsh(sortedmulti(1,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt
/0/*,/1/* /0/*,/1/*
bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
"; ";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(bsms, Assert.True(parsers.TryParseWalletFile(bsms,
mainnet, out var nunchuk, out error)); 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 //check that the account key settings match those in bsms string
Assert.Equal("5c9e228d", nunchuk.AccountKeySettings[0].RootFingerprint.ToString()); Assert.Equal("5c9e228d", nunchuk.AccountKeySettings[0].RootFingerprint.ToString());
Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[0].AccountKeyPath.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()); 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.IsType<P2WSHDerivationStrategy>(nunchuk.AccountDerivation).Inner);
Assert.True(multsig.LexicographicOrder); Assert.True(multsig.LexicographicOrder);
Assert.Equal(1, multsig.RequiredSignatures); Assert.Equal(1, multsig.RequiredSignatures);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line =nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0); var line = nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey, Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
line.ScriptPubKey); line.ScriptPubKey);
Assert.Equal("BSMS", nunchuk.Source); Assert.Equal("BSMS", nunchuk.Source);
Assert.Null(error); Assert.Null(error);
// Failure case // Failure case
Assert.False(DerivationSchemeSettings.TryParseFromWalletFile( Assert.False(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}", "{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}",
testnet, out settings, out error)); testnet, out settings, out error));
Assert.Null(settings); Assert.Null(settings);
Assert.NotNull(error); 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] [Fact]
@ -1879,7 +1898,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
// Passing electrum stuff // Passing electrum stuff
// Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit // Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit
result = testnetParser.Parse( result = testnetParser.Parse(
"zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t", false, false, false);
Assert.Equal( Assert.Equal(
"tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", "tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w",
result.ToString()); result.ToString());
@ -1903,7 +1922,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
// if prefix not recognize, assume it is segwit // if prefix not recognize, assume it is segwit
result = testnetParser.Parse( result = testnetParser.Parse(
"xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X"); "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X", false, false, false);
Assert.Equal( Assert.Equal(
"tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu", "tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu",
result.ToString()); result.ToString());
@ -1912,13 +1931,13 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
var tpub = var tpub =
"tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"; "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
result = testnetParser.Parse(tpub); result = testnetParser.Parse(tpub, false, true);
Assert.Equal(tpub, result.ToString()); Assert.Equal(tpub, result.ToString());
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("BTC")); var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
var parsed = var parsed =
regtestParser.Parse( regtestParser.Parse(
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false);
Assert.Equal( Assert.Equal(
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]",
parsed.ToString()); 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 // Let's make sure we can't generate segwit with dogecoin
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE")); regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE"));
parsed = regtestParser.Parse( parsed = regtestParser.Parse(
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false);
Assert.Equal( Assert.Equal(
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
parsed.ToString()); parsed.ToString());
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE")); regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE"));
parsed = regtestParser.Parse( parsed = regtestParser.Parse(
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]"); "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", false, false, false);
Assert.Equal( Assert.Equal(
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
parsed.ToString()); parsed.ToString());

View file

@ -288,7 +288,7 @@ namespace BTCPayServer.Tests
/// </summary> /// </summary>
/// <param name="cryptoCode"></param> /// <param name="cryptoCode"></param>
/// <param name="derivationScheme"></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")) if (!Driver.PageSource.Contains($"Setup {cryptoCode} Wallet"))
{ {

View file

@ -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/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" /> <Import Project="../Build/Common.csproj" />
@ -46,13 +46,13 @@
</ItemGroup> </ItemGroup>
<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="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" /> <PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" /> <PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.3" /> <PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.3" />
<PackageReference Include="CsvHelper" Version="15.0.5" /> <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" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" /> <PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="LNURL" Version="0.0.34" /> <PackageReference Include="LNURL" Version="0.0.34" />

View file

@ -122,10 +122,11 @@ namespace BTCPayServer.Controllers.Greenfield
try 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 deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.AccountDerivation.GetLineFor(deposit); var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData(); var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++) for (var i = offset; i < amount; i++)
{ {
@ -134,8 +135,9 @@ namespace BTCPayServer.Controllers.Greenfield
new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem() new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem()
{ {
KeyPath = deposit.GetKeyPath((uint)i).ToString(), KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address = address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork) Address =
.ToString() network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), address.ScriptPubKey)
.ToString()
}); });
} }
@ -168,10 +170,10 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid) if (!ModelState.IsValid)
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
DerivationSchemeSettings strategy; DerivationStrategyBase strategy;
try try
{ {
strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network); strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme, false, true);
} }
catch catch
{ {
@ -181,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
} }
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.AccountDerivation.GetLineFor(deposit); var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData(); var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++) for (var i = offset; i < amount; i++)
{ {
@ -192,9 +194,9 @@ namespace BTCPayServer.Controllers.Greenfield
OnChainPaymentMethodPreviewResultAddressItem() OnChainPaymentMethodPreviewResultAddressItem()
{ {
KeyPath = deposit.GetKeyPath((uint)i).ToString(), KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation, Address =
line.KeyPathTemplate.GetKeyPath((uint)i), network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
derivation.ScriptPubKey).ToString() .ToString()
}); });
} }
@ -244,12 +246,13 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
var store = Store; var store = Store;
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var strategy = DerivationSchemeSettings.Parse(request.DerivationScheme, network); var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme, false, true);
if (strategy != null) if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation); await wallet.TrackAsync(strategy);
strategy.Label = request.Label;
var signing = strategy.GetSigningAccountKeySettings(); var dss = new DerivationSchemeSettings(strategy, network) {Label = request.Label,};
if (request.AccountKeyPath is RootedKeyPath r) var signing = dss.GetSigningAccountKeySettings();
if (request.AccountKeyPath is { } r)
{ {
signing.AccountKeyPath = r.KeyPath; signing.AccountKeyPath = r.KeyPath;
signing.RootFingerprint = r.MasterFingerprint; signing.RootFingerprint = r.MasterFingerprint;
@ -260,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield
signing.RootFingerprint = null; signing.RootFingerprint = null;
} }
store.SetSupportedPaymentMethod(id, strategy); store.SetSupportedPaymentMethod(id, dss);
storeBlob.SetExcluded(id, !request.Enabled); storeBlob.SetExcluded(id, !request.Enabled);
store.SetStoreBlob(storeBlob); store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store); await _storeRepository.UpdateStore(store);

View file

@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
if (vm.WalletFile != null) 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}"); ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
return View(vm.ViewName, vm); return View(vm.ViewName, vm);
@ -98,7 +98,7 @@ namespace BTCPayServer.Controllers
} }
else if (!string.IsNullOrEmpty(vm.WalletFileContent)) 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}"); ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
return View(vm.ViewName, vm); return View(vm.ViewName, vm);

View file

@ -71,7 +71,8 @@ namespace BTCPayServer.Controllers
IOptions<ExternalServicesOptions> externalServiceOptions, IOptions<ExternalServicesOptions> externalServiceOptions,
IHtmlHelper html, IHtmlHelper html,
LightningClientFactoryService lightningClientFactoryService, LightningClientFactoryService lightningClientFactoryService,
EmailSenderFactory emailSenderFactory) EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers)
{ {
_RateFactory = rateFactory; _RateFactory = rateFactory;
_Repo = repo; _Repo = repo;
@ -97,6 +98,7 @@ namespace BTCPayServer.Controllers
_externalServiceOptions = externalServiceOptions; _externalServiceOptions = externalServiceOptions;
_lightningClientFactoryService = lightningClientFactoryService; _lightningClientFactoryService = lightningClientFactoryService;
_emailSenderFactory = emailSenderFactory; _emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers;
Html = html; Html = html;
} }
@ -121,6 +123,7 @@ namespace BTCPayServer.Controllers
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions; private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly LightningClientFactoryService _lightningClientFactoryService; private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly EmailSenderFactory _emailSenderFactory; private readonly EmailSenderFactory _emailSenderFactory;
private readonly WalletFileParsers _onChainWalletParsers;
public string? GeneratedPairingCode { get; set; } public string? GeneratedPairingCode { get; set; }
public WebhookSender WebhookNotificationManager { get; } public WebhookSender WebhookNotificationManager { get; }
@ -916,7 +919,7 @@ namespace BTCPayServer.Controllers
return derivationSchemeSettings; return derivationSchemeSettings;
} }
var strategy = parser.Parse(derivationScheme); var strategy = parser.Parse(derivationScheme, false, true);
return new DerivationSchemeSettings(strategy, network); return new DerivationSchemeSettings(strategy, network);
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using NBitcoin; using NBitcoin;
using NBitcoin.Scripting; using NBitcoin.Scripting;
@ -33,7 +34,7 @@ namespace BTCPayServer
{ {
throw new FormatException("Custom change paths are not supported."); 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: case PubKeyProvider.Origin origin:
var innerResult = ExtractFromPkProvider(origin.Inner, suffix); var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
return (innerResult.Item1, new[] { origin.KeyOriginInfo }); return (innerResult.Item1, new[] { origin.KeyOriginInfo });
@ -47,12 +48,14 @@ namespace BTCPayServer
var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider)); var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider));
return ( return (
Parse( 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()); xpubs.SelectMany(tuple => tuple.Item2).ToArray());
} }
ArgumentNullException.ThrowIfNull(str); ArgumentNullException.ThrowIfNull(str);
str = str.Trim(); 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); var outputDescriptor = OutputDescriptor.Parse(str, Network);
switch (outputDescriptor) switch (outputDescriptor)
{ {
@ -78,11 +81,13 @@ namespace BTCPayServer
sh.Inner is OutputDescriptor.WSH) sh.Inner is OutputDescriptor.WSH)
{ {
var ds = ParseOutputDescriptor(sh.Inner.ToString()); 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)"); 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: case OutputDescriptor.WPKH wpkh:
return ExtractFromPkProvider(wpkh.PkProvider, ""); return ExtractFromPkProvider(wpkh.PkProvider);
case OutputDescriptor.WSH { Inner: OutputDescriptor.Multi multi }: case OutputDescriptor.WSH { Inner: OutputDescriptor.Multi multi }:
return ExtractFromMulti(multi); return ExtractFromMulti(multi);
case OutputDescriptor.WSH: case OutputDescriptor.WSH:
@ -91,36 +96,7 @@ namespace BTCPayServer
throw new ArgumentOutOfRangeException(nameof(outputDescriptor)); throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
} }
} }
public DerivationStrategyBase Parse(string str, bool ignorePrefix = false, bool ignoreBasePrefix = false, bool enforceNetworkPrefix = true)
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)
{ {
ArgumentNullException.ThrowIfNull(str); ArgumentNullException.ThrowIfNull(str);
str = str.Trim(); str = str.Trim();
@ -133,14 +109,6 @@ namespace BTCPayServer
str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase); str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase);
} }
try
{
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str);
}
catch
{
}
var parts = str.Split('-'); var parts = str.Split('-');
bool hasLabel = false; bool hasLabel = false;
for (int i = 0; i < parts.Length; i++) 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()); hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue; continue;
} }
try try
{ {
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]); var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
@ -170,11 +139,15 @@ namespace BTCPayServer
var derivationScheme = GetBitcoinExtPubKeyByNetwork(Network, data).ToString(); 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) switch (type)
{ {
case DerivationType.Legacy: case DerivationType.Legacy when !ignoreBasePrefix:
hintedLabels.Add("legacy"); hintedLabels.Add("legacy");
break; break;
case DerivationType.SegwitP2SH: case DerivationType.SegwitP2SH:
@ -182,8 +155,13 @@ namespace BTCPayServer
break; break;
} }
} }
parts[i] = derivationScheme; parts[i] = derivationScheme;
} }
catch (FormatException e) when (e.Message.StartsWith("Invalid xpub"))
{
throw;
}
catch { continue; } catch { continue; }
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -22,8 +23,8 @@ namespace BTCPayServer
ArgumentNullException.ThrowIfNull(derivationStrategy); ArgumentNullException.ThrowIfNull(derivationStrategy);
var result = new DerivationSchemeSettings(); var result = new DerivationSchemeSettings();
result.Network = network; result.Network = network;
var parser = new DerivationSchemeParser(network); var parser = network.GetDerivationSchemeParser();
if (TryParseXpub(derivationStrategy, parser, ref result, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true)) if (parser.TryParseXpub(derivationStrategy, ref result, out error))
{ {
return result; return result;
} }
@ -50,299 +51,6 @@ namespace BTCPayServer
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString()); 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() public DerivationSchemeSettings()
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
@ -9,6 +10,7 @@ using System.Net.WebSockets;
using System.Reflection; using System.Reflection;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.BIP78.Sender; using BTCPayServer.BIP78.Sender;
@ -16,7 +18,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.NTag424; using BTCPayServer.NTag424;
@ -30,10 +31,11 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment; using NBitcoin.Payment;
using NBitpayClient; using NBitcoin.Scripting;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
@ -42,6 +44,66 @@ namespace BTCPayServer
{ {
public static class Extensions 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) public static CardKey CreatePullPaymentCardKey(this IssuerKey issuerKey, byte[] uid, int version, string pullPaymentId)
{ {
var data = Encoding.UTF8.GetBytes(pullPaymentId); var data = Encoding.UTF8.GetBytes(pullPaymentId);

View file

@ -73,6 +73,8 @@ using Newtonsoft.Json;
using NicolasDorier.RateLimits; using NicolasDorier.RateLimits;
using Serilog; using Serilog;
using BTCPayServer.Services.Reporting; using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.WalletFileParsing;
#if ALTCOINS #if ALTCOINS
using BTCPayServer.Services.Altcoins.Monero; using BTCPayServer.Services.Altcoins.Monero;
using BTCPayServer.Services.Altcoins.Zcash; using BTCPayServer.Services.Altcoins.Zcash;
@ -129,7 +131,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TorServices>()); services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TorServices>());
services.AddSingleton<ISwaggerProvider, DefaultSwaggerProvider>(); services.AddSingleton<ISwaggerProvider, DefaultSwaggerProvider>();
services.TryAddSingleton<SocketFactory>(); services.TryAddSingleton<SocketFactory>();
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(client => services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(client =>
new ChargeLightningConnectionStringHandler(client)); new ChargeLightningConnectionStringHandler(client));
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(_ => services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(_ =>
@ -145,8 +147,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<LightningClientFactoryService>(); services.TryAddSingleton<LightningClientFactoryService>();
services.AddHttpClient(LightningClientFactoryService.OnionNamedClient) services.AddHttpClient(LightningClientFactoryService.OnionNamedClient)
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>(); .ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
services.TryAddSingleton<InvoicePaymentNotification>(); services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => services.TryAddSingleton<BTCPayServerOptions>(o =>
o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value); o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
@ -159,6 +161,9 @@ namespace BTCPayServer.Hosting
AddSettingsAccessor<PoliciesSettings>(services); AddSettingsAccessor<PoliciesSettings>(services);
AddSettingsAccessor<ThemeSettings>(services); AddSettingsAccessor<ThemeSettings>(services);
// //
AddOnchainWalletParsers(services);
services.AddStartupTask<BlockExplorerLinkStartupTask>(); services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.TryAddSingleton<InvoiceRepository>(); services.TryAddSingleton<InvoiceRepository>();
services.AddSingleton<PaymentService>(); services.AddSingleton<PaymentService>();
@ -246,7 +251,7 @@ namespace BTCPayServer.Hosting
{ {
error = e.Message; error = e.Message;
} }
if (error is not null) if (error is not null)
{ {
logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " +
@ -363,7 +368,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<WalletReceiveService>(); services.TryAddSingleton<WalletReceiveService>();
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>()); services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance); services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
services.TryAddSingleton<IFeeProviderFactory,FeeProviderFactory>(); services.TryAddSingleton<IFeeProviderFactory, FeeProviderFactory>();
services.Configure<MvcOptions>((o) => services.Configure<MvcOptions>((o) =>
{ {
@ -412,7 +417,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<NotificationManager>(); services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>(); services.AddScoped<NotificationSender>();
services.AddSingleton<IHostedService, NBXplorerWaiters>(); services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceEventSaverService>(); services.AddSingleton<IHostedService, InvoiceEventSaverService>();
services.AddSingleton<IHostedService, BitpayIPNSender>(); services.AddSingleton<IHostedService, BitpayIPNSender>();
@ -513,6 +518,19 @@ namespace BTCPayServer.Hosting
return services; 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) internal static void RegisterRateSources(IServiceCollection services)
{ {
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request // We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request

View 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;
}
}

View file

@ -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}");
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}