mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Can broadcast PSBT, can decide to export something signed by the ledger via PSBT
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">×</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>
|
||||||
|
|||||||
35
BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml
Normal file
35
BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml
Normal 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">×</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>
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user