diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index f981aa59c..6dff08de5 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -116,21 +116,43 @@ namespace BTCPayServer.Tests public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit) { Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click(); - Driver.FindElement(By.Id("import-from-btn")).Click(); - Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).Click(); - Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed); - SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys); - SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys); + // Modify case + if (Driver.PageSource.Contains("id=\"change-wallet-link\"")) + { + Driver.FindElement(By.Id("change-wallet-link")).Click(); + } + + if (string.IsNullOrEmpty(seed)) + { + var option = privkeys ? "hotwallet" : "watchonly"; + Logs.Tester.LogInformation($"Generating new seed ({option})"); + Driver.FindElement(By.Id("generate-wallet-link")).Click(); + Driver.FindElement(By.Id($"generate-{option}-link")).Click(); + } + else + { + Logs.Tester.LogInformation("Progressing with existing seed"); + Driver.FindElement(By.Id("import-wallet-options-link")).Click(); + Driver.FindElement(By.Id("import-seed-link")).Click(); + Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed); + SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys); + } + Driver.FindElement(By.Id("ScriptPubKeyType")).Click(); Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click(); - Logs.Tester.LogInformation("Trying to click btn-generate"); - Driver.FindElement(By.Id("btn-generate")).Click(); + Driver.FindElement(By.Id("advanced-settings-button")).Click(); + SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys); + Driver.FindElement(By.Id("advanced-settings-button")).Click(); // close settings again , otherwise the button might not be clickable for Selenium + + Logs.Tester.LogInformation("Trying to click Continue button"); + Driver.FindElement(By.Id("Continue")).Click(); // Seed backup page FindAlertMessage(); if (string.IsNullOrEmpty(seed)) { seed = Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic"); } + // Confirm seed backup Driver.FindElement(By.Id("confirm")).Click(); Driver.FindElement(By.Id("submit")).Click(); @@ -142,7 +164,9 @@ namespace BTCPayServer.Tests public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]") { Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click(); - Driver.FindElement(By.ClassName("store-derivation-scheme")).SendKeys(derivationScheme); + Driver.FindElement(By.Id("import-wallet-options-link")).Click(); + Driver.FindElement(By.Id("import-xpub-link")).Click(); + Driver.FindElement(By.Id("DerivationScheme")).SendKeys(derivationScheme); Driver.FindElement(By.Id("Continue")).Click(); Driver.FindElement(By.Id("Confirm")).Click(); FindAlertMessage(); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 2235a9fcb..44fdb85ef 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -685,10 +685,10 @@ namespace BTCPayServer.Tests { await s.StartAsync(); s.RegisterNewUser(true); - var storeId = s.CreateNewStore(); + var (storeName, 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 + // 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 s.GenerateWallet("BTC", "", true); //let's test quickly the receive wallet page @@ -697,7 +697,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("WalletSend")).Click(); s.Driver.FindElement(By.Id("SendMenu")).Click(); - //you cant use the Sign with NBX option without saving private keys when generating the wallet. + //you cannot use the Sign with NBX option without saving private keys when generating the wallet. Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); s.Driver.FindElement(By.Id("WalletReceive")).Click(); @@ -714,10 +714,9 @@ namespace BTCPayServer.Tests //send money to addr and ensure it changed var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync(); - sess.ListenAllTrackedSource(); + await sess.ListenAllTrackedSourceAsync(); var nextEvent = sess.NextEventAsync(); - s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(receiveAddr, Network.RegTest), - Money.Parse("0.1")); + await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest), Money.Parse("0.1")); await nextEvent; await Task.Delay(200); s.Driver.Navigate().Refresh(); @@ -726,7 +725,7 @@ namespace BTCPayServer.Tests receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); //change the wallet and ensure old address is not there and generating a new one does not result in the prev one - s.GoToStore(storeId.storeId); + s.GoToStore(storeId); s.GenerateWallet("BTC", "", true); s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); @@ -735,19 +734,19 @@ namespace BTCPayServer.Tests Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); - var invoiceId = s.CreateInvoice(storeId.storeName); + var invoiceId = s.CreateInvoice(storeName); var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); var address = invoice.EntityToDTO().Addresses["BTC"]; //wallet should have been imported to bitcoin core wallet in watch only mode. var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); Assert.True(result.IsWatchOnly); - s.GoToStore(storeId.storeId); + s.GoToStore(storeId); var mnemonic = s.GenerateWallet("BTC", "", true, true); //lets import and save private keys var root = mnemonic.DeriveExtKey(); - invoiceId = s.CreateInvoice(storeId.storeName); + invoiceId = s.CreateInvoice(storeName); invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); address = invoice.EntityToDTO().Addresses["BTC"]; result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); @@ -831,7 +830,7 @@ namespace BTCPayServer.Tests Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value")); Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value")); - s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings); + s.GoToWallet(new WalletId(storeId, "BTC"), WalletsNavPages.Settings); var walletUrl = s.Driver.Url; s.Driver.FindElement(By.Id("SettingsMenu")).Click(); diff --git a/BTCPayServer/Controllers/StoresController.Onchain.cs b/BTCPayServer/Controllers/StoresController.Onchain.cs new file mode 100644 index 000000000..52a9ad42d --- /dev/null +++ b/BTCPayServer/Controllers/StoresController.Onchain.cs @@ -0,0 +1,513 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Models; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments; +using BTCPayServer.Services.Wallets; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBXplorer.DerivationStrategy; +using NBXplorer.Models; + +namespace BTCPayServer.Controllers +{ + public partial class StoresController + { + [HttpGet("{storeId}/onchain/{cryptoCode}")] + public ActionResult SetupWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out var store, out _); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); + vm.DerivationScheme = derivation?.AccountDerivation.ToString(); + + return View(vm); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/import/{method?}")] + public async Task ImportWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out _, out var network); + if (checkResult != null) + { + return checkResult; + } + + var (hotWallet, rpcImport) = await CanUseHotWallet(); + vm.Network = network; + vm.RootKeyPath = network.GetRootKeyPath(); + vm.CanUseHotWallet = hotWallet; + vm.CanUseRPCImport = rpcImport; + + if (vm.Method == null) + { + vm.Method = WalletSetupMethod.ImportOptions; + } + else if (vm.Method == WalletSetupMethod.Seed) + { + vm.SetupRequest = new GenerateWalletRequest(); + } + + return View(vm.ViewName, vm); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/modify")] + [HttpPost("{storeId}/onchain/{cryptoCode}/import/{method}")] + public async Task UpdateWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + vm.Network = network; + vm.RootKeyPath = network.GetRootKeyPath(); + DerivationSchemeSettings strategy = null; + + var wallet = _WalletProvider.GetWallet(network); + if (wallet == null) + { + return NotFound(); + } + + if (!string.IsNullOrEmpty(vm.Config)) + { + if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy)) + { + ModelState.AddModelError(nameof(vm.Config), "Config file was not in the correct format"); + return View(vm.ViewName, vm); + } + } + + if (vm.WalletFile != null) + { + if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy)) + { + ModelState.AddModelError(nameof(vm.WalletFile), "Wallet file was not in the correct format"); + return View(vm.ViewName, vm); + } + } + else if (!string.IsNullOrEmpty(vm.WalletFileContent)) + { + if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy)) + { + ModelState.AddModelError(nameof(vm.WalletFileContent), "QR import was not in the correct format"); + return View(vm.ViewName, vm); + } + } + else + { + try + { + if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network); + if (newStrategy.AccountDerivation != strategy?.AccountDerivation) + { + var accountKey = string.IsNullOrEmpty(vm.AccountKey) + ? null + : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork); + if (accountKey != null) + { + var accountSettings = + newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey); + if (accountSettings != null) + { + accountSettings.AccountKeyPath = + vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath); + accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) + ? (HDFingerprint?)null + : new HDFingerprint( + NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint)); + } + } + + strategy = newStrategy; + strategy.Source = vm.Source; + vm.DerivationScheme = strategy.AccountDerivation.ToString(); + } + } + else + { + ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key"); + return View(vm.ViewName, vm); + } + } + catch + { + ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid wallet format"); + return View(vm.ViewName, vm); + } + } + + var oldConfig = vm.Config; + vm.Config = strategy?.ToJson(); + var configChanged = oldConfig != vm.Config; + PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); + var storeBlob = store.GetStoreBlob(); + var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId); + var willBeExcluded = !vm.Enabled; + var excludedChanged = willBeExcluded != wasExcluded; + + var showAddress = // Show addresses if: + // - If the user is testing the hint address in confirmation screen + (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || + // - The user is clicking on continue after changing the config + (!vm.Confirmation && configChanged); + + showAddress = showAddress && strategy != null; + if (!showAddress) + { + try + { + if (strategy != null) + await wallet.TrackAsync(strategy.AccountDerivation); + store.SetSupportedPaymentMethod(paymentMethodId, strategy); + storeBlob.SetExcluded(paymentMethodId, willBeExcluded); + storeBlob.Hints.Wallet = false; + store.SetStoreBlob(storeBlob); + } + catch + { + ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid derivation scheme"); + return View(vm.ViewName, vm); + } + + await _Repo.UpdateStore(store); + _EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(vm.StoreId, vm.CryptoCode)}); + + if (excludedChanged) + { + var label = willBeExcluded ? "disabled" : "enabled"; + TempData[WellKnownTempData.SuccessMessage] = + $"On-Chain payments for {network.CryptoCode} have been {label}."; + } + else + { + TempData[WellKnownTempData.SuccessMessage] = + $"Derivation settings for {network.CryptoCode} have been modified."; + } + + // This is success case when derivation scheme is added to the store + return RedirectToAction(nameof(UpdateStore), new {storeId = vm.StoreId}); + } + + if (!string.IsNullOrEmpty(vm.HintAddress)) + { + BitcoinAddress address; + try + { + address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork); + } + catch + { + ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address"); + return ConfirmAddresses(vm, strategy); + } + + try + { + var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network); + if (newStrategy.AccountDerivation != strategy.AccountDerivation) + { + strategy.AccountDerivation = newStrategy.AccountDerivation; + strategy.AccountOriginal = null; + } + } + catch + { + ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address. Are you sure the wallet and address provided are correct and from the same source?"); + return ConfirmAddresses(vm, strategy); + } + + vm.HintAddress = ""; + TempData[WellKnownTempData.SuccessMessage] = + "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 ConfirmAddresses(vm, strategy); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")] + public async Task GenerateWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var isHotWallet = vm.Method == WalletSetupMethod.HotWallet; + var (hotWallet, rpcImport) = await CanUseHotWallet(); + if (isHotWallet && !hotWallet) + { + return NotFound(); + } + + var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); + if (derivation != null) + { + vm.DerivationScheme = derivation.AccountDerivation.ToString(); + vm.Config = derivation.ToJson(); + } + + vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike)); + vm.CanUseHotWallet = hotWallet; + vm.CanUseRPCImport = rpcImport; + vm.RootKeyPath = network.GetRootKeyPath(); + vm.Network = network; + + if (vm.Method == null) + { + vm.Method = WalletSetupMethod.GenerateOptions; + } + else + { + vm.SetupRequest = new GenerateWalletRequest { SavePrivateKeys = isHotWallet }; + } + + return View(vm.ViewName, vm); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")] + public async Task GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, GenerateWalletRequest request) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var (hotWallet, rpcImport) = await CanUseHotWallet(); + if (!hotWallet && request.SavePrivateKeys || !rpcImport && request.ImportKeysToRPC) + { + return NotFound(); + } + + var client = _ExplorerProvider.GetExplorerClient(cryptoCode); + var isImport = method == WalletSetupMethod.Seed; + var vm = new WalletSetupViewModel + { + StoreId = storeId, + CryptoCode = cryptoCode, + Method = method, + SetupRequest = request, + Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic), + Network = network, + RootKeyPath = network.GetRootKeyPath(), + Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike)), + Source = "NBXplorer", + DerivationSchemeFormat = "BTCPay", + CanUseHotWallet = true, + CanUseRPCImport = rpcImport + }; + + if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic)) + { + ModelState.AddModelError(nameof(request.ExistingMnemonic), "Please provide your existing seed"); + return View(vm.ViewName, vm); + } + + GenerateWalletResponse response; + try + { + response = await client.GenerateWalletAsync(request); + if (response == null) + { + throw new Exception("Node unavailable"); + } + } + catch (Exception e) + { + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Error, + Html = $"There was an error generating your wallet: {e.Message}" + }); + return View(vm.ViewName, vm); + } + + // Set wallet properties from generate response + vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(); + vm.DerivationScheme = response.DerivationScheme.ToString(); + vm.AccountKey = response.AccountHDKey.Neuter().ToWif(); + vm.KeyPath = response.AccountKeyPath.KeyPath.ToString(); + + var result = await UpdateWallet(vm); + + if (!ModelState.IsValid || !(result is RedirectToActionResult)) + return result; + + if (!isImport) + { + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Success, + Html = "Your wallet has been generated." + }); + var seedVm = new RecoverySeedBackupViewModel + { + CryptoCode = cryptoCode, + Mnemonic = response.Mnemonic, + Passphrase = response.Passphrase, + IsStored = request.SavePrivateKeys, + ReturnUrl = Url.Action(nameof(GenerateWalletConfirm), new {storeId, cryptoCode}) + }; + return this.RedirectToRecoverySeedBackup(seedVm); + } + + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Warning, + Html = "Please check your addresses and confirm." + }); + return result; + } + + // The purpose of this action is to show the user a success message, which confirms + // that the store settings have been updated after generating a new wallet. + [HttpGet("{storeId}/onchain/{cryptoCode}/generate/confirm")] + public ActionResult GenerateWalletConfirm(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out _, out var network); + if (checkResult != null) + { + return checkResult; + } + + TempData[WellKnownTempData.SuccessMessage] = + $"Derivation settings for {network.CryptoCode} have been modified."; + + return RedirectToAction(nameof(UpdateStore), new {storeId}); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/modify")] + public async Task ModifyWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); + if (derivation == null) + { + return NotFound(); + } + + var (hotWallet, rpcImport) = await CanUseHotWallet(); + vm.CanUseHotWallet = hotWallet; + vm.CanUseRPCImport = rpcImport; + vm.RootKeyPath = network.GetRootKeyPath(); + vm.Network = network; + vm.Source = derivation.Source; + vm.RootFingerprint = derivation.GetSigningAccountKeySettings().RootFingerprint.ToString(); + vm.DerivationScheme = derivation.AccountDerivation.ToString(); + vm.KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(); + vm.Config = derivation.ToJson(); + vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike)); + + return View(vm); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/delete")] + public IActionResult DeleteWallet(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + var description = + (derivation.IsHotWallet ? "

