From 87df34e0643aedd628a4956455f17b48e0cb538b Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 19 May 2019 23:27:18 +0900 Subject: [PATCH] Can actually upload PSBT file in PSBT Combine and PSBT view. Validate transaction before allowing any broadcast and show errors nicely. --- BTCPayServer/BTCPayServer.csproj | 4 +- .../Controllers/WalletsController.PSBT.cs | 67 ++++++++------ BTCPayServer/Controllers/WalletsController.cs | 4 +- .../WalletPSBTReadyViewModel.cs | 20 ++++- .../WalletViewModels/WalletPSBTViewModel.cs | 35 +++++++- BTCPayServer/Views/Wallets/WalletPSBT.cshtml | 6 +- .../Views/Wallets/WalletPSBTCombine.cshtml | 2 +- .../Views/Wallets/WalletPSBTReady.cshtml | 89 +++++++++++++------ 8 files changed, 162 insertions(+), 65 deletions(-) diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index f872d74d1..2e752efc5 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -47,10 +47,10 @@ all runtime; build; native; contentfiles; analyzers - + - + diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index 8b0fa5b14..33c4c2134 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -42,13 +42,14 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{walletId}/psbt")] - public IActionResult WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))] + public async Task WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletPSBTViewModel vm) { var network = NetworkProvider.GetNetwork(walletId.CryptoCode); - if (vm?.PSBT != null) + if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt) { - vm.Decoded = vm.GetPSBT(network.NBitcoinNetwork)?.ToString(); + vm.Decoded = psbt.ToString(); + vm.PSBT = psbt.ToBase64(); } return View(vm ?? new WalletPSBTViewModel()); } @@ -60,25 +61,26 @@ namespace BTCPayServer.Controllers WalletPSBTViewModel vm, string command = null) { var network = NetworkProvider.GetNetwork(walletId.CryptoCode); - var psbt = vm.GetPSBT(network.NBitcoinNetwork); + var psbt = await vm.GetPSBT(network.NBitcoinNetwork); if (psbt == null) { ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT"); return View(vm); } - switch (command) { case null: vm.Decoded = psbt.ToString(); - vm.FileName = string.Empty; + ModelState.Remove(nameof(vm.PSBT)); + ModelState.Remove(nameof(vm.FileName)); + ModelState.Remove(nameof(vm.UploadedPSBTFile)); + vm.PSBT = psbt.ToBase64(); + vm.FileName = vm.UploadedPSBTFile?.FileName; return View(vm); case "ledger": return ViewWalletSendLedger(psbt); case "seed": return SignWithSeed(walletId, psbt.ToBase64()); - case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors): - return ViewPSBT(psbt, errors); case "broadcast": { return await WalletPSBTReady(walletId, psbt.ToBase64()); @@ -132,9 +134,10 @@ namespace BTCPayServer.Controllers } catch { } + var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId); if (signingKey == null || signingKeyPath == null) { - var signingKeySettings = (await GetDerivationSchemeSettings(walletId)).GetSigningAccountKeySettings(); + var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings(); if (signingKey == null) { signingKey = signingKeySettings.AccountKey; @@ -147,16 +150,23 @@ namespace BTCPayServer.Controllers } } - var balanceChange = psbtObject.GetBalance(signingKey, signingKeyPath); - - vm.BalanceChange = ValueToString(balanceChange, network); - vm.Positive = balanceChange >= Money.Zero; + if (psbtObject.IsAllFinalized()) + { + vm.CanCalculateBalance = false; + } + else + { + var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath); + vm.BalanceChange = ValueToString(balanceChange, network); + vm.CanCalculateBalance = true; + vm.Positive = balanceChange >= Money.Zero; + } foreach (var output in psbtObject.Outputs) { var dest = new WalletPSBTReadyViewModel.DestinationViewModel(); vm.Destinations.Add(dest); - var mine = output.HDKeysFor(signingKey, signingKeyPath).Any(); + var mine = output.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any(); var balanceChange2 = output.Value; if (!mine) balanceChange2 = -balanceChange2; @@ -167,12 +177,22 @@ namespace BTCPayServer.Controllers if (psbtObject.TryGetFee(out var fee)) { - vm.Fee = ValueToString(fee, network); + vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel() + { + Positive = false, + Balance = ValueToString(- fee, network), + Destination = "Mining fees" + }); } if (psbtObject.TryGetEstimatedFeeRate(out var feeRate)) { vm.FeeRate = feeRate.ToString(); } + + if (!psbtObject.IsAllFinalized() && !psbtObject.TryFinalize(out var errors)) + { + vm.SetErrors(errors); + } } [HttpPost] @@ -190,16 +210,14 @@ namespace BTCPayServer.Controllers } catch { - vm.Errors = new List(); - vm.Errors.Add("Invalid PSBT"); + vm.GlobalError = "Invalid PSBT"; return View(vm); } if (command == "broadcast") { if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) { - vm.Errors = new List(); - vm.Errors.AddRange(errors.Select(e => e.ToString())); + vm.SetErrors(errors); return View(vm); } var transaction = psbt.ExtractTransaction(); @@ -208,15 +226,13 @@ namespace BTCPayServer.Controllers var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); if (!broadcastResult.Success) { - vm.Errors = new List(); - vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"); + vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; return View(vm); } } catch (Exception ex) { - vm.Errors = new List(); - vm.Errors.Add("Error while broadcasting: " + ex.Message); + vm.GlobalError = "Error while broadcasting: " + ex.Message; return View(vm); } return await RedirectToWalletTransaction(walletId, transaction); @@ -227,8 +243,7 @@ namespace BTCPayServer.Controllers } else { - vm.Errors = new List(); - vm.Errors.Add("Unknown command"); + vm.GlobalError = "Unknown command"; return View(vm); } } @@ -243,6 +258,8 @@ namespace BTCPayServer.Controllers } private IActionResult ViewPSBT(PSBT psbt, string fileName, IEnumerable errors = null) { + ModelState.Remove(nameof(WalletPSBTViewModel.PSBT)); + ModelState.Remove(nameof(WalletPSBTViewModel.FileName)); return View(nameof(WalletPSBT), new WalletPSBTViewModel() { Decoded = psbt.ToString(), diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index e42908c04..83c7058c1 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -331,13 +331,13 @@ namespace BTCPayServer.Controllers { signingKey = extKey; } - var balanceChange = psbt.GetBalance(signingKey, rootedKeyPath); + var balanceChange = psbt.GetBalance(settings.AccountDerivation, signingKey, rootedKeyPath); if (balanceChange == Money.Zero) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed does not seem to be able to sign this transaction. Either this is the wrong key, or Wallet Settings have not the correct account path in the wallet settings."); return View(viewModel); } - psbt.SignAll(signingKey, rootedKeyPath); + psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath); ModelState.Remove(nameof(viewModel.PSBT)); return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath.ToString()); } diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs index f8371cb5d..10047c8de 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs @@ -2,15 +2,22 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NBitcoin; namespace BTCPayServer.Models.WalletViewModels { public class WalletPSBTReadyViewModel { + public class FinalizeError + { + public int Index { get; set; } + public string Error { get; set; } + } public string PSBT { get; set; } public string SigningKey { get; set; } public string SigningKeyPath { get; set; } - public List Errors { get; set; } + public string GlobalError { get; set; } + public List Errors { get; set; } public class DestinationViewModel { @@ -20,9 +27,18 @@ namespace BTCPayServer.Models.WalletViewModels } public string BalanceChange { get; set; } + public bool CanCalculateBalance { get; set; } public bool Positive { get; set; } public List Destinations { get; set; } = new List(); - public string Fee { get; set; } public string FeeRate { get; set; } + + internal void SetErrors(IList errors) + { + Errors = new List(); + foreach (var err in errors) + { + Errors.Add(new FinalizeError() { Index = (int)err.InputIndex, Error = err.Message }); + } + } } } diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs index 61199f3a2..05f79dce4 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using NBitcoin; namespace BTCPayServer.Models.WalletViewModels @@ -24,13 +26,38 @@ namespace BTCPayServer.Models.WalletViewModels public string PSBT { get; set; } public List Errors { get; set; } = new List(); - internal PSBT GetPSBT(Network network) + [Display(Name = "Upload PSBT from file...")] + public IFormFile UploadedPSBTFile { get; set; } + + public async Task GetPSBT(Network network) { - try + if (UploadedPSBTFile != null) { - return NBitcoin.PSBT.Parse(PSBT, network); + 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 + { } } - catch { } return null; } } diff --git a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml index c2c80c5aa..73ee7e9b8 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml @@ -46,11 +46,15 @@
@Model.Decoded
}

