mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Wallet & PSBT: Sign with seed or key (#840)
* Allow signing a PSBT with an extkey/wif or mnemonic seed * reword things * small text
This commit is contained in:
committed by
Nicolas Dorier
parent
cf436e11ae
commit
eb54a18fcd
@@ -61,47 +61,48 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command == null)
|
switch (command)
|
||||||
{
|
{
|
||||||
vm.Decoded = psbt.ToString();
|
case null:
|
||||||
vm.FileName = string.Empty;
|
vm.Decoded = psbt.ToString();
|
||||||
return View(vm);
|
vm.FileName = string.Empty;
|
||||||
}
|
return View(vm);
|
||||||
else if (command == "ledger")
|
case "ledger":
|
||||||
{
|
return ViewWalletSendLedger(psbt);
|
||||||
return ViewWalletSendLedger(psbt);
|
case "seed":
|
||||||
}
|
return RedirectToAction("SignWithSeed", new
|
||||||
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}" });
|
psbt = psbt.ToBase64(),
|
||||||
}
|
walletId,
|
||||||
}
|
send = false
|
||||||
catch (Exception ex)
|
});
|
||||||
|
case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
|
||||||
|
return ViewPSBT(psbt, errors);
|
||||||
|
case "broadcast":
|
||||||
{
|
{
|
||||||
return ViewPSBT(psbt, "Error while broadcasting: " + ex.Message);
|
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 RedirectToWalletTransaction(walletId, transaction);
|
case "combine":
|
||||||
|
ModelState.Remove(nameof(vm.PSBT));
|
||||||
|
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
|
||||||
|
case "save-psbt":
|
||||||
|
return FilePSBT(psbt, vm.FileName);
|
||||||
|
default:
|
||||||
|
return View(vm);
|
||||||
}
|
}
|
||||||
else if (command == "combine")
|
|
||||||
{
|
|
||||||
ModelState.Remove(nameof(vm.PSBT));
|
|
||||||
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
|
|
||||||
}
|
|
||||||
else if (command == "save-psbt")
|
|
||||||
{
|
|
||||||
return FilePSBT(psbt, vm.FileName);
|
|
||||||
}
|
|
||||||
return View(vm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
@@ -21,6 +22,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
@@ -245,15 +247,21 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||||
if (command == "ledger")
|
switch (command)
|
||||||
{
|
{
|
||||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
case "ledger":
|
||||||
|
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||||
|
case "seed":
|
||||||
|
return RedirectToAction("SignWithSeed", new
|
||||||
|
{
|
||||||
|
psbt = psbt.PSBT.ToBase64(),
|
||||||
|
send = true
|
||||||
|
});
|
||||||
|
case "analyze-psbt":
|
||||||
|
return ViewPSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
||||||
|
default:
|
||||||
|
return View(vm);
|
||||||
}
|
}
|
||||||
else if (command == "analyze-psbt")
|
|
||||||
{
|
|
||||||
return ViewPSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
|
||||||
}
|
|
||||||
return View(vm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
|
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
|
||||||
@@ -266,7 +274,91 @@ namespace BTCPayServer.Controllers
|
|||||||
SuccessPath = this.Url.Action(nameof(WalletPSBTReady))
|
SuccessPath = this.Url.Action(nameof(WalletPSBTReady))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{walletId}/psbt/seed")]
|
||||||
|
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
|
WalletId walletId,string psbt, bool send)
|
||||||
|
{
|
||||||
|
return View(new SignWithSeedViewModel()
|
||||||
|
{
|
||||||
|
PSBT = psbt,
|
||||||
|
Send = send
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{walletId}/psbt/seed")]
|
||||||
|
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
|
WalletId walletId, SignWithSeedViewModel viewModel)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMnemonic && !isExtKey)
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork);
|
||||||
|
|
||||||
|
if (!psbt.IsReadyToSign())
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT is not ready to be signed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var signedpsbt = psbt.SignAll(extKey);
|
||||||
|
|
||||||
|
if (viewModel.Send)
|
||||||
|
{
|
||||||
|
return await WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
|
||||||
|
{
|
||||||
|
PSBT = signedpsbt.ToBase64()
|
||||||
|
}, "broadcast");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("WalletPSBTReady", new
|
||||||
|
{
|
||||||
|
walletId,
|
||||||
|
psbt = signedpsbt.ToBase64()
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private IDestination[] ParseDestination(string destination, Network network)
|
private IDestination[] ParseDestination(string destination, Network network)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.WalletViewModels
|
||||||
|
{
|
||||||
|
public class SignWithSeedViewModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string PSBT { get; set; }
|
||||||
|
[Required][Display(Name = "Seed(12/24 word mnemonic seed) Or HD private key(xprv...)")]
|
||||||
|
public string SeedOrKey { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Optional seed passphrase")]
|
||||||
|
public string Passphrase { get; set; }
|
||||||
|
|
||||||
|
public bool Send { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
35
BTCPayServer/Views/Wallets/SignWithSeed.cshtml
Normal file
35
BTCPayServer/Views/Wallets/SignWithSeed.cshtml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@model SignWithSeedViewModel
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
ViewData["Title"] = "Sign PSBT With an HD private key or mnemonic seed";
|
||||||
|
ViewData.SetActivePageAndTitle(Model.Send ? WalletsNavPages.Send : WalletsNavPages.PSBT);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
<form method="post">
|
||||||
|
|
||||||
|
<input type="hidden" asp-for="PSBT"/>
|
||||||
|
<input type="hidden" asp-for="Send"/>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="SeedOrKey"></label>
|
||||||
|
<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"/>
|
||||||
|
<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>
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
<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>
|
||||||
|
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
|
||||||
</div>
|
</div>
|
||||||
<button name="command" type="submit" class="btn btn-secondary" value="broadcast">Broadcast</button>
|
<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>
|
<button name="command" type="submit" class="btn btn-secondary" value="combine">Combine with another PSBT</button>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<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="seed">... an HD private key or mnemonic seed</button>
|
||||||
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
|
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user