Merge pull request #2296 from dennisreimann/wallet-setup-finetuning

Wallet setup finetuning
This commit is contained in:
Nicolas Dorier 2021-03-06 13:51:01 +09:00 committed by GitHub
commit 2b1efd9347
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 309 additions and 1086 deletions

View file

@ -1,61 +1,31 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F.Models;
using BTCPayServer.Validation;
using ExchangeSharp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitcoin.Scripting.Parser;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests
{
@ -72,7 +42,7 @@ namespace BTCPayServer.Tests
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
[Trait("Lightning", "Lightning")]
public async Task CanAddDerivationSchemes()
public async Task CanSetupWallet()
{
using (var tester = ServerTester.Create())
{
@ -99,37 +69,37 @@ namespace BTCPayServer.Tests
Assert.Equal(3, invoice.CryptoInfo.Length);
var controller = user.GetController<StoresController>();
var lightningVM =
(LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC"))
.Model;
Assert.True(lightningVM.Enabled);
lightningVM.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult();
lightningVM =
(LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC"))
.Model;
Assert.False(lightningVM.Enabled);
var lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.True(lightningVm.Enabled);
lightningVm.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVm, "save", "BTC").GetAwaiter().GetResult();
lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.False(lightningVm.Enabled);
WalletSetupViewModel setupVm;
var storeId = user.StoreId;
var cryptoCode = "BTC";
var response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new GenerateWalletRequest());
Assert.IsType<ViewResult>(response);
// Get setup view model from modify action
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Enabled);
// Only Enabling/Disabling the payment method must redirect to store page
var derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.True(derivationVM.Enabled);
derivationVM.Enabled = false;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.False(derivationVM.Enabled);
setupVm.Enabled = false;
response = controller.UpdateWallet(setupVm).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
// Clicking next without changing anything should send to the confirmation screen
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.False(setupVm.Enabled);
var oldScheme = setupVm.DerivationScheme;
invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 1.5m,
Currency = "USD",
@ -143,76 +113,57 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
// Removing the derivation scheme, should redirect to store page
var oldScheme = derivationVM.DerivationScheme;
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = null;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
response = controller.ConfirmDeleteWallet(user.StoreId, "BTC").GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
// Setting it again should redirect to the confirmation page
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = oldScheme;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
// Setting it again should show the confirmation page
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
// The following part posts a wallet update, confirms it and checks the result
//cobo vault file
// cobo vault file
var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet3.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
//wasabi wallet file
content =
"{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet4.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("CoboVault", setupVm.Source);
// wasabi wallet file
content = "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("WasabiFile", setupVm.Source);
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
content =
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM.WalletFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM
.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.False(setupVm.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
// And with a good file? (upub)
content =
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("ElectrumFile", setupVm.Source);
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult();
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks)
#pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
@ -295,7 +246,6 @@ namespace BTCPayServer.Tests
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")
{
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()

View file

@ -125,10 +125,12 @@ 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();
// Modify case
// Replace previous wallet case
if (Driver.PageSource.Contains("id=\"ChangeWalletLink\""))
{
Driver.FindElement(By.Id("ChangeWalletLink")).Click();
Driver.FindElement(By.Id("continue")).Click();
}
if (string.IsNullOrEmpty(seed))

View file

@ -184,9 +184,11 @@ namespace BTCPayServer.Tests
ScriptPubKeyType = segwit,
SavePrivateKeys = importKeysToNBX,
});
await store.AddDerivationScheme(StoreId,
new DerivationSchemeViewModel()
await store.UpdateWallet(
new WalletSetupViewModel
{
StoreId = StoreId,
Method = importKeysToNBX ? WalletSetupMethod.HotWallet : WalletSetupMethod.WatchOnly,
Enabled = true,
CryptoCode = cryptoCode,
Network = SupportedNetwork,
@ -198,7 +200,7 @@ namespace BTCPayServer.Tests
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
});
return new WalletId(StoreId, cryptoCode);
}

View file

@ -1,400 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
vm.Network = network;
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));
var hotWallet = await CanUseHotWallet();
vm.CanUseHotWallet = hotWallet.HotWallet;
vm.CanUseRPCImport = hotWallet.RPCImport;
return View(vm);
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm,
string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
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))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Config file was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network,
out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Wallet file was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "QR import was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), 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
{
strategy = null;
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
var oldConfig = vm.Config;
vm.Config = strategy == null ? null : strategy.ToJson();
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault();
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
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 && oldConfig != vm.Config) ||
// - The user is clicking on continue without changing config nor enabling/disabling
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
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);
}
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent() {WalletId = new WalletId(storeId, cryptoCode)});
if (willBeExcluded != wasExcluded)
{
var label = willBeExcluded ? "disabled" : "enabled";
TempData[WellKnownTempData.SuccessMessage] =
$"On-Chain payments for {network.CryptoCode} has been {label}.";
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"Derivation settings for {network.CryptoCode} has been modified.";
}
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
}
else if (!string.IsNullOrEmpty(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
{
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");
return ShowAddresses(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 ShowAddresses(vm, strategy);
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}/generatenbxwallet")]
public async Task<IActionResult> GenerateNBXWallet(string storeId, string cryptoCode,
GenerateWalletRequest request)
{
var hotWallet = await CanUseHotWallet();
if (!hotWallet.HotWallet || (!hotWallet.RPCImport && request.ImportKeysToRPC))
{
return NotFound();
}
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
GenerateWalletResponse response;
try
{
response = await client.GenerateWalletAsync(request);
}
catch (Exception e)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"There was an error generating your wallet: {e.Message}"
});
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
}
if (response == null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "There was an error generating your wallet. Is your node available?"
});
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
}
var store = HttpContext.GetStoreData();
var result = await AddDerivationScheme(storeId,
new DerivationSchemeViewModel()
{
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
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);
if (!ModelState.IsValid || !(result is RedirectToActionResult))
return result;
TempData.Clear();
if (string.IsNullOrEmpty(request.ExistingMnemonic))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"<span class='text-centered'>Your wallet has been generated.</span>"
});
var vm = new RecoverySeedBackupViewModel()
{
CryptoCode = cryptoCode,
Mnemonic = response.Mnemonic,
Passphrase = response.Passphrase,
IsStored = request.SavePrivateKeys,
ReturnUrl = Url.Action(nameof(UpdateStore), new {storeId})
};
return this.RedirectToRecoverySeedBackup(vm);
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = "Please check your addresses and confirm"
});
}
return result;
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
if (isAdmin)
return (true, true);
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
var hotWallet = policies?.AllowHotWalletForAll is true;
return (hotWallet, hotWallet && policies?.AllowHotWalletRPCImportForAll is true);
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
{
return await stream.ReadToEndAsync();
}
}
private IActionResult
ShowAddresses(DerivationSchemeViewModel 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 (int i = 0; i < 10; i++)
{
var keyPath = deposit.GetKeyPath((uint)i);
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
var derivation = line.Derive((uint)i);
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath((uint)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(nameof(AddDerivationScheme), vm);
}
}
}