PSBT to decode

-
+
+
+ + +
diff --git a/BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml b/BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml index d9d175e67..0a607bd0a 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBTCombine.cshtml @@ -8,7 +8,7 @@

Combine PSBT

-
+
diff --git a/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml b/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml index 1dcdb5c60..157b936a4 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml @@ -4,30 +4,34 @@ }
- @if (Model.Errors != null) + @if (Model.GlobalError != null) { }

Transaction review


-

- If you broadcast this transaction, your balance will change: @if (Model.Positive) - { - @Model.BalanceChange - } - else - { - @Model.BalanceChange - }, do you want to continue? -

+ @if (Model.CanCalculateBalance) + { +

+ If you broadcast this transaction, your balance will change: @if (Model.Positive) + { + @Model.BalanceChange + } + else + { + @Model.BalanceChange + }, do you want to continue? +

+ } + else + { +

This PSBT is already finalized. We can't properly detect which input or output belongs to you.

+ }
@@ -62,22 +66,48 @@
- @if (Model.Fee != null) - { -
-
-
-

Transaction fee: @Model.Fee

-
-
-
- } @if (Model.FeeRate != null) {
-
-

Transaction fee rate: @Model.FeeRate

+
+

Transaction fee rate: @Model.FeeRate

+
+
+
+ } + @if (Model.Errors != null) + { +
+
+

Errors

+

+ This PSBT can't be finalized for broadcast. Please review the errors. +

+
+
+
+
+
+ + + + + + + + + @foreach (var err in Model.Errors) + { + + + + + } + +
+ Input index + Error
@err.Index
@@ -88,7 +118,10 @@ - or + @if (Model.Errors == null) + { + or + }