Can combine PSBT

This commit is contained in:
nicolas.dorier 2019-05-12 13:13:52 +09:00
parent 572fe3eacb
commit 6da0a9a201
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
7 changed files with 184 additions and 55 deletions

View file

@ -47,7 +47,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.2.17" /> <PackageReference Include="NBitcoin" Version="4.1.2.19" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" /> <PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="DBriize" Version="1.0.0.4" /> <PackageReference Include="DBriize" Version="1.0.0.4" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" /> <PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
@ -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\WalletPSBTCombine.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTReady.cshtml"> <Content Update="Views\Wallets\WalletPSBTReady.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>

View file

@ -14,50 +14,6 @@ namespace BTCPayServer.Controllers
public partial class WalletsController public partial class WalletsController
{ {
[HttpPost]
[Route("{walletId}/psbt/sign")]
public async Task<IActionResult> WalletPSBTSign(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
WalletPSBTViewModel vm,
string command = null
)
{
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
if (command == "ledger")
{
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
{
(await GetDerivationSchemeSettings(walletId)).RebaseKeyPaths(psbt);
return FilePSBT(psbt, "psbt-export.psbt");
}
}
[NonAction] [NonAction]
public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken) public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
{ {
@ -93,21 +49,59 @@ namespace BTCPayServer.Controllers
} }
[HttpPost] [HttpPost]
[Route("{walletId}/psbt")] [Route("{walletId}/psbt")]
public IActionResult WalletPSBT( public async Task<IActionResult> WalletPSBT(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletId walletId,
WalletPSBTViewModel vm) WalletPSBTViewModel vm, string command = null)
{ {
try var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var psbt = vm.GetPSBT(network.NBitcoinNetwork);
if (psbt == null)
{ {
if (!string.IsNullOrEmpty(vm.PSBT)) ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
vm.Decoded = PSBT.Parse(vm.PSBT, NetworkProvider.GetNetwork(walletId.CryptoCode).NBitcoinNetwork).ToString(); return View(vm);
} }
catch (FormatException ex)
if (command == null)
{ {
ModelState.AddModelError(nameof(vm.PSBT), ex.Message); vm.Decoded = psbt.ToString();
return View(vm);
}
else if (command == "ledger")
{
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 if (command == "combine")
{
ModelState.Remove(nameof(vm.PSBT));
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
}
else
{
(await GetDerivationSchemeSettings(walletId)).RebaseKeyPaths(psbt);
return FilePSBT(psbt, "psbt-export.psbt");
} }
return View(vm);
} }
[HttpGet] [HttpGet]
@ -194,5 +188,28 @@ namespace BTCPayServer.Controllers
{ {
return File(psbt.ToBytes(), "application/octet-stream", fileName); return File(psbt.ToBytes(), "application/octet-stream", fileName);
} }
[HttpPost]
[Route("{walletId}/psbt/combine")]
public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTCombineViewModel vm)
{
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (psbt == null)
{
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
return View(vm);
}
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
if (sourcePSBT == null)
{
ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT");
return View(vm);
}
sourcePSBT = sourcePSBT.Combine(psbt);
StatusMessage = "PSBT Successfully combined!";
return ViewPSBT(sourcePSBT);
}
} }
} }

View file

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using NBitcoin;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletPSBTCombineViewModel
{
public string OtherPSBT { get; set; }
[Display(Name = "PSBT to combine with...")]
public string PSBT { get; set; }
[Display(Name = "Upload PSBT from file...")]
public IFormFile UploadedPSBTFile { get; set; }
public PSBT GetSourcePSBT(Network network)
{
if (!string.IsNullOrEmpty(OtherPSBT))
{
try
{
return NBitcoin.PSBT.Parse(OtherPSBT, network);
}
catch
{ }
}
return null;
}
public async Task<PSBT> GetPSBT(Network network)
{
if (UploadedPSBTFile != null)
{
if (UploadedPSBTFile.Length > 500 * 1024)
return null;
byte[] bytes = new byte[UploadedPSBTFile.Length];
using (var stream = UploadedPSBTFile.OpenReadStream())
{
await stream.ReadAsync(bytes, 0, (int)UploadedPSBTFile.Length);
}
try
{
return NBitcoin.PSBT.Load(bytes, network);
}
catch
{
return null;
}
}
if (!string.IsNullOrEmpty(PSBT))
{
try
{
return NBitcoin.PSBT.Parse(PSBT, network);
}
catch
{ }
}
return null;
}
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Models.WalletViewModels namespace BTCPayServer.Models.WalletViewModels
{ {
@ -10,5 +11,15 @@ 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>(); public List<string> Errors { get; set; } = new List<string>();
internal PSBT GetPSBT(Network network)
{
try
{
return NBitcoin.PSBT.Parse(PSBT, network);
}
catch { }
return null;
}
} }
} }

View file

@ -141,6 +141,8 @@ namespace BTCPayServer.Services
psbt = psbt.Clone(); psbt = psbt.Clone();
foreach (var signature in signatureRequests) foreach (var signature in signatureRequests)
{ {
if (signature.Signature == null)
continue;
var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint); var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
if (input == null) if (input == null)
continue; continue;

View file

@ -4,6 +4,12 @@
ViewData["Title"] = "PSBT"; ViewData["Title"] = "PSBT";
ViewData.SetActivePageAndTitle(WalletsNavPages.PSBT); ViewData.SetActivePageAndTitle(WalletsNavPages.PSBT);
} }
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
@if (Model.Errors != null && Model.Errors.Count != 0) @if (Model.Errors != null && Model.Errors.Count != 0)
@ -21,7 +27,7 @@
<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"> <form method="post" asp-action="WalletPSBT">
<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...
@ -31,8 +37,8 @@
<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>
or
<button name="command" type="submit" class="btn btn-primary" value="broadcast">Broadcast</button> <button name="command" type="submit" class="btn btn-primary" value="broadcast">Broadcast</button>
<button name="command" type="submit" class="btn btn-primary" value="combine">Combine with another PSBT</button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -0,0 +1,25 @@
@model WalletPSBTCombineViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "PSBT";
ViewData.SetActivePageAndTitle(WalletsNavPages.PSBT);
}
<h4>Combine PSBT</h4>
<div class="row">
<div class="col-md-10">
<form class="form-group" method="post" asp-action="WalletPSBTCombine">
<input type="hidden" asp-for="OtherPSBT" />
<div class="form-group">
<label asp-for="PSBT"></label>
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
<span asp-validation-for="PSBT" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UploadedPSBTFile"></label>
<input type="file" class="form-control-file" asp-for="UploadedPSBTFile">
</div>
<button type="submit" class="btn btn-primary">Combine</button>
</form>
</div>
</div>