Please note that this is a hot wallet!

" : "") + + "

Do not remove the wallet if you have not backed it up!

" + + "

Removing the wallet will erase the wallet data from the server. " + + $"The store won't be able to receive {network.CryptoCode} onchain payments until a new wallet is set up.

"; + + return View("Confirm", new ConfirmModel + { + Title = $"Remove {network.CryptoCode} wallet", + Description = description, + DescriptionHtml = true, + Action = "Remove" + }); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/delete")] + public async Task ConfirmDeleteWallet(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + if (derivation == null) + { + return NotFound(); + } + + PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); + store.SetSupportedPaymentMethod(paymentMethodId, null); + + await _Repo.UpdateStore(store); + _EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(storeId, cryptoCode)}); + + TempData[WellKnownTempData.SuccessMessage] = + $"On-Chain payment for {network.CryptoCode} has been removed."; + + return RedirectToAction(nameof(UpdateStore), new {storeId}); + } + + private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy) + { + vm.DerivationScheme = strategy.AccountDerivation.ToString(); + var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); + + if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + var line = strategy.AccountDerivation.GetLineFor(deposit); + + for (uint i = 0; i < 10; i++) + { + var keyPath = deposit.GetKeyPath(i); + var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath); + var derivation = line.Derive(i); + var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation, + line.KeyPathTemplate.GetKeyPath(i), + derivation.ScriptPubKey).ToString(); + vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath)); + } + } + + vm.Confirmation = true; + ModelState.Remove(nameof(vm.Config)); // Remove the cached value + + return View("ImportWallet/ConfirmAddresses", vm); + } + + private ActionResult IsAvailable(string cryptoCode, out StoreData store, out BTCPayNetwork network) + { + store = HttpContext.GetStoreData(); + network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode); + + return store == null || network == null ? NotFound() : null; + } + } +} diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index ae7b0103e..8138a3bb9 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -274,23 +274,23 @@ namespace BTCPayServer [JsonIgnore] public bool IsHotWallet => Source == "NBXplorer"; - [Obsolete("Use GetAccountKeySettings().AccountKeyPath instead")] + [Obsolete("Use GetSigningAccountKeySettings().AccountKeyPath instead")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public KeyPath AccountKeyPath { get; set; } public DerivationStrategyBase AccountDerivation { get; set; } public string AccountOriginal { get; set; } - [Obsolete("Use GetAccountKeySettings().RootFingerprint instead")] + [Obsolete("Use GetSigningAccountKeySettings().RootFingerprint instead")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public HDFingerprint? RootFingerprint { get; set; } - [Obsolete("Use GetAccountKeySettings().AccountKey instead")] + [Obsolete("Use GetSigningAccountKeySettings().AccountKey instead")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public BitcoinExtPubKey ExplicitAccountKey { get; set; } [JsonIgnore] - [Obsolete("Use GetAccountKeySettings().AccountKey instead")] + [Obsolete("Use GetSigningAccountKeySettings().AccountKey instead")] public BitcoinExtPubKey AccountKey { get diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index cf7522399..a88b277ef 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -8,15 +8,8 @@ namespace BTCPayServer.Models.StoreViewModels public class DerivationSchemeViewModel { - public DerivationSchemeViewModel() - { - } - [Display(Name = "Derivation scheme")] - public string DerivationScheme - { - get; set; - } + public string DerivationScheme { get; set; } public List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)> AddressSamples { @@ -25,6 +18,7 @@ namespace BTCPayServer.Models.StoreViewModels public string CryptoCode { get; set; } public string KeyPath { get; set; } + [Display(Name = "Root fingerprint")] public string RootFingerprint { get; set; } [Display(Name = "Hint address")] public string HintAddress { get; set; } @@ -33,16 +27,20 @@ namespace BTCPayServer.Models.StoreViewModels public KeyPath RootKeyPath { get; set; } - [Display(Name = "Wallet File")] + [Display(Name = "Wallet file")] public IFormFile WalletFile { get; set; } - [Display(Name = "Wallet File Content")] + [Display(Name = "Wallet file content")] public string WalletFileContent { get; set; } public string Config { get; set; } public string Source { get; set; } + [Display(Name = "Derivation scheme format")] public string DerivationSchemeFormat { get; set; } + [Display(Name = "Account key")] public string AccountKey { get; set; } public BTCPayNetwork Network { get; set; } + [Display(Name = "Can use hot wallet")] public bool CanUseHotWallet { get; set; } + [Display(Name = "Can use RPC import")] public bool CanUseRPCImport { get; set; } public RootedKeyPath GetAccountKeypath() diff --git a/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs b/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs new file mode 100644 index 000000000..82f400771 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs @@ -0,0 +1,39 @@ +using NBXplorer.Models; + +namespace BTCPayServer.Models.StoreViewModels +{ + public enum WalletSetupMethod + { + ImportOptions, + Hardware, + File, + Xpub, + Scan, + Seed, + GenerateOptions, + HotWallet, + WatchOnly + } + + public class WalletSetupViewModel : DerivationSchemeViewModel + { + public WalletSetupMethod? Method { get; set; } + public GenerateWalletRequest SetupRequest { get; set; } + public string StoreId { get; set; } + + public string ViewName => + Method switch + { + WalletSetupMethod.ImportOptions => "ImportWalletOptions", + WalletSetupMethod.Hardware => "ImportWallet/Hardware", + WalletSetupMethod.Xpub => "ImportWallet/Xpub", + WalletSetupMethod.File => "ImportWallet/File", + WalletSetupMethod.Scan => "ImportWallet/Scan", + WalletSetupMethod.Seed => "ImportWallet/Seed", + WalletSetupMethod.GenerateOptions => "GenerateWalletOptions", + WalletSetupMethod.HotWallet => "GenerateWallet", + WalletSetupMethod.WatchOnly => "GenerateWallet", + _ => "SetupWallet" + }; + } +} diff --git a/BTCPayServer/Views/Account/Login.cshtml b/BTCPayServer/Views/Account/Login.cshtml index 1cd754c5d..7e6833b21 100644 --- a/BTCPayServer/Views/Account/Login.cshtml +++ b/BTCPayServer/Views/Account/Login.cshtml @@ -6,6 +6,10 @@ Layout = "_LayoutSimple"; } +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} +
diff --git a/BTCPayServer/Views/Account/Register.cshtml b/BTCPayServer/Views/Account/Register.cshtml index c265a7418..812a02ae5 100644 --- a/BTCPayServer/Views/Account/Register.cshtml +++ b/BTCPayServer/Views/Account/Register.cshtml @@ -5,6 +5,10 @@ Layout = "_LayoutSimple"; } +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} +
diff --git a/BTCPayServer/Views/Account/SetPassword.cshtml b/BTCPayServer/Views/Account/SetPassword.cshtml index dd8f575c1..0a72969ae 100644 --- a/BTCPayServer/Views/Account/SetPassword.cshtml +++ b/BTCPayServer/Views/Account/SetPassword.cshtml @@ -5,6 +5,10 @@ Layout = "_LayoutSimple"; } +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} +
diff --git a/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml b/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml index f9f12af33..9e658ad1e 100644 --- a/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml +++ b/BTCPayServer/Views/Home/RecoverySeedBackup.cshtml @@ -4,6 +4,10 @@ ViewData["Title"] = "Your recovery phrase"; } +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} + - + + + +} + diff --git a/BTCPayServer/Views/Stores/ImportWallet/File.cshtml b/BTCPayServer/Views/Stores/ImportWallet/File.cshtml new file mode 100644 index 000000000..0a68b7a4f --- /dev/null +++ b/BTCPayServer/Views/Stores/ImportWallet/File.cshtml @@ -0,0 +1,58 @@ +@model WalletSetupViewModel +@addTagHelper *, BundlerMinifier.TagHelpers +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = "Import your wallet file"; +} + +@section Navbar { + + + +} + +
+

