From db6a4687d25f649bdca03aceb95f4f40051dc5ea Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 13 Feb 2020 14:06:00 +0100 Subject: [PATCH] Wallet prep work for BPU (#1331) * Wallet prep work for BPU This PR prepares the wallet for #1321. It makes transfers from the vault and ledger to go to their own post actions for processing (not particularly useful in this PR but is needed in BPU to propose a new tx) It also makes the Sign with seed consistent with redirect to /psbt/ready after signing which it did not do (it stayed on the seed route) * fix test * add assert --- BTCPayServer.Tests/PSBTTests.cs | 21 ++- BTCPayServer.Tests/SeleniumTests.cs | 3 +- .../Controllers/WalletsController.PSBT.cs | 26 ++-- BTCPayServer/Controllers/WalletsController.cs | 48 +++++-- BTCPayServer/Views/Wallets/WalletPSBT.cshtml | 4 +- .../Views/Wallets/WalletPSBTReady.cshtml | 126 ++++++++++-------- .../Views/Wallets/WalletSendLedger.cshtml | 12 +- .../Views/Wallets/WalletSendVault.cshtml | 12 +- 8 files changed, 157 insertions(+), 95 deletions(-) diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs index 1c41890e2..8649bc660 100644 --- a/BTCPayServer.Tests/PSBTTests.cs +++ b/BTCPayServer.Tests/PSBTTests.cs @@ -71,7 +71,7 @@ namespace BTCPayServer.Tests BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork); Assert.NotNull(vmLedger.WebsocketPath); - string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")); + string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync(); var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork); Assert.NotNull(vmPSBT.Decoded); @@ -80,14 +80,20 @@ namespace BTCPayServer.Tests PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork); await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync(); - var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync(); + var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel() + { + PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) + } ).AssertViewModelAsync(); Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null)); Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT); var signedPSBT = unsignedPSBT.Clone(); signedPSBT.SignAll(user.DerivationScheme, user.ExtKey); vmPSBT.PSBT = signedPSBT.ToBase64(); - var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync(); + var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel() + { + PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) + } ).AssertViewModelAsync(); Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive); Assert.Contains(psbtReady.Destinations, d => d.Positive); @@ -98,7 +104,7 @@ namespace BTCPayServer.Tests var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync(); Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT); combineVM.PSBT = signedPSBT.ToBase64(); - var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM)); + var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); Assert.True(signedPSBT.TryFinalize(out _)); @@ -108,7 +114,7 @@ namespace BTCPayServer.Tests // Can use uploaded file? combineVM.PSBT = null; combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes()); - psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM)); + psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); Assert.True(signedPSBT.TryFinalize(out _)); Assert.True(signedPSBT2.TryFinalize(out _)); @@ -116,17 +122,18 @@ namespace BTCPayServer.Tests var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel(); Assert.Equal(signedPSBT.ToBase64(), ready.PSBT); - psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt")); + psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); Assert.Equal(signedPSBT.ToBase64(), psbt); redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast")); Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); } } - private static string AssertRedirectedPSBT(IActionResult view) + private static string AssertRedirectedPSBT(IActionResult view, string actionName) { var postRedirectView = Assert.IsType(view); var postRedirectViewModel = Assert.IsType(postRedirectView.Model); + Assert.Equal(actionName, postRedirectViewModel.AspAction); var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value; return redirectedPSBT; } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index ff01ad6b9..5b072bdca 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -566,9 +566,10 @@ namespace BTCPayServer.Tests Assert.Contains(jack.ToString(), s.Driver.PageSource); Assert.Contains("0.01000000", s.Driver.PageSource); s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick(); + Assert.EndsWith("psbt", s.Driver.Url); s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); - Assert.EndsWith("psbt", s.Driver.Url); + Assert.EndsWith("psbt/ready", s.Driver.Url); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); Assert.Equal(walletTransactionLink, s.Driver.Url); diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index 82efa784d..e30d5ec7c 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -20,7 +20,6 @@ namespace BTCPayServer.Controllers { var nbx = ExplorerClientProvider.GetExplorerClient(network); CreatePSBTRequest psbtRequest = new CreatePSBTRequest(); - foreach (var transactionOutput in sendModel.Outputs) { var psbtDestination = new CreatePSBTDestination(); @@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers vm.Decoded = psbt.ToString(); vm.PSBT = psbt.ToBase64(); } - return View(vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode }); + return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode }); } [HttpPost] [Route("{walletId}/psbt")] @@ -107,7 +106,7 @@ namespace BTCPayServer.Controllers return View(vm); } TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!"; - return RedirectToWalletPSBT(walletId, psbt, vm.FileName); + return RedirectToWalletPSBT(psbt, vm.FileName); case "seed": return SignWithSeed(walletId, psbt.ToBase64()); case "nbx-seed": @@ -125,7 +124,7 @@ namespace BTCPayServer.Controllers return View(vm); case "broadcast": { - return await WalletPSBTReady(walletId, psbt.ToBase64()); + return RedirectToWalletPSBTReady(psbt.ToBase64()); } case "combine": ModelState.Remove(nameof(vm.PSBT)); @@ -162,7 +161,6 @@ namespace BTCPayServer.Controllers var vm = new WalletPSBTReadyViewModel() { PSBT = psbt }; vm.SigningKey = signingKey; vm.SigningKeyPath = signingKeyPath; - var derivationSchemeSettings = GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) return NotFound(); @@ -224,7 +222,7 @@ namespace BTCPayServer.Controllers vm.CanCalculateBalance = true; vm.Positive = balanceChange >= Money.Zero; } - + vm.Inputs = new List(); foreach (var input in psbtObject.Inputs) { var inputVm = new WalletPSBTReadyViewModel.InputViewModel(); @@ -237,7 +235,7 @@ namespace BTCPayServer.Controllers inputVm.Positive = balanceChange2 >= Money.Zero; inputVm.Index = (int)input.Index; } - + vm.Destinations = new List(); foreach (var output in psbtObject.Outputs) { var dest = new WalletPSBTReadyViewModel.DestinationViewModel(); @@ -297,14 +295,14 @@ namespace BTCPayServer.Controllers catch { vm.GlobalError = "Invalid PSBT"; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } if (command == "broadcast") { if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) { vm.SetErrors(errors); - return View(vm); + return View(nameof(WalletPSBTReady),vm); } var transaction = psbt.ExtractTransaction(); try @@ -313,24 +311,24 @@ namespace BTCPayServer.Controllers if (!broadcastResult.Success) { vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } } catch (Exception ex) { vm.GlobalError = "Error while broadcasting: " + ex.Message; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } return RedirectToWalletTransaction(walletId, transaction); } else if (command == "analyze-psbt") { - return RedirectToWalletPSBT(walletId, psbt); + return RedirectToWalletPSBT(psbt); } else { vm.GlobalError = "Unknown command"; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } } @@ -359,7 +357,7 @@ namespace BTCPayServer.Controllers } sourcePSBT = sourcePSBT.Combine(psbt); TempData[WellKnownTempData.SuccessMessage] = "PSBT Successfully combined!"; - return RedirectToWalletPSBT(walletId, sourcePSBT); + return RedirectToWalletPSBT(sourcePSBT); } } } diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index a5330c8df..0f2d63d5d 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Net.WebSockets; @@ -8,7 +7,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; -using BTCPayServer.Events; using BTCPayServer.HostedServices; using BTCPayServer.ModelBinders; using BTCPayServer.Models; @@ -19,21 +17,15 @@ using BTCPayServer.Services; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; -using LedgerWallet; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.DataEncoders; using NBXplorer; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; -using static BTCPayServer.Controllers.StoresController; namespace BTCPayServer.Controllers { @@ -420,7 +412,7 @@ namespace BTCPayServer.Controllers var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation); model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network) .GetMetadataAsync(GetDerivationSchemeSettings(walletId).AccountDerivation, - WellknownMetadataKeys.Mnemonic)); + WellknownMetadataKeys.MasterHDKey)); model.CurrentBalance = await balance; model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi; model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte; @@ -593,7 +585,7 @@ namespace BTCPayServer.Controllers case "analyze-psbt": var name = $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt"; - return RedirectToWalletPSBT(walletId, psbt.PSBT, name); + return RedirectToWalletPSBT(psbt.PSBT, name); default: return View(vm); } @@ -651,7 +643,31 @@ namespace BTCPayServer.Controllers }); } - private IActionResult RedirectToWalletPSBT(WalletId walletId, PSBT psbt, string fileName = null) + [HttpPost] + [Route("{walletId}/vault")] + public async Task SubmitVault([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletSendVaultModel model) + { + + return RedirectToWalletPSBTReady(model.PSBT); + } + private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null) + { + var vm = new PostRedirectViewModel() + { + AspController = "Wallets", + AspAction = nameof(WalletPSBTReady), + Parameters = + { + new KeyValuePair("psbt", psbt), + new KeyValuePair("SigningKey", signingKey), + new KeyValuePair("SigningKeyPath", signingKeyPath) + } + }; + return View("PostRedirect", vm); + } + + private IActionResult RedirectToWalletPSBT(PSBT psbt, string fileName = null) { var vm = new PostRedirectViewModel() { @@ -700,6 +716,14 @@ namespace BTCPayServer.Controllers }); } + [HttpPost] + [Route("{walletId}/ledger")] + public async Task SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletSendLedgerModel model) + { + return RedirectToWalletPSBTReady(model.PSBT); + } + [HttpGet("{walletId}/psbt/seed")] public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,string psbt) @@ -773,7 +797,7 @@ namespace BTCPayServer.Controllers return View(viewModel); } ModelState.Remove(nameof(viewModel.PSBT)); - return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString()); + return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString()); } private bool PSBTChanged(PSBT psbt, Action act) diff --git a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml index e090eb6af..b678237c9 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml @@ -28,7 +28,7 @@ {

Decoded PSBT

-
+ @@ -65,7 +65,7 @@
@Model.Decoded
}

