Wallet: Signing UI improvements (#2559)

* Refactoring to generalize wizard layout

* Wallet: Add intermediate signing options view

* Update BTCPayServer/Views/Wallets/WalletSigningOptions.cshtml

Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>

* Skip signing options for hot wallets

* Update signing options wordings, add PSBT doc link

* Fix test

* Remove form route params

* Use decode command for PSBT

Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>
This commit is contained in:
d11n
2021-06-14 07:06:56 +02:00
committed by GitHub
parent 371acc84a8
commit 3c0292f074
19 changed files with 244 additions and 140 deletions

View File

@@ -86,9 +86,9 @@ namespace BTCPayServer.Tests
var signedPSBT = unsignedPSBT.Clone(); var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath); signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
vmPSBT.PSBT = signedPSBT.ToBase64(); vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel() var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
{ {
SigningContext = new SigningContextModel() SigningContext = new SigningContextModel
{ {
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
} }
@@ -96,8 +96,6 @@ namespace BTCPayServer.Tests
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive); Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
Assert.Contains(psbtReady.Destinations, d => d.Positive); Assert.Contains(psbtReady.Destinations, d => d.Positive);
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
vmPSBT.PSBT = unsignedPSBT.ToBase64(); vmPSBT.PSBT = unsignedPSBT.ToBase64();
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>(); var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
@@ -119,14 +117,14 @@ namespace BTCPayServer.Tests
Assert.True(signedPSBT2.TryFinalize(out _)); Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2); Assert.Equal(signedPSBT, signedPSBT2);
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel() var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
{ {
SigningContext = new SigningContextModel(signedPSBT) SigningContext = new SigningContextModel(signedPSBT)
})).AssertViewModel<WalletPSBTReadyViewModel>(); })).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT); Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
Assert.Equal(signedPSBT.ToBase64(), psbt); Assert.Equal(signedPSBT.ToBase64(), psbt);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast")); var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
//test base64 psbt file //test base64 psbt file

View File

@@ -271,8 +271,7 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Alert().Accept(); s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21")) Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21"))
.GetAttribute("value"))); .GetAttribute("value")));
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() => await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{ {
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
@@ -307,8 +306,7 @@ namespace BTCPayServer.Tests
.GetAttribute("value"))); .GetAttribute("value")));
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear(); s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2"); s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2");
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() => var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{ {
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();

View File

@@ -363,8 +363,8 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("bip21parse")).Click(); Driver.FindElement(By.Id("bip21parse")).Click();
Driver.SwitchTo().Alert().SendKeys(bip21); Driver.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept(); Driver.SwitchTo().Alert().Accept();
Driver.FindElement(By.Id("SendDropdownToggle")).Click(); Driver.FindElement(By.Id("SignTransaction")).Click();
Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); Driver.FindElement(By.Id("SignWithSeed")).Click();
Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
} }

View File

@@ -621,8 +621,7 @@ namespace BTCPayServer.Tests
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 0.3m); SetTransactionOutput(s, 0, bob, 0.3m);
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("spendWithNBxplorer")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
var happyElement = s.FindAlertMessage(); var happyElement = s.FindAlertMessage();
var happyText = happyElement.Text; var happyText = happyElement.Text;
@@ -768,7 +767,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click(); s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
//you cannot 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); Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
@@ -839,8 +838,6 @@ namespace BTCPayServer.Tests
// We setup the fingerprint and the account key path // We setup the fingerprint and the account key path
s.Driver.FindElement(By.Id("WalletSettings")).Click(); s.Driver.FindElement(By.Id("WalletSettings")).Click();
// s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
// s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);
// Check the tx sent earlier arrived // Check the tx sent earlier arrived
s.Driver.FindElement(By.Id("WalletTransactions")).Click(); s.Driver.FindElement(By.Id("WalletTransactions")).Click();
@@ -848,27 +845,17 @@ namespace BTCPayServer.Tests
var walletTransactionLink = s.Driver.Url; var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource); Assert.Contains(tx.ToString(), s.Driver.PageSource);
void SignWith(Mnemonic signingSource)
{
// Send to bob // Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click(); s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1); SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();
// Input the seed
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter);
// Broadcast // Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource); Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource); Assert.Contains("1.00000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url); Assert.Equal(walletTransactionLink, s.Driver.Url);
}
SignWith(mnemonic);
s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click();
@@ -876,8 +863,7 @@ namespace BTCPayServer.Tests
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m); SetTransactionOutput(s, 0, jack, 0.01m);
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Assert.Contains(jack.ToString(), s.Driver.PageSource); Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource); Assert.Contains("0.01000000", s.Driver.PageSource);
@@ -990,8 +976,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click(); s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click(); s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
s.FindAlertMessage(); s.FindAlertMessage();