@ViewData["Title"]

+

Upload the file exported from your wallet.

+
+ +
+
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WalletInstructions
Cobo VaultSettings ❯ Watch-Only Wallet ❯ BTCPay ❯ Export Wallet
ColdCardAdvanced ❯ MicroSD Card ❯ Electrum Wallet
ElectrumFile ❯ Save backup (not encrypted with a password)
WasabiTools ❯ Wallet Manager ❯ Open Wallets Folder
SpecterWallet ❯ Settings ❯ Export ❯ Export To Wallet Software ❯ Save wallet file
diff --git a/BTCPayServer/Views/Stores/ImportWallet/Hardware.cshtml b/BTCPayServer/Views/Stores/ImportWallet/Hardware.cshtml new file mode 100644 index 000000000..9d249a0ff --- /dev/null +++ b/BTCPayServer/Views/Stores/ImportWallet/Hardware.cshtml @@ -0,0 +1,113 @@ +@model WalletSetupViewModel +@addTagHelper *, BundlerMinifier.TagHelpers +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = "Connect your hardware wallet"; +} + +@section Navbar { + + + +} + +
+

@ViewData["Title"]

+

In order to securely connect to your hardware wallet you must first download, install, and run the BTCPay Server Vault.

+
+ +@if (!ViewContext.ModelState.IsValid) +{ +
+} + +
+
+
+ +
+ + +
+
+
+ + + +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + @await Html.PartialAsync("VaultElements") + + + + + +} + diff --git a/BTCPayServer/Views/Stores/ImportWallet/Scan.cshtml b/BTCPayServer/Views/Stores/ImportWallet/Scan.cshtml new file mode 100644 index 000000000..919243c09 --- /dev/null +++ b/BTCPayServer/Views/Stores/ImportWallet/Scan.cshtml @@ -0,0 +1,75 @@ +@model WalletSetupViewModel +@addTagHelper *, BundlerMinifier.TagHelpers +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = "Scan QR code"; +} + +@section Navbar { + + + +} + +
+

