mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
Wallet setup redesign (#2164)
* Prepare existing layouts and views * Add icon view component and sprite svg * Add wallet setup basics * Add import method view basics * Use external sprite file instead of inline svg * Refactor hardware wallet setup flow * Manually enter an xpub * Prepare other views * Update views and models * Finalize wallet setup flow * Updat tests, part 1 * Update tests, part 2 * Vaul: Fix missing retry button * Add better Scan QR subtext Still tbd. * Make wallet account an advanced setting * Prevent empty xpub * Use textarea for seed input * Remove redundant error message for missing file upload * Confirm store updates after generating a new wallet * Update wording * Modify existing wallets * Fix proposed method name * Suggest using ColdCard Electrum export option only Advise the user to use the electrum export of the coldcard instead of saying either electrum or wasabi export file … the electurm one contains more info, e.g. the wasabi one doesn't include the account key path. * More concise WalletSetupMethod setting * Test fix * Update wallet removal code * Fix back navigation quirk in change wallet case * Fix behaviour on wallet enable/disable * Fix initial wallet setup * Improve modify view and messages * Test fixes * Seed import fix Uses the correct form url for confirming addresses * Quickfixes from design meeting * Add enable toggle switch on modify page * Confirm wallet removal * Update setup view * Update import view * Icon finetuning * Improve import options page * Refactor QR code scanner Allow for usage with and without modal * Update copy and instructions on import pages * Split generate options: Hot wallet and watch-only * Implement hot wallet options correctly * Minor test changes * Navbar improvements * Fix tables * Fix badge color * Routing related updates Thanks @kukks for the suggestions! * Wording updates Thanks @kukks for the suggestions! * Extend address types table for xpub import Thanks @kukks for the suggestions! * Rename controller * Unify precondition checks * Improve removal warning for hot wallets * Add tooltip on why seed import is not recommended * Add tooltip icon * Add Specter import info
This commit is contained in:
parent
3736cbc107
commit
f4fa7c927c
@ -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();
|
||||
|
@ -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();
|
||||
|
513
BTCPayServer/Controllers/StoresController.Onchain.cs
Normal file
513
BTCPayServer/Controllers/StoresController.Onchain.cs
Normal file
@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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 = "<span class='text-centered'>Your wallet has been generated.</span>"
|
||||
});
|
||||
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<IActionResult> 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 ? "<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-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>";
|
||||
|
||||
return View("Confirm", new ConfirmModel
|
||||
{
|
||||
Title = $"Remove {network.CryptoCode} wallet",
|
||||
Description = description,
|
||||
DescriptionHtml = true,
|
||||
Action = "Remove"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/delete")]
|
||||
public async Task<IActionResult> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
39
BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs
Normal file
39
BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs
Normal file
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
@ -6,6 +6,10 @@
|
||||
Layout = "_LayoutSimple";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
||||
<div class="row justify-content-center mb-2">
|
||||
<div class="col text-center">
|
||||
<a asp-controller="Home" asp-action="Index" tabindex="-1">
|
||||
|
@ -5,6 +5,10 @@
|
||||
Layout = "_LayoutSimple";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
||||
<div class="row justify-content-center mb-2">
|
||||
<div class="col text-center">
|
||||
<a asp-controller="Home" asp-action="Index">
|
||||
|
@ -5,6 +5,10 @@
|
||||
Layout = "_LayoutSimple";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
||||
<div class="row justify-content-center mb-2">
|
||||
<div class="col text-center">
|
||||
<a asp-controller="Home" asp-action="Index">
|
||||
|
@ -4,6 +4,10 @@
|
||||
ViewData["Title"] = "Your recovery phrase";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
||||
<style>
|
||||
@@media (min-width: 476px) { ol#recovery-phrase {max-height:16em;} }
|
||||
@@media (min-width: 768px) { ol#recovery-phrase {max-height:12em;} }
|
||||
|
@ -1,218 +1,182 @@
|
||||
<div id="camera-qr-scanner-modal-app" v-cloak class="only-for-js">
|
||||
<div class="modal fade" data-backdrop="static" :id="modalId">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{title}}
|
||||
<template id="camera-qr-scanner-wrap">
|
||||
<div v-if="modalId" :id="modalId" class="modal fade" data-backdrop="static">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{title}}
|
||||
<span v-if="workload.length > 0">Animated QR detected: {{workload.length}} / {{workload[0].total}} scanned</span>
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" v-on:click="close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-0" v-if="loaded" v-bind:class="{'alert-danger': errorMessage}">
|
||||
<qrcode-drop-zone v-on:decode="onDecode" v-on:init="logErrors">
|
||||
<qrcode-stream v-on:decode="onDecode" v-on:init="onInit" v-bind:camera="camera" v-bind:track="paint">
|
||||
<div v-if="data || errorMessage" class="pending-action">
|
||||
</h5>
|
||||
<button type="button" class="close" aria-label="Close" v-on:click="close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot></slot>
|
||||
<div v-if="workload.length > 0">Animated QR detected: {{workload.length}} / {{workload[0].total}} scanned</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="text-danger p-2" v-if="errorMessage">{{errorMessage}}</div>
|
||||
<span class="text-muted text-truncate">{{data}}</span>
|
||||
<div class="w-100 btn-group">
|
||||
<button v-if="data" type="button" class="btn btn-primary" data-dismiss="modal" v-on:click="submitData">Submit</button>
|
||||
<button type="button" class="btn btn-secondary" v-on:click="retry">Retry</button>
|
||||
<button type="button" class="btn btn-danger" data-dismiss="modal" v-on:click="close">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</qrcode-stream>
|
||||
</qrcode-drop-zone>
|
||||
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" v-bind:camera="camera"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="camera-qr-scanner-modal-app" v-cloak class="only-for-js">
|
||||
<scanner-wrap v-bind="$data" v-on:close="close">
|
||||
<div v-if="isLoaded && requestInput" class="d-flex justify-content-center align-items-center" :class="{'border border-secondary': !isModal}">
|
||||
<div class="spinner-border text-secondary position-absolute" role="status"></div>
|
||||
<qrcode-drop-zone v-on:decode="onDecode" v-on:init="logErrors">
|
||||
<qrcode-stream v-on:decode="onDecode" v-on:init="onInit" :camera="camera" :track="paint"/>
|
||||
</qrcode-drop-zone>
|
||||
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" :camera="camera"/>
|
||||
</div>
|
||||
<div v-else-if="qrData || errorMessage">
|
||||
<div v-if="errorMessage" class="alert alert-danger" role="alert">
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
<div class="text-break text-monospace">
|
||||
{{qrData}}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="button" class="btn btn-primary mr-1" v-if="qrData" v-on:click="submitData">Submit</button>
|
||||
<button type="button" class="btn btn-secondary mr-1" v-on:click="retry">Retry</button>
|
||||
<button type="button" class="btn btn-outline-secondary" v-if="isModal" v-on:click="close">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</scanner-wrap>
|
||||
</div>
|
||||
<style>
|
||||
.pending-action {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, .8);
|
||||
text-align: center;
|
||||
font-size: 1.4rem;
|
||||
padding: 10px;
|
||||
word-wrap: break-word;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<script>
|
||||
function initCameraScanningApp(title, onDataSubmit, modalId)
|
||||
{
|
||||
function initCameraScanningApp(title, onDataSubmit, modalId) {
|
||||
const isModal = !!modalId;
|
||||
|
||||
new Vue(
|
||||
{
|
||||
Vue.component('scanner-wrap', {
|
||||
props: ["modalId", "title", "workload"],
|
||||
template: "#camera-qr-scanner-wrap",
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: '#camera-qr-scanner-modal-app',
|
||||
data:
|
||||
{
|
||||
noStreamApiSupport: false,
|
||||
loaded: false,
|
||||
workload: [],
|
||||
data: "",
|
||||
title: title,
|
||||
errorMessage: "",
|
||||
modalId: modalId
|
||||
},
|
||||
mounted: function ()
|
||||
{
|
||||
var self = this;
|
||||
$("#" + this.modalId)
|
||||
.on("shown.bs.modal", function ()
|
||||
{
|
||||
self.loaded = true;
|
||||
})
|
||||
.on("hide.bs.modal", function ()
|
||||
{
|
||||
self.close();
|
||||
});
|
||||
},
|
||||
computed:
|
||||
{
|
||||
camera: function ()
|
||||
{
|
||||
return this.data ? "off" : "auto";
|
||||
data() {
|
||||
return {
|
||||
isModal,
|
||||
isLoaded: !isModal,
|
||||
title: title,
|
||||
modalId: modalId,
|
||||
noStreamApiSupport: false,
|
||||
qrData: null,
|
||||
errorMessage: null,
|
||||
workload: [],
|
||||
camera: "auto"
|
||||
}
|
||||
},
|
||||
methods:
|
||||
{
|
||||
retry: function ()
|
||||
{
|
||||
if (!this.data)
|
||||
{
|
||||
this.close();
|
||||
this.$nextTick(function ()
|
||||
{
|
||||
this.loaded = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.data = "";
|
||||
this.workload = [];
|
||||
this.errorMessage = "";
|
||||
mounted() {
|
||||
if (this.isModal) {
|
||||
const $modal = $("#" + this.modalId);
|
||||
$modal.on("shown.bs.modal", () => { this.isLoaded = true; });
|
||||
$modal.on("hide.bs.modal", () => { this.isLoaded = false; });
|
||||
} else {
|
||||
this.isLoaded = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
requestInput() {
|
||||
return this.camera === 'auto' && this.errorMessage === null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setQrData (qrData) {
|
||||
this.qrData = qrData;
|
||||
this.camera = qrData ? "off" : "auto";
|
||||
},
|
||||
close: function ()
|
||||
{
|
||||
this.loaded = false;
|
||||
this.data = "";
|
||||
this.workload = [];
|
||||
this.errorMessage = "";
|
||||
retry() {
|
||||
this.camera = "off";
|
||||
this.$nextTick(this.reset);
|
||||
},
|
||||
onDecode: function (content)
|
||||
{
|
||||
if (this.data)
|
||||
{
|
||||
return;
|
||||
reset() {
|
||||
this.setQrData(null);
|
||||
this.errorMessage = null;
|
||||
this.workload = [];
|
||||
},
|
||||
close() {
|
||||
if (this.modalId) {
|
||||
$("#" + this.modalId).modal('hide');
|
||||
}
|
||||
if (!content.toLowerCase().startsWith("ur:"))
|
||||
{
|
||||
this.data = content;
|
||||
this.reset();
|
||||
},
|
||||
onDecode(content) {
|
||||
if (this.qrData) return;
|
||||
|
||||
if (!content.toLowerCase().startsWith("ur:")) {
|
||||
this.setQrData(content);
|
||||
this.workload = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
const [index, total] = window.bcur.extractSingleWorkload(content);
|
||||
if (this.workload.length > 0)
|
||||
{
|
||||
if (this.workload.length > 0) {
|
||||
const currentTotal = this.workload[0].total;
|
||||
if (total !== currentTotal)
|
||||
{
|
||||
if (total !== currentTotal) {
|
||||
this.workload = [];
|
||||
}
|
||||
}
|
||||
if (!this.workload.find(i => i.index === index))
|
||||
{
|
||||
this.workload.push(
|
||||
{
|
||||
if (!this.workload.find(i => i.index === index)) {
|
||||
this.workload.push({
|
||||
index,
|
||||
total,
|
||||
data: content,
|
||||
});
|
||||
if (this.workload.length === total)
|
||||
{
|
||||
this.data = window.bcur.decodeUR(this.workload.map(i => i.data));
|
||||
if (this.workload.length === total) {
|
||||
const decoded = window.bcur.decodeUR(this.workload.map(i => i.data));
|
||||
this.setQrData(decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
submitData: function ()
|
||||
{
|
||||
if (onDataSubmit)
|
||||
{
|
||||
onDataSubmit(this.data);
|
||||
submitData() {
|
||||
if (onDataSubmit) {
|
||||
onDataSubmit(this.qrData);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
logErrors: function (promise)
|
||||
{
|
||||
logErrors(promise) {
|
||||
promise.catch(console.error)
|
||||
},
|
||||
paint: function (location, ctx)
|
||||
{
|
||||
paint(location, ctx) {
|
||||
ctx.fillStyle = '#137547';
|
||||
[
|
||||
location.topLeftFinderPattern,
|
||||
location.topRightFinderPattern,
|
||||
location.bottomLeftFinderPattern
|
||||
].forEach((
|
||||
{
|
||||
x,
|
||||
y
|
||||
}) =>
|
||||
{
|
||||
].forEach(({ x, y }) => {
|
||||
ctx.fillRect(x - 5, y - 5, 10, 10);
|
||||
})
|
||||
},
|
||||
onInit: function (promise)
|
||||
{
|
||||
var self = this;
|
||||
promise.then(() =>
|
||||
{
|
||||
self.errorMessage = "";
|
||||
})
|
||||
.catch(error =>
|
||||
{
|
||||
if (error.name === 'StreamApiNotSupportedError')
|
||||
{
|
||||
self.noStreamApiSupport = true;
|
||||
}
|
||||
else if (error.name === 'NotAllowedError')
|
||||
{
|
||||
self.errorMessage = 'A permission to the camera is needed to scan the QR code.'
|
||||
}
|
||||
else if (error.name === 'NotFoundError')
|
||||
{
|
||||
self.errorMessage = 'A camera was not detected on your device.'
|
||||
}
|
||||
else if (error.name === 'NotSupportedError')
|
||||
{
|
||||
self.errorMessage = 'This page is served in non-secure context (HTTPS, localhost or file://)'
|
||||
}
|
||||
else if (error.name === 'NotReadableError')
|
||||
{
|
||||
self.errorMessage = 'Couldn\'t access your camera. Is it already in use?'
|
||||
}
|
||||
else if (error.name === 'OverconstrainedError')
|
||||
{
|
||||
self.errorMessage = 'Constraints don\'t match any installed camera.'
|
||||
}
|
||||
else
|
||||
{
|
||||
self.errorMessage = 'UNKNOWN ERROR: ' + error.message
|
||||
}
|
||||
})
|
||||
onInit(promise) {
|
||||
promise.then(() => {
|
||||
this.errorMessage = null;
|
||||
}).catch(error => {
|
||||
if (error.name === 'StreamApiNotSupportedError') {
|
||||
this.noStreamApiSupport = true;
|
||||
} else if (error.name === 'NotAllowedError') {
|
||||
this.errorMessage = 'A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry.'
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
this.errorMessage = 'A camera was not detected on your device.'
|
||||
} else if (error.name === 'NotSupportedError') {
|
||||
this.errorMessage = 'This page is served in non-secure context (HTTPS, localhost or file://)'
|
||||
} else if (error.name === 'NotReadableError') {
|
||||
this.errorMessage = 'Couldn\'t access your camera. Is it already in use?'
|
||||
} else if (error.name === 'OverconstrainedError') {
|
||||
this.errorMessage = 'Constraints don\'t match any installed camera.'
|
||||
} else {
|
||||
this.errorMessage = 'UNKNOWN ERROR: ' + error.message
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -11,12 +11,12 @@
|
||||
}
|
||||
<title>@ViewData["Title"]</title>
|
||||
@* CSS *@
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.CreativeStartUri)" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.ThemeUri)" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.CreativeStartUri)" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.ThemeUri)" rel="stylesheet" asp-append-version="true" />
|
||||
@if (!String.IsNullOrWhiteSpace(themeManager.CustomThemeUri))
|
||||
{
|
||||
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.CustomThemeUri)" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.CustomThemeUri)" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
<bundle name="wwwroot/bundles/main-bundle.min.css" asp-append-version="true" />
|
||||
@* JS *@
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script id="VaultConnection" type="text/template">
|
||||
<div class="vault-feedback vault-feedback1">
|
||||
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
|
||||
<div class="vault-feedback vault-feedback1 mb-2 d-flex">
|
||||
<span class="vault-feedback-icon mt-1 mr-2"></span> <span class="vault-feedback-content flex-1"></span>
|
||||
</div>
|
||||
<div class="vault-feedback vault-feedback2">
|
||||
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
|
||||
<div class="vault-feedback vault-feedback2 mb-2 d-flex">
|
||||
<span class="vault-feedback-icon mt-1 mr-2"></span> <span class="vault-feedback-content flex-1"></span>
|
||||
</div>
|
||||
<div class="vault-feedback vault-feedback3">
|
||||
<span class="vault-feedback-icon"></span> <span class="vault-feedback-content"></span>
|
||||
<div class="vault-feedback vault-feedback3 mb-2 d-flex">
|
||||
<span class="vault-feedback-icon mt-1 mr-2"></span> <span class="vault-feedback-content flex-1"></span>
|
||||
</div>
|
||||
<div id="pin-input" class="mt-4" style="display: none;">
|
||||
<div class="row">
|
||||
|
@ -9,7 +9,6 @@
|
||||
|
||||
<head>
|
||||
<partial name="Header" />
|
||||
|
||||
@RenderSection("HeadScripts", required: false)
|
||||
@RenderSection("HeaderContent", false)
|
||||
</head>
|
||||
|
@ -1,10 +1,13 @@
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en"@(env.IsDeveloping ? " data-devenv" : "")>
|
||||
<head>
|
||||
<partial name="Header" />
|
||||
@RenderSection("HeadScripts", false)
|
||||
@RenderSection("HeaderContent", false)
|
||||
</head>
|
||||
<body>
|
||||
<section class="content-wrapper @(ViewBag.TopSmallMargin != null ? "pt-4" : "")">
|
||||
@ -14,6 +17,6 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
</section>
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
@RenderSection("Scripts", false)
|
||||
</body>
|
||||
</html>
|
||||
|
@ -102,7 +102,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary vault-retry" style="display:none;" type="button">Retry</button>
|
||||
<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>
|
||||
|
26
BTCPayServer/Views/Stores/GenerateWallet.cshtml
Normal file
26
BTCPayServer/Views/Stores/GenerateWallet.cshtml
Normal file
@ -0,0 +1,26 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
var isHotWallet = Model.Method == WalletSetupMethod.HotWallet;
|
||||
var type = isHotWallet ? "Hot" : "Watch-Only";
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = $"Create {Model.CryptoCode} {type} Wallet";
|
||||
ViewData.Add(nameof(Model.CanUseHotWallet), Model.CanUseHotWallet);
|
||||
ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport);
|
||||
ViewData.Add(nameof(Model.Method), Model.Method);
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<h1 class="text-center">@ViewData["Title"]</h1>
|
||||
<br>
|
||||
|
||||
@await Html.PartialAsync("_GenerateWalletForm", Model.SetupRequest)
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
60
BTCPayServer/Views/Stores/GenerateWalletOptions.cshtml
Normal file
60
BTCPayServer/Views/Stores/GenerateWalletOptions.cshtml
Normal file
@ -0,0 +1,60 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = $"Generate {Model.CryptoCode} Wallet";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<h1 class="text-center">Choose your wallet option</h1>
|
||||
|
||||
<div class="list-group mt-5">
|
||||
@if (Model.CanUseHotWallet)
|
||||
{
|
||||
<a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="hotwallet" id="generate-hotwallet-link" class="list-group-item list-group-item-action list-group-item-wallet-setup">
|
||||
<div class="image">
|
||||
<vc:icon symbol="seed"/>
|
||||
</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!
|
||||
</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list-group-item list-group-item-wallet-setup text-muted">
|
||||
<div class="image">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>Hot wallet</h4>
|
||||
<p class="mb-0">Please note that creating a hot wallet is not supported by this instance for non administrators.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<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="generate-watchonly-link" class="list-group-item list-group-item-action list-group-item-wallet-setup">
|
||||
<div class="image">
|
||||
<vc:icon symbol="xpub"/>
|
||||
</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>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
156
BTCPayServer/Views/Stores/ImportWallet/ConfirmAddresses.cshtml
Normal file
156
BTCPayServer/Views/Stores/ImportWallet/ConfirmAddresses.cshtml
Normal file
@ -0,0 +1,156 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = "Confirm addresses";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@Model.Method">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</p>
|
||||
</header>
|
||||
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
}
|
||||
|
||||
<template id="modal-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" Fid="exampleModalLabel">Address verification</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Confirm that you see the following address on the device:
|
||||
<code id="displayed-address"></code>
|
||||
</p>
|
||||
<div id="vault-status"></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>
|
||||
<div id="btcpayservervault" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="btcpayservervault" aria-hidden="true"></div>
|
||||
|
||||
<form method="post" asp-controller="Stores" asp-action="UpdateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
|
||||
<input asp-for="Config" type="hidden"/>
|
||||
<input asp-for="Confirmation" type="hidden"/>
|
||||
<input asp-for="DerivationScheme" type="hidden"/>
|
||||
<input asp-for="Enabled" type="hidden"/>
|
||||
|
||||
<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></th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var sample in Model.AddressSamples)
|
||||
{
|
||||
<tr>
|
||||
<td>@sample.KeyPath</td>
|
||||
<td><code>@sample.Address</code></td>
|
||||
@if (Model.Source == "Vault")
|
||||
{
|
||||
<td class="text-right">
|
||||
@* Using single quotes for the data attributes on purpose *@
|
||||
<a href="#" data-address='@Safe.Json(sample.Address)' data-rooted-key-path='@Safe.Json(sample.RootedKeyPath.ToString())'>Show on device</a>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#wrong-addresses" aria-expanded="false" aria-controls="wrong-addresses">
|
||||
Wrong addresses?
|
||||
</button>
|
||||
<div id="wrong-addresses" class="collapse @(ViewContext.ModelState.IsValid ? "" : "show")">
|
||||
<div class="pb-1">
|
||||
<label asp-for="HintAddress">Help us to find the correct settings by telling us the first address of your wallet.</label>
|
||||
<div class="form-group">
|
||||
<input asp-for="HintAddress" class="form-control"/>
|
||||
<span asp-validation-for="HintAddress" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button name="command" type="submit" class="btn btn-primary" value="save" id="Confirm">Confirm</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
@await Html.PartialAsync("VaultElements")
|
||||
|
||||
<script src="~/js/vaultbridge.js" type="text/javascript" defer asp-append-version="true"></script>
|
||||
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer asp-append-version="true"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener("load", async () => {
|
||||
const wsPath = "@Url.Action("VaultBridgeConnection", "Vault", new {cryptoCode = Model.CryptoCode})";
|
||||
const wsProto = location.protocol.replace(/^http/, "ws");
|
||||
|
||||
const statusHTML = document.getElementById("VaultConnection").innerHTML;
|
||||
const modalHTML = document.getElementById("modal-template").innerHTML;
|
||||
const $modal = document.getElementById("btcpayservervault");
|
||||
|
||||
document.querySelectorAll("[data-address]").forEach(link => {
|
||||
link.addEventListener("click", async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const $link = event.currentTarget;
|
||||
const address = JSON.parse($link.dataset.address);
|
||||
const rootedKeyPath = JSON.parse($link.dataset.rootedKeyPath);
|
||||
|
||||
$modal.innerHTML = modalHTML;
|
||||
|
||||
const $address = document.getElementById("displayed-address");
|
||||
const $status = document.getElementById("vault-status");
|
||||
|
||||
$status.innerHTML = statusHTML;
|
||||
$address.innerText = address;
|
||||
|
||||
const vaultUI = new vaultui.VaultBridgeUI(`${wsProto}//${location.host}${wsPath}`);
|
||||
|
||||
const $$modal = $($modal)
|
||||
$$modal.modal();
|
||||
$$modal.on('hidden.bs.modal', () => {
|
||||
vaultUI.closeBridge();
|
||||
});
|
||||
|
||||
while (!await vaultUI.askForDevice()) {}
|
||||
|
||||
await vaultUI.askForDisplayAddress(rootedKeyPath);
|
||||
|
||||
$$modal.modal("hide");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
58
BTCPayServer/Views/Stores/ImportWallet/File.cshtml
Normal file
58
BTCPayServer/Views/Stores/ImportWallet/File.cshtml
Normal file
@ -0,0 +1,58 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = "Import your wallet file";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">Upload the file exported from your wallet.</p>
|
||||
</header>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="my-5">
|
||||
<div class="form-group">
|
||||
<label asp-for="WalletFile"></label>
|
||||
<input asp-for="WalletFile" type="file" class="form-control-file" required>
|
||||
<span asp-validation-for="WalletFile" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Continue</button>
|
||||
</form>
|
||||
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wallet</th>
|
||||
<th>Instructions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-nowrap">Cobo Vault</td>
|
||||
<td>Settings ❯ Watch-Only Wallet ❯ BTCPay ❯ Export Wallet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ColdCard</td>
|
||||
<td>Advanced ❯ MicroSD Card ❯ Electrum Wallet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Electrum</td>
|
||||
<td>File ❯ Save backup (not encrypted with a password)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wasabi</td>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
113
BTCPayServer/Views/Stores/ImportWallet/Hardware.cshtml
Normal file
113
BTCPayServer/Views/Stores/ImportWallet/Hardware.cshtml
Normal file
@ -0,0 +1,113 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = "Connect your hardware wallet";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">In order to securely connect to your hardware wallet you must first download, install, and run the BTCPay Server Vault.</p>
|
||||
</header>
|
||||
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
}
|
||||
|
||||
<div class="row mt-5 mb-4">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div id="vault-status" class="mb-4"></div>
|
||||
<div id="vault-xpub" class="mt-4" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label for="addressType">Address type</label>
|
||||
<select id="addressType" name="addressType" class="form-control w-auto">
|
||||
<option value="segwit">Segwit (Recommended, cheapest fee)</option>
|
||||
<option value="segwitWrapped">Segwit wrapped (Compatible with old wallets)</option>
|
||||
<option value="legacy">Legacy (Not recommended)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="accountNumber">Account</label>
|
||||
<select id="accountNumber" name="accountNumber" class="form-control w-auto">
|
||||
@for (int i = 0; i < 20; i++)
|
||||
{
|
||||
<option value="@i">@i</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" id="vault-confirm" class="btn btn-primary" style="display:none;"></button>
|
||||
<button type="button" id="vault-retry" class="btn btn-secondary" style="display:none;">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="walletInfo" style="display:none;">
|
||||
<input asp-for="Config" type="hidden" />
|
||||
<input asp-for="CryptoCode" type="hidden" />
|
||||
<input asp-for="AccountKey" type="hidden" />
|
||||
<input asp-for="Source" type="hidden" value="Vault"/>
|
||||
<input asp-for="DerivationSchemeFormat" type="hidden" value="BTCPay" />
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<h4 class="mb-3">Public Key Information</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationScheme"></label>
|
||||
<textarea asp-for="DerivationScheme" class="form-control store-derivation-scheme text-monospace py-2" rows="3" readonly></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RootFingerprint"></label>
|
||||
<input asp-for="RootFingerprint" class="form-control" readonly />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="KeyPath"></label>
|
||||
<input asp-for="KeyPath" class="form-control" readonly />
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="save" id="Continue">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
@await Html.PartialAsync("VaultElements")
|
||||
|
||||
<script src="~/js/vaultbridge.js" defer asp-append-version="true"></script>
|
||||
<script src="~/js/vaultbridge.ui.js" defer asp-append-version="true"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener("load", async () => {
|
||||
const wsPath = "@Url.Action("VaultBridgeConnection", "Vault", new {cryptoCode = Model.CryptoCode})";
|
||||
const wsProto = location.protocol.replace(/^http/, "ws");
|
||||
const vaultUI = new vaultui.VaultBridgeUI(`${wsProto}//${location.host}${wsPath}`);
|
||||
|
||||
document.getElementById("vault-status").innerHTML = document.getElementById("VaultConnection").innerHTML;
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
vaultUI.closeBridge();
|
||||
});
|
||||
|
||||
while (!await vaultUI.askForDevice() || !await vaultUI.askForXPubs()) {};
|
||||
|
||||
const { xpub: { strategy, fingerprint, accountKey, keyPath } } = vaultUI;
|
||||
|
||||
document.getElementById("DerivationScheme").value = strategy;
|
||||
document.getElementById("RootFingerprint").value = fingerprint;
|
||||
document.getElementById("AccountKey").value = accountKey;
|
||||
document.getElementById("KeyPath").value = keyPath;
|
||||
|
||||
document.getElementById("walletInfo").style = null;
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
75
BTCPayServer/Views/Stores/ImportWallet/Scan.cshtml
Normal file
75
BTCPayServer/Views/Stores/ImportWallet/Scan.cshtml
Normal file
@ -0,0 +1,75 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = "Scan QR code";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">Scan the extended public key, also called "xpub", shown on your wallet's display.</p>
|
||||
</header>
|
||||
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
}
|
||||
|
||||
<div class="my-5">
|
||||
<partial name="CameraScanner"/>
|
||||
<form id="qr-import-form" method="post">
|
||||
<input asp-for="WalletFileContent" type="hidden" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="mt-5">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wallet</th>
|
||||
<th>Instructions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-nowrap">Cobo Vault</td>
|
||||
<td>Open Wallet Settings ❯ Show/Export XPUB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BlueWallet</td>
|
||||
<td>Open Wallet Settings ❯ Show Wallet XPUB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Specter DIY</td>
|
||||
<td>Master public keys ❯ Select key ❯ Disable "Show derivation path"</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
|
||||
<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 type="text/javascript">
|
||||
window.coinName = "@Model.Network.DisplayName.ToLowerInvariant()";
|
||||
window.addEventListener("load", async () => {
|
||||
initCameraScanningApp("Scan wallet QR", data => {
|
||||
document.getElementById("WalletFileContent").value = data;
|
||||
document.getElementById("qr-import-form").submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
35
BTCPayServer/Views/Stores/ImportWallet/Seed.cshtml
Normal file
35
BTCPayServer/Views/Stores/ImportWallet/Seed.cshtml
Normal file
@ -0,0 +1,35 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = "Enter the wallet seed";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">Manually enter your 12 or 24 word recovery seed.</p>
|
||||
</header>
|
||||
|
||||
<div class="my-5">
|
||||
@if (Model.CanUseHotWallet)
|
||||
{
|
||||
ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport);
|
||||
ViewData.Add(nameof(Model.Method), Model.Method);
|
||||
|
||||
@await Html.PartialAsync("_GenerateWalletForm", Model.SetupRequest)
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="mb-0">Please note that creating a wallet is not supported by your instance.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
107
BTCPayServer/Views/Stores/ImportWallet/Xpub.cshtml
Normal file
107
BTCPayServer/Views/Stores/ImportWallet/Xpub.cshtml
Normal file
@ -0,0 +1,107 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = $"Enter your extended public key";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">
|
||||
This key, also called "xpub", is used to generate individual destination addresses for your invoices.
|
||||
<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>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form method="post" class="my-5">
|
||||
<input asp-for="Config" type="hidden" />
|
||||
<input asp-for="CryptoCode" type="hidden" />
|
||||
<input asp-for="DerivationSchemeFormat" type="hidden" />
|
||||
<input asp-for="AccountKey" type="hidden" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationScheme">Extended public key</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>
|
||||
|
||||
<button name="command" type="submit" class="btn btn-primary" value="save" id="Continue">Continue</button>
|
||||
</form>
|
||||
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Address type</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="4">P2WPKH</td>
|
||||
<td class="text-monospace">xpub…</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">zpub…</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">wpkh(xpub…/0/*)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">wpkh([…/84'/0'/0']xpub…/0/*)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">P2SH-P2WPKH</td>
|
||||
<td class="text-monospace">xpub…-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">ypub…</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">sh(wpkh(xpub…/0/*)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">sh(wpkh([…/49'/0'/0']xpub…/0/*)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">P2PKH</td>
|
||||
<td class="text-monospace">xpub…-[legacy]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">pkh([…/44'/0'/0']xpub…/0/*)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">pkh(xpub…/0/*)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-nowrap" rowspan="2">Multi-sig P2WSH</td>
|
||||
<td class="text-monospace">2-of-xpub1…-xpub2…</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">wsh(multi(2,<br>[…/48'/0'/0'/2']xpub…/0/*,<br>[…/48'/0'/0'/2']xpub…/0/*))</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-nowrap" rowspan="2">Multi-sig P2SH-P2WSH</td>
|
||||
<td class="text-monospace">2-of-xpub1…-xpub2…-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<td class="text-nowrap" rowspan="2">Multi-sig P2SH</td>
|
||||
<td class="text-monospace">2-of-xpub1…-xpub2…-[legacy]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-monospace">sh(multi(2,<br>[…/45'/0]xpub…/0/*,<br>[…/45'/0]xpub…/0/*))</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
123
BTCPayServer/Views/Stores/ImportWalletOptions.cshtml
Normal file
123
BTCPayServer/Views/Stores/ImportWalletOptions.cshtml
Normal file
@ -0,0 +1,123 @@
|
||||
@model WalletSetupViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = $"Import {Model.CryptoCode} Wallet";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>Choose your import method</h1>
|
||||
<p class="lead text-secondary mt-3">The following methods assume that you already have an existing wallet created and backed up.</p>
|
||||
</header>
|
||||
|
||||
@if (Model.CryptoCode == "BTC")
|
||||
{
|
||||
<div class="mt-5">
|
||||
<div class="list-group">
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="hardware" id="import-hardware-link" class="list-group-item list-group-item-action list-group-item-wallet-setup only-for-js">
|
||||
<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 text-secondary">Import your keys using our Vault application</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
</a>
|
||||
<noscript>
|
||||
<div class="list-group-item list-group-item-wallet-setup disabled walletsetupcss">
|
||||
<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>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="list-group mt-4">
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="file" id="import-file-link" class="list-group-item list-group-item-action list-group-item-wallet-setup">
|
||||
<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>
|
||||
<vc:icon symbol="caret-right" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="list-group mt-4">
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="xpub" id="import-xpub-link" class="list-group-item list-group-item-action list-group-item-wallet-setup">
|
||||
<div class="image">
|
||||
<vc:icon symbol="xpub"/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>Enter extended public key</h4>
|
||||
<p class="mb-0 text-secondary">Input the key string manually</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="list-group mt-4">
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="scan" id="import-scan-link" class="list-group-item list-group-item-action list-group-item-wallet-setup only-for-js">
|
||||
<div class="image">
|
||||
<vc:icon symbol="scan-qr"/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>Scan wallet QR code</h4>
|
||||
<p class="mb-0 text-secondary">Supported by BlueWallet, Cobo Vault and Specter DIY</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
</a>
|
||||
<noscript>
|
||||
<div class="list-group-item list-group-item-action list-group-item-wallet-setup disabled hide-when-js">
|
||||
<div class="image">
|
||||
<vc:icon symbol="scan-qr"/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>Scan wallet QR code</h4>
|
||||
<p class="mb-0">Please enable JavaScript for this option to be available</p>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
|
||||
<div class="list-group mt-4">
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="seed" id="import-seed-link" class="list-group-item list-group-item-action list-group-item-wallet-setup">
|
||||
<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>
|
||||
<vc:icon symbol="caret-right" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
65
BTCPayServer/Views/Stores/ModifyWallet.cshtml
Normal file
65
BTCPayServer/Views/Stores/ModifyWallet.cshtml
Normal file
@ -0,0 +1,65 @@
|
||||
@model WalletSetupViewModel
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = $"Modify {Model.CryptoCode} Wallet";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-controller="Stores" asp-action="UpdateStore" asp-route-storeId="@Model.StoreId">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p class="lead text-secondary mt-3">Change your current wallet settings</p>
|
||||
</header>
|
||||
<br>
|
||||
|
||||
<div class="mt-5 position-relative">
|
||||
<h3 class="my-4">Current settings</h3>
|
||||
|
||||
<form method="post">
|
||||
<input asp-for="Config" type="hidden" />
|
||||
<input asp-for="DerivationScheme" type="hidden" />
|
||||
|
||||
<table class="table table-sm table-borderless table-responsive-md">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="w-150px">Derivation Scheme</th>
|
||||
<td class="text-break">@Model.DerivationScheme</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Root Fingerprint</th>
|
||||
<td>@Model.RootFingerprint</td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(Model.KeyPath))
|
||||
{
|
||||
<tr>
|
||||
<th>KeyPath</th>
|
||||
<td>@Model.KeyPath</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<td>@Model.Source</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Enabled
|
||||
</th>
|
||||
<td>
|
||||
<button type="submit" class="btcpay-toggle @if (Model.Enabled) { @("btcpay-toggle--active") }" id="Modify" name="Enabled" value="@(Model.Enabled ? "false" : "true")">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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="change-wallet-link" class="btn btn-secondary mr-2">
|
||||
Replace wallet
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger" id="Delete">Remove wallet</button>
|
||||
</form>
|
||||
</div>
|
52
BTCPayServer/Views/Stores/SetupWallet.cshtml
Normal file
52
BTCPayServer/Views/Stores/SetupWallet.cshtml
Normal file
@ -0,0 +1,52 @@
|
||||
@model WalletSetupViewModel
|
||||
@{
|
||||
Layout = "_LayoutWalletSetup";
|
||||
ViewData["Title"] = $"Setup {Model.CryptoCode} Wallet";
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
@if (string.IsNullOrWhiteSpace(Model.DerivationScheme)) {
|
||||
<a asp-controller="Stores" asp-action="UpdateStore" asp-route-storeId="@Model.StoreId">
|
||||
<vc:icon symbol="back"/>
|
||||
</a>
|
||||
} else {
|
||||
<a asp-controller="Stores" asp-action="ModifyWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
|
||||
<vc:icon symbol="back"/>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
<h1 class="text-center">Let's get started</h1>
|
||||
<br>
|
||||
<div class="mt-5">
|
||||
<h3 class="my-4">I have a wallet</h3>
|
||||
<div class="list-group">
|
||||
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="import-wallet-options-link" class="list-group-item list-group-item-action list-group-item-wallet-setup">
|
||||
<div class="image">
|
||||
<vc:icon symbol="existing-wallet"/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>Connect an existing wallet</h4>
|
||||
<p class="mb-0 text-secondary">Import an existing hardware or software wallet</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="mt-5">
|
||||
<h3 class="my-4">I don't have a wallet</h3>
|
||||
<div class="list-group">
|
||||
<a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="generate-wallet-link" class="list-group-item list-group-item-action list-group-item-wallet-setup">
|
||||
<div class="image">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>Create a new wallet</h4>
|
||||
<p class="mb-0 text-secondary">Generate a brand-new wallet to use</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
@ -66,10 +66,16 @@
|
||||
@if (isSetUp)
|
||||
{
|
||||
<span class="text-light ml-3 mr-2">|</span>
|
||||
<a asp-action="ModifyWallet" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Context.GetRouteValue("storeId")" id="@($"Modify{scheme.Crypto}")" class="btn btn-link px-1 py-1 fw-semibold">
|
||||
Modify
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-action="SetupWallet" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Context.GetRouteValue("storeId")" id="@($"Modify{scheme.Crypto}")" class="btn btn-primary btn-sm ml-4 px-3 py-1 fw-semibold">
|
||||
Setup
|
||||
</a>
|
||||
}
|
||||
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Context.GetRouteValue("storeId")" id="@($"Modify{scheme.Crypto}")" class="btn btn-@(isSetUp ? "link px-1" : "primary btn-sm ml-4 px-3") py-1 fw-semibold">
|
||||
@(isSetUp ? "Modify" : "Setup")
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
123
BTCPayServer/Views/Stores/_GenerateWalletForm.cshtml
Normal file
123
BTCPayServer/Views/Stores/_GenerateWalletForm.cshtml
Normal file
@ -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))
|
||||
{
|
||||
<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>
|
||||
}
|
||||
|
||||
<form id="generate-wallet-form" method="post" asp-action="GenerateWallet" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")" asp-route-method="@method">
|
||||
@if (isImport)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="ExistingMnemonic">Wallet Recovery Seed</label>
|
||||
<textarea asp-for="ExistingMnemonic" class="form-control text-monospace py-2" rows="2" autocomplete="off"></textarea>
|
||||
<span asp-validation-for="ExistingMnemonic" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="ScriptPubKeyType">Address type</label>
|
||||
<select class="form-control w-auto" asp-for="ScriptPubKeyType">
|
||||
<option value="@ScriptPubKeyType.Segwit">Segwit (Recommended, cheapest transaction fee)</option>
|
||||
<option value="@ScriptPubKeyType.SegwitP2SH">Segwit wrapped (Compatible with old wallets)</option>
|
||||
<option value="@ScriptPubKeyType.Legacy">Legacy (Not recommended)</option>
|
||||
</select>
|
||||
<span asp-validation-for="ScriptPubKeyType" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
@if (isImport && canUseHotWallet)
|
||||
{
|
||||
<div class="form-group mt-5">
|
||||
<label asp-for="SavePrivateKeys">Is hot wallet</label>
|
||||
<input type="checkbox" asp-for="SavePrivateKeys" class="btcpay-toggle ml-2" />
|
||||
<span asp-validation-for="SavePrivateKeys" class="text-danger"></span>
|
||||
<p class="text-muted pt-2">
|
||||
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!
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input asp-for="SavePrivateKeys" type="hidden" value="@isHotWallet" />
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<button class="btn btn-link px-0" type="button" id="advanced-settings-button" data-toggle="collapse" data-target="#advanced-settings" aria-expanded="false" aria-controls="advanced-settings">
|
||||
Advanced settings
|
||||
</button>
|
||||
<div id="advanced-settings" class="collapse @(string.IsNullOrEmpty(Model.Passphrase) && !Model.ImportKeysToRPC ? "" : "show")">
|
||||
<div class="pt-3 pb-1">
|
||||
@if (isImport) // hide account option when creating a wallet
|
||||
{
|
||||
<div class="form-group mb-5">
|
||||
<label asp-for="AccountNumber">Account</label>
|
||||
<select asp-for="AccountNumber" class="form-control w-auto">
|
||||
@for (var 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">
|
||||
<label asp-for="Passphrase">Optional passphrase (BIP39)</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">Confirm passphrase</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>
|
||||
|
||||
@if (canUseRpcImport)
|
||||
{
|
||||
<div class="form-group mt-5">
|
||||
<label asp-for="ImportKeysToRPC">Import keys to RPC</label>
|
||||
<input type="checkbox" asp-for="ImportKeysToRPC" class="btcpay-toggle ml-2" />
|
||||
<span asp-validation-for="ImportKeysToRPC" class="text-danger"></span>
|
||||
<p class="text-muted pt-2">
|
||||
Each address generated will be imported into the node wallet and you can view your balance through the node.
|
||||
@if (isImport || isHotWallet)
|
||||
{
|
||||
<span>When this is enabled for a hot wallet, you are also able to use the node wallet to spend.</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="Continue">Continue</button>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
document.getElementById("generate-wallet-form").addEventListener("submit", event => {
|
||||
const $form = event.currentTarget;
|
||||
|
||||
if ($form.elements["passphrase_conf"].value !== $form.elements["Passphrase"].value) {
|
||||
const $validation = document.getElementById("passphrase_conf_validation");
|
||||
$validation.classList.remove("field-validation-valid");
|
||||
$validation.innerText = "Invalid passphrase confirmation";
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
36
BTCPayServer/Views/Stores/_LayoutWalletSetup.cshtml
Normal file
36
BTCPayServer/Views/Stores/_LayoutWalletSetup.cshtml
Normal file
@ -0,0 +1,36 @@
|
||||
@{
|
||||
Layout = "_LayoutSimple";
|
||||
ViewData["Title"] = ViewData["Title"] ?? "Wallet Setup";
|
||||
}
|
||||
|
||||
@section HeadScripts {
|
||||
@await RenderSectionAsync("HeadScripts", false)
|
||||
}
|
||||
@section HeaderContent {
|
||||
@await RenderSectionAsync("HeaderContent", false)
|
||||
<link href="~/main/wallet-setup.css" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
@section Scripts {
|
||||
@await RenderSectionAsync("Scripts", false)
|
||||
}
|
||||
|
||||
<nav id="wizard-navbar">
|
||||
@RenderSection("Navbar", false)
|
||||
|
||||
<a asp-controller="Stores" asp-action="UpdateStore" asp-route-storeId="@Context.GetRouteValue("storeId")" class="cancel">
|
||||
<vc:icon symbol="close" />
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<main class="col-md-10 col-lg-8 col-xl-6">
|
||||
@if (TempData.HasStatusMessage())
|
||||
{
|
||||
<partial name="_StatusMessage"/>
|
||||
}
|
||||
@RenderBody()
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
<partial name="SigningContext" for="SigningContext" />
|
||||
</form>
|
||||
<div id="vaultPlaceholder"></div>
|
||||
<button class="btn btn-primary vault-retry" style="display:none;" type="button">Retry</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,14 @@
|
||||
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="note" viewBox="0 0 16 16"><path d="M14.2 16H1.8C.808 16 0 15.192 0 14.2V1.8C0 .808.808 0 1.8 0h12.4c.992 0 1.8.808 1.8 1.8v12.4c0 .992-.808 1.8-1.8 1.8zM1.8 1.2a.6.6 0 00-.6.6v12.4c0 .33.269.6.6.6h12.4a.6.6 0 00.6-.6V1.8a.6.6 0 00-.6-.6H1.8z" fill="currentColor"/><path d="M12 5.312H4a.6.6 0 010-1.2h8a.6.6 0 110 1.2zM12 8.6H4a.6.6 0 010-1.2h8a.6.6 0 010 1.2zm-4 3.288H4a.6.6 0 110-1.2h4a.6.6 0 010 1.2z" fill="currentColor"/></symbol>
|
||||
<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="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>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 5.1 KiB |
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
118
BTCPayServer/wwwroot/main/wallet-setup.css
Normal file
118
BTCPayServer/wwwroot/main/wallet-setup.css
Normal file
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user