mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
WalletPSBTReady show the summary of the transaction, signing with the seed respect the keypath of the wallet settings
This commit is contained in:
parent
210d680b21
commit
a68915d6cf
9 changed files with 232 additions and 101 deletions
|
@ -50,9 +50,10 @@ namespace BTCPayServer.Tests
|
|||
|
||||
var walletController = tester.PayTester.GetController<WalletsController>(user.UserId);
|
||||
var walletId = new WalletId(user.StoreId, "BTC");
|
||||
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Destination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(),
|
||||
Destination = sendDestination,
|
||||
Amount = 0.1m,
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
|
@ -63,7 +64,8 @@ namespace BTCPayServer.Tests
|
|||
Assert.NotNull(vmLedger.SuccessPath);
|
||||
Assert.NotNull(vmLedger.WebsocketPath);
|
||||
|
||||
var vmPSBT = await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var redirectedPSBT = (string)Assert.IsType<RedirectToActionResult>(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")).RouteValues["psbt"];
|
||||
var vmPSBT = walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModel<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
|
||||
|
@ -79,7 +81,11 @@ namespace BTCPayServer.Tests
|
|||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.ExtKey);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"));
|
||||
var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(2, psbtReady.Destinations.Count);
|
||||
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();
|
||||
|
@ -102,10 +108,10 @@ namespace BTCPayServer.Tests
|
|||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
var ready = walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64()).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
|
||||
vmPSBT = await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), vmPSBT.PSBT);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
|
||||
Assert.Equal(signedPSBT.ToBase64(), (string)redirect.RouteValues["psbt"]);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.24" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.25" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
||||
<PackageReference Include="DBriize" Version="1.0.0.4" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.13" />
|
||||
|
|
|
@ -42,9 +42,15 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt")]
|
||||
public IActionResult WalletPSBT()
|
||||
public IActionResult WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTViewModel vm)
|
||||
{
|
||||
return View(new WalletPSBTViewModel());
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (vm?.PSBT != null)
|
||||
{
|
||||
vm.Decoded = vm.GetPSBT(network.NBitcoinNetwork)?.ToString();
|
||||
}
|
||||
return View(vm ?? new WalletPSBTViewModel());
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt")]
|
||||
|
@ -70,30 +76,12 @@ namespace BTCPayServer.Controllers
|
|||
case "ledger":
|
||||
return ViewWalletSendLedger(psbt);
|
||||
case "seed":
|
||||
return RedirectToAction("SignWithSeed", new
|
||||
{
|
||||
psbt = psbt.ToBase64(),
|
||||
walletId,
|
||||
send = false
|
||||
});
|
||||
return SignWithSeed(walletId, psbt.ToBase64());
|
||||
case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
|
||||
return ViewPSBT(psbt, errors);
|
||||
case "broadcast":
|
||||
{
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
return ViewPSBT(psbt, new[] { $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ViewPSBT(psbt, "Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
return await WalletPSBTReady(walletId, psbt.ToBase64());
|
||||
}
|
||||
case "combine":
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
|
@ -107,11 +95,80 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public IActionResult WalletPSBTReady(
|
||||
public async Task<IActionResult> WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string psbt = null)
|
||||
WalletId walletId, string psbt = null,
|
||||
string signingKey = null,
|
||||
string signingKeyPath = null)
|
||||
{
|
||||
return View(new WalletPSBTReadyViewModel() { PSBT = psbt });
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var vm = new WalletPSBTReadyViewModel() { PSBT = psbt };
|
||||
vm.SigningKey = signingKey;
|
||||
vm.SigningKeyPath = signingKeyPath;
|
||||
await FetchTransactionDetails(walletId, vm, network);
|
||||
return View(nameof(WalletPSBTReady), vm);
|
||||
}
|
||||
|
||||
private async Task FetchTransactionDetails(WalletId walletId, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
|
||||
{
|
||||
var psbtObject = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
|
||||
IHDKey signingKey = null;
|
||||
RootedKeyPath signingKeyPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
signingKey = new BitcoinExtPubKey(vm.SigningKey, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
signingKey = signingKey ?? new BitcoinExtKey(vm.SigningKey, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
signingKeyPath = RootedKeyPath.Parse(vm.SigningKeyPath);
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (signingKey == null || signingKeyPath == null)
|
||||
{
|
||||
var signingKeySettings = (await GetDerivationSchemeSettings(walletId)).GetSigningAccountKeySettings();
|
||||
if (signingKey == null)
|
||||
{
|
||||
signingKey = signingKeySettings.AccountKey;
|
||||
vm.SigningKey = signingKey.ToString();
|
||||
}
|
||||
if (vm.SigningKeyPath == null)
|
||||
{
|
||||
signingKeyPath = signingKeySettings.GetRootedKeyPath();
|
||||
vm.SigningKeyPath = signingKeyPath?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
var balanceChange = psbtObject.GetBalance(signingKey, signingKeyPath);
|
||||
|
||||
vm.BalanceChange = ValueToString(balanceChange, network);
|
||||
vm.Positive = balanceChange >= Money.Zero;
|
||||
|
||||
foreach (var output in psbtObject.Outputs)
|
||||
{
|
||||
var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
|
||||
vm.Destinations.Add(dest);
|
||||
var mine = output.HDKeysFor(signingKey, signingKeyPath).Any();
|
||||
var balanceChange2 = output.Value;
|
||||
if (!mine)
|
||||
balanceChange2 = -balanceChange2;
|
||||
dest.Balance = ValueToString(balanceChange2, network);
|
||||
dest.Positive = balanceChange2 >= Money.Zero;
|
||||
dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
|
||||
}
|
||||
|
||||
if (psbtObject.TryGetFee(out var fee))
|
||||
{
|
||||
vm.Fee = ValueToString(fee, network);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -125,6 +182,7 @@ namespace BTCPayServer.Controllers
|
|||
try
|
||||
{
|
||||
psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
|
||||
await FetchTransactionDetails(walletId, vm, network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -161,7 +219,7 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
else if (command == "analyze-psbt")
|
||||
{
|
||||
return ViewPSBT(psbt);
|
||||
return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() });
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -252,13 +252,9 @@ namespace BTCPayServer.Controllers
|
|||
case "ledger":
|
||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||
case "seed":
|
||||
return RedirectToAction(nameof(SignWithSeed), new
|
||||
{
|
||||
psbt = psbt.PSBT.ToBase64(),
|
||||
send = true
|
||||
});
|
||||
return SignWithSeed(walletId, psbt.PSBT.ToBase64());
|
||||
case "analyze-psbt":
|
||||
return ViewPSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
||||
return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.PSBT.ToBase64(), FileName= $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt" });
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -277,12 +273,11 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[HttpGet("{walletId}/psbt/seed")]
|
||||
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,string psbt, bool send)
|
||||
WalletId walletId,string psbt)
|
||||
{
|
||||
return View(new SignWithSeedViewModel()
|
||||
return View(nameof(SignWithSeed), new SignWithSeedViewModel()
|
||||
{
|
||||
PSBT = psbt,
|
||||
Send = send
|
||||
PSBT = psbt
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -297,32 +292,10 @@ namespace BTCPayServer.Controllers
|
|||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
var isMnemonic = false;
|
||||
var isExtKey = false;
|
||||
ExtKey extKey = null;
|
||||
try
|
||||
{
|
||||
var mnemonic = new Mnemonic(viewModel.SeedOrKey);
|
||||
isMnemonic = true;
|
||||
extKey = mnemonic.DeriveExtKey(viewModel.Passphrase);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
if (!isMnemonic)
|
||||
{
|
||||
try
|
||||
{
|
||||
extKey = ExtKey.Parse(viewModel.SeedOrKey, network.NBitcoinNetwork);
|
||||
isExtKey = true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);
|
||||
|
||||
if (!isMnemonic && !isExtKey)
|
||||
if (extKey == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey),
|
||||
"Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv");
|
||||
|
@ -340,23 +313,32 @@ namespace BTCPayServer.Controllers
|
|||
return View(viewModel);
|
||||
}
|
||||
|
||||
|
||||
var signedpsbt = psbt.SignAll(extKey);
|
||||
|
||||
if (viewModel.Send)
|
||||
RootedKeyPath rootedKeyPath = null;
|
||||
var settings = (await GetDerivationSchemeSettings(walletId));
|
||||
var signingKeySettings = settings.GetSigningAccountKeySettings();
|
||||
if (signingKeySettings.RootFingerprint is null)
|
||||
signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint();
|
||||
if (signingKeySettings.GetRootedKeyPath()?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
|
||||
{
|
||||
return await WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
|
||||
{
|
||||
PSBT = signedpsbt.ToBase64()
|
||||
}, "broadcast");
|
||||
psbt.RebaseKeyPaths(signingKeySettings.AccountKey, signingKeySettings.GetRootedKeyPath());
|
||||
rootedKeyPath = signingKeySettings.GetRootedKeyPath();
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WalletPSBTReady), new
|
||||
ExtKey signingKey = rootedKeyPath == null ? extKey : extKey.Derive(rootedKeyPath.KeyPath);
|
||||
var balanceChange = psbt.GetBalance(signingKey, rootedKeyPath);
|
||||
if (balanceChange == Money.Zero)
|
||||
{
|
||||
walletId,
|
||||
psbt = signedpsbt.ToBase64()
|
||||
});
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed does not seem to be able to sign this transaction. Either this is the wrong key, or Wallet Settings have not the correct account path in the wallet settings.");
|
||||
return View(viewModel);
|
||||
}
|
||||
psbt.SignAll(extKey, rootedKeyPath);
|
||||
ModelState.Remove(nameof(viewModel.PSBT));
|
||||
return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath.ToString());
|
||||
}
|
||||
|
||||
private string ValueToString(Money v, BTCPayNetwork network)
|
||||
{
|
||||
return v.ToString() + " " + network.CryptoCode;
|
||||
}
|
||||
|
||||
private IDestination[] ParseDestination(string destination, Network network)
|
||||
|
@ -661,7 +643,7 @@ namespace BTCPayServer.Controllers
|
|||
if (derivationScheme == null)
|
||||
return NotFound();
|
||||
derivationScheme.Label = vm.Label;
|
||||
derivationScheme.SigningKey = new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
|
||||
derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey) ? null : new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
|
||||
for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++)
|
||||
{
|
||||
derivationScheme.AccountKeySettings[i].AccountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) ? null
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
|
@ -12,6 +15,29 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
[Display(Name = "Optional seed passphrase")]
|
||||
public string Passphrase { get; set; }
|
||||
|
||||
public bool Send { get; set; }
|
||||
public ExtKey GetExtKey(Network network)
|
||||
{
|
||||
ExtKey extKey = null;
|
||||
try
|
||||
{
|
||||
var mnemonic = new Mnemonic(SeedOrKey);
|
||||
extKey = mnemonic.DeriveExtKey(Passphrase);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
if (extKey == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
extKey = ExtKey.Parse(SeedOrKey, network);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
return extKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,20 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public class WalletPSBTReadyViewModel
|
||||
{
|
||||
public string PSBT { get; set; }
|
||||
public string SigningKey { get; set; }
|
||||
public string SigningKeyPath { get; set; }
|
||||
public List<string> Errors { get; set; }
|
||||
|
||||
public class DestinationViewModel
|
||||
{
|
||||
public bool Positive { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string Balance { get; set; }
|
||||
}
|
||||
|
||||
public string BalanceChange { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public List<DestinationViewModel> Destinations { get; set; } = new List<DestinationViewModel>();
|
||||
public string Fee { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,26 @@
|
|||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Sign PSBT With an HD private key or mnemonic seed";
|
||||
ViewData.SetActivePageAndTitle(Model.Send ? WalletsNavPages.Send : WalletsNavPages.PSBT);
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<form method="post">
|
||||
<form method="post" asp-action="SignWithSeed">
|
||||
|
||||
<input type="hidden" asp-for="PSBT"/>
|
||||
<input type="hidden" asp-for="Send"/>
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<div class="form-group">
|
||||
<label asp-for="SeedOrKey"></label>
|
||||
<input asp-for="SeedOrKey" class="form-control"/>
|
||||
<input asp-for="SeedOrKey" class="form-control" />
|
||||
<span asp-validation-for="SeedOrKey" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Passphrase"></label>
|
||||
<input asp-for="Passphrase" class="form-control"/>
|
||||
<input asp-for="Passphrase" class="form-control" />
|
||||
<span asp-validation-for="Passphrase" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Sign</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
|
||||
<script src="~/vendor/highlightjs/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
}
|
||||
|
|
|
@ -32,10 +32,11 @@
|
|||
Sign with...
|
||||
</button>
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<input type="hidden" asp-for="FileName" />
|
||||
<div class="dropdown-menu" aria-labelledby="SendMenu">
|
||||
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="save-psbt">... a wallet supporting PSBT files</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="save-psbt">... a wallet supporting PSBT (save as file)</button>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-secondary" value="broadcast">Broadcast</button>
|
||||
<button name="command" type="submit" class="btn btn-secondary" value="combine">Combine with another PSBT</button>
|
||||
|
|
|
@ -16,16 +16,68 @@
|
|||
}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">Transaction signed</h2>
|
||||
<h2 class="section-heading">Transaction review</h2>
|
||||
<hr class="primary">
|
||||
<p>Your transaction has been signed, what do you want to do next?</p>
|
||||
<p>
|
||||
If you broadcast this transaction, your balance will change: @if (Model.Positive)
|
||||
{
|
||||
<span style="color:green;">@Model.BalanceChange</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span style="color:red;">@Model.BalanceChange</span>
|
||||
}, do you want to continue?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
<div class="col-lg-6 text-center">
|
||||
<table class="table table-sm table-responsive-lg">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="text-align:left" class="col-md-auto">
|
||||
Destination
|
||||
</th>
|
||||
<th style="text-align:right">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var destination in Model.Destinations)
|
||||
{
|
||||
<tr>
|
||||
<td style="text-align:left">@destination.Destination</td>
|
||||
@if (destination.Positive)
|
||||
{
|
||||
<td style="text-align:right; color:green;">@destination.Balance</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td style="text-align:right; color:red;">@destination.Balance</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
</div>
|
||||
@if (Model.Fee != null)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
<div class="col-lg-6 text-left">
|
||||
<p>Transaction fee: <span style="color: red">@Model.Fee</span></p>
|
||||
</div>
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<form method="post" asp-action="WalletPSBTReady">
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<input type="hidden" asp-for="SigningKey" />
|
||||
<input type="hidden" asp-for="SigningKeyPath" />
|
||||
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast it</button> or
|
||||
<button type="submit" class="btn btn-secondary" name="command" value="analyze-psbt">Export as PSBT</button>
|
||||
</form>
|
||||
|
|
Loading…
Add table
Reference in a new issue