@ViewData["Title"]

+

Scan the extended public key, also called "xpub", shown on your wallet's display.

+
+ +@if (!ViewContext.ModelState.IsValid) +{ +
+} + +
+ +
+ +
+
+ +

+ Generate a QR code of the extended public key in your wallet (see instructions for supported wallets below). + Allow the browser access to your camera and hold the code to the camera when the scan prompt appears. +

+ + + + + + + + + + + + + + + + + + + + + + +
WalletInstructions
Cobo VaultOpen Wallet Settings ❯ Show/Export XPUB
BlueWalletOpen Wallet Settings ❯ Show Wallet XPUB
Specter DIYMaster public keys ❯ Select key ❯ Disable "Show derivation path"
+ + +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + + + + + +} diff --git a/BTCPayServer/Views/Stores/ImportWallet/Seed.cshtml b/BTCPayServer/Views/Stores/ImportWallet/Seed.cshtml new file mode 100644 index 000000000..0fae9967e --- /dev/null +++ b/BTCPayServer/Views/Stores/ImportWallet/Seed.cshtml @@ -0,0 +1,35 @@ +@model WalletSetupViewModel +@addTagHelper *, BundlerMinifier.TagHelpers +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = "Enter the wallet seed"; +} + +@section Navbar { + + + +} + +
+

