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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 244 additions and 140 deletions

View File

@ -86,9 +86,9 @@ namespace BTCPayServer.Tests
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
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))
}
@ -96,9 +96,7 @@ namespace BTCPayServer.Tests
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.Positive);
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
vmPSBT.PSBT = unsignedPSBT.ToBase64();
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
@ -119,14 +117,14 @@ namespace BTCPayServer.Tests
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
{
SigningContext = new SigningContextModel(signedPSBT)
})).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
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);
//test base64 psbt file

View File

@ -271,8 +271,7 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21"))
.GetAttribute("value")));
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
@ -307,8 +306,7 @@ namespace BTCPayServer.Tests
.GetAttribute("value")));
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2");
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
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.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept();
Driver.FindElement(By.Id("SendDropdownToggle")).Click();
Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Driver.FindElement(By.Id("SignTransaction")).Click();
Driver.FindElement(By.Id("SignWithSeed")).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);
SetTransactionOutput(s, 0, bob, 0.3m);
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click();
s.Driver.FindElement(By.Id("spendWithNBxplorer")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
var happyElement = s.FindAlertMessage();
var happyText = happyElement.Text;
@ -768,7 +767,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).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.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
@ -839,36 +838,24 @@ namespace BTCPayServer.Tests
// We setup the fingerprint and the account key path
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
s.Driver.FindElement(By.Id("WalletTransactions")).Click();
var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource);
// Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
void SignWith(Mnemonic signingSource)
{
// Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();
// Input the seed
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter);
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
}
SignWith(mnemonic);
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
@ -876,8 +863,7 @@ namespace BTCPayServer.Tests
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m);
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
Assert.Contains(jack.ToString(), 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}-approve-pay")).Click();
s.Driver.FindElement(By.Id("SendDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
s.FindAlertMessage();

View File

@ -70,8 +70,7 @@ namespace BTCPayServer.Controllers
return psbt;
}
[HttpGet]
[Route("{walletId}/psbt")]
[HttpGet("{walletId}/psbt")]
public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTViewModel vm)
{
@ -94,8 +93,8 @@ namespace BTCPayServer.Controllers
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
}
[HttpPost]
[Route("{walletId}/psbt")]
[HttpPost("{walletId}/psbt")]
public async Task<IActionResult> WalletPSBT(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
@ -120,7 +119,12 @@ namespace BTCPayServer.Controllers
}
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)
{
return res;
@ -145,7 +149,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
return RedirectToWalletPSBT(new WalletPSBTViewModel()
return RedirectToWalletPSBT(new WalletPSBTViewModel
{
PSBT = psbt.ToBase64(),
FileName = vm.FileName
@ -153,14 +157,14 @@ namespace BTCPayServer.Controllers
case "broadcast":
{
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel()
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel
{
SigningContext = new SigningContextModel(psbt)
});
}
case "combine":
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":
return FilePSBT(psbt, vm.FileName);
default:
@ -176,8 +180,7 @@ namespace BTCPayServer.Controllers
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cancellationToken);
}
[HttpGet]
[Route("{walletId}/psbt/ready")]
[HttpGet("{walletId}/psbt/ready")]
public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
@ -299,8 +302,7 @@ namespace BTCPayServer.Controllers
}
}
[HttpPost]
[Route("{walletId}/psbt/ready")]
[HttpPost("{walletId}/psbt/ready")]
public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))]
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);
}
[HttpPost]
[Route("{walletId}/psbt/combine")]
[HttpPost("{walletId}/psbt/combine")]
public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTCombineViewModel vm)
{
@ -477,11 +478,13 @@ namespace BTCPayServer.Controllers
}
private async Task<IActionResult> TryHandleSigningCommands(WalletId walletId, PSBT psbt, string command,
SigningContextModel signingContext)
SigningContextModel signingContext, Dictionary<string, string> routeBack)
{
signingContext.PSBT = psbt.ToBase64();
switch (command)
{
case "sign":
return View("WalletSigningOptions", new WalletSigningOptionsModel(signingContext, routeBack));
case "vault":
return ViewVault(walletId, signingContext);
case "seed":
@ -496,10 +499,10 @@ namespace BTCPayServer.Controllers
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.MasterHDKey);
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,
Message = "NBX seed functionality is not available"

View File

@ -423,8 +423,7 @@ namespace BTCPayServer.Controllers
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet;
}
[HttpGet]
[Route("{walletId}/send")]
[HttpGet("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string[] bip21 = null)
@ -533,8 +532,7 @@ namespace BTCPayServer.Controllers
!string.IsNullOrEmpty(seed) ? seed : null;
}
[HttpPost]
[Route("{walletId}/send")]
[HttpPost("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
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());
if (store == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
@ -683,16 +681,14 @@ namespace BTCPayServer.Controllers
"The fee rate should be above 0", this);
}
}
if (!ModelState.IsValid)
return View(vm);
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
CreatePSBTResponse psbt = null;
CreatePSBTResponse psbtResponse;
try
{
psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
psbtResponse = await CreatePSBT(network, derivationScheme, vm, cancellation);
}
catch (NBXplorerException ex)
{
@ -704,16 +700,24 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer");
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,
EnforceLowR = psbt.Suggestions?.ShouldEnforceLowR,
ChangeAddress = psbt.ChangeAddress?.ToString()
EnforceLowR = psbtResponse.Suggestions?.ShouldEnforceLowR,
ChangeAddress = psbtResponse.ChangeAddress?.ToString(),
NBXSeedAvailable = vm.NBXSeedAvailable
};
var routeBack = new Dictionary<string, string>
{
{"action", nameof(WalletSend)}, {"walletId", walletId.ToString()}
};
var res = await TryHandleSigningCommands(walletId, psbt.PSBT, command, signingContext);
var res = await TryHandleSigningCommands(walletId, psbt, command, signingContext, routeBack);
if (res != null)
{
return res;
@ -724,15 +728,14 @@ namespace BTCPayServer.Controllers
case "analyze-psbt":
var name =
$"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
});
default:
return View(vm);
}
}
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 bool? EnforceLowR { 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...")]
public IFormFile UploadedPSBTFile { get; set; }
public SigningContextModel SigningContext { get; set; } = new SigningContextModel();
public async Task<PSBT> GetPSBT(Network network)
{
if (UploadedPSBTFile != null)
@ -56,6 +58,10 @@ namespace BTCPayServer.Models.WalletViewModels
PSBT = await stream.ReadToEndAsync();
}
}
if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT))
{
PSBT = SigningContext.PSBT;
}
if (!string.IsNullOrEmpty(PSBT))
{
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">
@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">
<vc:icon symbol="hot-wallet"/>
</div>
@ -33,7 +33,7 @@
}
else
{
<div class="list-group-item list-group-item-wallet-setup text-muted">
<div class="list-group-item text-muted">
<div class="image">
<vc:icon symbol="hot-wallet"/>
</div>
@ -46,7 +46,7 @@
</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="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">
<vc:icon symbol="watchonly-wallet"/>
</div>

View File

@ -21,7 +21,7 @@
{
<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="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">
<vc:icon symbol="hardware-wallet"/>
</div>
@ -35,7 +35,7 @@
<vc:icon symbol="caret-right" />
</a>
<noscript>
<div class="list-group-item list-group-item-wallet-setup disabled">
<div class="list-group-item disabled">
<div class="image">
<vc:icon symbol="hardware-wallet"/>
</div>
@ -53,7 +53,7 @@
}
<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">
<vc:icon symbol="wallet-file"/>
</div>
@ -69,7 +69,7 @@
</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="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">
<vc:icon symbol="xpub"/>
</div>
@ -82,7 +82,7 @@
</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="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">
<vc:icon symbol="scan-qr"/>
</div>
@ -93,7 +93,7 @@
<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="list-group-item list-group-item-action disabled hide-when-js">
<div class="image">
<vc:icon symbol="scan-qr"/>
</div>
@ -106,7 +106,7 @@
</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="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">
<vc:icon symbol="seed"/>
</div>

View File

@ -21,7 +21,7 @@
<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="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">
<vc:icon symbol="existing-wallet"/>
</div>
@ -38,7 +38,7 @@
<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="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">
<vc:icon symbol="new-wallet"/>
</div>

View File

@ -1,30 +1,23 @@
@{
Layout = "_LayoutSimple";
Layout = "_LayoutWizard";
}
@section PageHeadContent {
@await RenderSectionAsync("PageHeadContent", false)
<link href="~/main/wallet-setup.css" rel="stylesheet" asp-append-version="true" />
}
@section PageFootContent {
@await RenderSectionAsync("PageFootContent", false)
}
<nav id="wizard-navbar">
@section Navbar {
@await RenderSectionAsync("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 pt-sm-3 pt-md-0">
<main class="col-md-10 col-lg-8 col-xl-7">
<partial name="_StatusMessage" />
@RenderBody()
</main>
</div>
}
@RenderBody()

View File

@ -46,13 +46,13 @@
@if (!string.IsNullOrEmpty(Model.Decoded))
{
<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="NBXSeedAvailable"/>
<input type="hidden" asp-for="PSBT"/>
<input type="hidden" asp-for="FileName"/>
<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">
<button class="btn btn-secondary dropdown-toggle" type="button" id="OtherActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Other actions...
@ -81,10 +81,10 @@
<label asp-for="UploadedPSBTFile" class="form-label"></label>
<input asp-for="UploadedPSBTFile" type="file" class="form-control">
</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>
</button>
<button type="submit" name="command" value="decode" class="btn btn-primary" id="Decode">Decode</button>
</form>
</div>
</div>

View File

@ -23,7 +23,7 @@
<partial name="CameraScanner"/>
<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>
<form method="post" asp-action="WalletSend" asp-route-walletId="@Context.GetRouteValue("walletId")">
<input type="hidden" asp-for="InputSelection" />
@ -35,11 +35,11 @@
<input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="CryptoCode" />
<input type="hidden" name="BIP21" id="BIP21" />
<ul class="text-danger">
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
{
foreach (var error in
errors.Value.Errors)
foreach (var error in errors.Value.Errors)
{
<li>@error.ErrorMessage</li>
}
@ -222,7 +222,7 @@
</div>
</div>
<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="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>

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