diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 50131ad81..a58e06011 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -109,6 +109,25 @@ namespace BTCPayServer.Tests return (usr, Driver.FindElement(By.Id("Id")).GetAttribute("value")); } + public string GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false) + { + Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick(); + Driver.FindElement(By.Id("import-from-btn")).ForceClick(); + Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick(); + Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed); + SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys); + SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys); + Driver.FindElement(By.Id("btn-generate")).ForceClick(); + AssertHappyMessage(); + if (string.IsNullOrEmpty(seed)) + { + seed = Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text; + } + Driver.FindElement(By.Id("Confirm")).ForceClick(); + AssertHappyMessage(); + return seed; + } + public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]") { Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick(); @@ -219,6 +238,11 @@ namespace BTCPayServer.Tests { element.Click(); } + + if (value != element.Selected) + { + SetCheckbox(element, value); + } } public void SetCheckbox(SeleniumTester s, string inputName, bool value) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index e6b2c23da..79e7a50c7 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -408,14 +408,28 @@ namespace BTCPayServer.Tests { await s.StartAsync(); s.RegisterNewUser(true); - s.CreateNewStore(); + var storeId = s.CreateNewStore(); // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed // to sign the transaction - var mnemonic = "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage"; + var mnemonic = s.GenerateWallet("BTC", "", true, false); + + var invoiceId = s.CreateInvoice(storeId.storeId); + var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId); + var address = invoice.EntityToDTO().Addresses["BTC"]; + + var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); + Assert.True(result.IsWatchOnly); + s.GoToStore(storeId.storeId); + mnemonic = s.GenerateWallet("BTC", "", true, true); + var root = new Mnemonic(mnemonic).DeriveExtKey(); - s.AddDerivationScheme("BTC", "ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD"); - var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m)); + invoiceId = s.CreateInvoice(storeId.storeId); + invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId); + address = invoice.EntityToDTO().Addresses["BTC"]; + result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); + Assert.False(result.IsWatchOnly); + var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m)); s.Server.ExplorerNode.Generate(1); s.Driver.FindElement(By.Id("Wallets")).Click(); @@ -429,8 +443,8 @@ namespace BTCPayServer.Tests // We setup the fingerprint and the account key path s.Driver.FindElement(By.Id("WalletSettings")).ForceClick(); - s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160"); - s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter); +// s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160"); +// s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter); // Check the tx sent earlier arrived s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick(); @@ -471,7 +485,7 @@ namespace BTCPayServer.Tests } } SignWith(mnemonic); - var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString(); + var accountKey = root.Derive(new KeyPath("m/84'/1'/0'")).GetWif(Network.RegTest).ToString(); SignWith(accountKey); } } diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 23082fb37..4809d296f 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBXplorer.DerivationStrategy; +using NBXplorer.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -131,6 +132,7 @@ namespace BTCPayServer.Controllers vm.Config = derivation.ToJson(); } vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike)); + vm.CanUseGenerateWallet = CanUseGenerateWallet(); } private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store) @@ -178,7 +180,7 @@ namespace BTCPayServer.Controllers Message = "Config file was not in the correct format" }); vm.Confirmation = false; - return View(vm); + return View(nameof(AddDerivationScheme),vm); } } @@ -192,7 +194,7 @@ namespace BTCPayServer.Controllers Message = "Coldcard public file was not in the correct format" }); vm.Confirmation = false; - return View(vm); + return View(nameof(AddDerivationScheme),vm); } } else @@ -228,7 +230,7 @@ namespace BTCPayServer.Controllers { ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme"); vm.Confirmation = false; - return View(vm); + return View(nameof(AddDerivationScheme),vm); } } @@ -319,6 +321,48 @@ namespace BTCPayServer.Controllers return ShowAddresses(vm, strategy); } + [HttpPost] + [Route("{storeId}/derivations/{cryptoCode}/generatenbxwallet")] + public async Task GenerateNBXWallet(string storeId, string cryptoCode, + GenerateWalletRequest request) + { + if (!CanUseGenerateWallet()) + { + return NotFound(); + } + + var network = _NetworkProvider.GetNetwork(cryptoCode); + var client = _ExplorerProvider.GetExplorerClient(cryptoCode); + var response = await client.GenerateWalletAsync(request); + + var store = HttpContext.GetStoreData(); + var result = await AddDerivationScheme(storeId, + new DerivationSchemeViewModel() + { + Confirmation = false, + Network = network, + RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(), + RootKeyPath = network.GetRootKeyPath(), + CryptoCode = cryptoCode, + DerivationScheme = response.DerivationScheme.ToString(), + Source = "NBXplorer", + AccountKey = response.AccountHDKey.Neuter().ToWif(), + DerivationSchemeFormat = "BTCPay", + KeyPath = response.AccountKeyPath.KeyPath.ToString(), + Enabled = !store.GetStoreBlob() + .IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike)) + }, cryptoCode); + + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Success, + Html = !string.IsNullOrEmpty(request.ExistingMnemonic) + ? "Your wallet has been imported." + : $"Your wallet has been generated. Please store your seed securely!
{response.Mnemonic}" + }); + return result; + } + private async Task ReadAllText(IFormFile file) { using (var stream = new StreamReader(file.OpenReadStream())) @@ -345,7 +389,12 @@ namespace BTCPayServer.Controllers } vm.Confirmation = true; ModelState.Remove(nameof(vm.Config)); // Remove the cached value - return View(vm); + return View(nameof(AddDerivationScheme),vm); + } + + private bool CanUseGenerateWallet() + { + return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowGenerateWalletForAll); } } } diff --git a/BTCPayServer/HostedServices/CssThemeManager.cs b/BTCPayServer/HostedServices/CssThemeManager.cs index ddba26c96..c615a898d 100644 --- a/BTCPayServer/HostedServices/CssThemeManager.cs +++ b/BTCPayServer/HostedServices/CssThemeManager.cs @@ -88,8 +88,11 @@ namespace BTCPayServer.HostedServices RootAppId = data.RootAppId; DomainToAppMapping = data.DomainToAppMapping; AllowLightningInternalNodeForAll = data.AllowLightningInternalNodeForAll; + AllowGenerateWalletForAll = data.AllowGenerateWalletForAll; } + public bool AllowGenerateWalletForAll { get; set; } + public bool AllowLightningInternalNodeForAll { get; set; } } diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index 474d03dd6..74b833966 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -41,6 +41,7 @@ namespace BTCPayServer.Models.StoreViewModels public string DerivationSchemeFormat { get; set; } public string AccountKey { get; set; } public BTCPayNetwork Network { get; set; } + public bool CanUseGenerateWallet { get; set; } public RootedKeyPath GetAccountKeypath() { diff --git a/BTCPayServer/Services/PoliciesSettings.cs b/BTCPayServer/Services/PoliciesSettings.cs index 192a7be34..72e66f7e9 100644 --- a/BTCPayServer/Services/PoliciesSettings.cs +++ b/BTCPayServer/Services/PoliciesSettings.cs @@ -23,7 +23,9 @@ namespace BTCPayServer.Services public bool DiscourageSearchEngines { get; set; } [Display(Name = "Allow non-admins to use the internal lightning node in their stores")] public bool AllowLightningInternalNodeForAll { get; set; } - + [Display(Name = "Allow non-admins to use the NBXplorer wallet generator in their stores")] + public bool AllowGenerateWalletForAll { get; set; } + [Display(Name = "Display app on website root")] public string RootAppId { get; set; } public AppType? RootAppType { get; set; } diff --git a/BTCPayServer/Views/Server/Policies.cshtml b/BTCPayServer/Views/Server/Policies.cshtml index 89516c016..4103083a6 100644 --- a/BTCPayServer/Views/Server/Policies.cshtml +++ b/BTCPayServer/Views/Server/Policies.cshtml @@ -32,6 +32,11 @@ +
+ + + +
diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index 24b32dd3e..f2c3cba43 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -34,7 +34,7 @@ { } -
+ @@ -75,7 +75,7 @@ diff --git a/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml b/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml index ad310e1b3..47b2c21da 100644 --- a/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationSchemes_HardwareWalletDialogs.cshtml @@ -1,5 +1,9 @@ +@using NBXplorer.Models @model DerivationSchemeViewModel - +@if (Model.CanUseGenerateWallet) +{ + +}