@ViewData["Title"]

+

Manually enter your 12 or 24 word recovery seed.

+
+ +
+ @if (Model.CanUseHotWallet) + { + ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport); + ViewData.Add(nameof(Model.Method), Model.Method); + + @await Html.PartialAsync("_GenerateWalletForm", Model.SetupRequest) + } + else + { +

Please note that creating a wallet is not supported by your instance.

+ } +
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Stores/ImportWallet/Xpub.cshtml b/BTCPayServer/Views/Stores/ImportWallet/Xpub.cshtml new file mode 100644 index 000000000..abd348c99 --- /dev/null +++ b/BTCPayServer/Views/Stores/ImportWallet/Xpub.cshtml @@ -0,0 +1,107 @@ +@model WalletSetupViewModel +@addTagHelper *, BundlerMinifier.TagHelpers +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = $"Enter your extended public key"; +} + +@section Navbar { + + + +} + +
+

@ViewData["Title"]

+

+ This key, also called "xpub", is used to generate individual destination addresses for your invoices. + +

+
+ +
+ + + + + +
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Address typeExample
P2WPKHxpub…
zpub…
wpkh(xpub…/0/*)
wpkh([…/84'/0'/0']xpub…/0/*)
P2SH-P2WPKHxpub…-[p2sh]
ypub…
sh(wpkh(xpub…/0/*)
sh(wpkh([…/49'/0'/0']xpub…/0/*)
P2PKHxpub…-[legacy]
pkh([…/44'/0'/0']xpub…/0/*)
pkh(xpub…/0/*)
Multi-sig P2WSH2-of-xpub1…-xpub2…
wsh(multi(2,
[…/48'/0'/0'/2']xpub…/0/*,
[…/48'/0'/0'/2']xpub…/0/*))
Multi-sig P2SH-P2WSH2-of-xpub1…-xpub2…-[p2sh]
sh(wsh(multi(2,
[…/48'/0'/0'/1']xpub…/0/*,
[…/48'/0'/0'/1']xpub…/0/*)))
Multi-sig P2SH2-of-xpub1…-xpub2…-[legacy]
sh(multi(2,
[…/45'/0]xpub…/0/*,
[…/45'/0]xpub…/0/*))
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Stores/ImportWalletOptions.cshtml b/BTCPayServer/Views/Stores/ImportWalletOptions.cshtml new file mode 100644 index 000000000..fb7e38004 --- /dev/null +++ b/BTCPayServer/Views/Stores/ImportWalletOptions.cshtml @@ -0,0 +1,123 @@ +@model WalletSetupViewModel +@addTagHelper *, BundlerMinifier.TagHelpers +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = $"Import {Model.CryptoCode} Wallet"; +} + +@section Navbar { + + + +} + +
+

Choose your import method

+

The following methods assume that you already have an existing wallet created and backed up.

+
+ +@if (Model.CryptoCode == "BTC") +{ +
+
+ +
+ +
+
+

+ Connect hardware wallet + Recommended +

+

Import your keys using our Vault application

+
+ +
+ +
+
+} + + + + + +
+ +
+ +
+
+

Scan wallet QR code

+

Supported by BlueWallet, Cobo Vault and Specter DIY

+
+ +
+ +
+ + + + diff --git a/BTCPayServer/Views/Stores/ModifyWallet.cshtml b/BTCPayServer/Views/Stores/ModifyWallet.cshtml new file mode 100644 index 000000000..0f0830026 --- /dev/null +++ b/BTCPayServer/Views/Stores/ModifyWallet.cshtml @@ -0,0 +1,65 @@ +@model WalletSetupViewModel +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = $"Modify {Model.CryptoCode} Wallet"; +} + +@section Navbar { + + + +} + +
+

@ViewData["Title"]

+

Change your current wallet settings

+
+
+ +
+

Current settings

+ +
+ + + + + + + + + + + + + + @if (!string.IsNullOrEmpty(Model.KeyPath)) + { + + + + + } + + + + + + + + + +
Derivation Scheme@Model.DerivationScheme
Root Fingerprint@Model.RootFingerprint
KeyPath@Model.KeyPath
Source@Model.Source
+ Enabled + + +
+
+
+
+ + Replace wallet + + +
+
diff --git a/BTCPayServer/Views/Stores/SetupWallet.cshtml b/BTCPayServer/Views/Stores/SetupWallet.cshtml new file mode 100644 index 000000000..fae6e163b --- /dev/null +++ b/BTCPayServer/Views/Stores/SetupWallet.cshtml @@ -0,0 +1,52 @@ +@model WalletSetupViewModel +@{ + Layout = "_LayoutWalletSetup"; + ViewData["Title"] = $"Setup {Model.CryptoCode} Wallet"; +} + +@section Navbar { + @if (string.IsNullOrWhiteSpace(Model.DerivationScheme)) { + + + + } else { + + + + } +} + +

Let's get started

+
+ + +
+ diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index 9cc5f1160..fcec15e0e 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -66,10 +66,16 @@ @if (isSetUp) { | + + Modify + + } + else + { + + Setup + } - - @(isSetUp ? "Modify" : "Setup") -
diff --git a/BTCPayServer/Views/Stores/_GenerateWalletForm.cshtml b/BTCPayServer/Views/Stores/_GenerateWalletForm.cshtml new file mode 100644 index 000000000..f14837a5b --- /dev/null +++ b/BTCPayServer/Views/Stores/_GenerateWalletForm.cshtml @@ -0,0 +1,123 @@ +@using NBitcoin +@model NBXplorer.Models.GenerateWalletRequest + +@{ + var method = ViewData["Method"]; + var isImport = method is WalletSetupMethod.Seed; + var isHotWallet = method is WalletSetupMethod.HotWallet; + var canUseHotWallet = ViewData["CanUseHotWallet"] is true; + var canUseRpcImport = ViewData["CanUseRPCImport"] is true; +} + +@if (!User.IsInRole(Roles.ServerAdmin)) +{ +
+ You are not an admin on this server. While you are able to import or generate a wallet via seed with + your account, please understand that you are trusting the server admins not just with your + privacy + but also with trivial access to your funds. + If you NEED to use this feature, please reconsider hosting your own BTCPay Server instance. +
+} + +
+ @if (isImport) + { +
+ + + +
+ } + +
+ + + +
+ + @if (isImport && canUseHotWallet) + { +
+ + + +

+ If checked, each private key associated with an address generated will be stored as metadata + and would be accessible to anyone with admin access to your server. Enable at your own risk! +

+
+ } + else + { + + } + +
+ +
+
+ @if (isImport) // hide account option when creating a wallet + { +
+ + + +
+ } +
+ + + +
+
+ + + +
+ + @if (canUseRpcImport) + { +
+ + + +

+ Each address generated will be imported into the node wallet and you can view your balance through the node. + @if (isImport || isHotWallet) + { + When this is enabled for a hot wallet, you are also able to use the node wallet to spend. + } +

+
+ } +
+
+
+ + +
+ + diff --git a/BTCPayServer/Views/Stores/_LayoutWalletSetup.cshtml b/BTCPayServer/Views/Stores/_LayoutWalletSetup.cshtml new file mode 100644 index 000000000..0b9062667 --- /dev/null +++ b/BTCPayServer/Views/Stores/_LayoutWalletSetup.cshtml @@ -0,0 +1,36 @@ +@{ + Layout = "_LayoutSimple"; + ViewData["Title"] = ViewData["Title"] ?? "Wallet Setup"; +} + +@section HeadScripts { + @await RenderSectionAsync("HeadScripts", false) +} +@section HeaderContent { + @await RenderSectionAsync("HeaderContent", false) + +} +@section Scripts { + @await RenderSectionAsync("Scripts", false) +} + + + +
+
+ @if (TempData.HasStatusMessage()) + { + + } + @RenderBody() +
+
+ + + diff --git a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml index bad3c064b..86cc3ec75 100644 --- a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml @@ -26,7 +26,7 @@
- +
diff --git a/BTCPayServer/wwwroot/img/icon-sprite.svg b/BTCPayServer/wwwroot/img/icon-sprite.svg index ff4c31aa3..b814d7f12 100644 --- a/BTCPayServer/wwwroot/img/icon-sprite.svg +++ b/BTCPayServer/wwwroot/img/icon-sprite.svg @@ -1,3 +1,14 @@ - + + + + + + + + + + + + diff --git a/BTCPayServer/wwwroot/js/vaultbridge.ui.js b/BTCPayServer/wwwroot/js/vaultbridge.ui.js index d97fc54a7..aa9ab9013 100644 --- a/BTCPayServer/wwwroot/js/vaultbridge.ui.js +++ b/BTCPayServer/wwwroot/js/vaultbridge.ui.js @@ -77,7 +77,7 @@ var vaultui = (function () { this.retryShowing = false; function showRetry() { - var button = $(".vault-retry"); + var button = $("#vault-retry"); self.retryShowing = true; button.show(); } @@ -88,7 +88,7 @@ var vaultui = (function () { function show(feedback) { var icon = $(".vault-feedback." + feedback.category + " " + ".vault-feedback-icon"); icon.removeClass(); - icon.addClass("vault-feedback-icon"); + icon.addClass("vault-feedback-icon mt-1 mr-2"); if (feedback.type == "?") { icon.addClass("fa fa-question-circle feedback-icon-loading"); } @@ -160,7 +160,7 @@ var vaultui = (function () { } this.waitRetryPushed = function () { - var button = $(".vault-retry"); + var button = $("#vault-retry"); return new Promise(function (resolve) { button.click(function () { // Cleanup old feedback diff --git a/BTCPayServer/wwwroot/main/bootstrap/bootstrap.css b/BTCPayServer/wwwroot/main/bootstrap/bootstrap.css index 9ef9eaa65..6b267fec1 100644 --- a/BTCPayServer/wwwroot/main/bootstrap/bootstrap.css +++ b/BTCPayServer/wwwroot/main/bootstrap/bootstrap.css @@ -3574,9 +3574,8 @@ input[type="button"].btn-block { border-bottom-right-radius: 0.25rem; border-bottom-left-radius: 0.25rem; } .list-group-item.disabled, .list-group-item:disabled { - color: var(--btcpay-color-neutral-600); - pointer-events: none; - background-color: var(--btcpay-color-white); } + opacity: .5; + pointer-events: none; } .list-group-item.active { z-index: 2; color: var(--btcpay-color-white); diff --git a/BTCPayServer/wwwroot/main/site.css b/BTCPayServer/wwwroot/main/site.css index 10db21f5c..baa0f366a 100644 --- a/BTCPayServer/wwwroot/main/site.css +++ b/BTCPayServer/wwwroot/main/site.css @@ -265,6 +265,16 @@ pre { font-weight: 600; } +.text-monospace { + font-size: .95rem; +} + +input.w-auto, +select.w-auto, +textarea.w-auto { + max-width: 100%; +} + /* Chrome, Safari, Edge, Opera */ input[type=number].hide-number-spin::-webkit-outer-spin-button, input[type=number].hide-number-spin::-webkit-inner-spin-button { @@ -358,6 +368,60 @@ html[data-devenv]:before { background-color: #FB383D; } + +.btcpay-toggle { + --border-size: 2px; + --toggle-width: 40px; + --toggle-height: 24px; + --switch-size: calc(var(--toggle-height) - 2 * var(--border-size)); + + position: relative; + display: inline-block; + width: var(--toggle-width); + height: var(--toggle-height); + border: 0; + border-radius: calc(var(--toggle-height) / 2); + background: var(--btcpay-color-neutral-500); + font-size: 0; +} + +input.btcpay-toggle { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; +} + +input.btcpay-toggle:checked, +.btcpay-toggle.btcpay-toggle--active { + background: var(--btcpay-color-primary); +} + +.btcpay-toggle::after { + content: ""; + position: absolute; + border-radius: 50%; + top: var(--border-size); + left: var(--border-size); + width: var(--switch-size); + height: var(--switch-size); + background-color: var(--btcpay-color-white); +} + +.btcpay-toggle, +.btcpay-toggle::after { + transition: all 0.2s; +} +input.btcpay-toggle:checked::after, +.btcpay-toggle.btcpay-toggle--active::after { + left: calc(var(--toggle-width) - var(--switch-size) - var(--border-size)); +} + +label + input.btcpay-toggle { + position: relative; + top: .45rem; +} + + svg.icon { display: inline-block; width: 16px; @@ -372,7 +436,7 @@ svg.icon-note { .notification-dropdown { border: 0; border-radius: 4px; - box-shadow: 0px 2px 16px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08); padding: 0; } diff --git a/BTCPayServer/wwwroot/main/themes/casa.css b/BTCPayServer/wwwroot/main/themes/casa.css index e7c360a35..82aef8711 100644 --- a/BTCPayServer/wwwroot/main/themes/casa.css +++ b/BTCPayServer/wwwroot/main/themes/casa.css @@ -54,20 +54,23 @@ --btcpay-color-dark-text: var(--btcpay-color-neutral-800); /* Color definitions for specific sections - try to reuse colors defined above */ - --btcpay-body-bg: var(--btcpay-brand-darkest); --btcpay-bg-dark: var(--btcpay-brand-dark); --btcpay-bg-tile: var(--btcpay-brand-tertiary); --btcpay-bg-cta: var(--btcpay-brand-tertiary); + --btcpay-body-bg: var(--btcpay-brand-darkest); --btcpay-body-color: var(--btcpay-color-neutral-100); --btcpay-body-color-link: var(--btcpay-color-primary); --btcpay-body-color-link-accent: var(--btcpay-color-primary-accent); + --btcpay-header-bg: var(--btcpay-brand-dark); + + --btcpay-wizard-bg: var(--btcpay-body-bg); + --btcpay-wizard-color: var(--btcpay-body-color); + --btcpay-nav-color-link-accent: var(--btcpay-color-neutral-100); - --btcpay-header-bg: var(--btcpay-brand-dark); --btcpay-footer-bg: var(--btcpay-brand-darkest); - --btcpay-footer-color: var(--btcpay-color-neutral-600); --btcpay-preformatted-text-color: var(--btcpay-color-white); diff --git a/BTCPayServer/wwwroot/main/themes/classic.css b/BTCPayServer/wwwroot/main/themes/classic.css index 594648297..a0b3c09e8 100644 --- a/BTCPayServer/wwwroot/main/themes/classic.css +++ b/BTCPayServer/wwwroot/main/themes/classic.css @@ -54,11 +54,11 @@ --btcpay-color-dark-text: var(--btcpay-color-neutral-200); /* Color definitions for specific sections - try to reuse colors defined above */ - --btcpay-body-bg: var(--btcpay-color-neutral-100); --btcpay-bg-dark: var(--btcpay-brand-dark); --btcpay-bg-tile: var(--btcpay-color-white); --btcpay-bg-cta: var(--btcpay-bg-dark); + --btcpay-body-bg: var(--btcpay-color-neutral-100); --btcpay-body-color: var(--btcpay-color-neutral-900); --btcpay-body-color-link: var(--btcpay-color-primary); --btcpay-body-color-link-accent: var(--btcpay-color-primary-accent); @@ -68,6 +68,9 @@ --btcpay-header-color-link: var(--btcpay-color-white); --btcpay-header-color-link-accent: var(--btcpay-color-white); + --btcpay-wizard-bg: var(--btcpay-body-bg); + --btcpay-wizard-color: var(--btcpay-body-color); + --btcpay-footer-bg: var(--btcpay-bg-dark); --btcpay-footer-color: var(--btcpay-color-neutral-400); diff --git a/BTCPayServer/wwwroot/main/themes/default-dark.css b/BTCPayServer/wwwroot/main/themes/default-dark.css index fd3af5d5b..7b2398e7f 100644 --- a/BTCPayServer/wwwroot/main/themes/default-dark.css +++ b/BTCPayServer/wwwroot/main/themes/default-dark.css @@ -7,11 +7,16 @@ --btcpay-bg-dark: var(--btcpay-color-neutral-950); --btcpay-header-bg: var(--btcpay-bg-dark); - --btcpay-footer-bg: var(--btcpay-bg-dark); - --btcpay-footer-color: var(--btcpay-color-neutral-600); + --btcpay-body-bg: var(--btcpay-color-neutral-900); --btcpay-body-color: var(--btcpay-color-white); + --btcpay-wizard-bg: var(--btcpay-body-bg); + --btcpay-wizard-color: var(--btcpay-body-color); + + --btcpay-footer-bg: var(--btcpay-bg-dark); + --btcpay-footer-color: var(--btcpay-color-neutral-600); + --btcpay-nav-color-link: var(--btcpay-color-neutral-500); --btcpay-nav-color-link-accent: var(--btcpay-color-neutral-300); --btcpay-nav-color-link-active: var(--btcpay-color-white); diff --git a/BTCPayServer/wwwroot/main/themes/default.css b/BTCPayServer/wwwroot/main/themes/default.css index 9e5c06de7..2bbe0b2d4 100644 --- a/BTCPayServer/wwwroot/main/themes/default.css +++ b/BTCPayServer/wwwroot/main/themes/default.css @@ -53,7 +53,6 @@ --btcpay-color-dark-text: var(--btcpay-color-neutral-200); /* Color definitions for specific sections - try to reuse colors defined above */ - --btcpay-body-bg: var(--btcpay-color-neutral-100); --btcpay-bg-dark: var(--btcpay-brand-dark); --btcpay-bg-tile: var(--btcpay-color-white); --btcpay-bg-cta: var(--btcpay-brand-dark); @@ -61,6 +60,7 @@ --btcpay-border-color-light: var(--btcpay-color-neutral-200); --btcpay-border-color-medium: var(--btcpay-color-neutral-300); + --btcpay-body-bg: var(--btcpay-color-neutral-100); --btcpay-body-color: var(--btcpay-color-neutral-900); --btcpay-body-color-link: var(--btcpay-color-primary); --btcpay-body-color-link-accent: var(--btcpay-color-primary); @@ -70,6 +70,9 @@ --btcpay-header-color-link: var(--btcpay-body-color); --btcpay-header-color-link-accent: var(--btcpay-body-color); + --btcpay-wizard-bg: var(--btcpay-color-white); + --btcpay-wizard-color: var(--btcpay-body-color); + --btcpay-footer-bg: var(--btcpay-brand-dark); --btcpay-footer-color: var(--btcpay-color-neutral-400); diff --git a/BTCPayServer/wwwroot/main/wallet-setup.css b/BTCPayServer/wwwroot/main/wallet-setup.css new file mode 100644 index 000000000..99c17d07f --- /dev/null +++ b/BTCPayServer/wwwroot/main/wallet-setup.css @@ -0,0 +1,118 @@ +body { + color: var(--btcpay-wizard-color); + background-color: var(--btcpay-wizard-bg); +} + +#wizard-navbar { + 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: 30px; + } +} + +#wizard-navbar a { + color: var(--btcpay-body-color); + background-color: var(--btcpay-border-color-light); + display: inline-flex; + justify-content: center; + align-items: center; + width: 48px; + height: 48px; + border-radius: 50%; +} + +#wizard-navbar a svg.icon { + width: 19px; + height: 16px; +} + +#wizard-navbar a:hover { + background-color: var(--btcpay-border-color-medium); +} + +#wizard-navbar .cancel { + margin-left: auto; +} + +.list-group-item-wallet-setup { + display: flex; + padding: 0; +} + +.list-group-item-wallet-setup.hide-when-js { + display: flex !important; +} + +.list-group-item-wallet-setup:active, +.list-group-item-wallet-setup.active { + background-color: transparent; +} + +.list-group-item-wallet-setup .image { + display: flex; + flex: 0 0 90px; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.list-group-item-wallet-setup .image .icon, +.list-group-item-wallet-setup .image .icon-new-wallet, +.list-group-item-wallet-setup .image .icon-existing-wallet { + height: 48px; +} + +.list-group-item-wallet-setup .image .icon-new-wallet, +.list-group-item-wallet-setup .image .icon-existing-wallet, +.list-group-item-wallet-setup .image .icon-xpub, +.list-group-item-wallet-setup .image .icon-seed { + width: 48px; +} + +.list-group-item-wallet-setup .image .icon-hardware-wallet { + width: 40px; +} +.list-group-item-wallet-setup .image .icon-scan-qr { + width: 32px; +} + +.list-group-item-wallet-setup .image .icon-wallet-file { + width: 24px; +} + +.list-group-item-wallet-setup .content { + flex: 1; + padding: 1.5rem; +} + +.list-group-item-wallet-setup .content .badge { + position: relative; + top: -1px; + font-size: 60%; + color: var(--btcpay-color-white); +} + +.list-group-item-wallet-setup .image + .content { + padding-left: .5rem; +} + +.list-group-item-wallet-setup .icon-caret-right { + width: 24px; + height: 24px; + align-self: center; + margin-right: 1.5rem; +}