diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 07167c9df..ca786d773 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -59,8 +59,14 @@ namespace BTCPayServer.Tests DerivationScheme = new DerivationStrategyFactory(parent.Network).Parse(ExtKey.Neuter().ToString() + "-[legacy]"); await store.UpdateStore(StoreId, new StoreViewModel() { - DerivationScheme = DerivationScheme.ToString(), SpeedPolicy = SpeedPolicy.MediumSpeed + }); + + await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel() + { + CryptoCurrency = "BTC", + DerivationSchemeFormat = "BTCPay", + DerivationScheme = DerivationScheme.ToString(), }, "Save"); return store; } diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index f8ca445f3..68cf1024f 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -34,6 +34,7 @@ namespace BTCPayServer.Controllers AccessTokenController tokenController, BTCPayWallet wallet, BTCPayNetworkProvider networkProvider, + ExplorerClientProvider explorerProvider, IHostingEnvironment env) { _Repo = repo; @@ -43,9 +44,10 @@ namespace BTCPayServer.Controllers _Wallet = wallet; _Env = env; _NetworkProvider = networkProvider; + _ExplorerProvider = explorerProvider; } BTCPayNetworkProvider _NetworkProvider; - + private ExplorerClientProvider _ExplorerProvider; BTCPayWallet _Wallet; AccessTokenController _TokenController; StoreRepository _Repo; @@ -154,15 +156,109 @@ namespace BTCPayServer.Controllers vm.StoreWebsite = store.StoreWebsite; vm.NetworkFee = !storeBlob.NetworkFeeDisabled; vm.SpeedPolicy = store.SpeedPolicy; - vm.DerivationScheme = store.DerivationStrategy; + AddDerivationSchemes(store, vm); vm.StatusMessage = StatusMessage; vm.MonitoringExpiration = storeBlob.MonitoringExpiration; return View(vm); } + private void AddDerivationSchemes(StoreData store, StoreViewModel vm) + { + var strategies = store + .GetDerivationStrategies(_NetworkProvider) + .ToDictionary(s => s.Network.CryptoCode); + foreach (var explorerProvider in _ExplorerProvider.GetAll()) + { + if (strategies.TryGetValue(explorerProvider.Item1.CryptoCode, out DerivationStrategy strat)) + { + vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() + { + Crypto = explorerProvider.Item1.CryptoCode, + Value = strat.DerivationStrategyBase.ToString() + }); + } + } + } + + [HttpGet] + [Route("{storeId}/derivations")] + public async Task AddDerivationScheme(string storeId, string selectedScheme = null) + { + selectedScheme = selectedScheme ?? "BTC"; + var store = await _Repo.FindStore(storeId, GetUserId()); + if (store == null) + return NotFound(); + DerivationSchemeViewModel vm = new DerivationSchemeViewModel(); + vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme); + return View(vm); + } + + [HttpPost] + [Route("{storeId}/derivations")] + public async Task AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string command, string selectedScheme = null) + { + selectedScheme = selectedScheme ?? "BTC"; + var store = await _Repo.FindStore(storeId, GetUserId()); + if (store == null) + return NotFound(); + + var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency); + vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme); + if (network == null) + { + ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network"); + return View(vm); + } + + if (command == "Save") + { + try + { + if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + var strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network); + await _Wallet.TrackAsync(strategy); + vm.DerivationScheme = strategy.ToString(); + } + store.SetDerivationStrategy(network, vm.DerivationScheme); + } + catch + { + ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme"); + return View(vm); + } + + await _Repo.UpdateStore(store); + StatusMessage = $"Derivation scheme for {vm.CryptoCurrency} has been modified."; + return RedirectToAction(nameof(UpdateStore), new { storeId = storeId }); + } + else + { + if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + try + { + var scheme = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network); + var line = scheme.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit); + + for (int i = 0; i < 10; i++) + { + var address = line.Derive((uint)i); + vm.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(scheme.Network.NBitcoinNetwork).ToString())); + } + } + catch + { + ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme"); + } + } + return View(vm); + } + } + [HttpPost] [Route("{storeId}")] - public async Task UpdateStore(string storeId, StoreViewModel model, string command) + public async Task UpdateStore(string storeId, StoreViewModel model) { if (!ModelState.IsValid) { @@ -171,88 +267,44 @@ namespace BTCPayServer.Controllers var store = await _Repo.FindStore(storeId, GetUserId()); if (store == null) return NotFound(); + AddDerivationSchemes(store, model); - if (command == "Save") + bool needUpdate = false; + if (store.SpeedPolicy != model.SpeedPolicy) { - bool needUpdate = false; - if (store.SpeedPolicy != model.SpeedPolicy) - { - needUpdate = true; - store.SpeedPolicy = model.SpeedPolicy; - } - if (store.StoreName != model.StoreName) - { - needUpdate = true; - store.StoreName = model.StoreName; - } - if (store.StoreWebsite != model.StoreWebsite) - { - needUpdate = true; - store.StoreWebsite = model.StoreWebsite; - } - - if (store.DerivationStrategy != model.DerivationScheme) - { - needUpdate = true; - try - { - if (!string.IsNullOrEmpty(model.DerivationScheme)) - { - var strategy = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat, _NetworkProvider.BTC); - await _Wallet.TrackAsync(strategy); - model.DerivationScheme = strategy.ToString(); - } - store.DerivationStrategy = model.DerivationScheme; - } - catch - { - ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme"); - return View(model); - } - } - - var blob = store.GetStoreBlob(); - blob.NetworkFeeDisabled = !model.NetworkFee; - blob.MonitoringExpiration = model.MonitoringExpiration; - - if (store.SetStoreBlob(blob)) - { - needUpdate = true; - } - - if (needUpdate) - { - await _Repo.UpdateStore(store); - StatusMessage = "Store successfully updated"; - } - - return RedirectToAction(nameof(UpdateStore), new - { - storeId = storeId - }); + needUpdate = true; + store.SpeedPolicy = model.SpeedPolicy; } - else + if (store.StoreName != model.StoreName) { - if (!string.IsNullOrEmpty(model.DerivationScheme)) - { - try - { - var scheme = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat, _NetworkProvider.BTC); - var line = scheme.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit); - - for (int i = 0; i < 10; i++) - { - var address = line.Derive((uint)i); - model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(scheme.Network.NBitcoinNetwork).ToString())); - } - } - catch - { - ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme"); - } - } - return View(model); + needUpdate = true; + store.StoreName = model.StoreName; } + if (store.StoreWebsite != model.StoreWebsite) + { + needUpdate = true; + store.StoreWebsite = model.StoreWebsite; + } + + var blob = store.GetStoreBlob(); + blob.NetworkFeeDisabled = !model.NetworkFee; + blob.MonitoringExpiration = model.MonitoringExpiration; + + if (store.SetStoreBlob(blob)) + { + needUpdate = true; + } + + if (needUpdate) + { + await _Repo.UpdateStore(store); + StatusMessage = "Store successfully updated"; + } + + return RedirectToAction(nameof(UpdateStore), new + { + storeId = storeId + }); } private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network) diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 84a5e4ecc..c8c5fa00d 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -64,13 +64,53 @@ namespace BTCPayServer.Data { if (network == networks.BTC && btcReturned) continue; - yield return BTCPayServer.DerivationStrategy.Parse(strat.Value(), network); + if (strat.Value.Type == JTokenType.Null) + continue; + yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value(), network); } } } #pragma warning restore CS0618 } + public void SetDerivationStrategy(BTCPayNetwork network, string derivationScheme) + { +#pragma warning disable CS0618 + JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies); + bool existing = false; + foreach (var strat in strategies.Properties().ToList()) + { + if (strat.Name == network.CryptoCode) + { + if (network.IsBTC) + DerivationStrategy = null; + if (string.IsNullOrEmpty(derivationScheme)) + { + strat.Remove(); + } + else + { + strat.Value = new JValue(derivationScheme); + } + existing = true; + break; + } + } + + if (!existing && string.IsNullOrEmpty(derivationScheme)) + { + if(network.IsBTC) + DerivationStrategy = null; + } + else if(!existing) + strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme))); + // This is deprecated so we don't have to set anymore + //if (network.IsBTC) + // DerivationStrategy = derivationScheme; + DerivationStrategies = strategies.ToString(); +#pragma warning restore CS0618 + } + public string StoreName { get; set; diff --git a/BTCPayServer/ExplorerClientProvider.cs b/BTCPayServer/ExplorerClientProvider.cs index 8a91bcdeb..90688bc8b 100644 --- a/BTCPayServer/ExplorerClientProvider.cs +++ b/BTCPayServer/ExplorerClientProvider.cs @@ -37,6 +37,16 @@ namespace BTCPayServer return GetExplorerClient(network.CryptoCode); } + public BTCPayNetwork GetNetwork(string cryptoCode) + { + var network = _NetworkProviders.GetNetwork(cryptoCode); + if (network == null) + return null; + if (_Options.ExplorerFactories.ContainsKey(network.CryptoCode)) + return network; + return null; + } + public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll() { foreach(var net in _NetworkProviders.GetAll()) diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs new file mode 100644 index 000000000..ca7f5334a --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; + +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 + { + get; set; + } + + public List<(string KeyPath, string Address)> AddressSamples + { + get; set; + } = new List<(string KeyPath, string Address)>(); + + [Display(Name = "Derivation Scheme format")] + public string DerivationSchemeFormat + { + get; + set; + } + + [Display(Name = "Crypto currency")] + public string CryptoCurrency + { + get; + set; + } + + public SelectList CryptoCurrencies { get; set; } + public SelectList DerivationSchemeFormats { get; set; } + + + public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme) + { + var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray(); + var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault(); + CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen); + CryptoCurrency = chosen.Name; + } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index da5b90a2f..0f7fdfba1 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -11,21 +11,16 @@ namespace BTCPayServer.Models.StoreViewModels { public class StoreViewModel { - class Format + public class DerivationScheme { - public string Name { get; set; } + public string Crypto { get; set; } public string Value { get; set; } } public StoreViewModel() { - 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 Id { get; set; } [Display(Name = "Store Name")] [Required] @@ -45,19 +40,7 @@ namespace BTCPayServer.Models.StoreViewModels set; } - public string DerivationScheme - { - get; set; - } - - [Display(Name = "Derivation Scheme format")] - public string DerivationSchemeFormat - { - get; - set; - } - - public SelectList DerivationSchemeFormats { get; set; } + public List DerivationSchemes { get; set; } = new List(); [Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")] [Range(10, 60 * 24 * 31)] @@ -79,11 +62,6 @@ namespace BTCPayServer.Models.StoreViewModels get; set; } - public List<(string KeyPath, string Address)> AddressSamples - { - get; set; - } = new List<(string KeyPath, string Address)>(); - public string StatusMessage { get; set; diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml new file mode 100644 index 000000000..6b8e22979 --- /dev/null +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -0,0 +1,106 @@ +@model DerivationSchemeViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["Title"] = "Add derivation scheme"; + ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index); +} + +