View file

@ -1,16 +1,21 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
@ -105,42 +110,34 @@ namespace BTCPayServer.Controllers
return View(vm.ViewName, vm);
}
}
else
else if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
{
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 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)
{
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));
}
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);
strategy = newStrategy;
strategy.Source = vm.Source;
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
}
catch
@ -149,6 +146,11 @@ namespace BTCPayServer.Controllers
return View(vm.ViewName, vm);
}
}
else
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key");
return View(vm.ViewName, vm);
}
var oldConfig = vm.Config;
vm.Config = strategy?.ToJson();
@ -409,6 +411,8 @@ namespace BTCPayServer.Controllers
}
var (hotWallet, rpcImport) = await CanUseHotWallet();
var isHotWallet = await IsHotWallet(vm.CryptoCode, derivation);
vm.CanUseHotWallet = hotWallet;
vm.CanUseRPCImport = rpcImport;
vm.RootKeyPath = network.GetRootKeyPath();
@ -419,12 +423,13 @@ namespace BTCPayServer.Controllers
vm.KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString();
vm.Config = derivation.ToJson();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
vm.IsHotWallet = isHotWallet;
return View(vm);
}
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
public IActionResult DeleteWallet(string storeId, string cryptoCode)
[HttpGet("{storeId}/onchain/{cryptoCode}/replace")]
public async Task<IActionResult> ReplaceWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
@ -433,9 +438,62 @@ namespace BTCPayServer.Controllers
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
var isHotWallet = await IsHotWallet(cryptoCode, derivation);
var walletType = isHotWallet ? "hot" : "watch-only";
var additionalText = isHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
(derivation.IsHotWallet ? "<p class=\"text-danger font-weight-bold\">Please note that this is a hot wallet!</p> " : "") +
"<p class=\"text-danger font-weight-bold\">Do not remove the wallet if you have not backed it up!</p>" +
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger font-weight-bold\">Do not replace the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-left mb-0\">Replacing the wallet will erase the current wallet data from the server. " +
"The current wallet will be replaced once you finish the setup of the new wallet. If you cancel the setup, the current wallet will stay active .</p>";
return View("Confirm", new ConfirmModel
{
Title = $"Replace {network.CryptoCode} wallet",
Description = description,
DescriptionHtml = true,
Action = "Setup new wallet"
});
}
[HttpPost("{storeId}/onchain/{cryptoCode}/replace")]
public IActionResult ConfirmReplaceWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out _);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
if (derivation == null)
{
return NotFound();
}
return RedirectToAction(nameof(SetupWallet), new {storeId, cryptoCode});
}
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
public async Task<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 isHotWallet = await IsHotWallet(cryptoCode, derivation);
var walletType = isHotWallet ? "hot" : "watch-only";
var additionalText = isHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger font-weight-bold\">Do not remove the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-left mb-0\">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.</p>";
@ -478,7 +536,7 @@ namespace BTCPayServer.Controllers
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
@ -509,5 +567,39 @@ namespace BTCPayServer.Controllers
return store == null || network == null ? NotFound() : null;
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
if (isAdmin)
return (true, true);
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
var hotWallet = policies?.AllowHotWalletForAll is true;
return (hotWallet, hotWallet && policies.AllowHotWalletRPCImportForAll is true);
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
{
return await stream.ReadToEndAsync();
}
}
private async Task<bool> IsHotWallet(string cryptoCode, DerivationSchemeSettings derivation)
{
return derivation.IsHotWallet && await _ExplorerProvider.GetExplorerClient(cryptoCode)
.GetMetadataAsync<string>(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) != null;
}
}
}

View file

@ -20,6 +20,7 @@ namespace BTCPayServer.Models.StoreViewModels
public WalletSetupMethod? Method { get; set; }
public GenerateWalletRequest SetupRequest { get; set; }
public string StoreId { get; set; }
public bool IsHotWallet { get; set; }
public string ViewName =>
Method switch

View file

@ -31,8 +31,8 @@
@if (!String.IsNullOrEmpty(Model.Action))
{
<form method="post" class="modal-footer justify-content-center" action="@Model.ActionUrl">
<button type="submit" class="btn @Model.ButtonClass w-25 mx-2" id="continue">@Model.Action</button>
<button type="submit" class="btn btn-secondary w-25 mx-2" onclick="history.back(); return false;">Go back</button>
<button type="submit" class="btn @Model.ButtonClass xmx-2" id="continue" style="min-width:25%;">@Model.Action</button>
<button type="submit" class="btn btn-secondary mx-2" onclick="history.back(); return false;" style="min-width:25%;">Go back</button>
</form>
}
</div>

View file