View File

@@ -70,8 +70,7 @@ namespace BTCPayServer.Controllers
return psbt; return psbt;
} }
[HttpGet] [HttpGet("{walletId}/psbt")]
[Route("{walletId}/psbt")]
public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))] public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTViewModel vm) WalletId walletId, WalletPSBTViewModel vm)
{ {
@@ -94,8 +93,8 @@ namespace BTCPayServer.Controllers
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode }); return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
} }
[HttpPost]
[Route("{walletId}/psbt")] [HttpPost("{walletId}/psbt")]
public async Task<IActionResult> WalletPSBT( public async Task<IActionResult> WalletPSBT(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletId walletId,
@@ -120,7 +119,12 @@ namespace BTCPayServer.Controllers
} }
vm.PSBTHex = psbt.ToHex(); vm.PSBTHex = psbt.ToHex();
var res = await TryHandleSigningCommands(walletId, psbt, command, new SigningContextModel(psbt)); vm.SigningContext.NBXSeedAvailable = vm.NBXSeedAvailable;
var routeBack = new Dictionary<string, string>
{
{"action", nameof(WalletPSBT)}, {"walletId", walletId.ToString()}
};
var res = await TryHandleSigningCommands(walletId, psbt, command, vm.SigningContext, routeBack);
if (res != null) if (res != null)
{ {
return res; return res;
@@ -145,7 +149,7 @@ namespace BTCPayServer.Controllers
return View(vm); return View(vm);
} }
TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!"; TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
return RedirectToWalletPSBT(new WalletPSBTViewModel() return RedirectToWalletPSBT(new WalletPSBTViewModel
{ {
PSBT = psbt.ToBase64(), PSBT = psbt.ToBase64(),
FileName = vm.FileName FileName = vm.FileName
@@ -153,14 +157,14 @@ namespace BTCPayServer.Controllers
case "broadcast": case "broadcast":
{ {
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel() return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel
{ {
SigningContext = new SigningContextModel(psbt) SigningContext = new SigningContextModel(psbt)
}); });
} }
case "combine": case "combine":
ModelState.Remove(nameof(vm.PSBT)); ModelState.Remove(nameof(vm.PSBT));
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() }); return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel { OtherPSBT = psbt.ToBase64() });
case "save-psbt": case "save-psbt":
return FilePSBT(psbt, vm.FileName); return FilePSBT(psbt, vm.FileName);
default: default:
@@ -176,8 +180,7 @@ namespace BTCPayServer.Controllers
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cancellationToken); return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cancellationToken);
} }
[HttpGet] [HttpGet("{walletId}/psbt/ready")]
[Route("{walletId}/psbt/ready")]
public async Task<IActionResult> WalletPSBTReady( public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletId walletId,
@@ -299,8 +302,7 @@ namespace BTCPayServer.Controllers
} }
} }
[HttpPost] [HttpPost("{walletId}/psbt/ready")]
[Route("{walletId}/psbt/ready")]
public async Task<IActionResult> WalletPSBTReady( public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default) WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default)
@@ -450,8 +452,7 @@ namespace BTCPayServer.Controllers
return File(psbt.ToBytes(), "application/octet-stream", fileName); return File(psbt.ToBytes(), "application/octet-stream", fileName);
} }
[HttpPost] [HttpPost("{walletId}/psbt/combine")]
[Route("{walletId}/psbt/combine")]
public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))] public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTCombineViewModel vm) WalletId walletId, WalletPSBTCombineViewModel vm)
{ {
@@ -477,11 +478,13 @@ namespace BTCPayServer.Controllers
} }
private async Task<IActionResult> TryHandleSigningCommands(WalletId walletId, PSBT psbt, string command, private async Task<IActionResult> TryHandleSigningCommands(WalletId walletId, PSBT psbt, string command,
SigningContextModel signingContext) SigningContextModel signingContext, Dictionary<string, string> routeBack)
{ {
signingContext.PSBT = psbt.ToBase64(); signingContext.PSBT = psbt.ToBase64();
switch (command) switch (command)
{ {
case "sign":
return View("WalletSigningOptions", new WalletSigningOptionsModel(signingContext, routeBack));
case "vault": case "vault":
return ViewVault(walletId, signingContext); return ViewVault(walletId, signingContext);
case "seed": case "seed":
@@ -496,10 +499,10 @@ namespace BTCPayServer.Controllers
.GetMetadataAsync<string>(derivationScheme.AccountDerivation, .GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.MasterHDKey); WellknownMetadataKeys.MasterHDKey);
return SignWithSeed(walletId, return SignWithSeed(walletId,
new SignWithSeedViewModel() { SeedOrKey = extKey, SigningContext = signingContext }); new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = signingContext });
} }
} }
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel
{ {
Severity = StatusMessageModel.StatusSeverity.Error, Severity = StatusMessageModel.StatusSeverity.Error,
Message = "NBX seed functionality is not available" Message = "NBX seed functionality is not available"

View File

@@ -423,8 +423,7 @@ namespace BTCPayServer.Controllers
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet; return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet;
} }
[HttpGet] [HttpGet("{walletId}/send")]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend( public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string[] bip21 = null) WalletId walletId, string defaultDestination = null, string defaultAmount = null, string[] bip21 = null)
@@ -533,8 +532,7 @@ namespace BTCPayServer.Controllers
!string.IsNullOrEmpty(seed) ? seed : null; !string.IsNullOrEmpty(seed) ? seed : null;
} }
[HttpPost] [HttpPost("{walletId}/send")]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend( public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "") WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "")
@@ -544,7 +542,7 @@ namespace BTCPayServer.Controllers
var store = await Repository.FindStore(walletId.StoreId, GetUserId()); var store = await Repository.FindStore(walletId.StoreId, GetUserId());
if (store == null) if (store == null)
return NotFound(); return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode); var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null || network.ReadonlyWallet) if (network == null || network.ReadonlyWallet)
return NotFound(); return NotFound();
vm.SupportRBF = network.SupportRBF; vm.SupportRBF = network.SupportRBF;
@@ -683,16 +681,14 @@ namespace BTCPayServer.Controllers
"The fee rate should be above 0", this); "The fee rate should be above 0", this);
} }
} }
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(vm); return View(vm);
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId); DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
CreatePSBTResponse psbtResponse;
CreatePSBTResponse psbt = null;
try try
{ {
psbt = await CreatePSBT(network, derivationScheme, vm, cancellation); psbtResponse = await CreatePSBT(network, derivationScheme, vm, cancellation);
} }
catch (NBXplorerException ex) catch (NBXplorerException ex)
{ {
@@ -704,16 +700,24 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer"); ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer");
return View(vm); return View(vm);
} }
derivationScheme.RebaseKeyPaths(psbt.PSBT);
var signingContext = new SigningContextModel() var psbt = psbtResponse.PSBT;
derivationScheme.RebaseKeyPaths(psbt);
var signingContext = new SigningContextModel
{ {
PayJoinBIP21 = vm.PayJoinBIP21, PayJoinBIP21 = vm.PayJoinBIP21,
EnforceLowR = psbt.Suggestions?.ShouldEnforceLowR, EnforceLowR = psbtResponse.Suggestions?.ShouldEnforceLowR,
ChangeAddress = psbt.ChangeAddress?.ToString() ChangeAddress = psbtResponse.ChangeAddress?.ToString(),
NBXSeedAvailable = vm.NBXSeedAvailable
}; };
var res = await TryHandleSigningCommands(walletId, psbt.PSBT, command, signingContext); var routeBack = new Dictionary<string, string>
{
{"action", nameof(WalletSend)}, {"walletId", walletId.ToString()}
};
var res = await TryHandleSigningCommands(walletId, psbt, command, signingContext, routeBack);
if (res != null) if (res != null)
{ {
return res; return res;
@@ -724,15 +728,14 @@ namespace BTCPayServer.Controllers
case "analyze-psbt": case "analyze-psbt":
var name = var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt"; $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
return RedirectToWalletPSBT(new WalletPSBTViewModel() return RedirectToWalletPSBT(new WalletPSBTViewModel
{ {
PSBT = psbt.PSBT.ToBase64(), PSBT = psbt.ToBase64(),
FileName = name FileName = name
}); });
default: default:
return View(vm); return View(vm);
} }
} }
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network) private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)

