From 8e38da80e02d59f25dfe6ef46e2d1bba22be0fc4 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 24 Mar 2018 20:40:26 +0900 Subject: [PATCH] Better UX to set the xpub correctly --- BTCPayServer.Tests/TestAccount.cs | 1 - BTCPayServer.Tests/UnitTest1.cs | 38 ++++ .../Controllers/StoresController.BTCLike.cs | 72 +++++-- BTCPayServer/Controllers/StoresController.cs | 54 ++---- BTCPayServer/DerivationSchemeParser.cs | 180 ++++++++++++++++++ .../DerivationSchemeViewModel.cs | 24 +-- .../Views/Stores/AddDerivationScheme.cshtml | 24 ++- 7 files changed, 300 insertions(+), 93 deletions(-) create mode 100644 BTCPayServer/DerivationSchemeParser.cs diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 1e02933e5..61639ed30 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -82,7 +82,6 @@ namespace BTCPayServer.Tests await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel() { - DerivationSchemeFormat = "BTCPay", DerivationScheme = DerivationScheme.ToString(), Confirmation = true }, cryptoCode); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 1dbc136bd..25cde0032 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -806,6 +806,44 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanParseDerivationScheme() + { + var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test); + NBXplorer.DerivationStrategy.DerivationStrategyBase result; + // Passing electrum stuff + // Native + result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); + Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString()); + // P2SH + result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5"); + Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString()); + result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X"); + Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString()); + //////////////// + + result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"); + Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o", result.ToString()); + parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey; + result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"); + Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o", result.ToString()); + + parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey; + result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"); + Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[p2sh]", result.ToString()); + + parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey; + result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"); + Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[legacy]", result.ToString()); + + parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey; + result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[legacy]"); + Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[p2sh]", result.ToString()); + + result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"); + Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[p2sh]", result.ToString()); + } + [Fact] public void InvoiceFlowThroughDifferentStatesCorrectly() { diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 3dedfc40e..ba91ad99a 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -75,7 +75,7 @@ namespace BTCPayServer.Controllers { if (!string.IsNullOrEmpty(vm.DerivationScheme)) { - strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network); + strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network); vm.DerivationScheme = strategy.ToString(); } } @@ -86,8 +86,38 @@ namespace BTCPayServer.Controllers return View(vm); } + if (!vm.Confirmation && strategy != null) + return ShowAddresses(vm, strategy); - if (vm.Confirmation || strategy == null) + if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) + { + BitcoinAddress address = null; + try + { + address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork); + } + catch + { + ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address"); + return ShowAddresses(vm, strategy); + } + + try + { + strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network); + } + catch + { + ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address"); + return ShowAddresses(vm, strategy); + } + vm.HintAddress = ""; + vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\""; + ModelState.Remove(nameof(vm.HintAddress)); + ModelState.Remove(nameof(vm.DerivationScheme)); + return ShowAddresses(vm, strategy); + } + else { try { @@ -105,23 +135,24 @@ namespace BTCPayServer.Controllers StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified."; return RedirectToAction(nameof(UpdateStore), new { storeId = storeId }); } - else - { - if (!string.IsNullOrEmpty(vm.DerivationScheme)) - { - var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit); - - for (int i = 0; i < 10; i++) - { - var address = line.Derive((uint)i); - vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString())); - } - } - vm.Confirmation = true; - return View(vm); - } } + private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy) + { + vm.DerivationScheme = strategy.DerivationStrategyBase.ToString(); + if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit); + + for (int i = 0; i < 10; i++) + { + var address = line.Derive((uint)i); + vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString())); + } + } + vm.Confirmation = true; + return View(vm); + } public class GetInfoResult @@ -219,7 +250,8 @@ namespace BTCPayServer.Controllers } if (command == "getxpub") { - var getxpubResult = await hw.GetExtPubKey(network, account); ; + var getxpubResult = await hw.GetExtPubKey(network, account); + ; getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000); result = getxpubResult; } @@ -240,13 +272,13 @@ namespace BTCPayServer.Controllers if (command == "sendtoaddress") { - if(!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) + if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) throw new Exception($"{network.CryptoCode}: not started or fully synched"); var strategy = GetDirectDerivationStrategy(store, network); var strategyBase = GetDerivationStrategy(store, network); var wallet = _WalletProvider.GetWallet(network); var change = wallet.GetChangeAddressAsync(strategyBase); - + var unspentCoins = await wallet.GetUnspentCoins(strategyBase); var changeAddress = await change; var transaction = await hw.SendToAddress(strategy, unspentCoins, network, diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index b0d6abce0..31a6c99b9 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -103,7 +103,7 @@ namespace BTCPayServer.Controllers private string GetStoreUrl(string storeId) { return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/"; - } + } [HttpGet] [Route("{storeId}/users")] @@ -131,22 +131,22 @@ namespace BTCPayServer.Controllers public async Task StoreUsers(string storeId, StoreUsersViewModel vm) { await FillUsers(storeId, vm); - if(!ModelState.IsValid) + if (!ModelState.IsValid) { return View(vm); } var user = await _UserManager.FindByEmailAsync(vm.Email); - if(user == null) + if (user == null) { ModelState.AddModelError(nameof(vm.Email), "User not found"); return View(vm); } - if(!StoreRoles.AllRoles.Contains(vm.Role)) + if (!StoreRoles.AllRoles.Contains(vm.Role)) { ModelState.AddModelError(nameof(vm.Role), "Invalid role"); return View(vm); } - if(!await _Repo.AddStoreUser(storeId, user.Id, vm.Role)) + if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role)) { ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store"); return View(vm); @@ -209,11 +209,11 @@ namespace BTCPayServer.Controllers vm.AllowCoinConversion = storeBlob.AllowCoinConversion; return View(vm); } - + private void AddPaymentMethods(StoreData store, StoreViewModel vm) { - var derivationByCryptoCode = + var derivationByCryptoCode = store .GetSupportedPaymentMethods(_NetworkProvider) .OfType() @@ -327,41 +327,11 @@ namespace BTCPayServer.Controllers }); } - private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network) + private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) { - if (format == "Electrum") - { - //Unsupported Electrum - //var p2wsh_p2sh = 0x295b43fU; - //var p2wsh = 0x2aa7ed3U; - Dictionary electrumMapping = new Dictionary(); - //Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py - var standard = 0x0488b21eU; - electrumMapping.Add(standard, new[] { "legacy" }); - var p2wpkh_p2sh = 0x049d7cb2U; - electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" }); - var p2wpkh = 0x4b24746U; - electrumMapping.Add(p2wpkh, Array.Empty()); - - var data = Encoders.Base58Check.DecodeData(derivationScheme); - if (data.Length < 4) - throw new FormatException("data.Length < 4"); - var prefix = Utils.ToUInt32(data, false); - if (!electrumMapping.TryGetValue(prefix, out string[] labels)) - throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)"); - var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false); - - for (int i = 0; i < 4; i++) - data[i] = standardPrefix[i]; - - derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString(); - foreach (var label in labels) - { - derivationScheme = derivationScheme + $"-[{label}]"; - } - } - - return new DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network); + var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType); + parser.HintScriptPubKey = hint; + return new DerivationStrategy(parser.Parse(derivationScheme), network); } [HttpGet] @@ -519,7 +489,7 @@ namespace BTCPayServer.Controllers if (store == null || pairing == null) return NotFound(); - if(store.Role != StoreRoles.Owner) + if (store.Role != StoreRoles.Owner) { StatusMessage = "Error: You can't approve a pairing without being owner of the store"; return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs new file mode 100644 index 000000000..f1f6f7956 --- /dev/null +++ b/BTCPayServer/DerivationSchemeParser.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NBitcoin; +using NBitcoin.DataEncoders; +using NBXplorer; +using NBXplorer.DerivationStrategy; + +namespace BTCPayServer +{ + public class DerivationSchemeParser + { + public Network Network { get; set; } + public ChainType ChainType { get; set; } + public Script HintScriptPubKey { get; set; } + + public DerivationSchemeParser(Network expectedNetwork, ChainType chainType) + { + Network = expectedNetwork; + ChainType = chainType; + } + + public DerivationStrategyBase Parse(string str) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + str = str.Trim(); + + HashSet hintedLabels = new HashSet(); + + var hintDestination = HintScriptPubKey?.GetDestination(); + if (hintDestination != null) + { + if (hintDestination is KeyId) + { + hintedLabels.Add("legacy"); + } + if (hintDestination is ScriptId) + { + hintedLabels.Add("p2sh"); + } + } + + try + { + var result = new DerivationStrategyFactory(Network).Parse(str); + return FindMatch(hintedLabels, result); + } + catch + { + } + + Dictionary electrumMapping = new Dictionary(); + //Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py + var standard = 0x0488b21eU; + electrumMapping.Add(standard, new[] { "legacy" }); + var p2wpkh_p2sh = 0x049d7cb2U; + electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" }); + var p2wpkh = 0x4b24746U; + electrumMapping.Add(p2wpkh, Array.Empty()); + + var parts = str.Split('-'); + for (int i = 0; i < parts.Length; i++) + { + if (IsLabel(parts[i])) + { + hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant()); + continue; + } + try + { + var data = Encoders.Base58Check.DecodeData(parts[i]); + if (data.Length < 4) + continue; + var prefix = Utils.ToUInt32(data, false); + var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false); + for (int ii = 0; ii < 4; ii++) + data[ii] = standardPrefix[ii]; + + var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString(); + electrumMapping.TryGetValue(prefix, out string[] labels); + if (labels != null) + { + foreach (var label in labels) + { + hintedLabels.Add(label.ToLowerInvariant()); + } + } + parts[i] = derivationScheme; + } + catch { continue; } + } + + if (hintDestination != null) + { + if (hintDestination is WitKeyId) + { + hintedLabels.Remove("legacy"); + hintedLabels.Remove("p2sh"); + } + } + + str = string.Join('-', parts.Where(p => !IsLabel(p))); + foreach (var label in hintedLabels) + { + str = $"{str}-[{label}]"; + } + + return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str)); + } + + private DerivationStrategyBase FindMatch(HashSet hintLabels, DerivationStrategyBase result) + { + var facto = new DerivationStrategyFactory(Network); + var firstKeyPath = new KeyPath("0/0"); + if (HintScriptPubKey == null) + return result; + if (HintScriptPubKey == result.Derive(firstKeyPath).ScriptPubKey) + return result; + + if (result is MultisigDerivationStrategy) + hintLabels.Add("keeporder"); + + var resultNoLabels = result.ToString(); + resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p))); + foreach (var labels in ItemCombinations(hintLabels.ToList())) + { + var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray())); + if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey) + return hinted; + } + throw new FormatException("Could not find any match"); + } + + private static bool IsLabel(string v) + { + return v.StartsWith('[') && v.EndsWith(']'); + } + + /// + /// Method to create lists containing possible combinations of an input list of items. This is + /// basically copied from code by user "jaolho" on this thread: + /// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values + /// + /// type of the items on the input list + /// list of items + /// minimum number of items wanted in the generated combinations, + /// if zero the empty combination is included, + /// default is one + /// maximum number of items wanted in the generated combinations, + /// default is no maximum limit + /// list of lists for possible combinations of the input items + public static List> ItemCombinations(List inputList, int minimumItems = 1, + int maximumItems = int.MaxValue) + { + int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1; + List> listOfLists = new List>(nonEmptyCombinations + 1); + + if (minimumItems == 0) // Optimize default case + listOfLists.Add(new List()); + + for (int i = 1; i <= nonEmptyCombinations; i++) + { + List thisCombination = new List(inputList.Count); + for (int j = 0; j < inputList.Count; j++) + { + if ((i >> j & 1) == 1) + thisCombination.Add(inputList[j]); + } + + if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems) + listOfLists.Add(thisCombination); + } + + return listOfLists; + } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index 7758f2045..3a50c0a45 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -9,20 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels { public class DerivationSchemeViewModel { - class Format - { - public string Name { get; set; } - public string Value { get; set; } - } public DerivationSchemeViewModel() { - var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" }; - DerivationSchemeFormat = btcPay.Value; - DerivationSchemeFormats = new SelectList(new Format[] - { - btcPay, - new Format { Name = "Electrum", Value = "Electrum" }, - }, nameof(btcPay.Value), nameof(btcPay.Name), btcPay); } public string DerivationScheme { @@ -34,18 +22,12 @@ namespace BTCPayServer.Models.StoreViewModels get; set; } = new List<(string KeyPath, string Address)>(); - [Display(Name = "Derivation Scheme format")] - public string DerivationSchemeFormat - { - get; - set; - } - public string CryptoCode { get; set; } + [Display(Name = "Hint address")] + public string HintAddress { get; set; } public bool Confirmation { get; set; } - public SelectList DerivationSchemeFormats { get; set; } - public string ServerUrl { get; set; } + public string StatusMessage { get; internal set; } } } diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index a318ad645..2c637b1dd 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -5,6 +5,7 @@ ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index); } +@Html.Partial("_StatusMessage", Model.StatusMessage)

@ViewData["Title"]

@@ -32,17 +33,13 @@
-
- - -
BTCPay format memo @@ -90,7 +87,6 @@ -
@@ -110,6 +106,16 @@
+ +
+
Wrong addresses?
+ Help us to find the correct settings by telling us the first address of your wallet +
+
+ + + +
}