@ViewData["Title"]

+ +
+
+
+
+
+
+
+
+
+
Derivation Scheme
+ @if(Model.AddressSamples.Count == 0) + { + The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey' +} +
+
+ + +
+ +
+ + +
+
+ + +
+
+ @if(Model.AddressSamples.Count == 0) + { + BTCPay format memo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Address typeExample
P2WPKHxpub
P2SH-P2WPKHxpub-[p2sh]
P2PKHxpub-[legacy]
Multi-sig P2WSH2-of-xpub1-xpub2
Multi-sig P2SH-P2WSH2-of-xpub1-xpub2-[p2sh]
Multi-sig P2SH2-of-xpub1-xpub2-[legacy]
+} +else +{ + + + + + + + + + @foreach(var sample in Model.AddressSamples) + { + + + + +} + +
Key pathAddress
@sample.KeyPath@sample.Address
+} +
+ + +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index 9b05104f2..d0ece8112 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -55,81 +55,30 @@
Derivation Scheme
- @if(Model.AddressSamples.Count == 0) - { - The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey' -} + The DerivationScheme represents the destination of the funds received by your invoice.
+
- - -
-
- - -
-
- @if(Model.AddressSamples.Count == 0) - { - BTCPay format memo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Address typeExample
P2WPKHxpub
P2SH-P2WPKHxpub-[p2sh]
P2PKHxpub-[legacy]
Multi-sig P2WSH2-of-xpub1-xpub2
Multi-sig P2SH-P2WSH2-of-xpub1-xpub2-[p2sh]
Multi-sig P2SH2-of-xpub1-xpub2-[legacy]
-} -else -{ - - - - - - - - - @foreach(var sample in Model.AddressSamples) - { - - - - -} - -
Key pathAddress
@sample.KeyPath@sample.Address
-} + Add or modify a derivation scheme + + + + + + + + + @foreach(var scheme in Model.DerivationSchemes) + { + + + + + } + +
CryptoDerivation Scheme
@scheme.Crypto@scheme.Value
-