Change flow of payjoin in wallet + fix tests

This commit is contained in:
Kukks 2020-03-30 08:31:30 +02:00
parent b56d026fdb
commit 4d2e59e1a1
7 changed files with 255 additions and 224 deletions

View File

@ -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<PayJoinStateProvider>();
//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<NewOnChainTransactionEvent>(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<InvoiceRepository>().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<NewOnChainTransactionEvent>(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<InvoiceRepository>().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<PayJoinStateProvider>();
var btcPayNetwork = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().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<WalletsController>();
var senderWalletSendVM = await senderWallerController.WalletSend(senderWalletId)
.AssertViewModelAsync<WalletSendModel>();
senderWalletSendVM = await senderWallerController
.WalletSend(senderWalletId, senderWalletSendVM, "", CancellationToken.None, bip21)
.AssertViewModelAsync<WalletSendModel>();
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<PostRedirectViewModel>();
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<RedirectToActionResult>(
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<PayJoinStateProvider>();
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<WalletSendModel>();
senderWalletSendVM = await senderWallerController
.WalletSend(senderWalletId, senderWalletSendVM, "", CancellationToken.None, bip21)
.AssertViewModelAsync<WalletSendModel>();
postRedirectViewModel = await senderWallerController.WalletSend(senderWalletId,
senderWalletSendVM, "nbx-seed", CancellationToken.None)
.AssertViewModelAsync<PostRedirectViewModel>();
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<RedirectToActionResult>(
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<InvoiceController>();
var invoiceVM =
await receiverController.Invoice(invoice.Id).AssertViewModelAsync<InvoiceDetailsModel>();
Assert.Single(invoiceVM.Payments);
Assert.True(Assert.IsType<BitcoinLikePaymentData>(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<NewBlockEvent>(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<InvoiceEvent>(async () =>

View File

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

View File

@ -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 <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> 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<BTCPayNetwork>(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<BTCPayNetwork>(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.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>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 <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> 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.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>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);
}
}

View File

@ -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<IActionResult> WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendVaultModel model)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(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<string, string>("psbt", psbt),
new KeyValuePair<string, string>("originalPsbt", originalPsbt),
new KeyValuePair<string, string>("payJoinEndpointUrl", payJoinEndpointUrl),
new KeyValuePair<string, string>("SigningKey", signingKey),
new KeyValuePair<string, string>("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() });
}

View File

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

View File

@ -52,7 +52,7 @@
<tr class="@(payment.Replaced ? "linethrough" : "")" >
<td>@payment.Crypto</td>
<td>@payment.DepositAddress</td>
<td>@payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.CryptoPaymentData.PayJoinSelfContributedAmount == 0? string.Empty : $"<br/>(Payjoin {payment.CryptoPaymentData.PayJoinSelfContributedAmount})")</td>
<td class="payment-value">@payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.CryptoPaymentData.PayJoinSelfContributedAmount == 0? string.Empty : $"<br/>(Payjoin {payment.CryptoPaymentData.PayJoinSelfContributedAmount})")</td>
<td>
<div class="wraptextAuto">
<a href="@payment.TransactionLink" target="_blank">

View File

@ -141,17 +141,18 @@
<input type="hidden" asp-for="OriginalPSBT"/>
<input type="hidden" asp-for="SigningKey"/>
<input type="hidden" asp-for="SigningKeyPath"/>
<input type="hidden" asp-for="PayJoinEndpointUrl"/>
@if (!Model.HasErrors)
{
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast it</button>
<span> or </span>
}
<button type="submit" class="btn btn-secondary" name="command" value="analyze-psbt">Export as PSBT</button>
@if (!string.IsNullOrEmpty(Model.OriginalPSBT))
{
<span> or </span>
<button type="submit" class="btn btn-secondary" name="command" value="use-original">Skip payjoin</button>
}
@if (!string.IsNullOrEmpty(Model.PayJoinEndpointUrl))
{
<span> or </span>
<button type="submit" class="btn btn-secondary" name="command" value="payjoin">Pay with payjoin</button>
}
</form>
</div>
</div>