Compare commits

...

15 Commits

Author SHA1 Message Date
2111b67e2c Update changelog 2024-01-25 21:03:27 +09:00
b96cfcd14d Apps: Allow authenticated, non-owner users permissioned access (#5702)
Fixes #5698. Before this, the app lookup was constrained by the user having at least `CanModifyStoreSettings` permissions. This changes it to require the user being associated with a store, leaving the fine-grained authorization checks up to the individual actions.
2024-01-25 21:00:33 +09:00
086f713752 Wizard UI: Constrain navigation width (#5697)
This way the back and close buttons stay within the regular container size on and don't stick to the left and right end on wide screens.

Closes #5693.
2024-01-25 16:38:05 +09:00
fd67e09cf0 In Wallet Send, label were not applied to transactions (#5700) 2024-01-25 16:37:49 +09:00
6f4ca47532 Add documentation for wallet export on sparrow 2024-01-25 16:37:15 +09:00
f97f23c8a5 Do not dispose connections created by EF 2024-01-25 10:45:02 +09:00
b62985faf4 Update changelog 2024-01-24 22:55:23 +09:00
09c761aa31 Fix: Sometimes importing a wallet file from Electrum would fail 2024-01-24 22:53:40 +09:00
8089a938f3 Guest role: Fix redirect after store creation (#5689)
This ensures that guests land on the invoices list, which tehy are allowed to see — rather than the dashboard, which they don't have permissions for.

Fixes #5688.
2024-01-24 11:34:16 +01:00
35b3fef7c5 Fix wallet import (#5695)
* Fix wallet import

* Improve error message for import of wallet file
2024-01-24 17:49:15 +09:00
f31aa43c6a Wallet file parsing: Add Wasabi test case and re-add Electrum distinction (#5694)
* Extend tests, add Wasabi file

* Re-add Electrum distinction

* Specter: Fix indentation

* Cleanups
2024-01-24 09:28:22 +09:00
b03f8db06b Refactor wallet file parsing (Fix: #5690) (#5692) 2024-01-23 21:33:45 +09:00
27e70a169e Do not show warning about browser compatibility to vault on confirm address 2024-01-23 21:30:29 +09:00
6a1d17dda2 Remove ESC as a supporter (#5685)
Closes #5684.
2024-01-22 09:00:10 +09:00
31bc6dd48c More tests on interpolation 2024-01-20 12:21:58 +09:00
30 changed files with 516 additions and 429 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -217,7 +217,6 @@ The BTCPay Server Project is proudly supported by these entities through the [BT
[![Baillie Gifford](BTCPayServer/wwwroot/img/readme/supporter_bailliegifford.svg)](https://www.bailliegifford.com)
[![Strike](BTCPayServer/wwwroot/img/readme/supporter_strike.svg)](https://strike.me)
[![Human Rights Foundation](BTCPayServer/wwwroot/img/readme/supporter_hrf.svg)](https://hrf.org)
[![Escape to El Salvador](BTCPayServer/wwwroot/img/readme/supporter_esc.svg)](https://escapetoelsalvador.org/)
[![LunaNode](BTCPayServer/wwwroot/img/readme/supporter_lunanode.svg)](https://lunanode.com)
[![Wallet of Satoshi](BTCPayServer/wwwroot/img/readme/supporter_walletofsatoshi.svg)](https://walletofsatoshi.com/)
[![Coincards](BTCPayServer/wwwroot/img/readme/supporter_coincards.svg)](https://coincards.com/)