Can broadcast PSBT, can decide to export something signed by the ledger via PSBT

This commit is contained in:
nicolas.dorier
2019-05-12 00:05:30 +09:00
parent 925dc869a2
commit cb9130fdf9
7 changed files with 198 additions and 52 deletions

View File

@@ -182,6 +182,9 @@
<Content Update="Views\Wallets\ListWallets.cshtml"> <Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>
<Content Update="Views\Wallets\WalletPSBTReady.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBT.cshtml"> <Content Update="Views\Wallets\WalletPSBT.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>

View File

@@ -240,11 +240,7 @@ namespace BTCPayServer.Controllers
try try
{ {
if (command == "analyze-psbt") if (command == "analyze-psbt")
return View(nameof(WalletPSBT), new WalletPSBTViewModel() return ViewPSBT(psbt.PSBT);
{
Decoded = psbt.PSBT.ToString(),
PSBT = psbt.PSBT.ToBase64()
});
return FilePSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt"); return FilePSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
} }
catch (NBXplorerException ex) catch (NBXplorerException ex)
@@ -260,6 +256,20 @@ namespace BTCPayServer.Controllers
} }
} }
private IActionResult ViewPSBT<T>(PSBT psbt, IEnumerable<T> errors = null)
{
return ViewPSBT(psbt, errors?.Select(e => e.ToString()).ToList());
}
private IActionResult ViewPSBT(PSBT psbt, IEnumerable<string> errors = null)
{
return View(nameof(WalletPSBT), new WalletPSBTViewModel()
{
Decoded = psbt.ToString(),
PSBT = psbt.ToBase64(),
Errors = errors?.ToList()
});
}
private IActionResult FilePSBT(PSBT psbt, string fileName) private IActionResult FilePSBT(PSBT psbt, string fileName)
{ {
return File(psbt.ToBytes(), "application/octet-stream", fileName); return File(psbt.ToBytes(), "application/octet-stream", fileName);
@@ -272,7 +282,7 @@ namespace BTCPayServer.Controllers
PSBT = psbt.ToBase64(), PSBT = psbt.ToBase64(),
HintChange = hintChange?.ToString(), HintChange = hintChange?.ToString(),
WebsocketPath = this.Url.Action(nameof(LedgerConnection)), WebsocketPath = this.Url.Action(nameof(LedgerConnection)),
SuccessPath = this.Url.Action(nameof(WalletSendLedgerSuccess)) SuccessPath = this.Url.Action(nameof(WalletPSBTReady))
}); });
} }
@@ -354,24 +364,126 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("{walletId}/psbt/sign")] [Route("{walletId}/psbt/sign")]
public IActionResult WalletPSBTSign( public async Task<IActionResult> WalletPSBTSign(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletId walletId,
WalletPSBTViewModel vm, WalletPSBTViewModel vm,
string command = null string command = null
) )
{ {
var psbt = PSBT.Parse(vm.PSBT, NetworkProvider.GetNetwork(walletId.CryptoCode).NBitcoinNetwork); var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
if (command == "ledger") if (command == "ledger")
{ {
return ViewWalletSendLedger(psbt); return ViewWalletSendLedger(psbt);
} }
else if (command == "broadcast")
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{
return ViewPSBT(psbt, errors);
}
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);
}
else else
{ {
return FilePSBT(psbt, "psbt-export.psbt"); return FilePSBT(psbt, "psbt-export.psbt");
} }
} }
private async Task<IActionResult> RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
{
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
if (transaction != null)
{
var wallet = _walletProvider.GetWallet(network);
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationSettings = GetPaymentMethod(walletId, storeData);
wallet.InvalidateCache(derivationSettings.AccountDerivation);
StatusMessage = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})";
}
return RedirectToAction(nameof(WalletTransactions));
}
[HttpGet]
[Route("{walletId}/psbt/ready")]
public IActionResult WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string psbt = null)
{
return View(new WalletPSBTReadyViewModel() { PSBT = psbt });
}
[HttpPost]
[Route("{walletId}/psbt/ready")]
public async Task<IActionResult> WalletPSBTReady(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
{
PSBT psbt = null;
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
try
{
psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
}
catch
{
vm.Errors = new List<string>();
vm.Errors.Add("Invalid PSBT");
return View(vm);
}
if (command == "broadcast")
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{
vm.Errors = new List<string>();
vm.Errors.AddRange(errors.Select(e => e.ToString()));
return View(vm);
}
var transaction = psbt.ExtractTransaction();
try
{
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
{
vm.Errors = new List<string>();
vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
return View(vm);
}
}
catch (Exception ex)
{
vm.Errors = new List<string>();
vm.Errors.Add("Error while broadcasting: " + ex.Message);
return View(vm);
}
return await RedirectToWalletTransaction(walletId, transaction);
}
else if (command == "analyze-psbt")
{
return ViewPSBT(psbt);
}
else
{
vm.Errors = new List<string>();
vm.Errors.Add("Unknown command");
return View(vm);
}
}
[HttpGet] [HttpGet]
[Route("{walletId}/rescan")] [Route("{walletId}/rescan")]
public async Task<IActionResult> WalletRescan( public async Task<IActionResult> WalletRescan(
@@ -485,17 +597,6 @@ namespace BTCPayServer.Controllers
return _userManager.GetUserId(User); return _userManager.GetUserId(User);
} }
[HttpGet]
[Route("{walletId}/send/ledger/success")]
public IActionResult WalletSendLedgerSuccess(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string txid)
{
StatusMessage = $"Transaction broadcasted ({txid})";
return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() });
}
[HttpGet] [HttpGet]
[Route("{walletId}/send/ledger/ws")] [Route("{walletId}/send/ledger/ws")]
public async Task<IActionResult> LedgerConnection( public async Task<IActionResult> LedgerConnection(
@@ -586,26 +687,7 @@ namespace BTCPayServer.Controllers
}; };
signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token); psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
if(!psbtResponse.PSBT.TryFinalize(out var errors)) result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
{
throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})");
}
var transaction = psbtResponse.PSBT.ExtractTransaction();
try
{
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
var wallet = _walletProvider.GetWallet(network);
wallet.InvalidateCache(derivationSettings.AccountDerivation);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -639,6 +721,7 @@ namespace BTCPayServer.Controllers
public class SendToAddressResult public class SendToAddressResult
{ {
public string TransactionId { get; set; } [JsonProperty("psbt")]
public string PSBT { get; set; }
} }
} }

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletPSBTReadyViewModel
{
public string PSBT { get; set; }
public List<string> Errors { get; set; }
}
}