@ -1,234 +0,0 @@
@model DerivationSchemeViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, $"{Model.CryptoCode} Derivation scheme");
}
@section HeadScripts {
<style type="text/css">
.hw-fields {
display: none;
}
</style>
}
<partial name="_StatusMessage" />
@if (!ViewContext.ModelState.IsValid)
{
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
}
<div class="modal fade" id="btcpayservervault" tabindex="-1" role="dialog" aria-labelledby="btcpayservervault" aria-hidden="true"></div>
<partial name="VaultElements" />
<div class="row">
<div class="col-md-8">
<div id="WebsocketPath" style="display:none;">@Url.Action("VaultBridgeConnection", "Vault", new { cryptoCode = Model.CryptoCode })</div>
@if (!Model.Confirmation)
{
<partial name="AddDerivationSchemes_HardwareWalletDialogs" model="@Model" />
}
else
{
<template id="btcpayservervault_template">
<div class="modal-dialog" role="document">
<form class="modal-content" method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Address verification</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Confirm on the device that you see address <b id="displayedAddress"></b></p>
<div class="form-group">
<div id="vaultPlaceholder"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
</div>
</form>
</div>
</template>
}
<form method="post" asp-action="AddDerivationScheme"
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@this.Context.GetRouteValue("storeId")">
<input id="Config" asp-for="Config" type="hidden" />
@if (!Model.Confirmation)
{
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
<input id="DerivationSchemeFormat" asp-for="DerivationSchemeFormat" type="hidden" />
<input id="AccountKey" asp-for="AccountKey" type="hidden" />
<div class="form-group">
<h5>
Derivation scheme
<a href="https://docs.btcpayserver.org/FAQ/FAQ-Wallet/#what-is-a-derivation-scheme" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</h5>
<p>
A derivation scheme facilitates generation of the destination addresses for your invoices so funds can be received on-chain.
</p>
</div>
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<textarea asp-for="DerivationScheme" class="form-control store-derivation-scheme text-monospace py-2" rows="2"></textarea>
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<div class="dropdown mt-2 text-right">
<div class="btn-group">
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" id="import-from-btn">
Import from...
</button>
<div class="dropdown-menu dropdown-menu-right">
@if (Model.CryptoCode == "BTC")
{
<button class="dropdown-item check-for-vault" type="button">... a hardware wallet</button>
}
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#electrumimport">... a wallet file (Electrum, Wasabi, Cobo Vault, ColdCard)</button>
<button class="dropdown-item" type="button" data-toggle="modal" data-target="#scanqrModal">... a QR code</button>
@if (Model.CanUseHotWallet)
{
<button class="dropdown-item" data-toggle="modal" data-target="#nbxplorergeneratewallet" type="button" id="nbxplorergeneratewalletbtn">... a new/existing seed.</button>
}
</div>
</div>
</div>
</div>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub...</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub...-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub...-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1...-xpub2...</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1...-xpub2...-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1...-xpub2...-[legacy]</td>
</tr>
</tbody>
</table>
<div class="form-group hw-fields">
<h5>Additional pairing information</h5>
</div>
<div class="form-group hw-fields">
<label asp-for="Source"></label>
<input asp-for="Source" class="form-control" readonly />
</div>
<div class="form-group hw-fields">
<label asp-for="RootFingerprint"></label>
<input asp-for="RootFingerprint" class="form-control" readonly />
</div>
<div class="form-group hw-fields">
<label asp-for="KeyPath"></label>
<input asp-for="KeyPath" class="form-control" readonly />
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="Enabled" type="checkbox" class="form-check-input" />
<label asp-for="Enabled" class="form-check-label"></label>
</div>
</div>
<button name="command" type="submit" class="btn btn-primary" value="save" id="Continue">Continue</button>
}
else
{
<div class="form-group">
<h5>Confirm the addresses (@Model.CryptoCode)</h5>
<span>Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</span>
</div>
<input asp-for="Confirmation" type="hidden" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="Enabled" />
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Key path</th>
<th>Address</th>
@if (Model.Source == "Vault")
{
<th>Actions</th>
}
</tr>
</thead>
<tbody>
@foreach (var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
@if (Model.Source == "Vault")
{
<td><a class="showaddress" href="#" onclick='showAddress(@Safe.Json(sample.RootedKeyPath.ToString()), @Safe.Json(sample.Address)); return false;'>Show on device</a></td>
}
</tr>
}
</tbody>
</table>
</div>
<div class="form-group">
<h5>Wrong addresses?</h5>
<span>Help us to find the correct settings by telling us the first address of your wallet</span>
</div>
<div class="form-group">
<label asp-for="HintAddress"></label>
<input asp-for="HintAddress" class="form-control" />
<span asp-validation-for="HintAddress" class="text-danger"></span>
</div>
<button name="command" type="submit" class="btn btn-primary" value="save" id="Confirm">Confirm</button>
}
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
<bundle name="wwwroot/bundles/camera-bundle.min.js"></bundle>
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
<script>
window.coinName = "@Model.Network.DisplayName.ToLowerInvariant()";
$(function () {
initCameraScanningApp("Scan wallet QR", function(data){
$("#WalletFileContent").val(data);
$("#qr-import-form").submit();
},"scanqrModal");
});
</script>
}

View file

@ -1,111 +0,0 @@
@using NBXplorer.Models
@model DerivationSchemeViewModel
@if (Model.CanUseHotWallet)
{
ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport);
<partial name="AddDerivationSchemes_NBXWalletGenerate" model="@(new GenerateWalletRequest())"/>
}
<partial name="CameraScanner"/>
<form id="qr-import-form" method="post">
<input type="hidden" asp-for="WalletFileContent"/>
</form>
<div class="modal fade" id="electrumimport" tabindex="-1" role="dialog" aria-labelledby="electrumimport" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="electrumimportLabel">Import Wallet from file</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Import your air-gapped hardware wallet by exporting a file and uploading it here.</p>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Wallet</th>
<th>Instructions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cobo Vault</td>
<td><kbd>Settings Watch-Only Wallet BTCPay Export Wallet</kbd></td>
</tr>
<tr>
<td>ColdCard</td>
<td>
<kbd>Advanced MicroSD Card Electrum Wallet</kbd> or
<kbd>Advanced MicroSD Card Wasabi Wallet</kbd>
</td>
</tr>
<tr>
<td>Electrum</td>
<td><kbd>File Save backup</kbd></td>
</tr>
<tr>
<td>Wasabi</td>
<td><kbd>Tools Wallet Manager Open Wallets Folder</kbd></td>
</tr>
<tr>
<td>Specter</td>
<td><kbd>Wallet Settings Export Export To Wallet Software Save wallet file</kbd></td>
</tr>
</tbody>
</table>
<div class="form-group">
<label asp-for="WalletFile"></label>
<input type="file" class="form-control-file" asp-for="WalletFile" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
<template id="btcpayservervault_template">
<div class="modal-dialog" role="document">
<form class="modal-content" method="post" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Import from BTCPayServer Vault</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>You may import from BTCPayServer Vault.</p>
<div class="form-group">
<div id="vaultPlaceholder"></div>
</div>
<div id="vault-xpub" style="display:none;">
<div class="form-group">
<label for="addressType">Address type</label>
<select name="addressType" class="form-control">
<option value="segwit">Segwit (Recommended, cheapest transaction fee)</option>
<option value="segwitWrapped">Segwit wrapped (less cheap but compatible with old wallets)</option>
<option value="legacy">Legacy (Not recommended)</option>
</select>
</div>
<div class="form-group">
<label for="accountNumber">Account</label>
<select name="accountNumber" class="form-control">
@for (int i = 0; i < 20; i++)
{
<option value="@i">@i</option>
}
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button id="vault-retry" class="btn btn-primary" style="display:none;" type="button">Retry</button>
<button id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</template>

View file