View File

@@ -17,5 +17,6 @@ namespace BTCPayServer.Models.WalletViewModels
public string PayJoinBIP21 { get; set; } public string PayJoinBIP21 { get; set; }
public bool? EnforceLowR { get; set; } public bool? EnforceLowR { get; set; }
public string ChangeAddress { get; set; } public string ChangeAddress { get; set; }
public bool NBXSeedAvailable { get; set; }
} }
} }

View File

@@ -34,6 +34,8 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Upload PSBT from file...")] [Display(Name = "Upload PSBT from file...")]
public IFormFile UploadedPSBTFile { get; set; } public IFormFile UploadedPSBTFile { get; set; }
public SigningContextModel SigningContext { get; set; } = new SigningContextModel();
public async Task<PSBT> GetPSBT(Network network) public async Task<PSBT> GetPSBT(Network network)
{ {
if (UploadedPSBTFile != null) if (UploadedPSBTFile != null)
@@ -56,6 +58,10 @@ namespace BTCPayServer.Models.WalletViewModels
PSBT = await stream.ReadToEndAsync(); PSBT = await stream.ReadToEndAsync();
} }
} }
if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT))
{
PSBT = SigningContext.PSBT;
}
if (!string.IsNullOrEmpty(PSBT)) if (!string.IsNullOrEmpty(PSBT))
{ {
try try

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletSigningOptionsModel
{
public WalletSigningOptionsModel(
SigningContextModel signingContext,
IDictionary<string, string> routeDataBack)
{
SigningContext = signingContext;
RouteDataBack = routeDataBack;
}
public SigningContextModel SigningContext { get; }
public IDictionary<string, string> RouteDataBack { get; }
}
}

View File

@@ -0,0 +1,26 @@
@{
Layout = "_LayoutSimple";
}
@section PageHeadContent {
<link href="~/main/wizard.css" rel="stylesheet" asp-append-version="true" />
@await RenderSectionAsync("PageHeadContent", false)
}
@section PageFootContent {
@await RenderSectionAsync("PageFootContent", false)
}
<nav id="wizard-navbar">
@await RenderSectionAsync("Navbar", false)
</nav>
<div class="row justify-content-md-center mt-5 pt-sm-3 pt-md-0">
<main class="col-md-10 col-lg-8 col-xl-7">
<partial name="_StatusMessage" />
@RenderBody()
</main>
</div>

View File

@@ -16,7 +16,7 @@
<div class="list-group mt-5"> <div class="list-group mt-5">
@if (Model.CanUseHotWallet) @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="GenerateHotwalletLink" class="list-group-item list-group-item-action list-group-item-wallet-setup"> <a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="hotwallet" id="GenerateHotwalletLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="hot-wallet"/> <vc:icon symbol="hot-wallet"/>
</div> </div>
@@ -33,7 +33,7 @@
} }
else else
{ {
<div class="list-group-item list-group-item-wallet-setup text-muted"> <div class="list-group-item text-muted">
<div class="image"> <div class="image">
<vc:icon symbol="hot-wallet"/> <vc:icon symbol="hot-wallet"/>
</div> </div>
@@ -46,7 +46,7 @@
</div> </div>
<div class="list-group mt-4"> <div class="list-group mt-4">
<a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="watchonly" id="GenerateWatchonlyLink" class="list-group-item list-group-item-action list-group-item-wallet-setup"> <a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="watchonly" id="GenerateWatchonlyLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="watchonly-wallet"/> <vc:icon symbol="watchonly-wallet"/>
</div> </div>

View File

@@ -21,7 +21,7 @@
{ {
<div class="mt-5"> <div class="mt-5">
<div class="list-group"> <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="ImportHardwareLink" class="list-group-item list-group-item-action list-group-item-wallet-setup only-for-js"> <a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="hardware" id="ImportHardwareLink" class="list-group-item list-group-item-action only-for-js">
<div class="image"> <div class="image">
<vc:icon symbol="hardware-wallet"/> <vc:icon symbol="hardware-wallet"/>
</div> </div>
@@ -35,7 +35,7 @@
<vc:icon symbol="caret-right" /> <vc:icon symbol="caret-right" />
</a> </a>
<noscript> <noscript>
<div class="list-group-item list-group-item-wallet-setup disabled"> <div class="list-group-item disabled">
<div class="image"> <div class="image">
<vc:icon symbol="hardware-wallet"/> <vc:icon symbol="hardware-wallet"/>
</div> </div>
@@ -53,7 +53,7 @@
} }
<div class="list-group mt-4"> <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="ImportFileLink" class="list-group-item list-group-item-action list-group-item-wallet-setup"> <a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="file" id="ImportFileLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="wallet-file"/> <vc:icon symbol="wallet-file"/>
</div> </div>
@@ -69,7 +69,7 @@
</div> </div>
<div class="list-group mt-4"> <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="ImportXpubLink" class="list-group-item list-group-item-action list-group-item-wallet-setup"> <a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="xpub" id="ImportXpubLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="xpub"/> <vc:icon symbol="xpub"/>
</div> </div>
@@ -82,7 +82,7 @@
</div> </div>
<div class="list-group mt-4"> <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="ImportScanLink" class="list-group-item list-group-item-action list-group-item-wallet-setup only-for-js"> <a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="scan" id="ImportScanLink" class="list-group-item list-group-item-action only-for-js">
<div class="image"> <div class="image">
<vc:icon symbol="scan-qr"/> <vc:icon symbol="scan-qr"/>
</div> </div>
@@ -93,7 +93,7 @@
<vc:icon symbol="caret-right" /> <vc:icon symbol="caret-right" />
</a> </a>
<noscript> <noscript>
<div class="list-group-item list-group-item-action list-group-item-wallet-setup disabled hide-when-js"> <div class="list-group-item list-group-item-action disabled hide-when-js">
<div class="image"> <div class="image">
<vc:icon symbol="scan-qr"/> <vc:icon symbol="scan-qr"/>
</div> </div>
@@ -106,7 +106,7 @@
</div> </div>
<div class="list-group mt-4"> <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="ImportSeedLink" class="list-group-item list-group-item-action list-group-item-wallet-setup"> <a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="seed" id="ImportSeedLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="seed"/> <vc:icon symbol="seed"/>
</div> </div>

View File

@@ -21,7 +21,7 @@
<div class="mt-5"> <div class="mt-5">
<h3 class="my-4">I have a wallet</h3> <h3 class="my-4">I have a wallet</h3>
<div class="list-group"> <div class="list-group">
<a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="ImportWalletOptionsLink" class="list-group-item list-group-item-action list-group-item-wallet-setup"> <a asp-controller="Stores" asp-action="ImportWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="ImportWalletOptionsLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="existing-wallet"/> <vc:icon symbol="existing-wallet"/>
</div> </div>
@@ -38,7 +38,7 @@
<div class="mt-5"> <div class="mt-5">
<h3 class="my-4">I don't have a wallet</h3> <h3 class="my-4">I don't have a wallet</h3>
<div class="list-group"> <div class="list-group">
<a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="GenerateWalletLink" class="list-group-item list-group-item-action list-group-item-wallet-setup"> <a asp-controller="Stores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="GenerateWalletLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="new-wallet"/> <vc:icon symbol="new-wallet"/>
</div> </div>

View File

@@ -1,30 +1,23 @@
@{ @{
Layout = "_LayoutSimple"; Layout = "_LayoutWizard";
} }
@section PageHeadContent { @section PageHeadContent {
@await RenderSectionAsync("PageHeadContent", false) @await RenderSectionAsync("PageHeadContent", false)
<link href="~/main/wallet-setup.css" rel="stylesheet" asp-append-version="true" />
} }
@section PageFootContent { @section PageFootContent {
@await RenderSectionAsync("PageFootContent", false) @await RenderSectionAsync("PageFootContent", false)
} }
<nav id="wizard-navbar"> @section Navbar {
@await RenderSectionAsync("Navbar", false) @await RenderSectionAsync("Navbar", false)
<a asp-controller="Stores" asp-action="UpdateStore" asp-route-storeId="@Context.GetRouteValue("storeId")" class="cancel"> <a asp-controller="Stores" asp-action="UpdateStore" asp-route-storeId="@Context.GetRouteValue("storeId")" class="cancel">
<vc:icon symbol="close" /> <vc:icon symbol="close" />
</a> </a>
</nav> }
<div class="row justify-content-md-center mt-5 pt-sm-3 pt-md-0">
<main class="col-md-10 col-lg-8 col-xl-7">
<partial name="_StatusMessage" />
@RenderBody() @RenderBody()
</main>
</div>

View File

@@ -46,13 +46,13 @@
@if (!string.IsNullOrEmpty(Model.Decoded)) @if (!string.IsNullOrEmpty(Model.Decoded))
{ {
<div class="form-group"> <div class="form-group">
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")"> <form method="post" asp-action="WalletPSBT" asp-route-walletId="@Context.GetRouteValue("walletId")">
<input type="hidden" asp-for="CryptoCode"/> <input type="hidden" asp-for="CryptoCode"/>
<input type="hidden" asp-for="NBXSeedAvailable"/> <input type="hidden" asp-for="NBXSeedAvailable"/>
<input type="hidden" asp-for="PSBT"/> <input type="hidden" asp-for="PSBT"/>
<input type="hidden" asp-for="FileName"/> <input type="hidden" asp-for="FileName"/>
<div class="d-flex"> <div class="d-flex">
<partial name="WalletSigningMenu" model="@((Model.CryptoCode, Model.NBXSeedAvailable))"/> <button type="submit" id="SignTransaction" name="command" value="@(Model.SigningContext.NBXSeedAvailable ? "nbx-seed" : "sign")" class="btn btn-primary">Sign transaction</button>
<div class="ms-2 dropdown"> <div class="ms-2 dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="OtherActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button class="btn btn-secondary dropdown-toggle" type="button" id="OtherActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Other actions... Other actions...
@@ -81,10 +81,10 @@
<label asp-for="UploadedPSBTFile" class="form-label"></label> <label asp-for="UploadedPSBTFile" class="form-label"></label>
<input asp-for="UploadedPSBTFile" type="file" class="form-control"> <input asp-for="UploadedPSBTFile" type="file" class="form-control">
</div> </div>
<button type="button" id="scanqrcode" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan with camera"> <button type="submit" name="command" value="decode" class="btn btn-primary" id="Decode">Decode</button>
<button type="button" id="scanqrcode" class="btn btn-secondary only-for-js ms-2" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan with camera">
<i class="fa fa-camera"></i> <i class="fa fa-camera"></i>
</button> </button>
<button type="submit" name="command" value="decode" class="btn btn-primary" id="Decode">Decode</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -23,7 +23,7 @@
<partial name="CameraScanner"/> <partial name="CameraScanner"/>
<div class="row"> <div class="row">
<div class="@(!Model.InputSelection && Model.Outputs.Count==1? "col-lg-7 transaction-output-form": "col-lg-8")"> <div class="col-lg-8 col-xl-6 @(!Model.InputSelection && Model.Outputs.Count == 1 ? "transaction-output-form" : "")">
<h4 class="mb-3">@ViewData["PageTitle"]</h4> <h4 class="mb-3">@ViewData["PageTitle"]</h4>
<form method="post" asp-action="WalletSend" asp-route-walletId="@Context.GetRouteValue("walletId")"> <form method="post" asp-action="WalletSend" asp-route-walletId="@Context.GetRouteValue("walletId")">
<input type="hidden" asp-for="InputSelection" /> <input type="hidden" asp-for="InputSelection" />
@@ -35,11 +35,11 @@
<input type="hidden" asp-for="CurrentBalance" /> <input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="CryptoCode" /> <input type="hidden" asp-for="CryptoCode" />
<input type="hidden" name="BIP21" id="BIP21" /> <input type="hidden" name="BIP21" id="BIP21" />
<ul class="text-danger"> <ul class="text-danger">
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid)) @foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
{ {
foreach (var error in foreach (var error in errors.Value.Errors)
errors.Value.Errors)
{ {
<li>@error.ErrorMessage</li> <li>@error.ErrorMessage</li>
} }
@@ -222,7 +222,7 @@
</div> </div>
</div> </div>
<div class="form-group d-flex mt-2"> <div class="form-group d-flex mt-2">
<partial name="WalletSigningMenu" model="@((Model.CryptoCode, Model.NBXSeedAvailable))"/> <button type="submit" id="SignTransaction" name="command" value="@(Model.NBXSeedAvailable ? "nbx-seed" : "sign")" class="btn btn-primary">Sign transaction</button>
<button type="submit" name="command" value="add-output" class="ms-2 btn btn-secondary">Add another destination</button> <button type="submit" name="command" value="add-output" class="ms-2 btn btn-secondary">Add another destination</button>
<button type="button" id="bip21parse" class="ms-2 btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button> <button type="button" id="bip21parse" class="ms-2 btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button>
<button type="button" id="scanqrcode" class="ms-2 btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan BIP21/Address with camera"><i class="fa fa-camera"></i></button> <button type="button" id="scanqrcode" class="ms-2 btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan BIP21/Address with camera"><i class="fa fa-camera"></i></button>

View File

@@ -1,20 +0,0 @@
@inject BTCPayNetworkProvider BTCPayNetworkProvider
@model (string CryptoCode, bool NBXSeedAvailable)
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="SendDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Sign with...
</button>
<div class="dropdown-menu" aria-labelledby="SendDropdownToggle">
@if (BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>(Model.CryptoCode).VaultSupported)
{
<button name="command" type="submit" class="dropdown-item" value="vault">... a hardware wallet</button>
}
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
@if (Model.NBXSeedAvailable)
{
<button id="spendWithNBxplorer" name="command" type="submit" class="dropdown-item" value="nbx-seed">... the hot wallet</button>
}
</div>
</div>

View File

@@ -0,0 +1,93 @@
@model WalletSigningOptionsModel
@inject BTCPayNetworkProvider BTCPayNetworkProvider
@addTagHelper *, BundlerMinifier.TagHelpers
@{
Layout = "_LayoutWizard";
ViewData.SetActivePageAndTitle(WalletsNavPages.Send, "Sign the transaction", Context.GetStoreData().StoreName);
var walletId = WalletId.Parse(Context.GetRouteValue("walletId").ToString());
}
@section Navbar {
<a asp-all-route-data="Model.RouteDataBack">
<vc:icon symbol="back" />
</a>
<a asp-all-route-data="Model.RouteDataBack" class="cancel">
<vc:icon symbol="close" />
</a>
}
<header class="text-center">
<h1>Choose your signing method</h1>
<p class="lead text-secondary mt-3">You can sign the transaction using one of the following methods.</p>
</header>
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId">
<partial name="SigningContext" for="SigningContext" />
@if (BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).VaultSupported)
{
<div class="list-group mt-4">
<button type="submit" name="command" value="vault" class="list-group-item list-group-item-action only-for-js" id="SignWithVault">
<div class="image">
<vc:icon symbol="hardware-wallet"/>
</div>
<div class="content d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between me-2">
<div>
<h4>Hardware wallet</h4>
<p class="mb-0 text-secondary">Sign using our Vault application</p>
</div>
<small class="d-block text-primary mt-2 mt-lg-0">Recommended</small>
</div>
<vc:icon symbol="caret-right"/>
</button>
<noscript>
<div class="list-group-item disabled">
<div class="image">
<vc:icon symbol="hardware-wallet"/>
</div>
<div class="content d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between me-2">
<div><h4>Hardware wallet</h4>
<p class="mb-0">Please enable JavaScript for this option to be available</p>
</div>
</div>
</div>
</noscript>
</div>
}
<div class="list-group mt-4">
<button type="submit" name="command" value="decode" class="list-group-item list-group-item-action" id="SignWithPSBT">
<div class="image">
<vc:icon symbol="wallet-file"/>
</div>
<div class="content">
<h4>
Partially Signed Bitcoin Transaction
<small>
<a href="https://docs.btcpayserver.org/Wallet/#signing-with-a-wallet-supporting-psbt" target="_blank">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</small>
</h4>
<p class="mb-0 text-secondary">Offline signing, without connecting your wallet to the internet</p>
</div>
<vc:icon symbol="caret-right"/>
</button>
</div>
<div class="list-group mt-4">
<button type="submit" name="command" value="seed" class="list-group-item list-group-item-action" id="SignWithSeed">
<div class="image">
<vc:icon symbol="seed"/>
</div>
<div class="content d-flex flex-column flex-lg-row align-items-lg-center justify-content-lg-between me-2">
<div>
<h4>Private key or seed</h4>
<p class="mb-0 text-secondary">Provide the 12 or 24 word recovery seed</p>
</div>
<small class="d-block text-danger mt-2 mt-lg-0" data-bs-toggle="tooltip" data-bs-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"></span></small>
</div>
<vc:icon symbol="caret-right"/>
</button>
</div>
</form>

View File

@@ -65,21 +65,21 @@ body {
margin-left: auto; margin-left: auto;
} }
.list-group-item-wallet-setup { .list-group-item {
display: flex; display: flex;
padding: 0; padding: 0;
} }
.list-group-item-wallet-setup.hide-when-js { .list-group-item.hide-when-js {
display: flex !important; display: flex !important;
} }
.list-group-item-wallet-setup:active, .list-group-item:active,
.list-group-item-wallet-setup.active { .list-group-item.active {
background-color: transparent; background-color: transparent;
} }
.list-group-item-wallet-setup .image { .list-group-item .image {
display: flex; display: flex;
flex: 0 0 90px; flex: 0 0 90px;
align-items: center; align-items: center;
@@ -87,28 +87,28 @@ body {
padding: 1.5rem; padding: 1.5rem;
} }
.list-group-item-wallet-setup .image .icon { .list-group-item .image .icon {
width: 32px; width: 32px;
height: 32px; height: 32px;
} }
.list-group-item-wallet-setup .content { .list-group-item .content {
flex: 1; flex: 1;
padding: 1.5rem; padding: 1.5rem;
} }
.list-group-item-wallet-setup .content small { .list-group-item .content small {
font-size: 90%; font-size: 90%;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
} }
.list-group-item-wallet-setup .image + .content { .list-group-item .image + .content {
padding-left: .5rem; padding-left: .5rem;
} }
.list-group-item-wallet-setup .icon-caret-right { .list-group-item .icon-caret-right {
width: 24px; flex: 0 0 24px;
height: 24px; height: 24px;
align-self: center; align-self: center;
margin-right: 1.5rem; margin-right: 1.5rem;