Compare commits
15 Commits
v1.12.4-rc
...
v1.12.4
Author | SHA1 | Date | |
---|---|---|---|
2111b67e2c | |||
b96cfcd14d | |||
086f713752 | |||
fd67e09cf0 | |||
6f4ca47532 | |||
f97f23c8a5 | |||
b62985faf4 | |||
09c761aa31 | |||
8089a938f3 | |||
35b3fef7c5 | |||
f31aa43c6a | |||
b03f8db06b | |||
27e70a169e | |||
6a1d17dda2 | |||
31bc6dd48c |
@ -24,7 +24,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
|
||||
<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="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -47,6 +47,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -175,6 +176,11 @@ namespace BTCPayServer.Tests
|
||||
([(0, 0m), (500, 50m), (1000, 100m)], 100, 10m),
|
||||
([(0, 0m), (100, 100m)], 80, 80m),
|
||||
([(0, 0m), (100, 100m)], 25, 25m),
|
||||
([(0, 0m), (25, 25m), (50, 50m), (100, 100m), (110, 120m)], 75, 75m),
|
||||
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 75, 75m),
|
||||
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 50, 50m),
|
||||
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 100, 100m),
|
||||
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 102, 80m),
|
||||
};
|
||||
foreach (var t in testData)
|
||||
{
|
||||
@ -850,8 +856,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// xpub
|
||||
var xpub = "xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw";
|
||||
Assert.Throws<FormatException>(() => parser.Parse(xpub, false, false, true));
|
||||
DerivationStrategyBase strategyBase = parser.Parse(xpub, false, false, false);
|
||||
DerivationStrategyBase strategyBase = parser.Parse(xpub);
|
||||
Assert.IsType<DirectDerivationStrategy>(strategyBase);
|
||||
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS", strategyBase.ToString());
|
||||
@ -931,9 +936,10 @@ namespace BTCPayServer.Tests
|
||||
// xpub
|
||||
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS";
|
||||
Assert.True(parsers.TryParseWalletFile(tpub, testnet, out var settings, out var error));
|
||||
Assert.Null(error);
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
|
||||
Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
|
||||
Assert.Equal("GenericFile", settings.Source);
|
||||
Assert.Null(error);
|
||||
|
||||
// xpub with fingerprint and account
|
||||
tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca";
|
||||
@ -948,6 +954,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(tpub, ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
|
||||
Assert.Equal(HDFingerprint.TryParse(fingerprint, out var hd) ? hd : default, settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.Equal("GenericFile", settings.Source);
|
||||
Assert.Null(error);
|
||||
|
||||
// ColdCard
|
||||
Assert.True(parsers.TryParseWalletFile(
|
||||
@ -964,12 +972,15 @@ namespace BTCPayServer.Tests
|
||||
settings.AccountOriginal);
|
||||
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey,
|
||||
settings.AccountDerivation.GetDerivation().ScriptPubKey);
|
||||
Assert.Equal("ElectrumFile", settings.Source);
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be legacy
|
||||
Assert.True(parsers.TryParseWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
|
||||
Assert.Equal("ElectrumFile", settings.Source);
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit p2sh
|
||||
@ -977,6 +988,7 @@ namespace BTCPayServer.Tests
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } });
|
||||
Assert.Equal("ElectrumFile", settings.Source);
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit
|
||||
@ -984,6 +996,7 @@ namespace BTCPayServer.Tests
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
Assert.Equal("ElectrumFile", settings.Source);
|
||||
Assert.Null(error);
|
||||
|
||||
// Specter
|
||||
@ -993,11 +1006,21 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(specter.AccountKeySettings[0].RootFingerprint, hd);
|
||||
Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.True(specter.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
Assert.Equal("Specter", specter.Label);
|
||||
Assert.Null(error);
|
||||
|
||||
//BSMS BIP129, Nunchuk
|
||||
|
||||
|
||||
// Wasabi
|
||||
var wasabiJson = @"{""EncryptedSecret"": ""6PYNUAZZLS1ShkhHhm9ayiNwXPAPLN669fN5mY2WbGm1Hqc88tomqWXabU"",""ChainCode"": ""UoHIB+2mDbZSowo11TfDQbsYK6q1DrZ2H2yqQBxu6m8="",""MasterFingerprint"": ""0f215605"",""ExtPubKey"": ""xpub6DUXFa6fMrFpg7x4nEd8jBU6xDN3vkSXsVUrSbUB2dadbYaPE31czwVdv146JRStGsc2U6TywdKnGoVcP8Rtp2AZQyzXxQb7HrgmR9LrqLA"",""TaprootExtPubKey"": ""xpub6D2thLU5KwUk3axkJu1UT3yKFshCGU7TMuxhPgZMd91VvrcDwHdRwdzLk61cSHtZC6BkaipPgfFwjoDBY4m1WxyznxZLukYgM4dC6iRJVf8"",""SkipSynchronization"": true,""UseTurboSync"": true,""MinGapLimit"": 21,""AccountKeyPath"": ""84'/0'/0'"",""TaprootAccountKeyPath"": ""86'/0'/0'"",""BlockchainState"": {""Network"": ""Main"",""Height"": ""503723"",""TurboSyncHeight"": ""503723""},""PreferPsbtWorkflow"": false,""AutoCoinJoin"": true,""PlebStopThreshold"": ""0.01"",""AnonScoreTarget"": 5,""FeeRateMedianTimeFrameHours"": 0,""IsCoinjoinProfileSelected"": true,""RedCoinIsolation"": false,""ExcludedCoinsFromCoinJoin"": [],""HdPubKeys"": [{""PubKey"": ""03f88b9c3e16e40a5a9eaf8b36b9bcee7bbc93fd9eea640b541efb931ac55f7ff5"",""FullKeyPath"": ""84'/0'/0'/1/0"",""Label"": """",""KeyState"": 0},{""PubKey"": ""03e5241fc28aa556d7cb826b9a9f5ecee85287e7476746126263574a5e27fbf569"",""FullKeyPath"": ""84'/0'/0'/0/0"",""Label"": """",""KeyState"": 0}]}";
|
||||
Assert.True(parsers.TryParseWalletFile(wasabiJson, mainnet, out var wasabi, out error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal("WasabiFile", wasabi.Source);
|
||||
Assert.Single(wasabi.AccountKeySettings);
|
||||
Assert.Equal("84'/0'/0'", wasabi.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.Equal("0f215605", wasabi.AccountKeySettings[0].RootFingerprint.ToString());
|
||||
Assert.True(wasabi.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
|
||||
// BSMS BIP129, Nunchuk
|
||||
var bsms = @"BSMS 1.0
|
||||
wsh(sortedmulti(1,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/**,[2b0e251e/48'/0'/0'/2']xpub6DrimHB8KUSkPvmJ8Pk8RE769EdDm2VEoZ8MBz76w9QupP8Py4wexs4Pa3aRB1LUEhc9GyY6ypDWEFFRCgqeDQePcyWQfjtmintrehq3JCL/**))
|
||||
/0/*,/1/*
|
||||
@ -1046,6 +1069,72 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
Assert.True(passport.AccountDerivation is TaprootDerivationStrategy);
|
||||
Assert.Equal("5c9e228d", passport.AccountKeySettings[0].RootFingerprint.ToString());
|
||||
Assert.Equal("86'/0'/0'", passport.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
|
||||
//electrum
|
||||
var electrumText =
|
||||
"""
|
||||
{
|
||||
"keystore": {
|
||||
"xpub": "vpub5Z14bnDNoEQeFdwZYSpVHcpzRpH99CnvSemzqTAvhjcgBTzPUVnaA5GhjgZc9J46duUprxQRUVUuqchazanXD6bLuVyarviNHBFUu6fBZNj",
|
||||
"xprv": "vprv9ENJcv8RKwqMTqyhLSuBz5bEV7hpdZjisjUBuV9K8azz1vpop6xJFEDRdfDwgWBpYgUUhEVxdvpxgV3f8NircysfebnBaPu5y2dcnSDAEEw",
|
||||
"type": "bip32",
|
||||
"pw_hash_version": 1
|
||||
},
|
||||
"wallet_type": "standard",
|
||||
"use_encryption": false,
|
||||
"seed_type": "bip39"
|
||||
}
|
||||
""";
|
||||
Assert.True(parsers.TryParseWalletFile(electrumText, testnet, out var electrum, out _));
|
||||
Assert.Equal("ElectrumFile", electrum.Source);
|
||||
|
||||
electrumText =
|
||||
"""
|
||||
{
|
||||
"keystore": {
|
||||
"derivation": "m/0h",
|
||||
"pw_hash_version": 1,
|
||||
"root_fingerprint": "fbb5b37d",
|
||||
"seed": "tiger room acoustic bracket thing film umbrella rather pepper tired vault remain",
|
||||
"seed_type": "segwit",
|
||||
"type": "bip32",
|
||||
"xprv": "zprvAaQyp6mTAX53zY4j2BbecRNtmTq2kSEKgy2y4yK3bFPKgPJLxrMmPxzZdRkWq5XvmtH2R4ko5YmJYH2MgnVkWr32pHi4Dc5627WyML32KTW",
|
||||
"xpub": "zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br"
|
||||
},
|
||||
"wallet_type": "standard",
|
||||
"use_encryption": false,
|
||||
"seed_type": "bip39"
|
||||
}
|
||||
""";
|
||||
Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _));
|
||||
Assert.Equal("ElectrumFile", electrum.Source);
|
||||
Assert.Equal("0'", electrum.GetSigningAccountKeySettings().AccountKeyPath.ToString());
|
||||
Assert.True(electrum.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
Assert.Equal("fbb5b37d", electrum.GetSigningAccountKeySettings().RootFingerprint.ToString());
|
||||
Assert.Equal("zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br", electrum.AccountOriginal);
|
||||
Assert.Equal(((DirectDerivationStrategy)electrum.AccountDerivation).GetExtPubKeys().First().ParentFingerprint.ToString(), electrum.GetSigningAccountKeySettings().RootFingerprint.ToString());
|
||||
|
||||
// Electrum with strange garbage at the end caused by the lightning support
|
||||
electrumText =
|
||||
"""
|
||||
{
|
||||
"keystore": {
|
||||
"derivation": "m/0h",
|
||||
"pw_hash_version": 1,
|
||||
"root_fingerprint": "fbb5b37d",
|
||||
"seed": "tiger room acoustic bracket thing film umbrella rather pepper tired vault remain",
|
||||
"seed_type": "segwit",
|
||||
"type": "bip32",
|
||||
"xprv": "zprvAaQyp6mTAX53zY4j2BbecRNtmTq2kSEKgy2y4yK3bFPKgPJLxrMmPxzZdRkWq5XvmtH2R4ko5YmJYH2MgnVkWr32pHi4Dc5627WyML32KTW",
|
||||
"xpub": "zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br"
|
||||
},
|
||||
"wallet_type": "standard",
|
||||
"use_encryption": false,
|
||||
"seed_type": "bip39"
|
||||
},
|
||||
{"op": "remove", "path": "/channels"}
|
||||
""";
|
||||
Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1926,7 +2015,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
// 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", false, false, false);
|
||||
"zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
|
||||
Assert.Equal(
|
||||
"tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w",
|
||||
result.ToString());
|
||||
@ -1950,7 +2039,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
|
||||
// if prefix not recognize, assume it is segwit
|
||||
result = testnetParser.Parse(
|
||||
"xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X", false, false, false);
|
||||
"xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
|
||||
Assert.Equal(
|
||||
"tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu",
|
||||
result.ToString());
|
||||
@ -1959,13 +2048,13 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
var tpub =
|
||||
"tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
|
||||
|
||||
result = testnetParser.Parse(tpub, false, true);
|
||||
result = testnetParser.Parse(tpub);
|
||||
Assert.Equal(tpub, result.ToString());
|
||||
|
||||
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
|
||||
var parsed =
|
||||
regtestParser.Parse(
|
||||
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false);
|
||||
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
|
||||
Assert.Equal(
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]",
|
||||
parsed.ToString());
|
||||
@ -1973,14 +2062,14 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
// Let's make sure we can't generate segwit with dogecoin
|
||||
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE"));
|
||||
parsed = regtestParser.Parse(
|
||||
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]", false, false, false);
|
||||
"xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
|
||||
Assert.Equal(
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
|
||||
parsed.ToString());
|
||||
|
||||
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("DOGE"));
|
||||
parsed = regtestParser.Parse(
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", false, false, false);
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
|
||||
Assert.Equal(
|
||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
|
||||
parsed.ToString());
|
||||
@ -2204,7 +2293,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"
|
||||
};
|
||||
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", CreateNetworkProvider(ChainName.Regtest).BTC);
|
||||
|
||||
Assert.True(scheme.AccountDerivation is DirectDerivationStrategy { Segwit: true });
|
||||
scheme.Source = "ManualDerivationScheme";
|
||||
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf";
|
||||
var legacy2 = new JObject()
|
||||
|
@ -2332,10 +2332,27 @@ namespace BTCPayServer.Tests
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
// Create users
|
||||
var user = s.RegisterNewUser();
|
||||
var userAccount = s.AsTestAccount();
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
// Setup store and associate user
|
||||
s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.GoToStore(StoreNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("Email")).Clear();
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(user);
|
||||
new SelectElement(s.Driver.FindElement(By.Id("Role"))).SelectByValue("Guest");
|
||||
s.Driver.FindElement(By.Id("AddUser")).Click();
|
||||
Assert.Contains("User added successfully", s.FindAlertMessage().Text);
|
||||
|
||||
// Setup POS
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
@ -2360,6 +2377,8 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.WaitForElement(By.ClassName("keypad"));
|
||||
|
||||
// basic checks
|
||||
var keypadUrl = s.Driver.Url;
|
||||
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
|
||||
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
|
||||
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
@ -2405,6 +2424,19 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
|
||||
// Guest user can access recent transactions
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
s.LogIn(user, userAccount.RegisterDetails.Password);
|
||||
s.GoToUrl(keypadUrl);
|
||||
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
|
||||
// Unauthenticated user can't access recent transactions
|
||||
s.GoToUrl(keypadUrl);
|
||||
s.Driver.ElementDoesNotExist(By.Id("RecentTransactionsToggle"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -60,7 +60,7 @@ else
|
||||
{
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
}
|
||||
<li ><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
|
||||
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
|
||||
@if (Model.ArchivedCount > 0)
|
||||
{
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
@ -123,7 +123,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
try
|
||||
{
|
||||
|
||||
var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme, false, true);
|
||||
var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme);
|
||||
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
||||
|
||||
var line = strategy.GetLineFor(deposit);
|
||||
@ -173,7 +173,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
DerivationStrategyBase strategy;
|
||||
try
|
||||
{
|
||||
strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme, false, true);
|
||||
strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -246,7 +246,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
var store = Store;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme, false, true);
|
||||
var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme);
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy);
|
||||
|
||||
|
@ -90,9 +90,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (vm.WalletFile != null)
|
||||
{
|
||||
if (!_onChainWalletParsers.TryParseWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
|
||||
string fileContent = null;
|
||||
try
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
|
||||
fileContent = await ReadAllText(vm.WalletFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
|
@ -919,7 +919,7 @@ namespace BTCPayServer.Controllers
|
||||
return derivationSchemeSettings;
|
||||
}
|
||||
|
||||
var strategy = parser.Parse(derivationScheme, false, true);
|
||||
var strategy = parser.Parse(derivationScheme);
|
||||
return new DerivationSchemeSettings(strategy, network);
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
|
||||
await _repo.CreateStore(GetUserId(), store);
|
||||
CreatedStoreId = store.Id;
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ namespace BTCPayServer
|
||||
{
|
||||
throw new FormatException("Custom change paths are not supported.");
|
||||
}
|
||||
return (Parse($"{hd.Extkey}{suffix}", true, false, false), null);
|
||||
return (Parse($"{hd.Extkey}{suffix}"), null);
|
||||
case PubKeyProvider.Origin origin:
|
||||
var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
|
||||
return (innerResult.Item1, new[] { origin.KeyOriginInfo });
|
||||
@ -48,14 +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]")}", true ,false, false),
|
||||
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}"),
|
||||
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)
|
||||
{
|
||||
@ -81,7 +81,7 @@ namespace BTCPayServer
|
||||
sh.Inner is OutputDescriptor.WSH)
|
||||
{
|
||||
var ds = ParseOutputDescriptor(sh.Inner.ToString());
|
||||
return (Parse(ds.Item1 + suffix, true, false, false), ds.Item2);
|
||||
return (Parse(ds.Item1 + suffix), ds.Item2);
|
||||
};
|
||||
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
|
||||
case OutputDescriptor.Tr tr:
|
||||
@ -96,19 +96,25 @@ namespace BTCPayServer
|
||||
throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
|
||||
}
|
||||
}
|
||||
public DerivationStrategyBase Parse(string str, bool ignorePrefix = false, bool ignoreBasePrefix = false, bool enforceNetworkPrefix = true)
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(str);
|
||||
str = str.Trim();
|
||||
|
||||
HashSet<string> hintedLabels = new HashSet<string>();
|
||||
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
{
|
||||
hintedLabels.Add("legacy");
|
||||
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++)
|
||||
@ -125,29 +131,23 @@ namespace BTCPayServer
|
||||
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
|
||||
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
var derivationScheme = GetBitcoinExtPubKeyByNetwork(Network, data).ToString();
|
||||
|
||||
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))
|
||||
if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case DerivationType.Legacy when !ignoreBasePrefix:
|
||||
case DerivationType.Legacy:
|
||||
hintedLabels.Add("legacy");
|
||||
break;
|
||||
case DerivationType.SegwitP2SH:
|
||||
@ -155,13 +155,8 @@ namespace BTCPayServer
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parts[i] = derivationScheme;
|
||||
}
|
||||
catch (FormatException e) when (e.Message.StartsWith("Invalid xpub"))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch { continue; }
|
||||
}
|
||||
|
||||
@ -170,10 +165,35 @@ namespace BTCPayServer
|
||||
{
|
||||
str = $"{str}-[{label}]";
|
||||
}
|
||||
|
||||
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str);
|
||||
}
|
||||
|
||||
internal 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 static BitcoinExtPubKey GetBitcoinExtPubKeyByNetwork(Network network, byte[] data)
|
||||
{
|
||||
try
|
||||
|
@ -1,16 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.Client;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -18,18 +13,17 @@ namespace BTCPayServer
|
||||
{
|
||||
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
string error = null;
|
||||
ArgumentNullException.ThrowIfNull(network);
|
||||
ArgumentNullException.ThrowIfNull(derivationStrategy);
|
||||
var result = new DerivationSchemeSettings();
|
||||
result.Network = network;
|
||||
var result = new DerivationSchemeSettings { Network = network };
|
||||
var parser = network.GetDerivationSchemeParser();
|
||||
if (parser.TryParseXpub(derivationStrategy, ref result, out error))
|
||||
if (parser.TryParseXpub(derivationStrategy, ref result) ||
|
||||
parser.TryParseXpub(derivationStrategy, ref result, electrum: true))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid Derivation Scheme: {error}");
|
||||
throw new FormatException($"Invalid Derivation Scheme");
|
||||
}
|
||||
|
||||
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
||||
|
@ -32,9 +32,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
using NBitcoin.Scripting;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
@ -50,8 +48,32 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
public static bool TryParseXpub(this DerivationSchemeParser derivationSchemeParser, string xpub,
|
||||
ref DerivationSchemeSettings derivationSchemeSettings, out string error)
|
||||
ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = false)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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.
|
||||
@ -69,37 +91,32 @@ namespace BTCPayServer
|
||||
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.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();
|
||||
.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
|
||||
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)
|
||||
catch (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);
|
||||
|
@ -51,17 +51,31 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
// find all wallet objects that fit this transaction
|
||||
// that means see if there are any utxo objects that match in/outs and scripts/addresses that match outs
|
||||
var matchedObjects = transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs
|
||||
.Select<TxIn, ObjectTypeId>(txIn => new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString()))
|
||||
.Concat(transactionEvent.NewTransactionEvent.Outputs.SelectMany<NBXplorer.Models.MatchedOutput, ObjectTypeId>(txOut =>
|
||||
|
||||
new[]{
|
||||
new ObjectTypeId(WalletObjectData.Types.Address, GetAddress(derivation, txOut, network).ToString()),
|
||||
new ObjectTypeId(WalletObjectData.Types.Utxo, new OutPoint(transactionEvent.NewTransactionEvent.TransactionData.TransactionHash, (uint)txOut.Index).ToString())
|
||||
var matchedObjects = new List<ObjectTypeId>();
|
||||
// Check if inputs match some UTXOs
|
||||
foreach (var txIn in transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs)
|
||||
{
|
||||
matchedObjects.Add(new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString()));
|
||||
}
|
||||
|
||||
})).Distinct().ToArray();
|
||||
// Check if outputs match some UTXOs
|
||||
var walletOutputsByIndex = transactionEvent.NewTransactionEvent.Outputs.ToDictionary(o => (uint)o.Index);
|
||||
foreach (var txOut in transactionEvent.NewTransactionEvent.TransactionData.Transaction.Outputs.AsIndexedOutputs())
|
||||
{
|
||||
BitcoinAddress? address = null;
|
||||
// Technically, walletTxOut.Address can be calculated.
|
||||
// However in liquid for example, this returns the blinded address
|
||||
// rather than the unblinded one.
|
||||
if (walletOutputsByIndex.TryGetValue(txOut.N, out var walletTxOut))
|
||||
address = walletTxOut.Address;
|
||||
address ??= txOut.TxOut.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
|
||||
if (address is not null)
|
||||
matchedObjects.Add(new ObjectTypeId(WalletObjectData.Types.Address, address.ToString()));
|
||||
matchedObjects.Add(new ObjectTypeId(WalletObjectData.Types.Utxo, new OutPoint(transactionEvent.NewTransactionEvent.TransactionData.TransactionHash, txOut.N).ToString()));
|
||||
}
|
||||
|
||||
var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery() { TypesIds = matchedObjects });
|
||||
var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery() { TypesIds = matchedObjects.Distinct().ToArray() });
|
||||
var links = new List<WalletObjectLinkData>();
|
||||
foreach (var walletObjectDatas in objs.GroupBy(data => data.Key.WalletId))
|
||||
{
|
||||
@ -112,11 +126,5 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BitcoinAddress GetAddress(DerivationStrategyBase derivationStrategy, NBXplorer.Models.MatchedOutput txOut, BTCPayNetwork network)
|
||||
{
|
||||
// Old version of NBX doesn't give address in the event, so we need to guess
|
||||
return (txOut.Address ?? network.NBXplorerNetwork.CreateAddress(derivationStrategy, txOut.KeyPath, txOut.ScriptPubKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
@ -74,10 +73,10 @@ namespace BTCPayServer.Security
|
||||
// resolve from app
|
||||
if (routeData.Values.TryGetValue("appId", out var vAppId) && vAppId is string appId)
|
||||
{
|
||||
app = await _appService.GetAppDataIfOwner(userId, appId);
|
||||
app = await _appService.GetAppData(userId, appId);
|
||||
if (storeId == null)
|
||||
{
|
||||
storeId = app?.StoreDataId ?? String.Empty;
|
||||
storeId = app?.StoreDataId ?? string.Empty;
|
||||
}
|
||||
else if (app?.StoreDataId != storeId)
|
||||
{
|
||||
@ -90,7 +89,7 @@ namespace BTCPayServer.Security
|
||||
paymentRequest = await _paymentRequestRepository.FindPaymentRequest(payReqId, userId);
|
||||
if (storeId == null)
|
||||
{
|
||||
storeId = paymentRequest?.StoreDataId ?? String.Empty;
|
||||
storeId = paymentRequest?.StoreDataId ?? string.Empty;
|
||||
}
|
||||
else if (paymentRequest?.StoreDataId != storeId)
|
||||
{
|
||||
@ -103,7 +102,7 @@ namespace BTCPayServer.Security
|
||||
invoice = await _invoiceRepository.GetInvoice(invoiceId);
|
||||
if (storeId == null)
|
||||
{
|
||||
storeId = invoice?.StoreId ?? String.Empty;
|
||||
storeId = invoice?.StoreId ?? string.Empty;
|
||||
}
|
||||
else if (invoice?.StoreId != storeId)
|
||||
{
|
||||
|
@ -371,10 +371,26 @@ namespace BTCPayServer.Services.Apps
|
||||
return null;
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var app = await ctx.UserStore
|
||||
.Include(store => store.StoreRole)
|
||||
.Where(us => us.ApplicationUserId == userId && us.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings))
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
.Include(store => store.StoreRole)
|
||||
.Where(us => us.ApplicationUserId == userId && us.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings))
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
}
|
||||
|
||||
public async Task<AppData?> GetAppData(string userId, string appId, string? type = null)
|
||||
{
|
||||
if (userId == null || appId == null)
|
||||
return null;
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var app = await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.StoreData != null && us.StoreData.UserStores.Any(u => u.ApplicationUserId == userId))
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type != app.AppType)
|
||||
|
@ -19,7 +19,7 @@ public class WalletFileParsers
|
||||
public bool TryParseWalletFile(string fileContents, BTCPayNetwork network, [MaybeNullWhen(false)] out DerivationSchemeSettings settings, [MaybeNullWhen(true)] out string error)
|
||||
{
|
||||
settings = null;
|
||||
error = string.Empty;
|
||||
error = null;
|
||||
ArgumentNullException.ThrowIfNull(fileContents);
|
||||
ArgumentNullException.ThrowIfNull(network);
|
||||
if (HexEncoder.IsWellFormed(fileContents))
|
||||
@ -29,19 +29,16 @@ public class WalletFileParsers
|
||||
|
||||
foreach (IWalletFileParser onChainWalletParser in Parsers)
|
||||
{
|
||||
var result = onChainWalletParser.TryParse(network, fileContents);
|
||||
if (result.DerivationSchemeSettings is not null)
|
||||
try
|
||||
{
|
||||
settings = result.DerivationSchemeSettings;
|
||||
error = null;
|
||||
return true;
|
||||
if (onChainWalletParser.TryParse(network, fileContents, out settings))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result.Error is not null)
|
||||
catch (Exception)
|
||||
{
|
||||
error = result.Error;
|
||||
}
|
||||
}
|
||||
error = "Unsupported file format";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using BTCPayServer;
|
||||
using NBitcoin;
|
||||
@ -10,67 +11,51 @@ using BTCPayNetwork = BTCPayServer.BTCPayNetwork;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public class BSMSWalletFileParser : IWalletFileParser
|
||||
{
|
||||
public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(
|
||||
BTCPayNetwork network,
|
||||
string data)
|
||||
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
try
|
||||
derivationSchemeSettings = null;
|
||||
string[] lines = data.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
);
|
||||
|
||||
if (lines.Length < 4 || !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/*")
|
||||
return false;
|
||||
|
||||
|
||||
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 false;
|
||||
|
||||
derivationSchemeSettings = new BTCPayServer.DerivationSchemeSettings()
|
||||
{
|
||||
string[] lines = data.Split(
|
||||
new[] {"\r\n", "\r", "\n"},
|
||||
StringSplitOptions.None
|
||||
);
|
||||
|
||||
if (!lines[0].Trim().Equals("BSMS 1.0"))
|
||||
Network = network,
|
||||
Source = "BSMS",
|
||||
AccountDerivation = result.Item1,
|
||||
AccountOriginal = descriptor.Trim(),
|
||||
AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
|
||||
{
|
||||
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}");
|
||||
}
|
||||
RootFingerprint = path?.MasterFingerprint,
|
||||
AccountKeyPath = path?.KeyPath,
|
||||
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(network.NBitcoinNetwork)
|
||||
}).ToArray()
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,88 +1,68 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using BTCPayServer;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public class ElectrumWalletFileParser : IWalletFileParser
|
||||
{
|
||||
public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
|
||||
string data)
|
||||
class ElectrumFormat
|
||||
{
|
||||
try
|
||||
internal class KeyStoreFormat
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public string? xpub { get; set; }
|
||||
public string? label { get; set; }
|
||||
public string? root_fingerprint { get; set; }
|
||||
public uint? ckcc_xfp { get; set; }
|
||||
public string? derivation { get; set; }
|
||||
public string? ColdCardFirmwareVersion { get; set; }
|
||||
public string? CoboVaultFirmwareVersion { get; set; }
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return (null, "invalid xpub");
|
||||
}
|
||||
return (null, null);
|
||||
public KeyStoreFormat? keystore { get; set; }
|
||||
}
|
||||
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
derivationSchemeSettings = null;
|
||||
var jobj = DeserializeObject<ElectrumFormat>(data);
|
||||
if (jobj?.keystore is null)
|
||||
return false;
|
||||
|
||||
var result = new DerivationSchemeSettings { Network = network };
|
||||
var derivationSchemeParser = network.GetDerivationSchemeParser();
|
||||
result.Source = "ElectrumFile";
|
||||
|
||||
if (jobj.keystore.xpub is null)
|
||||
return false;
|
||||
|
||||
if (!derivationSchemeParser.TryParseXpub(jobj.keystore.xpub, ref result, true))
|
||||
return false;
|
||||
|
||||
if (jobj.keystore.label is not null)
|
||||
result.Label = jobj.keystore.label;
|
||||
|
||||
if (jobj.keystore.ckcc_xfp is not null)
|
||||
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj.keystore.ckcc_xfp.Value);
|
||||
if (jobj.keystore.root_fingerprint is not null)
|
||||
result.AccountKeySettings[0].RootFingerprint = HDFingerprint.Parse(jobj.keystore.root_fingerprint);
|
||||
if (jobj.keystore.derivation is not null)
|
||||
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj.keystore.derivation);
|
||||
if (jobj.keystore.ColdCardFirmwareVersion is not null)
|
||||
result.Source = "ColdCard";
|
||||
else if (jobj.keystore.CoboVaultFirmwareVersion is not null)
|
||||
result.Source = "CoboVault";
|
||||
derivationSchemeSettings = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
private T? DeserializeObject<T>(string data)
|
||||
{
|
||||
// We can't call JsonConvert.DeserializeObject directly
|
||||
// because some export of Electrum file can have more than one
|
||||
// JSON object separated by commas in the file
|
||||
JsonTextReader reader = new JsonTextReader(new StringReader(data));
|
||||
var o = JObject.ReadFrom(reader);
|
||||
return JsonConvert.DeserializeObject<T>(o.ToString());
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using BTCPayServer;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public interface IWalletFileParser
|
||||
{
|
||||
(BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network, string data);
|
||||
bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings);
|
||||
}
|
||||
|
@ -1,21 +1,20 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using BTCPayServer;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public class NBXDerivGenericWalletFileParser : IWalletFileParser
|
||||
{
|
||||
public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
|
||||
string data)
|
||||
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
try
|
||||
var result = new DerivationSchemeSettings { Network = network };
|
||||
var parser = network.GetDerivationSchemeParser();
|
||||
if (parser.TryParseXpub(data, ref result, electrum: true) ||
|
||||
parser.TryParseXpub(data, ref result))
|
||||
{
|
||||
var result = BTCPayServer.DerivationSchemeSettings.Parse(data, network);
|
||||
result.Source = "Generic";
|
||||
return (result, null);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return (null, null);
|
||||
derivationSchemeSettings = result;
|
||||
derivationSchemeSettings.Source = "GenericFile";
|
||||
return true;
|
||||
}
|
||||
derivationSchemeSettings = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,34 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using BTCPayServer;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public class OutputDescriptorJsonWalletFileParser : IWalletFileParser
|
||||
{
|
||||
private readonly OutputDescriptorWalletFileParser _outputDescriptorOnChainWalletParser;
|
||||
|
||||
class OutputDescriptorJsonWalletFileFormat
|
||||
{
|
||||
public string? Descriptor { get; set; }
|
||||
public string? Source { get; set; }
|
||||
}
|
||||
public OutputDescriptorJsonWalletFileParser(OutputDescriptorWalletFileParser outputDescriptorOnChainWalletParser)
|
||||
{
|
||||
_outputDescriptorOnChainWalletParser = outputDescriptorOnChainWalletParser;
|
||||
}
|
||||
public (DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
|
||||
string data)
|
||||
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
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);
|
||||
derivationSchemeSettings = null;
|
||||
var jobj = JsonConvert.DeserializeObject<OutputDescriptorJsonWalletFileFormat>(data);
|
||||
if (jobj?.Descriptor is null)
|
||||
return false;
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
if (!_outputDescriptorOnChainWalletParser.TryParse(network, jobj.Descriptor, out derivationSchemeSettings))
|
||||
return false;
|
||||
if (jobj.Source is not null)
|
||||
derivationSchemeSettings.Source = jobj.Source;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +1,33 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using BTCPayServer;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public class OutputDescriptorWalletFileParser : IWalletFileParser
|
||||
{
|
||||
public (BTCPayServer.DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
|
||||
string data)
|
||||
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
try
|
||||
derivationSchemeSettings = null;
|
||||
var maybeOutputDesc = !data.Trim().StartsWith("{", StringComparison.OrdinalIgnoreCase);
|
||||
if (!maybeOutputDesc)
|
||||
return false;
|
||||
var derivationSchemeParser = network.GetDerivationSchemeParser();
|
||||
var descriptor = derivationSchemeParser.ParseOutputDescriptor(data);
|
||||
derivationSchemeSettings = new DerivationSchemeSettings()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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);
|
||||
}
|
||||
RootFingerprint = path?.MasterFingerprint,
|
||||
AccountKeyPath = path?.KeyPath,
|
||||
AccountKey =
|
||||
descriptor.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
|
||||
}).ToArray()
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,37 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using BTCPayServer;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public class SpecterWalletFileParser : IWalletFileParser
|
||||
{
|
||||
private readonly OutputDescriptorWalletFileParser _outputDescriptorOnChainWalletParser;
|
||||
|
||||
class SpecterFormat
|
||||
{
|
||||
public string? descriptor { get; set; }
|
||||
public int? blockheight { get; set; }
|
||||
public string? label { get; set; }
|
||||
}
|
||||
public SpecterWalletFileParser(OutputDescriptorWalletFileParser outputDescriptorOnChainWalletParser)
|
||||
{
|
||||
_outputDescriptorOnChainWalletParser = outputDescriptorOnChainWalletParser;
|
||||
}
|
||||
public (DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
|
||||
string data)
|
||||
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
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);
|
||||
derivationSchemeSettings = null;
|
||||
var jobj = JsonConvert.DeserializeObject<SpecterFormat>(data);
|
||||
if (jobj?.descriptor is null || jobj.blockheight is null)
|
||||
return false;
|
||||
if (!_outputDescriptorOnChainWalletParser.TryParse(network, jobj.descriptor, out derivationSchemeSettings))
|
||||
return false;
|
||||
|
||||
|
||||
var result = _outputDescriptorOnChainWalletParser.TryParse(network, desc);
|
||||
if (result.DerivationSchemeSettings is not null)
|
||||
result.DerivationSchemeSettings.Source = "Specter";
|
||||
derivationSchemeSettings.Source = "Specter";
|
||||
if (jobj.label is not null)
|
||||
derivationSchemeSettings.Label = jobj.label;
|
||||
|
||||
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);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,105 +1,70 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using BTCPayServer;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
namespace BTCPayServer.Services.WalletFileParsing;
|
||||
public class WasabiWalletFileParser : IWalletFileParser
|
||||
{
|
||||
|
||||
public (DerivationSchemeSettings? DerivationSchemeSettings, string? Error) TryParse(BTCPayNetwork network,
|
||||
string data)
|
||||
class WasabiFormat
|
||||
{
|
||||
try
|
||||
public string? ExtPubKey { get; set; }
|
||||
public string? MasterFingerprint { get; set; }
|
||||
public string? AccountKeyPath { get; set; }
|
||||
public string? ColdCardFirmwareVersion { get; set; }
|
||||
public string? CoboVaultFirmwareVersion { get; set; }
|
||||
public string? DerivationPath { get; set; }
|
||||
public string? Source { get; set; }
|
||||
}
|
||||
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
|
||||
{
|
||||
derivationSchemeSettings = null;
|
||||
var jobj = JsonConvert.DeserializeObject<WasabiFormat>(data);
|
||||
var derivationSchemeParser = network.GetDerivationSchemeParser();
|
||||
var result = new DerivationSchemeSettings()
|
||||
{
|
||||
var jobj = JObject.Parse(data);
|
||||
if (jobj["ExtPubKey"]?.Value<string>() is not string extPubKey)
|
||||
return (null, null);
|
||||
Network = network
|
||||
};
|
||||
|
||||
var derivationSchemeParser = network.GetDerivationSchemeParser();
|
||||
var result = new DerivationSchemeSettings()
|
||||
{
|
||||
Network = network
|
||||
};
|
||||
if (jobj is null || !derivationSchemeParser.TryParseXpub(jobj.ExtPubKey, ref result))
|
||||
return false;
|
||||
|
||||
if (!derivationSchemeParser.TryParseXpub(extPubKey, ref result, out var error))
|
||||
if (jobj.MasterFingerprint is not null)
|
||||
{
|
||||
// https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
|
||||
if (uint.TryParse(jobj.MasterFingerprint, out var fingerprint))
|
||||
{
|
||||
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>();
|
||||
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint);
|
||||
}
|
||||
else
|
||||
result.Source = "WasabiFile";
|
||||
|
||||
|
||||
return (result, null);
|
||||
{
|
||||
var bytes = Encoders.Hex.DecodeData(jobj.MasterFingerprint);
|
||||
var shouldReverseMfp = jobj.ColdCardFirmwareVersion == "2.1.0";
|
||||
if (shouldReverseMfp) // Bug in previous version of coldcard
|
||||
bytes = bytes.Reverse().ToArray();
|
||||
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(bytes);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
if (jobj.AccountKeyPath is not null)
|
||||
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj.AccountKeyPath);
|
||||
|
||||
if (jobj.ColdCardFirmwareVersion is not null)
|
||||
{
|
||||
return (null, null);
|
||||
result.Source = "ColdCard";
|
||||
}
|
||||
else if (jobj.CoboVaultFirmwareVersion is not null)
|
||||
{
|
||||
result.Source = "CoboVault";
|
||||
}
|
||||
else
|
||||
result.Source = jobj.Source ?? "WasabiFile";
|
||||
|
||||
derivationSchemeSettings = result;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -393,8 +393,7 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
await conn.ExecuteAsync("INSERT INTO \"WalletObjectLinks\" VALUES (@WalletId, @AType, @AId, @BType, @BId, @Data::JSONB) ON CONFLICT DO NOTHING", links);
|
||||
await connection.ExecuteAsync("INSERT INTO \"WalletObjectLinks\" VALUES (@WalletId, @AType, @AId, @BType, @BId, @Data::JSONB) ON CONFLICT DO NOTHING", links);
|
||||
}
|
||||
}
|
||||
|
||||
@ -700,11 +699,9 @@ namespace BTCPayServer.Services
|
||||
walletObjectLinks ??= new List<WalletObjectLinkData>();
|
||||
var objs = walletObjects.Concat(ExtractObjectsFromLinks(walletObjectLinks).Except(walletObjects)).ToArray();
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
await using var connection = ctx.Database.GetDbConnection();
|
||||
await connection.OpenAsync();
|
||||
var connection = ctx.Database.GetDbConnection();
|
||||
await EnsureWalletObjects(ctx,connection, objs);
|
||||
await EnsureWalletObjectLinks(ctx,connection, walletObjectLinks);
|
||||
await connection.CloseAsync();
|
||||
}
|
||||
#nullable restore
|
||||
}
|
||||
|
@ -19,8 +19,10 @@
|
||||
{
|
||||
<div asp-validation-summary="All"></div>
|
||||
}
|
||||
|
||||
<partial name="LocalhostBrowserSupport" />
|
||||
@if (this.Model.Method is WalletSetupMethod.Hardware)
|
||||
{
|
||||
<partial name="LocalhostBrowserSupport" />
|
||||
}
|
||||
|
||||
<template id="modal-template">
|
||||
<div class="modal-dialog" role="document">
|
||||
|
@ -45,6 +45,10 @@
|
||||
<td>Electrum</td>
|
||||
<td>File ❯ Save backup (not encrypted with a password)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sparrow</td>
|
||||
<td>File ❯ Export wallet… ❯ Electrum: export file…</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wasabi</td>
|
||||
<td>Tools ❯ Wallet Manager ❯ Open Wallets Folder</td>
|
||||
|
@ -33,7 +33,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<button type="submit" role="button" class="btn btn-primary">Add User</button>
|
||||
<button type="submit" role="button" class="btn btn-primary" id="AddUser">Add User</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -7,21 +7,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
#wizard-navbar {
|
||||
margin-top: -35px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
#wizard-navbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
margin-top: -2rem;
|
||||
}
|
||||
|
||||
#wizard-navbar a,
|
||||
|
@ -13,6 +13,10 @@
|
||||
* Fix: Checkout v1 was not applying the custom style (#5628 #5615 #5616) @dennisreimann
|
||||
* Fix: Test email with multiple recipients was crashing (#5649 #5648) @dennisreimann
|
||||
* Fix: Test webhook for payment requests (#5680) @Kukks
|
||||
* Fix: Sometimes importing a wallet file from Electrum would fail @NicolasDorier
|
||||
* Fix: Creating a Store as a Guest generates a 403 error (#5688 #5689) @dennisreimann
|
||||
* Fix: In Wallet Send, label were not applied to transactions (#5700) @NicolasDorier
|
||||
* Fix: "View recent invoices" in Keypad PoS should be accessible for authenticated Guest users (#5702 #5698) @dennisreimann
|
||||
|
||||
### Improvements
|
||||
|
||||
|
@ -217,7 +217,6 @@ The BTCPay Server Project is proudly supported by these entities through the [BT
|
||||
[](https://www.bailliegifford.com)
|
||||
[](https://strike.me)
|
||||
[](https://hrf.org)
|
||||
[](https://escapetoelsalvador.org/)
|
||||
[](https://lunanode.com)
|
||||
[](https://walletofsatoshi.com/)
|
||||
[](https://coincards.com/)
|
||||
|
Reference in New Issue
Block a user