@ -1,101 +0,0 @@
@using NBitcoin
@model NBXplorer.Models.GenerateWalletRequest
<div class="modal fade" id="nbxplorergeneratewallet" tabindex="-1" role="dialog" aria-labelledby="nbxplorergeneratewallet" aria-hidden="true">
<div class="modal-dialog" role="document">
<form id="generate-wallet-form" class="modal-content" method="post" onsubmit="return validatePassphraseConf();"
asp-action="GenerateNBXWallet"
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@this.Context.GetRouteValue("cryptoCode")"
enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Generate a wallet with a seed</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
@if (!User.IsInRole(Roles.ServerAdmin))
{
<div class="alert alert-warning">
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 <a href="https://docs.btcpayserver.org/ThirdPartyHosting/#privacy-concerns" target="_blank" class="alert-link">privacy</a> but also with <a href="https://docs.btcpayserver.org/ThirdPartyHosting/#trust-concerns" target="_blank" class="alert-link">trivial access to your funds.</a> If you NEED to use this feature, please reconsider hosting your own BTCPay Server instance.
</div>
}
<p>You may generate a wallet with a seed and import the xpub into BTCPay. You can optionally also tell NBX to import the keys to the node wallet to be able to view & spend received funds from it.</p>
<div class="form-group">
<label asp-for="ExistingMnemonic">Existing Seed</label>
<input type="text" asp-for="ExistingMnemonic" class="form-control" autocomplete="off"/>
<span asp-validation-for="ExistingMnemonic" class="text-danger"></span>
<small class="form-text text-muted">
You can choose to import an existing mnemonic seed phrase.<br/>
If you leave it blank, we will generate one for you.
</small>
</div>
<div class="form-group">
<label asp-for="Passphrase">Passphrase (optional)</label>
<input type="text" asp-for="Passphrase" class="form-control" autocomplete="off"/>
<span asp-validation-for="Passphrase" class="text-danger"></span>
</div>
<div class="form-group">
<label for="passphrase_conf">Passphrase confirmation</label>
<input type="text" name="passphrase_conf" id="passphrase_conf" class="form-control"/>
<span class="text-danger field-validation-valid" id="passphrase_conf_validation"></span>
</div>
<div class="form-group">
<label asp-for="ScriptPubKeyType">Address type</label>
<select class="form-control" asp-for="ScriptPubKeyType">
<option value="@ScriptPubKeyType.Segwit">Segwit (Recommended, cheapest transaction fee)</option>
<option value="@ScriptPubKeyType.SegwitP2SH">Segwit wrapped (less cheap but compatible with old wallets)</option>
<option value="@ScriptPubKeyType.Legacy">Legacy (Not recommended)</option>
</select>
<span asp-validation-for="ScriptPubKeyType" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AccountNumber">Account</label>
<select asp-for="AccountNumber" class="form-control">
@for (int i = 0; i < 20; i++)
{
<option value="@i">@i</option>
}
</select>
<span asp-validation-for="AccountNumber" class="text-danger"></span>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" asp-for="SavePrivateKeys"/>
<label asp-for="SavePrivateKeys" class="form-check-label">Is hot wallet</label>
<span asp-validation-for="SavePrivateKeys" class="text-danger"></span>
<small class="form-text text-danger">
If checked, each private key associated with an address generated will be stored as metadata in NBXplorer. While convenient, this means that anyone with access to your server will have access to your private keys and will be able to steal your funds.
</small>
</div>
@if (ViewData["CanUseRPCImport"] is true)
{
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" asp-for="ImportKeysToRPC"/>
<label asp-for="ImportKeysToRPC" class="form-check-label">Import keys to RPC</label>
<span asp-validation-for="ImportKeysToRPC" class="text-danger"></span>
<small class="form-text text-muted">
If checked, each address generated will be imported into the node wallet so that you can view your balance through your node. When this is enabled alongside <code>Is hot wallet</code>, you're also able to use the node wallet to spend (this works pretty well in conjunction with apps such as FullyNoded).
</small>
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" id="btn-generate">Generate</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">
function validatePassphraseConf() {
if (document.forms["generate-wallet-form"].elements["passphrase_conf"].value !==
document.forms["generate-wallet-form"].elements["Passphrase"].value) {
$("#passphrase_conf_validation").removeClass("field-validation-valid");
$("#passphrase_conf_validation").text("Invalid passphrase confirmation");
return false;
}
return true;
}
</script>

View file

@ -18,13 +18,14 @@
{
<a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="hotwallet" id="GenerateHotwalletLink" class="list-group-item list-group-item-action list-group-item-wallet-setup">
<div class="image">
<vc:icon symbol="seed"/>
<vc:icon symbol="hot-wallet"/>
</div>
<div class="content">
<h4>Hot wallet</h4>
<p class="mb-0 text-secondary">
Allows spending directly from your BTCPay Server.
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. Use at your own risk!
Wallet's private key is stored on the server.
Spending the funds you received is convenient.
To minimize the risk of theft, regularly withdraw funds to a different wallet.
</p>
</div>
<vc:icon symbol="caret-right"/>
@ -34,7 +35,7 @@
{
<div class="list-group-item list-group-item-wallet-setup text-muted">
<div class="image">
<vc:icon symbol="new-wallet"/>
<vc:icon symbol="hot-wallet"/>
</div>
<div class="content">
<h4>Hot wallet</h4>
@ -47,11 +48,14 @@
<div class="list-group mt-4">
<a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="watchonly" id="GenerateWatchonlyLink" class="list-group-item list-group-item-action list-group-item-wallet-setup">
<div class="image">
<vc:icon symbol="xpub"/>
<vc:icon symbol="watchonly-wallet"/>
</div>
<div class="content">
<h4>Watch-only wallet</h4>
<p class="mb-0 text-secondary">Needs to be imported into an external wallet to spend or provide the seed while spending.</p>
<p class="mb-0 text-secondary">
Wallet's private key is erased from the server. Higher security.
To spend, you have to manually input the private key or import it into an external wallet.
</p>
</div>
<vc:icon symbol="caret-right" />
</a>

View file

@ -51,8 +51,8 @@
<td>Tools Wallet Manager Open Wallets Folder</td>
</tr>
<tr>
<td>Specter</td>
<td><kbd>Wallet Settings Export Export To Wallet Software Save wallet file</kbd></td>
<td class="text-nowrap">Specter Desktop</td>
<td>Wallet Settings Export Export To Wallet Software Save wallet file</td>
</tr>
</tbody>
</table>

View file

@ -33,11 +33,14 @@
<button name="command" type="submit" class="btn btn-primary" value="save" id="Continue">Continue</button>
</form>
<table class="table table-sm">
<style>
#AddressTypes .additional { display: none; }
#AddressTypes.expanded .additional { display: table-row; }
</style>
<table id="AddressTypes" class="table table-sm table-sm-nohover">
<thead>
<tr>
<th>Address type</th>
<th class="w-175px">Address type</th>
<th>Example</th>
</tr>
</thead>
@ -78,30 +81,39 @@
<tr>
<td class="text-monospace">pkh(xpub…/0/*)</td>
</tr>
<tr>
<tr class="additional">
<td class="text-nowrap" rowspan="2">Multi-sig P2WSH</td>
<td class="text-monospace">2-of-xpub1…-xpub2…</td>
</tr>
<tr>
<tr class="additional">
<td class="text-monospace">wsh(multi(2,<br>[…/48'/0'/0'/2']xpub…/0/*,<br>[…/48'/0'/0'/2']xpub…/0/*))</td>
</tr>
<tr>
<tr class="additional">
<td class="text-nowrap" rowspan="2">Multi-sig P2SH-P2WSH</td>
<td class="text-monospace">2-of-xpub1…-xpub2…-[p2sh]</td>
</tr>
<tr>
<tr class="additional">
<td class="text-monospace">sh(wsh(multi(2,<br>[…/48'/0'/0'/1']xpub…/0/*,<br>[…/48'/0'/0'/1']xpub…/0/*)))</td>
</tr>
<tr>
<tr class="additional">
<td class="text-nowrap" rowspan="2">Multi-sig P2SH</td>
<td class="text-monospace">2-of-xpub1…-xpub2…-[legacy]</td>
</tr>
<tr>
<tr class="additional">
<td class="text-monospace">sh(multi(2,<br>[…/45'/0]xpub…/0/*,<br>[…/45'/0]xpub…/0/*))</td>
</tr>
</tbody>
</table>
<a id="ToggleAdditional" href="#additional">Show multi-sig examples</a>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script type="text/javascript">
document.getElementById('ToggleAdditional').addEventListener("click", function(e) {
e.preventDefault();
document.getElementById('AddressTypes').classList.toggle('expanded');
});
</script>
}

View file

@ -24,26 +24,26 @@
<div class="image">
<vc:icon symbol="hardware-wallet"/>
</div>
<div class="content">
<h4>
<span class="mr-2">Connect hardware&nbsp;wallet</span>
<span class="badge bg-primary">Recommended</span>
</h4>
<p class="mb-0 text-secondary">Import your keys using our Vault application</p>
<div class="content d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between mr-2">
<div>
<h4>Connect hardware&nbsp;wallet</h4>
<p class="mb-0 text-secondary">Import your keys using our Vault application</p>
</div>
<small class="d-block text-primary mt-2 mt-lg-0">Recommended</small>
</div>
<vc:icon symbol="caret-right" />
</a>
<noscript>
<div class="list-group-item list-group-item-wallet-setup disabled walletsetupcss">
<div class="list-group-item list-group-item-wallet-setup disabled">
<div class="image">
<vc:icon symbol="hardware-wallet"/>
</div>
<div class="content">
<h4>
<span class="mr-2">Connect hardware wallet</span>
<span class="badge bg-primary">Recommended</span>
</h4>
<p class="mb-0">Please enable JavaScript for this option to be available</p>
<div class="content d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between mr-2">
<div>
<h4>Connect hardware wallet</h4>
<p class="mb-0">Please enable JavaScript for this option to be available</p>
</div>
<small class="d-block text-primary mt-2 mt-lg-0">Recommended</small>
</div>
</div>
</noscript>
@ -56,12 +56,12 @@
<div class="image">
<vc:icon symbol="wallet-file"/>
</div>
<div class="content">
<h4>
<span class="mr-2">Import wallet file</span>
<span class="badge bg-primary">Recommended</span>
</h4>
<p class="mb-0 text-secondary">Upload a file exported from your wallet</p>
<div class="content d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between mr-2">
<div>
<h4>Import wallet file</h4>
<p class="mb-0 text-secondary">Upload a file exported from your wallet</p>
</div>
<small class="d-block text-primary mt-2 mt-lg-0">Recommended</small>
</div>
<vc:icon symbol="caret-right" />
</a>
@ -109,12 +109,12 @@
<div class="image">
<vc:icon symbol="seed"/>
</div>
<div class="content">
<h4>
<span class="mr-2">Enter wallet seed</span>
<span class="badge bg-danger" data-toggle="tooltip" data-placement="top" title="You really should not type your seed into a device that is connected to the internet.">Not recommended <span class="fa fa-question-circle-o" title="More information..."></span></span>
</h4>
<p class="mb-0 text-secondary">Provide the 12 or 24 word recovery seed</p>
<div class="content d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between mr-2">
<div>
<h4>Enter wallet seed</h4>
<p class="mb-0 text-secondary">Provide the 12 or 24 word recovery seed</p>
</div>
<small class="d-block text-danger mt-2 mt-lg-0" data-toggle="tooltip" data-placement="top" title="You really should not type your seed into a device that is connected to the internet.">Not recommended <span class="fa fa-question-circle-o" title="More information..."></span></small>
</div>
<vc:icon symbol="caret-right" />
</a>

View file

@ -44,6 +44,10 @@
<th>Source</th>
<td>@Model.Source</td>
</tr>
<tr>
<th>Type</th>
<td>@(Model.IsHotWallet ? "Hot wallet" : "Watch-only wallet")</td>
</tr>
<tr>
<th>
Enabled
@ -57,7 +61,7 @@
</form>
<br>
<form method="get" asp-controller="Stores" asp-action="DeleteWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" class="mt-5">
<a asp-controller="Stores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="ChangeWalletLink" class="btn btn-secondary mr-2">
<a asp-controller="Stores" asp-action="ReplaceWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="ChangeWalletLink" class="btn btn-secondary mr-2">
Replace wallet
</a>
<button type="submit" class="btn btn-danger" id="Delete">Remove wallet</button>

View file

@ -22,8 +22,8 @@
</a>
</nav>
<div class="row justify-content-md-center mt-5">
<main class="col-md-10 col-lg-8 col-xl-6">
<div class="row justify-content-md-center mt-5 pt-sm-3 pt-md-0">
<main class="col-md-10 col-lg-8 col-xl-7">
@if (TempData.HasStatusMessage())
{
<partial name="_StatusMessage"/>

View file

@ -3,12 +3,13 @@
<symbol id="back" viewBox="0 0 21 18"><path d="M7.63754 1.10861L0.578503 8.16764C0.119666 8.62648 0.119666 9.37121 0.578503 9.83122L7.63754 16.8902C8.09637 17.3491 8.8411 17.3491 9.30111 16.8902C9.53053 16.6608 9.64583 16.3608 9.64583 16.0585C9.64583 15.7561 9.53053 15.4561 9.30111 15.2267L4.25038 10.1759H19.0579C19.7085 10.1759 20.2344 9.65004 20.2344 8.99943C20.2344 8.34882 19.7085 7.82293 19.0579 7.82293L4.25038 7.82293L9.30111 2.77219C9.53053 2.54277 9.64583 2.24276 9.64583 1.9404C9.64583 1.63804 9.53053 1.33803 9.30111 1.10861C8.84228 0.649771 8.09755 0.649771 7.63754 1.10861Z" fill="currentColor" /></symbol>
<symbol id="close" viewBox="0 0 16 16"><path d="M9.38526 8.08753L15.5498 1.85558C15.9653 1.43545 15.9653 0.805252 15.5498 0.385121C15.1342 -0.0350102 14.5108 -0.0350102 14.0952 0.385121L7.93072 6.61707L1.76623 0.315098C1.35065 -0.105033 0.727273 -0.105033 0.311688 0.315098C-0.103896 0.73523 -0.103896 1.36543 0.311688 1.78556L6.47618 8.0175L0.311688 14.2495C-0.103896 14.6696 -0.103896 15.2998 0.311688 15.7199C0.519481 15.93 0.796499 16 1.07355 16C1.35061 16 1.62769 15.93 1.83548 15.7199L7.99997 9.48797L14.1645 15.7199C14.3722 15.93 14.6493 16 14.9264 16C15.2034 16 15.4805 15.93 15.6883 15.7199C16.1039 15.2998 16.1039 14.6696 15.6883 14.2495L9.38526 8.08753Z" fill="currentColor"/></symbol>
<symbol id="caret-right" viewBox="0 0 24 24"><path d="M9.5 17L14.5 12L9.5 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></symbol>
<symbol id="new-wallet" viewBox="0 0 48 48"><rect x="0.5" y="0.5" width="47" height="47" rx="1.5" stroke="currentColor" fill="none"/><rect x="6" y="17" width="10" height="4" fill="currentColor"/><rect x="6" y="24" width="10" height="4" fill="currentColor"/><rect x="19" y="17" width="10" height="4" fill="currentColor"/><rect x="32" y="17" width="10" height="4" fill="currentColor"/><rect x="19" y="24" width="10" height="4" fill="currentColor"/><rect x="32" y="24" width="10" height="4" fill="currentColor"/><rect x="6" y="31" width="10" height="4" fill="currentColor"/><rect x="6" y="38" width="10" height="4" fill="currentColor"/><rect x="19" y="31" width="10" height="4" fill="currentColor"/><rect x="19" y="38" width="10" height="4" fill="currentColor"/><rect x="32" y="31" width="10" height="4" fill="currentColor"/><rect x="32" y="38" width="10" height="4" fill="currentColor"/></symbol>
<symbol id="existing-wallet" viewBox="0 0 30 48"><rect x="0.5" y="0.5" width="29" height="47" rx="3.5" stroke="currentColor" fill="none"/></symbol>
<symbol id="hardware-wallet" viewBox="0 0 41 32"><rect x="26.3242" y="5.61324" width="8.83426" height="10.1573" rx="1.125" transform="rotate(-30 26.3242 5.61324)" fill="none" stroke="currentColor" stroke-width="1.75"/><path d="M2.75777 18.9875C1.89483 17.4929 2.40694 15.5817 3.9016 14.7187L23.9126 3.16535C24.4507 2.85469 25.1387 3.03905 25.4494 3.57712L32.7106 16.1539C33.0213 16.692 32.8369 17.3801 32.2988 17.6907L12.2878 29.2441C10.7932 30.107 8.88195 29.5949 8.019 28.1003L2.75777 18.9875Z" fill="none" stroke="currentColor" stroke-width="1.75"/></symbol>
<symbol id="xpub" viewBox="0 0 48 30"><rect x="0.875" y="0.875" width="46.25" height="28.25" rx="1.125" fill="none" stroke="currentColor" stroke-width="1.75"/></symbol>
<symbol id="wallet-file" viewBox="0 0 25 32"><path d="M0.875 0.875H17.1759L23.506 6.85336V31.125H0.875V0.875Z" fill="none" stroke="currentColor" stroke-width="1.75"/></symbol>
<symbol id="new-wallet" viewBox="0 0 32 32"><path d="M16 10V22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 16H10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle fill="none" cx="16" cy="16" r="15" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="existing-wallet" viewBox="0 0 32 32"><g clip-path="url(#clip0)"><path d="M26.5362 7.08746H25.9614V3.25512C25.9614 2.10542 25.3865 1.14734 24.6201 0.572488C23.8536 -0.00236247 22.7039 -0.193979 21.7458 -0.00236246L4.11707 5.36291C2.00929 5.93776 0.667969 7.85392 0.667969 9.96171V12.0695V12.836V27.3988C0.667969 30.0815 2.77575 32.1893 5.45839 32.1893H26.5362C29.2189 32.1893 31.3267 30.0815 31.3267 27.3988V12.0695C31.3267 9.38686 29.2189 7.08746 26.5362 7.08746ZM4.69192 7.08746L22.129 1.91381C22.5123 1.72219 23.0871 1.91381 23.4704 2.10542C23.8536 2.29704 24.0452 2.87189 24.0452 3.25512V7.08746H5.45839C4.88354 7.08746 4.5003 7.27908 3.92545 7.47069C4.11707 7.27908 4.5003 7.08746 4.69192 7.08746ZM29.4105 27.2072C29.4105 28.7402 28.0692 30.0815 26.5362 30.0815H5.45839C3.92545 30.0815 2.58414 28.7402 2.58414 27.2072V12.836V11.8779C2.58414 10.3449 3.92545 9.00362 5.45839 9.00362H26.5362C28.0692 9.00362 29.4105 10.3449 29.4105 11.8779V27.2072Z" fill="currentColor"/><path d="M25.9591 21.6487C27.0174 21.6487 27.8753 20.7908 27.8753 19.7326C27.8753 18.6743 27.0174 17.8164 25.9591 17.8164C24.9009 17.8164 24.043 18.6743 24.043 19.7326C24.043 20.7908 24.9009 21.6487 25.9591 21.6487Z" fill="currentColor"/></g><defs><clipPath id="clip0"><rect width="32" height="32" fill="white"/></clipPath></defs></symbol>
<symbol id="hot-wallet" viewBox="0 0 32 32"><g clip-path="url(#clip0)"><path d="M26.5362 7.08746H25.9614V3.25512C25.9614 2.10542 25.3865 1.14734 24.6201 0.572488C23.8536 -0.00236247 22.7039 -0.193979 21.7458 -0.00236246L4.11707 5.36291C2.00929 5.93776 0.667969 7.85392 0.667969 9.96171V12.0695V12.836V27.3988C0.667969 30.0815 2.77575 32.1893 5.45839 32.1893H26.5362C29.2189 32.1893 31.3267 30.0815 31.3267 27.3988V12.0695C31.3267 9.38686 29.2189 7.08746 26.5362 7.08746ZM4.69192 7.08746L22.129 1.91381C22.5123 1.72219 23.0871 1.91381 23.4704 2.10542C23.8536 2.29704 24.0452 2.87189 24.0452 3.25512V7.08746H5.45839C4.88354 7.08746 4.5003 7.27908 3.92545 7.47069C4.11707 7.27908 4.5003 7.08746 4.69192 7.08746ZM29.4105 27.2072C29.4105 28.7402 28.0692 30.0815 26.5362 30.0815H5.45839C3.92545 30.0815 2.58414 28.7402 2.58414 27.2072V12.836V11.8779C2.58414 10.3449 3.92545 9.00362 5.45839 9.00362H26.5362C28.0692 9.00362 29.4105 10.3449 29.4105 11.8779V27.2072Z" fill="currentColor"/><path d="M25.9591 21.6487C27.0174 21.6487 27.8753 20.7908 27.8753 19.7326C27.8753 18.6743 27.0174 17.8164 25.9591 17.8164C24.9009 17.8164 24.043 18.6743 24.043 19.7326C24.043 20.7908 24.9009 21.6487 25.9591 21.6487Z" fill="currentColor"/></g><defs><clipPath id="clip0"><rect width="32" height="32" fill="white"/></clipPath></defs></symbol>
<symbol id="watchonly-wallet" viewBox="0 0 32 32"><g clip-path="url(#clip0)"><path d="M26.5362 7.08746H25.9614V3.25512C25.9614 2.10542 25.3865 1.14734 24.6201 0.572488C23.8536 -0.00236247 22.7039 -0.193979 21.7458 -0.00236246L4.11707 5.36291C2.00929 5.93776 0.667969 7.85392 0.667969 9.96171V12.0695V12.836V27.3988C0.667969 30.0815 2.77575 32.1893 5.45839 32.1893H26.5362C29.2189 32.1893 31.3267 30.0815 31.3267 27.3988V12.0695C31.3267 9.38686 29.2189 7.08746 26.5362 7.08746ZM4.69192 7.08746L22.129 1.91381C22.5123 1.72219 23.0871 1.91381 23.4704 2.10542C23.8536 2.29704 24.0452 2.87189 24.0452 3.25512V7.08746H5.45839C4.88354 7.08746 4.5003 7.27908 3.92545 7.47069C4.11707 7.27908 4.5003 7.08746 4.69192 7.08746ZM29.4105 27.2072C29.4105 28.7402 28.0692 30.0815 26.5362 30.0815H5.45839C3.92545 30.0815 2.58414 28.7402 2.58414 27.2072V12.836V11.8779C2.58414 10.3449 3.92545 9.00362 5.45839 9.00362H26.5362C28.0692 9.00362 29.4105 10.3449 29.4105 11.8779V27.2072Z" fill="currentColor"/><path d="M25.9591 21.6487C27.0174 21.6487 27.8753 20.7908 27.8753 19.7326C27.8753 18.6743 27.0174 17.8164 25.9591 17.8164C24.9009 17.8164 24.043 18.6743 24.043 19.7326C24.043 20.7908 24.9009 21.6487 25.9591 21.6487Z" fill="currentColor"/></g><defs><clipPath id="clip0"><rect width="32" height="32" fill="white"/></clipPath></defs></symbol>
<symbol id="hardware-wallet" viewBox="0 0 32 32"><rect x="18.9767" y="6.57031" width="6" height="8" rx="1" transform="rotate(-45 18.9767 6.57031)" fill="none" stroke="currentColor" stroke-width="2"/><path d="M3.8871 21.1057C2.71552 19.9341 2.71552 18.0346 3.8871 16.8631L15.888 4.86213C16.2785 4.4716 16.9117 4.4716 17.3022 4.86212L25.7898 13.3497C26.1804 13.7402 26.1804 14.3734 25.7898 14.7639L13.7889 26.7649C12.6173 27.9364 10.7178 27.9364 9.54626 26.7649L3.8871 21.1057Z" fill="none" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="xpub" viewBox="0 0 32 32"><path d="M21.3911 14.0298C20.4238 14.0396 19.4831 13.713 18.73 13.1059C17.9769 12.4988 17.4581 11.649 17.2622 10.7017C17.0664 9.75436 17.2057 8.76844 17.6564 7.91249C18.1071 7.05655 18.8412 6.38377 19.733 6.00919C20.6249 5.6346 21.6192 5.58148 22.5459 5.85891C23.4726 6.13634 24.2742 6.72709 24.8134 7.53015C25.3528 8.33319 25.5964 9.29866 25.5026 10.2614C25.4088 11.2242 24.9834 12.1246 24.2992 12.8084C23.5288 13.5829 22.4836 14.022 21.3911 14.0298ZM21.3911 7.5228C20.9277 7.52249 20.4746 7.65927 20.0888 7.91592C19.703 8.17258 19.4017 8.53764 19.223 8.96514C19.0442 9.39264 18.9959 9.86347 19.0842 10.3184C19.1724 10.7733 19.3933 11.1919 19.7189 11.5215C20.1653 11.9482 20.759 12.1863 21.3765 12.1863C21.9941 12.1863 22.5878 11.9482 23.0342 11.5215C23.359 11.1928 23.5796 10.7755 23.6683 10.3219C23.7571 9.86838 23.71 9.39874 23.5329 8.97182C23.356 8.54491 23.057 8.1797 22.6734 7.92194C22.2898 7.66419 21.8387 7.52534 21.3765 7.5228H21.3911Z" fill="currentColor"/><path d="M11.3293 29.9927C10.6744 29.9903 10.0472 29.7289 9.58436 29.2657L7.81038 27.4844L7.71586 27.608C7.18174 28.1431 6.45693 28.444 5.70089 28.4448C4.94485 28.4454 4.2195 28.1458 3.68441 27.6117C3.14933 27.0776 2.84834 26.3527 2.84766 25.5967C2.84698 24.8406 3.14666 24.1153 3.68078 23.5802L14.172 13.0672C13.4303 11.3826 13.301 9.49181 13.8065 7.722C14.312 5.9522 15.4204 4.41487 16.9399 3.37617C18.4594 2.33747 20.2942 1.8628 22.1268 2.03435C23.9594 2.20589 25.6743 3.01285 26.9746 4.31551C28.2749 5.61816 29.0787 7.3345 29.2469 9.16737C29.4152 11.0002 28.9372 12.8343 27.8957 14.3519C26.8543 15.8695 25.315 16.9751 23.5443 17.4774C21.7736 17.9797 19.883 17.847 18.1998 17.1023L15.0954 20.2067L16.3241 21.4354C16.5544 21.6639 16.7373 21.9357 16.8621 22.2352C16.9868 22.5346 17.0511 22.8559 17.0511 23.1803C17.0511 23.5048 16.9868 23.826 16.8621 24.1255C16.7373 24.425 16.5544 24.6968 16.3241 24.9252C15.8548 25.3728 15.2312 25.6225 14.5828 25.6225C13.9343 25.6225 13.3107 25.3728 12.8415 24.9252L11.6128 23.6893L11.2929 24.0092L13.0742 25.7904C13.4162 26.1364 13.6484 26.5757 13.742 27.0532C13.8354 27.5307 13.7859 28.0252 13.5996 28.4746C13.4132 28.9241 13.0984 29.3086 12.6946 29.5799C12.2908 29.8512 11.8158 29.9974 11.3293 30V29.9927ZM7.81038 25.296C7.92899 25.2954 8.04656 25.3182 8.15636 25.3631C8.26615 25.408 8.36599 25.4742 8.45017 25.5578L10.8712 27.9861C10.9961 28.1011 11.1596 28.1649 11.3293 28.1649C11.4989 28.1649 11.6624 28.1011 11.7873 27.9861C11.8474 27.9259 11.8949 27.8545 11.9274 27.7759C11.9598 27.6973 11.9764 27.613 11.9763 27.5281C11.9769 27.443 11.9604 27.3587 11.928 27.28C11.8955 27.2013 11.8477 27.1299 11.7873 27.07L9.36624 24.649C9.27688 24.5611 9.2068 24.4557 9.16049 24.3393C9.11417 24.2228 9.09263 24.098 9.09724 23.9728C9.09677 23.8536 9.12035 23.7354 9.16656 23.6255C9.21278 23.5156 9.2807 23.4161 9.36624 23.333L10.9948 21.7917C11.0792 21.707 11.1795 21.6399 11.2899 21.594C11.4003 21.5482 11.5187 21.5247 11.6383 21.5247C11.7578 21.5247 11.8762 21.5482 11.9865 21.594C12.0969 21.6399 12.1973 21.707 12.2817 21.7917L14.1575 23.6675C14.2802 23.7835 14.4428 23.8481 14.6119 23.8481C14.7808 23.8481 14.9434 23.7835 15.0663 23.6675C15.1276 23.6078 15.1766 23.5367 15.2102 23.4581C15.2439 23.3795 15.2618 23.2949 15.2626 23.2094C15.2605 23.0381 15.1929 22.8742 15.0735 22.7514L13.176 20.8465C13.0041 20.675 12.9073 20.4423 12.907 20.1995C12.9065 20.0802 12.93 19.9621 12.9763 19.8521C13.0225 19.7423 13.0904 19.6427 13.176 19.5597L17.3855 15.3501C17.5244 15.2094 17.7056 15.1183 17.9014 15.0906C18.0971 15.063 18.2965 15.1006 18.4688 15.1974C19.7515 15.9077 21.2475 16.131 22.6816 15.8261C24.1158 15.5214 25.3917 14.7091 26.2747 13.5387C27.1577 12.3681 27.5884 10.9182 27.4877 9.45553C27.3869 7.99281 26.7614 6.61566 25.7262 5.57732C24.691 4.539 23.3158 3.90933 21.8534 3.8041C20.391 3.69889 18.9398 4.1252 17.7666 5.00464C16.5935 5.88408 15.7773 7.15751 15.4681 8.59074C15.1591 10.024 15.3777 11.5206 16.0841 12.8055C16.1792 12.977 16.2157 13.1749 16.1881 13.369C16.1606 13.5632 16.0705 13.7432 15.9314 13.8815L4.96764 24.8307C4.8026 25.0286 4.71754 25.281 4.72917 25.5385C4.74081 25.796 4.84829 26.0397 5.0305 26.2219C5.21272 26.4041 5.4565 26.5117 5.71392 26.5233C5.97135 26.5349 6.22383 26.4499 6.42173 26.2848L7.14877 25.5578C7.32593 25.3863 7.56388 25.2922 7.81038 25.296Z" fill="currentColor"/></symbol>
<symbol id="wallet-file" viewBox="0 0 32 32"><path d="M5 1H20.8479L27 6.90258V31H5V1Z" fill="none" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="scan-qr" viewBox="0 0 32 32"><path d="M20 .875h10c.621 0 1.125.504 1.125 1.125v10m0 8v10c0 .621-.504 1.125-1.125 1.125H20m-8 0H2A1.125 1.125 0 01.875 30V20m0-8V2C.875 1.379 1.379.875 2 .875h10" stroke="currentColor" stroke-width="1.75" fill="none" fill-rule="evenodd"/></symbol>
<symbol id="seed" viewBox="0 0 48 37"><rect x="0.875" y="0.875" width="46.25" height="35.25" rx="1.125" fill="none" stroke="currentColor" stroke-width="1.75"/><rect x="6" y="6" width="10" height="4" fill="currentColor"/><rect x="6" y="13" width="10" height="4" fill="currentColor"/><rect x="19" y="6" width="10" height="4" fill="currentColor"/><rect x="32" y="6" width="10" height="4" fill="currentColor"/><rect x="19" y="13" width="10" height="4" fill="currentColor"/><rect x="32" y="13" width="10" height="4" fill="currentColor"/><rect x="6" y="20" width="10" height="4" fill="currentColor"/><rect x="6" y="27" width="10" height="4" fill="currentColor"/><rect x="19" y="20" width="10" height="4" fill="currentColor"/><rect x="19" y="27" width="10" height="4" fill="currentColor"/><rect x="32" y="20" width="10" height="4" fill="currentColor"/><rect x="32" y="27" width="10" height="4" fill="currentColor"/></symbol>
<symbol id="seed" viewBox="0 0 32 32"><rect x="0.875" y="2.875" width="30.25" height="26.25" rx="1.125" fill="none" stroke="currentColor" stroke-width="1.75"/><rect x="5" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="21" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="21" width="9" height="4" rx="0.5" fill="currentColor"/></symbol>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -132,6 +132,10 @@ h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
background-color: var(--btcpay-color-white);
}
.table-sm.table-sm-nohover tbody tr:hover {
background-color: transparent;
}
.removetopborder tr:first-child th,
.removetopborder tr:first-child td {
border-top: none;

View file

@ -20,30 +20,47 @@ body {
top: 0;
left: 0;
right: 0;
padding: 30px;
padding: 24px;
}
}
#wizard-navbar a {
position: relative;
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;
width: 56px;
height: 56px;
border-radius: 50%;
}
#wizard-navbar a::after {
position: absolute;
top: 0;
left: 0;
z-index: -1;
content: "";
width: 100%;
height: 100%;
border-radius: 50%;
transition: background-color .2s, transform .2s;
background-color: transparent;
}
#wizard-navbar a svg.icon {
width: 19px;
height: 16px;
}
#wizard-navbar a:hover {
#wizard-navbar a:hover::after {
background-color: var(--btcpay-border-color-medium);
}
#wizard-navbar a:active::after {
transform: scale(.825);
}
#wizard-navbar .cancel {
margin-left: auto;
}
@ -70,28 +87,9 @@ body {
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 {
.list-group-item-wallet-setup .image .icon {
width: 32px;
}
.list-group-item-wallet-setup .image .icon-wallet-file {
width: 24px;
height: 32px;
}
.list-group-item-wallet-setup .content {
@ -99,11 +97,10 @@ body {
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 .content small {
font-size: 90%;
text-transform: uppercase;
white-space: nowrap;
}
.list-group-item-wallet-setup .image + .content {