diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs index ee12569ed..d677eea0b 100644 --- a/BTCPayServer.Tests/PSBTTests.cs +++ b/BTCPayServer.Tests/PSBTTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using BTCPayServer.Controllers; using BTCPayServer.Models; @@ -8,12 +9,13 @@ using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitpayClient; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.Extensions; using Xunit; using Xunit.Abstractions; namespace BTCPayServer.Tests { - [Collection(nameof(NonParallelizableCollectionDefinition))] public class PSBTTests : UnitTestBase { public PSBTTests(ITestOutputHelper helper) : base(helper) @@ -23,125 +25,110 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanPlayWithPSBT() { - using var tester = CreateServerTester(); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - var invoice = user.BitPay.CreateInvoice(new Invoice() - { - Price = 10, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some \", description", - FullNotifications = true - }, Facade.Merchant); - var cashCow = tester.ExplorerNode; - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m)); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal("paid", invoice.Status); - }); + using var s = CreateSeleniumTester(newDb: true); + await s.StartAsync(); - var walletController = user.GetController(); - var walletId = new WalletId(user.StoreId, "BTC"); - var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(); - var sendModel = new WalletSendModel() - { - Outputs = new List() - { - new WalletSendModel.TransactionOutput() - { - DestinationAddress = sendDestination, - Amount = 0.1m, - } - }, - FeeSatoshiPerByte = 1, - CurrentBalance = 1.5m - }; + var u1 = s.RegisterNewUser(true); + var hot = s.CreateNewStore(); + var seed = s.GenerateWallet(isHotWallet: true); + var cold = s.CreateNewStore(); + s.GenerateWallet(isHotWallet: false, seed: seed.ToString()); - 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); + // Scenario 1: one user has two stores sharing same seed + // one store is hot wallet, the other not. - var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt")); - PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork); + // Here, the cold wallet create a PSBT, then we switch to hot wallet to sign + // the PSBT and broadcast + s.GoToStore(cold.storeId); + var address = await s.FundStoreWallet(); + Thread.Sleep(1000); + s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); + SendAllTo(s, address); + s.Driver.TakeScreenshot().SaveAsFile(@"C:\Users\NicolasDorier\AppData\Local\Temp\1721425323\0--.png"); + s.Driver.FindElement(By.Id("SignWithPSBT")).Click(); - var vmPSBT2 = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel - { - SigningContext = new SigningContextModel - { - 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.SigningContext.PSBT); + var psbt = ExtractPSBT(s); - var signedPSBT = unsignedPSBT.Clone(); - signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath); - vmPSBT.PSBT = signedPSBT.ToBase64(); - var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel - { - SigningContext = new SigningContextModel - { - 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); + s.GoToStore(hot.storeId); + s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbt); + s.Driver.FindElement(By.Id("Decode")).Click(); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); + s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); + s.Driver.TakeScreenshot().SaveAsFile(@"C:\Users\NicolasDorier\AppData\Local\Temp\1721425323\0-.png"); + s.FindAlertMessage(); - vmPSBT.PSBT = unsignedPSBT.ToBase64(); - 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), nameof(walletController.WalletPSBT)); + // Scenario 2: Same as scenario 1, except we create a PSBT from hot wallet, then sign by manually + // entering the seed on the cold wallet. + s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); + SendAllTo(s, address); + psbt = ExtractPSBT(s); - var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); - Assert.True(signedPSBT.TryFinalize(out _)); - Assert.True(signedPSBT2.TryFinalize(out _)); - Assert.Equal(signedPSBT, signedPSBT2); + // Let's check it has been signed, then remove the signature. + // Also remove the hdkeys so we can test the update later + var psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); + var signedPSBT = psbtParsed.Clone(); + Assert.True(psbtParsed.Clone().TryFinalize(out _)); + Assert.Single(psbtParsed.Inputs[0].PartialSigs); + psbtParsed.Inputs[0].PartialSigs.Clear(); + Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); + psbtParsed.Inputs[0].HDKeyPaths.Clear(); + var skeletonPSBT = psbtParsed; - // Can use uploaded file? - combineVM.PSBT = null; - combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes()); - 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 _)); - Assert.Equal(signedPSBT, signedPSBT2); + s.GoToStore(cold.storeId); + s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64()); + s.Driver.FindElement(By.Id("Decode")).Click(); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); + s.Driver.FindElement(By.Id("SignWithSeed")).Click(); + s.Driver.FindElement(By.Name("SeedOrKey")).SendKeys(seed.ToString()); + s.Driver.FindElement(By.Id("Submit")).Click(); + s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); + s.FindAlertMessage(); - var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel - { - SigningContext = new SigningContextModel(signedPSBT) - })).AssertViewModel(); - Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT); - psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); - Assert.Equal(signedPSBT.ToBase64(), psbt); - var redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast")); - Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); + // Let's check if the update feature works + s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64()); + s.Driver.FindElement(By.Id("Decode")).Click(); + s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click(); + s.Driver.WaitForElement(By.Id("update-psbt")).Click(); - //test base64 psbt file - Assert.False(string.IsNullOrEmpty(Assert.IsType( - Assert.IsType( - await walletController.WalletPSBT(walletId, - new WalletPSBTViewModel - { - UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64()) - })).Model).PSBT)); + psbt = ExtractPSBT(s); + psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); + Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); + Assert.Empty(psbtParsed.Inputs[0].PartialSigs); + + // Let's if we can combine the updated psbt (which has hdkeys, but no sig) + // with the signed psbt (which has sig, but no hdkeys) + s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbtParsed.ToBase64()); + s.Driver.FindElement(By.Id("Decode")).Click(); + s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click(); + s.Driver.WaitForElement(By.Id("combine-psbt")).Click(); + signedPSBT.Inputs[0].HDKeyPaths.Clear(); + s.Driver.FindElement(By.Name("PSBT")).SendKeys(signedPSBT.ToBase64()); + s.Driver.WaitForElement(By.Id("Submit")).Click(); + + psbt = ExtractPSBT(s); + psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); + Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); + Assert.Single(psbtParsed.Inputs[0].PartialSigs); } - private static string AssertRedirectedPSBT(IActionResult view, string actionName) + private static void SendAllTo(SeleniumTester s, string address) { - var postRedirectView = Assert.IsType(view); - var postRedirectViewModel = Assert.IsType(postRedirectView.Model); - Assert.Equal(actionName, postRedirectViewModel.AspAction); - var redirectedPSBT = postRedirectViewModel.FormParameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value?.FirstOrDefault(); - return redirectedPSBT; + s.Driver.FindElement(By.Name("Outputs[0].DestinationAddress")).SendKeys(address); + s.Driver.FindElement(By.ClassName("crypto-balance-link")).Click(); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); + } + + private static string ExtractPSBT(SeleniumTester s) + { + var pageSource = s.Driver.PageSource; + var start = pageSource.IndexOf("id=\"psbt-base64\">"); + start += "id=\"psbt-base64\">".Length; + var end = pageSource.IndexOf("<", start); + return pageSource[start..end]; } } } diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index e77104e39..832d99497 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -27,6 +27,7 @@ using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json.Linq; using OpenQA.Selenium; +using OpenQA.Selenium.Support.Extensions; using OpenQA.Selenium.Support.UI; using Xunit; using Xunit.Abstractions; @@ -252,6 +253,7 @@ namespace BTCPayServer.Tests await s.Server.WaitForEvent(() => { + s.Driver.TakeScreenshot().SaveAsFile(@"C:\Users\NicolasDorier\AppData\Local\Temp\1721425323\fwefw.png"); s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); return Task.CompletedTask; }); diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 46179897c..3cd12046a 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -15,6 +15,7 @@ using BTCPayServer.Views.Stores; using BTCPayServer.Views.Wallets; using Microsoft.Extensions.Configuration; using NBitcoin; +using NBitcoin.RPC; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Support.UI; @@ -376,6 +377,8 @@ namespace BTCPayServer.Tests { GoToUrl($"/stores/{storeId}/"); StoreId = storeId; + if (WalletId != null) + WalletId = new WalletId(storeId, WalletId.CryptoCode); } Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click(); @@ -422,7 +425,7 @@ namespace BTCPayServer.Tests Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click(); } - public void GoToInvoiceCheckout(string? invoiceId = null) + public void GoToInvoiceCheckout(string invoiceId = null) { invoiceId ??= InvoiceId; Driver.FindElement(By.Id("StoreNav-Invoices")).Click(); @@ -507,7 +510,7 @@ namespace BTCPayServer.Tests } string InvoiceId; - public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m) + public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m) { walletId ??= WalletId; GoToWallet(walletId, WalletsNavPages.Receive); @@ -516,8 +519,21 @@ namespace BTCPayServer.Tests var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork); for (var i = 0; i < coins; i++) { - await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination)); + bool mined = false; + retry: + try + { + await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination)); + } + catch (RPCException) when (!mined) + { + mined = true; + await Server.ExplorerNode.GenerateAsync(1); + goto retry; + } } + Driver.Navigate().Refresh(); + return addressStr; } private void CheckForJSErrors() diff --git a/BTCPayServer/Controllers/UIWalletsController.PSBT.cs b/BTCPayServer/Controllers/UIWalletsController.PSBT.cs index 424fdcb53..a4009f358 100644 --- a/BTCPayServer/Controllers/UIWalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/UIWalletsController.PSBT.cs @@ -146,17 +146,22 @@ namespace BTCPayServer.Controllers WalletId walletId, WalletPSBTViewModel vm, string returnUrl = null, string command = null) { var network = NetworkProvider.GetNetwork(walletId.CryptoCode); - var psbt = await vm.GetPSBT(network.NBitcoinNetwork); - vm.SigningContext.PSBT ??= psbt.ToBase64(); if (returnUrl is null) returnUrl = Url.Action(nameof(WalletTransactions), new { walletId }); - + var psbt = await vm.GetPSBT(network.NBitcoinNetwork); + if (psbt is null || vm.InvalidPSBT) + { + ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT"); + return View("WalletSigningOptions", new WalletSigningOptionsModel(vm.SigningContext, returnUrl)); + } switch (command) { case "vault": return ViewVault(walletId, vm.SigningContext); case "seed": return SignWithSeed(walletId, vm.SigningContext); + case "decode": + return await WalletPSBT(walletId, vm, "decode"); default: break; } @@ -171,7 +176,7 @@ namespace BTCPayServer.Controllers WellknownMetadataKeys.MasterHDKey); if (extKey != null) { - return SignWithSeed(walletId, + return await SignWithSeed(walletId, new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = vm.SigningContext }); } } @@ -181,26 +186,16 @@ namespace BTCPayServer.Controllers [HttpGet("{walletId}/psbt")] public async Task WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, WalletPSBTViewModel vm) + WalletId walletId) { var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + var vm = new WalletPSBTViewModel(); vm.CryptoCode = network.CryptoCode; var derivationSchemeSettings = GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) return NotFound(); - vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet; - - if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt) - { - vm.PSBT = vm.SigningContext.PSBT = psbt.ToBase64(); - vm.PSBTHex = psbt.ToHex(); - vm.Decoded = psbt.ToString(); - await FetchTransactionDetails(derivationSchemeSettings, vm, network); - return View("WalletPSBTDecoded", vm); - } - return View(vm); } @@ -208,10 +203,8 @@ namespace BTCPayServer.Controllers public async Task WalletPSBT( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, - WalletPSBTViewModel vm, string command = null) + WalletPSBTViewModel vm, string command) { - if (command == null) - return await WalletPSBT(walletId, vm); var network = NetworkProvider.GetNetwork(walletId.CryptoCode); vm.CryptoCode = network.CryptoCode; @@ -221,25 +214,23 @@ namespace BTCPayServer.Controllers vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet; var psbt = await vm.GetPSBT(network.NBitcoinNetwork); - if (psbt == null) + if (vm.InvalidPSBT) { ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT"); return View(vm); } - - vm.PSBT = psbt.ToBase64(); - vm.PSBTHex = psbt.ToHex(); + if (psbt is null) + { + return View("WalletPSBT", vm); + } switch (command) { case "sign": - return await WalletSign(walletId, vm, nameof(WalletPSBT)); + return await WalletSign(walletId, vm); case "decode": ModelState.Remove(nameof(vm.PSBT)); ModelState.Remove(nameof(vm.FileName)); ModelState.Remove(nameof(vm.UploadedPSBTFile)); - vm.PSBT = vm.SigningContext.PSBT; - vm.PSBTHex = psbt.ToHex(); - vm.Decoded = psbt.ToString(); await FetchTransactionDetails(derivationSchemeSettings, vm, network); return View("WalletPSBTDecoded", vm); @@ -273,8 +264,7 @@ namespace BTCPayServer.Controllers } default: - var viewName = string.IsNullOrEmpty(vm.PSBT) ? "WalletPSBT" : "WalletPSBTDecoded"; - return View(viewName, vm); + return View("WalletPSBTDecoded", vm); } } @@ -393,28 +383,21 @@ namespace BTCPayServer.Controllers [HttpPost("{walletId}/psbt/ready")] public async Task WalletPSBTReady( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, WalletPSBTViewModel vm, string command = null, CancellationToken cancellationToken = default) + WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default) { - if (command == null) - return await WalletPSBT(walletId, vm); - - PSBT psbt; var network = NetworkProvider.GetNetwork(walletId.CryptoCode); - DerivationSchemeSettings derivationSchemeSettings; - try + PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork); + if (vm.InvalidPSBT || psbt is null) { - psbt = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork); - derivationSchemeSettings = GetDerivationSchemeSettings(walletId); - if (derivationSchemeSettings == null) - return NotFound(); - - await FetchTransactionDetails(derivationSchemeSettings, vm, network); - } - catch - { - vm.GlobalError = "Invalid PSBT"; + if (vm.InvalidPSBT) + vm.GlobalError = "Invalid PSBT"; return View(nameof(WalletPSBT), vm); } + DerivationSchemeSettings derivationSchemeSettings = GetDerivationSchemeSettings(walletId); + if (derivationSchemeSettings == null) + return NotFound(); + + await FetchTransactionDetails(derivationSchemeSettings, vm, network); switch (command) { @@ -543,6 +526,9 @@ namespace BTCPayServer.Controllers { PSBT = psbt.ToBase64() }); + case "decode": + await FetchTransactionDetails(derivationSchemeSettings, vm, network); + return View("WalletPSBTDecoded", vm); default: vm.GlobalError = "Unknown command"; return View(nameof(WalletPSBT), vm); diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 517262680..ba76449ba 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -826,7 +826,8 @@ namespace BTCPayServer.Controllers FormParameters = { { "SigningKey", vm.SigningKey }, - { "SigningKeyPath", vm.SigningKeyPath } + { "SigningKeyPath", vm.SigningKeyPath }, + { "command", "decode" } } }; AddSigningContext(redirectVm, vm.SigningContext); @@ -834,6 +835,7 @@ namespace BTCPayServer.Controllers !string.IsNullOrEmpty(vm.SigningContext.PSBT)) { //if a hw device signed a payjoin, we want it broadcast instantly + redirectVm.FormParameters.Remove("command"); redirectVm.FormParameters.Add("command", "broadcast"); } if (this.HttpContext.Request.Query["returnUrl"].FirstOrDefault() is string returnUrl) @@ -864,7 +866,8 @@ namespace BTCPayServer.Controllers FormParameters = { { "psbt", vm.PSBT }, - { "fileName", vm.FileName } + { "fileName", vm.FileName }, + { "command", "decode" }, } }; return View("PostRedirect", redirectVm); @@ -876,12 +879,12 @@ namespace BTCPayServer.Controllers { return View(nameof(SignWithSeed), new SignWithSeedViewModel { - SigningContext = signingContext, + SigningContext = signingContext }); } [HttpPost("{walletId}/psbt/seed")] - public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] + public async Task SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, SignWithSeedViewModel viewModel) { if (!ModelState.IsValid) @@ -891,7 +894,6 @@ namespace BTCPayServer.Controllers var network = NetworkProvider.GetNetwork(walletId.CryptoCode); if (network == null) throw new FormatException("Invalid value for crypto code"); - ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork); if (extKey == null) @@ -943,8 +945,19 @@ namespace BTCPayServer.Controllers var changed = psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath)); if (!changed) { - ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed."); - return View(nameof(SignWithSeed), viewModel); + var update = new UpdatePSBTRequest() + { + PSBT = psbt, + DerivationScheme = settings.AccountDerivation + }; + update.RebaseKeyPaths = settings.GetPSBTRebaseKeyRules().ToList(); + psbt = (await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(update))?.PSBT; + changed = psbt is not null && psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath)); + if (!changed) + { + ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed."); + return View(nameof(SignWithSeed), viewModel); + } } ModelState.Remove(nameof(viewModel.SigningContext.PSBT)); viewModel.SigningContext.PSBT = psbt.ToBase64(); diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs index 6ee0edc12..5ec47f6eb 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs @@ -34,7 +34,24 @@ namespace BTCPayServer.Models.WalletViewModels [Display(Name = "Upload PSBT from file")] public IFormFile UploadedPSBTFile { get; set; } + public async Task GetPSBT(Network network) + { + var psbt = await GetPSBTCore(network); + if (psbt != null) + { + Decoded = psbt.ToString(); + PSBTHex = psbt.ToHex(); + PSBT = psbt.ToBase64(); + if (SigningContext is null) + SigningContext = new SigningContextModel(psbt); + else + SigningContext.PSBT = psbt.ToBase64(); + } + return psbt; + } + public bool InvalidPSBT { get; set; } + async Task GetPSBTCore(Network network) { if (UploadedPSBTFile != null) { @@ -54,6 +71,7 @@ namespace BTCPayServer.Models.WalletViewModels { using var stream = new StreamReader(UploadedPSBTFile.OpenReadStream()); PSBT = await stream.ReadToEndAsync(); + InvalidPSBT = true; } } if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT)) @@ -64,10 +82,11 @@ namespace BTCPayServer.Models.WalletViewModels { try { + InvalidPSBT = false; return NBitcoin.PSBT.Parse(PSBT, network); } catch - { } + { InvalidPSBT = true; } } return null; } diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs index 33f335d1e..a9c2c0183 100644 --- a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs +++ b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Immutable; +using System.Globalization; using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Services.Altcoins.Zcash.Configuration; @@ -117,7 +118,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services public bool DaemonAvailable { get; set; } public bool WalletAvailable { get; set; } - public override String ToString() { return String.Format("{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); } + public override String ToString() { return String.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); } } } } diff --git a/BTCPayServer/TagHelpers/PermissionTagHelper.cs b/BTCPayServer/TagHelpers/PermissionTagHelper.cs index 63fcbcae7..fdc8f387d 100644 --- a/BTCPayServer/TagHelpers/PermissionTagHelper.cs +++ b/BTCPayServer/TagHelpers/PermissionTagHelper.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; diff --git a/BTCPayServer/Views/UIWallets/SignWithSeed.cshtml b/BTCPayServer/Views/UIWallets/SignWithSeed.cshtml index 067a75267..2dd583048 100644 --- a/BTCPayServer/Views/UIWallets/SignWithSeed.cshtml +++ b/BTCPayServer/Views/UIWallets/SignWithSeed.cshtml @@ -47,5 +47,5 @@ - + diff --git a/BTCPayServer/Views/UIWallets/WalletPSBTCombine.cshtml b/BTCPayServer/Views/UIWallets/WalletPSBTCombine.cshtml index a307a92a5..bb9b52396 100644 --- a/BTCPayServer/Views/UIWallets/WalletPSBTCombine.cshtml +++ b/BTCPayServer/Views/UIWallets/WalletPSBTCombine.cshtml @@ -1,4 +1,4 @@ -@model WalletPSBTCombineViewModel +@model WalletPSBTCombineViewModel @{ var walletId = Context.GetRouteValue("walletId").ToString(); Layout = "_LayoutWizard"; @@ -15,7 +15,7 @@

