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>
<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>

View file

@ -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>

View file

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

View file

@ -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>

View file

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

View file

@ -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"))
{

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/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" />

View file

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

View file

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

View file

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

View file

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

View file

@ -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()
{

View file

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

View file

@ -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

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