PSBT to decode

- +
diff --git a/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml b/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml index 0072f9cdc..ee491ac41 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml @@ -2,13 +2,24 @@ @{ Layout = "../Shared/_Layout.cshtml"; } +
+ @if (TempData.HasStatusMessage()) + { +
+
+ +
+
+ } @if (Model.GlobalError != null) { }
@@ -18,14 +29,16 @@ @if (Model.CanCalculateBalance) {

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

} else @@ -40,36 +53,38 @@

Inputs

- - - - + + + + - @foreach (var input in Model.Inputs) - { - - @if (input.Error != null) - { - - } - else - { - - } + @foreach (var input in Model.Inputs) + { + + @if (input.Error != null) + { + + } + else + { + + } - @if (input.Positive) - { - - } - else - { - - } - - } + @if (input.Positive) + { + + } + else + { + + } + + }
- Index - Amount
+ Index + Amount
@input.Index @input.Index
+ @input.Index + @input.Index@input.BalanceChange@input.BalanceChange
@input.BalanceChange@input.BalanceChange
@@ -82,28 +97,28 @@

Outputs

- - - - + + + + - @foreach (var destination in Model.Destinations) - { - - - @if (destination.Positive) - { - - } - else - { - - } - - } + @foreach (var destination in Model.Destinations) + { + + + @if (destination.Positive) + { + + } + else + { + + } + + }
- Destination - Amount
+ Destination + Amount
@destination.Destination@destination.Balance@destination.Balance
@destination.Destination@destination.Balance@destination.Balance
@@ -122,12 +137,13 @@
- - - + + + @if (!Model.HasErrors) { - or + + or } diff --git a/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml b/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml index 053809314..0e5e4a8c4 100644 --- a/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml @@ -6,14 +6,22 @@ }

Sign the transaction with Ledger

+@if (TempData.HasStatusMessage()) +{ +
+
+ +
+
+}
- diff --git a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml index 2ae23445b..fa72b664e 100644 --- a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml @@ -6,15 +6,23 @@ }

Sign the transaction with BTCPayServer Vault

+@if (TempData.HasStatusMessage()) +{ +
+
+ +
+
+}
-