mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 09:54:30 +01:00
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:
parent
371acc84a8
commit
3c0292f074
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
26
BTCPayServer/Views/Shared/_LayoutWizard.cshtml
Normal file
26
BTCPayServer/Views/Shared/_LayoutWizard.cshtml
Normal 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>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
93
BTCPayServer/Views/Wallets/WalletSigningOptions.cshtml
Normal file
93
BTCPayServer/Views/Wallets/WalletSigningOptions.cshtml
Normal 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>
|
@ -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;
|
Loading…
Reference in New Issue
Block a user