diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index 9b9416e3e..47bc46c1e 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -13,6 +13,7 @@ using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.PayJoin; +using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Wallets; using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; @@ -21,6 +22,7 @@ using Microsoft.Extensions.Hosting; using NBitcoin; using NBitcoin.Payment; using NBitpayClient; +using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; @@ -36,6 +38,108 @@ namespace BTCPayServer.Tests Logs.LogProvider = new XUnitLogProvider(helper); } + [Fact] + [Trait("Selenium", "Selenium")] + public async Task CanUseBIP79Client() + { + using (var s = SeleniumTester.Create()) + { + await s.StartAsync(); + s.RegisterNewUser(true); + var receiver = s.CreateNewStore(); + var receiverSeed = s.GenerateWallet("BTC", "", true, true); + var receiverWalletId = new WalletId(receiver.storeId, "BTC"); + var payJoinStateProvider = s.Server.PayTester.GetService(); + //payjoin is not enabled by default. + var invoiceId = s.CreateInvoice(receiver.storeId); + s.GoToInvoiceCheckout(invoiceId); + var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.DoesNotContain("bpu", bip21); + + s.GoToHome(); + s.GoToStore(receiver.storeId); + //payjoin is not enabled by default. + Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); + s.SetCheckbox(s,"PayJoinEnabled", true); + s.Driver.FindElement(By.Id("Save")).Click(); + Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); + var sender = s.CreateNewStore(); + var senderSeed = s.GenerateWallet("BTC", "", true, true); + var senderWalletId = new WalletId(sender.storeId, "BTC"); + await s.Server.ExplorerNode.GenerateAsync(1); + await s.FundStoreWallet(senderWalletId); + + invoiceId = s.CreateInvoice(receiver.storeId); + s.GoToInvoiceCheckout(invoiceId); + bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.Contains("bpu", bip21); + + s.GoToWalletSend(senderWalletId); + s.Driver.FindElement(By.Id("bip21parse")).Click(); + s.Driver.SwitchTo().Alert().SendKeys(bip21); + s.Driver.SwitchTo().Alert().Accept(); + Assert.False(string.IsNullOrEmpty( s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value"))); + s.Driver.ScrollTo(By.Id("SendMenu")); + s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); + await s.Server.WaitForEvent(async () => + { + s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick(); + }); + //no funds in receiver wallet to do payjoin + s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning); + await TestUtils.EventuallyAsync(async () => + { + var invoice = await s.Server.PayTester.GetService().GetInvoice(invoiceId); + Assert.Equal(InvoiceStatus.Paid, invoice.Status); + }); + + s.GoToInvoices(); + var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value")); + Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase)); + + //let's do it all again, except now the receiver has funds and is able to payjoin + invoiceId = s.CreateInvoice(receiver.storeId); + s.GoToInvoiceCheckout(invoiceId); + bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.Contains("bpu", bip21); + + s.GoToWalletSend(senderWalletId); + s.Driver.FindElement(By.Id("bip21parse")).Click(); + s.Driver.SwitchTo().Alert().SendKeys(bip21); + s.Driver.SwitchTo().Alert().Accept(); + Assert.False(string.IsNullOrEmpty( s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value"))); + s.Driver.ScrollTo(By.Id("SendMenu")); + s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); + await s.Server.WaitForEvent(async () => + { + s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick(); + }); + s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success); + await TestUtils.EventuallyAsync(async () => + { + var invoice = await s.Server.PayTester.GetService().GetInvoice(invoiceId); + Assert.Equal(InvoiceStatus.Paid, invoice.Status); + }); + s.GoToInvoices(); + paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value")); + Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase)); + + + //the state should now hold that there is an ongoing utxo + var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId); + Assert.NotNull(receiverWalletPayJoinState); + Assert.Single(receiverWalletPayJoinState.GetRecords()); + Assert.Equal(0.02m, receiverWalletPayJoinState.GetRecords().First().ContributedAmount); + Assert.Single(receiverWalletPayJoinState.GetRecords().First().CoinsExposed); + + } + } + [Fact] // [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] @@ -44,6 +148,8 @@ namespace BTCPayServer.Tests using (var tester = ServerTester.Create()) { await tester.StartAsync(); + + var payJoinStateProvider = tester.PayTester.GetService(); var btcPayNetwork = tester.NetworkProvider.GetNetwork("BTC"); var btcPayWallet = tester.PayTester.GetService().GetWallet(btcPayNetwork); var cashCow = tester.ExplorerNode; @@ -68,155 +174,9 @@ namespace BTCPayServer.Tests // payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer invoice = receiverUser.BitPay.CreateInvoice( new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true}); - - //check that the BIP21 has an endpoint - var bip21 = invoice.CryptoInfo.First().PaymentUrls.BIP21; - Assert.Contains("bpu", bip21); - var parsedBip21 = new BitcoinUrlBuilder(bip21, tester.ExplorerClient.Network.NBitcoinNetwork); - var endpoint = parsedBip21.UnknowParameters["bpu"]; - - - //see if the btcpay send wallet supports BIP21 properly and also the payjoin endpoint + cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network), + Money.Coins(0.06m)); var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC"); - var senderWalletId = new WalletId(senderUser.StoreId, "BTC"); - var senderWallerController = senderUser.GetController(); - var senderWalletSendVM = await senderWallerController.WalletSend(senderWalletId) - .AssertViewModelAsync(); - senderWalletSendVM = await senderWallerController - .WalletSend(senderWalletId, senderWalletSendVM, "", CancellationToken.None, bip21) - .AssertViewModelAsync(); - - Assert.Single(senderWalletSendVM.Outputs); - Assert.Equal(endpoint, senderWalletSendVM.PayJoinEndpointUrl); - Assert.Equal(parsedBip21.Address.ToString(), senderWalletSendVM.Outputs.First().DestinationAddress); - Assert.Equal(parsedBip21.Amount.ToDecimal(MoneyUnit.BTC), senderWalletSendVM.Outputs.First().Amount); - - //the nbx wallet option should also be available - Assert.True(senderWalletSendVM.NBXSeedAvailable); - - //pay the invoice with the nbx seed wallet option + also the invoice - var postRedirectViewModel = await senderWallerController.WalletSend(senderWalletId, - senderWalletSendVM, "nbx-seed", CancellationToken.None) - .AssertViewModelAsync(); - var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value; - var psbt = PSBT.Parse(redirectedPSBT, tester.ExplorerClient.Network.NBitcoinNetwork); - var senderWalletSendPSBTResult = new WalletPSBTReadyViewModel() - { - PSBT = redirectedPSBT, - SigningKeyPath = postRedirectViewModel.Parameters.Single(p => p.Key == "SigningKeyPath").Value, - SigningKey = postRedirectViewModel.Parameters.Single(p => p.Key == "SigningKey").Value - }; - //While the endpoint was set, the receiver had no utxos. The payment should fall back to original payment terms instead - Assert.Equal(parsedBip21.Amount.ToDecimal(MoneyUnit.BTC).ToString(), - psbt.Outputs.Single(model => model.ScriptPubKey == parsedBip21.Address.ScriptPubKey).Value); - - Assert.Equal("WalletTransactions", - Assert.IsType( - await senderWallerController.WalletPSBTReady(senderWalletId, senderWalletSendPSBTResult, - "broadcast")) - .ActionName); - - //we used the bip21 link straight away to pay the invoice so it should be paid straight away. - TestUtils.Eventually(() => - { - invoice = receiverUser.BitPay.GetInvoice(invoice.Id); - Assert.Equal(Invoice.STATUS_PAID, invoice.Status); - }); - //verify that there is nothing in the payment state - - var payJoinStateProvider = tester.PayTester.GetService(); - var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId); - Assert.NotNull(receiverWalletPayJoinState); - Assert.Empty(receiverWalletPayJoinState.GetRecords()); - - //now that there is a utxo, let's do it again - - invoice = receiverUser.BitPay.CreateInvoice( - new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true}); - bip21 = invoice.CryptoInfo.First().PaymentUrls.BIP21; - parsedBip21 = new BitcoinUrlBuilder(bip21, tester.ExplorerClient.Network.NBitcoinNetwork); - senderWalletSendVM = await senderWallerController.WalletSend(senderWalletId) - .AssertViewModelAsync(); - senderWalletSendVM = await senderWallerController - .WalletSend(senderWalletId, senderWalletSendVM, "", CancellationToken.None, bip21) - .AssertViewModelAsync(); - postRedirectViewModel = await senderWallerController.WalletSend(senderWalletId, - senderWalletSendVM, "nbx-seed", CancellationToken.None) - .AssertViewModelAsync(); - redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value; - psbt = PSBT.Parse(redirectedPSBT, tester.ExplorerClient.Network.NBitcoinNetwork); - senderWalletSendPSBTResult = new WalletPSBTReadyViewModel() - { - PSBT = redirectedPSBT, - SigningKeyPath = postRedirectViewModel.Parameters.Single(p => p.Key == "SigningKeyPath").Value, - SigningKey = postRedirectViewModel.Parameters.Single(p => p.Key == "SigningKey").Value - }; - //the payjoin should make the amount being paid to the address higher - Assert.True(parsedBip21.Amount.ToDecimal(MoneyUnit.BTC) < psbt.Outputs - .Single(model => model.ScriptPubKey == parsedBip21.Address.ScriptPubKey).Value - .ToDecimal(MoneyUnit.BTC)); - - //the state should now hold that there is an ongoing utxo - Assert.Single(receiverWalletPayJoinState.GetRecords()); - Assert.Equal(0.02m, receiverWalletPayJoinState.GetRecords().First().ContributedAmount); - Assert.Single(receiverWalletPayJoinState.GetRecords().First().CoinsExposed); - Assert.False(receiverWalletPayJoinState.GetRecords().First().TxSeen); - Assert.Equal(psbt.Finalize().ExtractTransaction().GetHash(), - receiverWalletPayJoinState.GetRecords().First().ProposedTransactionHash); - - Assert.Equal("WalletTransactions", - Assert.IsType( - await senderWallerController.WalletPSBTReady(senderWalletId, senderWalletSendPSBTResult, - "broadcast")) - .ActionName); - - TestUtils.Eventually(() => - { - invoice = receiverUser.BitPay.GetInvoice(invoice.Id); - - Assert.Equal(Invoice.STATUS_PAID, invoice.Status); - Assert.Equal(Invoice.EXSTATUS_FALSE, invoice.ExceptionStatus.ToString().ToLowerInvariant()); - }); - - //verify that we have a record that it was a payjoin - var receiverController = receiverUser.GetController(); - var invoiceVM = - await receiverController.Invoice(invoice.Id).AssertViewModelAsync(); - Assert.Single(invoiceVM.Payments); - Assert.True(Assert.IsType(invoiceVM.Payments.First().GetCryptoPaymentData()) - .PayJoinSelfContributedAmount > 0); - - - //we dont remove the payjoin tx state even if we detect it, for cases of RBF - Assert.NotEmpty(receiverWalletPayJoinState.GetRecords()); - Assert.Single(receiverWalletPayJoinState.GetRecords()); - Assert.True(receiverWalletPayJoinState.GetRecords().First().TxSeen); - - var debugData = new - { - StoreId = receiverWalletId.StoreId, - InvoiceId = receiverWalletPayJoinState.GetRecords().First().InvoiceId, - PayJoinTx = receiverWalletPayJoinState.GetRecords().First().ProposedTransactionHash - }; - for (int i = 0; i < 6; i++) - { - await tester.WaitForEvent(async () => - { - await cashCow.GenerateAsync(1); - }); - } - - //check that the state has cleared that ongoing tx - receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId); - Assert.NotNull(receiverWalletPayJoinState); - Assert.Empty(receiverWalletPayJoinState.GetRecords()); - Assert.Empty(receiverWalletPayJoinState.GetExposedCoins()); - - //Cool, so the payjoin works! - //The cool thing with payjoin is that your utxos don't grow - Assert.Single(await btcPayWallet.GetUnspentCoins(receiverUser.DerivationScheme)); - - //Let's be as malicious as CSW //give the cow some cash await cashCow.GenerateAsync(1); @@ -260,9 +220,9 @@ namespace BTCPayServer.Tests invoice = receiverUser.BitPay.CreateInvoice( new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true}); - parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, + var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); - endpoint = parsedBip21.UnknowParameters["bpu"]; + var endpoint = parsedBip21.UnknowParameters["bpu"]; var invoice2 = receiverUser.BitPay.CreateInvoice( new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true}); @@ -558,6 +518,9 @@ namespace BTCPayServer.Tests var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx); var contributedInputsInvoice7Coin6Response1TxSigned = Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut); + + + var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId); Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id); //broadcast the payjoin await tester.WaitForEvent(async () => diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 259e8a417..9a070f83f 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -120,6 +120,7 @@ namespace BTCPayServer.Tests return (usr, Driver.FindElement(By.Id("Id")).GetAttribute("value")); } + public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false) { @@ -314,8 +315,38 @@ namespace BTCPayServer.Tests return id; } + public async Task FundStoreWallet(WalletId walletId, int coins = 1, decimal denomination = 1m) + { + GoToWalletReceive(walletId); + Driver.FindElement(By.Id("generateButton")).Click(); + var addressStr = Driver.FindElement(By.Id("vue-address")).GetProperty("value"); + var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork); + for (int i = 0; i < coins; i++) + { + await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination)); + } + } + + public void PayInvoice(WalletId walletId, string invoiceId) + { + GoToInvoiceCheckout(invoiceId); + var bip21 = Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.Contains("bpu", bip21); + + GoToWalletSend(walletId); + Driver.FindElement(By.Id("bip21parse")).Click(); + Driver.SwitchTo().Alert().SendKeys(bip21); + Driver.SwitchTo().Alert().Accept(); + Driver.ScrollTo(By.Id("SendMenu")); + Driver.FindElement(By.Id("SendMenu")).ForceClick(); + Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); + Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + } + + private void CheckForJSErrors() { //wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index d3f8ac069..6753483f9 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -217,13 +217,6 @@ namespace BTCPayServer.Controllers } newPSBT = await UpdatePSBT(derivationSchemeSettings, newPSBT, btcPayNetwork); - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Severity = StatusMessageModel.StatusSeverity.Info, - AllowDismiss = false, - Html = - $"This transaction has been coordinated between the receiver and you to create a payjoin transaction by adding inputs from the receiver. The amount being sent may appear higher but is in fact the same" - }); return newPSBT; } } @@ -239,13 +232,15 @@ namespace BTCPayServer.Controllers WalletId walletId, string psbt = null, string signingKey = null, string signingKeyPath = null, - string originalPsbt = null) + string originalPsbt = null, + string payJoinEndpointUrl = null) { var network = NetworkProvider.GetNetwork(walletId.CryptoCode); var vm = new WalletPSBTReadyViewModel() { PSBT = psbt }; vm.SigningKey = signingKey; vm.SigningKeyPath = signingKeyPath; vm.OriginalPSBT = originalPsbt; + vm.PayJoinEndpointUrl = payJoinEndpointUrl; var derivationSchemeSettings = GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) return NotFound(); @@ -366,13 +361,14 @@ namespace BTCPayServer.Controllers WalletId walletId, WalletPSBTReadyViewModel vm, string command = null) { if (command == null) - return await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath, vm.OriginalPSBT); + return await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath, vm.OriginalPSBT, vm.PayJoinEndpointUrl); PSBT psbt = null; var network = NetworkProvider.GetNetwork(walletId.CryptoCode); + DerivationSchemeSettings derivationSchemeSettings = null; try { psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); - var derivationSchemeSettings = GetDerivationSchemeSettings(walletId); + derivationSchemeSettings = GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) return NotFound(); await FetchTransactionDetails(derivationSchemeSettings, vm, network); @@ -382,54 +378,99 @@ namespace BTCPayServer.Controllers vm.GlobalError = "Invalid PSBT"; return View(nameof(WalletPSBTReady),vm); } - - if (command == "use-original") + + switch (command) { - return await WalletPSBTReady(walletId, vm.OriginalPSBT, vm.SigningKey, vm.SigningKeyPath); - } - if (command == "broadcast") - { - if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) - { - vm.SetErrors(errors); - return View(nameof(WalletPSBTReady),vm); - } - var transaction = psbt.ExtractTransaction(); - try - { - var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); - if (!broadcastResult.Success) + case "payjoin": + var proposedPayjoin =await + TryGetPayjoinProposedTX(vm.PayJoinEndpointUrl, psbt, derivationSchemeSettings, network); + if (proposedPayjoin == null) { - if (!string.IsNullOrEmpty(vm.OriginalPSBT)) + //we possibly exposed the tx to the receiver, so we need to broadcast straight away + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Warning, + AllowDismiss = false, + Html = $"The payjoin transaction could not be created. The original transaction was broadcast instead." + }); + return await WalletPSBTReady(walletId, vm, "broadcast"); + } + else + { + + try + { + var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork); + var payjoinSigned = PSBTChanged(proposedPayjoin, + () => proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation, + extKey, + RootedKeyPath.Parse(vm.SigningKeyPath))); + if (!payjoinSigned) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Warning, + AllowDismiss = false, + Html = $"The payjoin transaction could not be signed. The original transaction was broadcast instead." + }); + return await WalletPSBTReady(walletId, vm, "broadcast"); + } + vm.PSBT = proposedPayjoin.ToBase64(); + vm.OriginalPSBT = psbt.ToBase64(); + return await WalletPSBTReady(walletId, vm, "broadcast"); + } + catch (Exception e) { TempData.SetStatusMessageModel(new StatusMessageModel() { - Severity = StatusMessageModel.StatusSeverity.Error, + Severity = StatusMessageModel.StatusSeverity.Info, AllowDismiss = false, - Html = $"The payjoin transaction could not be broadcast.
({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).
The transaction has been reverted back to its original format and is ready to be broadcast." + Html = + $"This transaction has been coordinated between the receiver and you to create a payjoin transaction by adding inputs from the receiver. The amount being sent may appear higher but is in fact the same" }); - return await WalletPSBTReady(walletId, vm.OriginalPSBT, vm.SigningKey, vm.SigningKeyPath); + return ViewVault(walletId, proposedPayjoin, vm.PayJoinEndpointUrl, psbt); } + } + case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors): + vm.SetErrors(errors); + return View(nameof(WalletPSBTReady),vm); + case "broadcast": + { + var transaction = psbt.ExtractTransaction(); + try + { + var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); + if (!broadcastResult.Success) + { + if (!string.IsNullOrEmpty(vm.OriginalPSBT)) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Warning, + AllowDismiss = false, + Html = $"The payjoin transaction could not be broadcast.
({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).
The transaction has been reverted back to its original format and has been broadcast." + }); + vm.PSBT = vm.OriginalPSBT; + vm.OriginalPSBT = null; + return await WalletPSBTReady(walletId, vm, "broadcast"); + } - vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; + vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; + return View(nameof(WalletPSBTReady),vm); + } + } + catch (Exception ex) + { + vm.GlobalError = "Error while broadcasting: " + ex.Message; return View(nameof(WalletPSBTReady),vm); } + return RedirectToWalletTransaction(walletId, transaction); } - catch (Exception ex) - { - vm.GlobalError = "Error while broadcasting: " + ex.Message; + case "analyze-psbt": + return RedirectToWalletPSBT(psbt); + default: + vm.GlobalError = "Unknown command"; return View(nameof(WalletPSBTReady),vm); - } - return RedirectToWalletTransaction(walletId, transaction); - } - else if (command == "analyze-psbt") - { - return RedirectToWalletPSBT(psbt); - } - else - { - vm.GlobalError = "Unknown command"; - return View(nameof(WalletPSBTReady),vm); } } diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 86d9a3b6e..174aeedea 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -671,12 +671,13 @@ namespace BTCPayServer.Controllers ModelState.Clear(); } - private IActionResult ViewVault(WalletId walletId, PSBT psbt, string payJoinEndpointUrl) + private IActionResult ViewVault(WalletId walletId, PSBT psbt, string payJoinEndpointUrl, PSBT originalPSBT = null) { return View(nameof(WalletSendVault), new WalletSendVaultModel() { PayJoinEndpointUrl = payJoinEndpointUrl, WalletId = walletId.ToString(), + OriginalPSBT = originalPSBT?.ToBase64(), PSBT = psbt.ToBase64(), WebsocketPath = this.Url.Action(nameof(VaultController.VaultBridgeConnection), "Vault", new { walletId = walletId.ToString() }) }); @@ -687,19 +688,9 @@ namespace BTCPayServer.Controllers public async Task WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletSendVaultModel model) { - var network = NetworkProvider.GetNetwork(walletId.CryptoCode); - var newPSBT = await TryGetPayjoinProposedTX(model.PayJoinEndpointUrl, PSBT.Parse(model.PSBT, network.NBitcoinNetwork), GetDerivationSchemeSettings(walletId), network); - model.PayJoinEndpointUrl = null; - if (newPSBT != null) - { - model.OriginalPSBT = model.PSBT; - model.PSBT = newPSBT.ToBase64(); - return View(nameof(WalletSendVault), model); - } - - return RedirectToWalletPSBTReady(model.PSBT, originalPsbt: model.OriginalPSBT); + return RedirectToWalletPSBTReady(model.PSBT, originalPsbt: model.OriginalPSBT, payJoinEndpointUrl: model.PayJoinEndpointUrl); } - private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null, string originalPsbt = null) + private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null, string originalPsbt = null, string payJoinEndpointUrl = null) { var vm = new PostRedirectViewModel() { @@ -709,6 +700,7 @@ namespace BTCPayServer.Controllers { new KeyValuePair("psbt", psbt), new KeyValuePair("originalPsbt", originalPsbt), + new KeyValuePair("payJoinEndpointUrl", payJoinEndpointUrl), new KeyValuePair("SigningKey", signingKey), new KeyValuePair("SigningKeyPath", signingKeyPath) } @@ -849,15 +841,7 @@ namespace BTCPayServer.Controllers return View(viewModel); } ModelState.Remove(nameof(viewModel.PSBT)); - var newPSBT = await TryGetPayjoinProposedTX(viewModel.PayJoinEndpointUrl,psbt, GetDerivationSchemeSettings(walletId), network); - viewModel.PayJoinEndpointUrl = null; - if (newPSBT != null) - { - viewModel.OriginalPSBT = psbt.ToBase64(); - viewModel.PSBT = newPSBT.ToBase64(); - return await SignWithSeed(walletId, viewModel); - } - return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString(), viewModel.OriginalPSBT); + return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString(), viewModel.OriginalPSBT, viewModel.PayJoinEndpointUrl); } private bool PSBTChanged(PSBT psbt, Action act) @@ -881,7 +865,17 @@ namespace BTCPayServer.Controllers var wallet = _walletProvider.GetWallet(network); var derivationSettings = GetDerivationSchemeSettings(walletId); wallet.InvalidateCache(derivationSettings.AccountDerivation); - TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})"; + if (TempData.GetStatusMessageModel() == null) + { + TempData[WellKnownTempData.SuccessMessage] = + $"Transaction broadcasted successfully ({transaction.GetHash()})"; + } + else + { + var statusMessageModel = TempData.GetStatusMessageModel(); + statusMessageModel.Message += $" ({transaction.GetHash()})"; + TempData.SetStatusMessageModel(statusMessageModel); + } } return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() }); } diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs index 411a22430..daed6978e 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs @@ -8,6 +8,7 @@ namespace BTCPayServer.Models.WalletViewModels { public class WalletPSBTReadyViewModel { + public string PayJoinEndpointUrl { get; set; } public string OriginalPSBT { get; set; } public string PSBT { get; set; } public string SigningKey { get; set; } diff --git a/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml b/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml index 2ebe9c0c1..b0f756cca 100644 --- a/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml @@ -52,7 +52,7 @@ @payment.Crypto @payment.DepositAddress - @payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.CryptoPaymentData.PayJoinSelfContributedAmount == 0? string.Empty : $"
(Payjoin {payment.CryptoPaymentData.PayJoinSelfContributedAmount})") + @payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.CryptoPaymentData.PayJoinSelfContributedAmount == 0? string.Empty : $"
(Payjoin {payment.CryptoPaymentData.PayJoinSelfContributedAmount})")