@ViewData["Title"]

-
+
@@ -26,5 +26,5 @@
- +
diff --git a/BTCPayServer/Views/UIWallets/WalletPSBTDecoded.cshtml b/BTCPayServer/Views/UIWallets/WalletPSBTDecoded.cshtml index 588780807..bcd861c51 100644 --- a/BTCPayServer/Views/UIWallets/WalletPSBTDecoded.cshtml +++ b/BTCPayServer/Views/UIWallets/WalletPSBTDecoded.cshtml @@ -3,7 +3,7 @@ @{ var walletId = Context.GetRouteValue("walletId").ToString(); var isReady = !Model.HasErrors; - var isSignable = !isReady && Model.NBXSeedAvailable; + var isSignable = !isReady; var needsExport = !isSignable && !isReady; Layout = "_LayoutWizard"; ViewData.SetActivePage(WalletsNavPages.PSBT, isReady ? "Confirm broadcasting this transaction" : "Transaction Details", walletId); @@ -60,7 +60,7 @@
- +
} @@ -108,7 +108,7 @@ else
- Show raw versions + Show raw versions
@@ -126,7 +126,7 @@ else
-
@Model.PSBT
+
@Model.PSBT
@Model.PSBTHex
@@ -179,14 +179,11 @@ else
- - -

For exporting the signed PSBT and transaction information to a wallet, update the PSBT.

- +

For batching transactions, you can combine this PSBT with another one.

- +