mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
Can combine PSBT
This commit is contained in:
parent
572fe3eacb
commit
6da0a9a201
7 changed files with 184 additions and 55 deletions
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
25
BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml
Normal file
25
BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml
Normal 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>
|
Loading…
Add table
Reference in a new issue