View File

@@ -9,5 +9,6 @@ namespace BTCPayServer.Models.WalletViewModels
{ {
public string Decoded { get; set; } public string Decoded { get; set; }
public string PSBT { get; set; } public string PSBT { get; set; }
public List<string> Errors { get; set; } = new List<string>();
} }
} }

View File

@@ -6,23 +6,35 @@
} }
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
@if (Model.Errors != null && Model.Errors.Count != 0)
{
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@foreach (var error in Model.Errors)
{
<span>@error</span><br />
}
</div>
}
@if (!string.IsNullOrEmpty(Model.Decoded)) @if (!string.IsNullOrEmpty(Model.Decoded))
{ {
<h3>Decoded PSBT</h3> <h3>Decoded PSBT</h3>
<pre><code class="json">@Model.Decoded</code></pre> <pre><code class="json">@Model.Decoded</code></pre>
<div class="form-group"> <div class="form-group">
<form method="post" asp-action="WalletPSBTSign">
<div class="dropdown" style="margin-top:16px;"> <div class="dropdown" style="margin-top:16px;">
<button class="btn btn-primary dropdown-toggle" type="button" id="SendMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button class="btn btn-primary dropdown-toggle" type="button" id="SendMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Sign with... Sign with...
</button> </button>
<form method="post" asp-action="WalletPSBTSign">
<input type="hidden" asp-for="PSBT" /> <input type="hidden" asp-for="PSBT" />
<div class="dropdown-menu" aria-labelledby="SendMenu"> <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="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="save-psbt">... a wallet supporting PSBT files</button>
</div> </div>
</form> or
<button name="command" type="submit" class="btn btn-primary" value="broadcast">Broadcast</button>
</div> </div>
</form>
</div> </div>
} }
<h3>PSBT to decode</h3> <h3>PSBT to decode</h3>

View File

@@ -0,0 +1,35 @@
@model WalletPSBTReadyViewModel
@{
Layout = "../Shared/_Layout.cshtml";
}
<section>
<div class="container">
@if (Model.Errors != null)
{
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@foreach (var error in Model.Errors)
{
<span>@error</span><br />
}
</div>
}
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">Transaction signed</h2>
<hr class="primary">
<p>Your transaction has been signed, what do you want to do next?</p>
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<form method="post" asp-action="WalletPSBTReady">
<input type="hidden" asp-for="PSBT" />
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast it</button> or
<button type="submit" class="btn btn-primary" name="command" value="analyze-psbt">Export as PSBT</button>
</form>
</div>
</div>
</div>
</section>

View File

@@ -62,8 +62,7 @@
if (result.error) { if (result.error) {
WriteAlert("danger", result.error); WriteAlert("danger", result.error);
} else { } else {
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')'); window.location.replace(loc.protocol + "//" + loc.host + successPath + "?psbt=" + encodeURIComponent(result.psbt));
window.location.replace(loc.protocol + "//" + loc.host + successPath + "?txid=" + result.transactionId);
} }
}); });
}; };