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:
Andrew Camilleri
2019-05-14 16:03:48 +00:00
committed by Nicolas Dorier
parent cf436e11ae
commit eb54a18fcd
6 changed files with 190 additions and 43 deletions

View File

@@ -61,22 +61,25 @@ namespace BTCPayServer.Controllers
return View(vm); return View(vm);
} }
if (command == null) switch (command)
{ {
case null:
vm.Decoded = psbt.ToString(); vm.Decoded = psbt.ToString();
vm.FileName = string.Empty; vm.FileName = string.Empty;
return View(vm); return View(vm);
} case "ledger":
else if (command == "ledger")
{
return ViewWalletSendLedger(psbt); return ViewWalletSendLedger(psbt);
} case "seed":
else if (command == "broadcast") return RedirectToAction("SignWithSeed", new
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{ {
psbt = psbt.ToBase64(),
walletId,
send = false
});
case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
return ViewPSBT(psbt, errors); return ViewPSBT(psbt, errors);
} case "broadcast":
{
var transaction = psbt.ExtractTransaction(); var transaction = psbt.ExtractTransaction();
try try
{ {
@@ -92,17 +95,15 @@ namespace BTCPayServer.Controllers
} }
return await RedirectToWalletTransaction(walletId, transaction); return await RedirectToWalletTransaction(walletId, transaction);
} }
else if (command == "combine") case "combine":
{
ModelState.Remove(nameof(vm.PSBT)); ModelState.Remove(nameof(vm.PSBT));
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() }); return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
} case "save-psbt":
else if (command == "save-psbt")
{
return FilePSBT(psbt, vm.FileName); return FilePSBT(psbt, vm.FileName);
} default:
return View(vm); return View(vm);
} }
}
[HttpGet] [HttpGet]
[Route("{walletId}/psbt/ready")] [Route("{walletId}/psbt/ready")]

View File

@@ -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,16 +247,22 @@ namespace BTCPayServer.Controllers
return View(vm); return View(vm);
} }
derivationScheme.RebaseKeyPaths(psbt.PSBT); derivationScheme.RebaseKeyPaths(psbt.PSBT);
if (command == "ledger") switch (command)
{ {
case "ledger":
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress); return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
} case "seed":
else if (command == "analyze-psbt") 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"); return ViewPSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
} default:
return View(vm); return View(vm);
} }
}
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null) private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
{ {
@@ -267,6 +275,90 @@ namespace BTCPayServer.Controllers
}); });
} }
[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

View File

@@ -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; }
}
}

View 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>
}

View File

@@ -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>

View File

@@ -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>