mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-01-28 02:14:23 +01:00
566 lines
32 KiB
C#
566 lines
32 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using BTCPayServer.Controllers;
|
|
using BTCPayServer.Data;
|
|
using BTCPayServer.Events;
|
|
using BTCPayServer.Models;
|
|
using BTCPayServer.Models.InvoicingModels;
|
|
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;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using NBitcoin;
|
|
using NBitcoin.Payment;
|
|
using NBitpayClient;
|
|
using OpenQA.Selenium;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace BTCPayServer.Tests
|
|
{
|
|
public class PayJoinTests
|
|
{
|
|
public const int TestTimeout = 60_000;
|
|
|
|
public PayJoinTests(ITestOutputHelper helper)
|
|
{
|
|
Logs.Tester = new XUnitLog(helper) {Name = "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")]
|
|
public async Task CanUseBIP79()
|
|
{
|
|
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;
|
|
cashCow.Generate(2); // get some money in case
|
|
|
|
var senderUser = tester.NewAccount();
|
|
senderUser.GrantAccess(true);
|
|
senderUser.RegisterDerivationScheme("BTC", true, true);
|
|
|
|
var invoice = senderUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 100, Currency = "USD", FullNotifications = true});
|
|
//payjoin is not enabled by default.
|
|
Assert.DoesNotContain("bpu", invoice.CryptoInfo.First().PaymentUrls.BIP21);
|
|
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
|
Money.Coins(0.06m));
|
|
|
|
var receiverUser = tester.NewAccount();
|
|
receiverUser.GrantAccess(true);
|
|
receiverUser.RegisterDerivationScheme("BTC", true, true);
|
|
|
|
await receiverUser.EnablePayJoin();
|
|
// 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});
|
|
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
|
Money.Coins(0.06m));
|
|
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
|
|
|
|
//give the cow some cash
|
|
await cashCow.GenerateAsync(1);
|
|
//let's get some more utxos first
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address,
|
|
new Money(0.011m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address,
|
|
new Money(0.012m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address,
|
|
new Money(0.013m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(senderUser.DerivationScheme)).Address,
|
|
new Money(0.021m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(senderUser.DerivationScheme)).Address,
|
|
new Money(0.022m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(senderUser.DerivationScheme)).Address,
|
|
new Money(0.023m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(senderUser.DerivationScheme)).Address,
|
|
new Money(0.024m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(senderUser.DerivationScheme)).Address,
|
|
new Money(0.025m, MoneyUnit.BTC)));
|
|
Assert.NotNull(await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(senderUser.DerivationScheme)).Address,
|
|
new Money(0.026m, MoneyUnit.BTC)));
|
|
|
|
await cashCow.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(senderUser.DerivationScheme)).Address,
|
|
new Money(0.014m, MoneyUnit.BTC));
|
|
|
|
|
|
var senderChange = (await btcPayWallet.GetChangeAddressAsync(senderUser.DerivationScheme)).Item1;
|
|
|
|
//Let's start the harassment
|
|
invoice = receiverUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
|
|
|
var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
|
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
|
var endpoint = parsedBip21.UnknowParameters["bpu"];
|
|
|
|
var invoice2 = receiverUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
|
var secondInvoiceParsedBip21 = new BitcoinUrlBuilder(invoice2.CryptoInfo.First().PaymentUrls.BIP21,
|
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
|
var endpoint2 = secondInvoiceParsedBip21.UnknowParameters["bpu"];
|
|
|
|
var senderStore = await tester.PayTester.StoreRepository.FindStore(senderUser.StoreId);
|
|
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
|
var derivationSchemeSettings = senderStore.GetSupportedPaymentMethods(tester.NetworkProvider)
|
|
.OfType<DerivationSchemeSettings>().SingleOrDefault(settings =>
|
|
settings.PaymentId == paymentMethodId);
|
|
|
|
ReceivedCoin[] senderCoins = null;
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme);
|
|
Assert.Contains(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
|
|
});
|
|
var coin = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
|
|
var coin2 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
|
|
var coin3 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
|
|
var coin4 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
|
|
var coin5 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
|
|
var coin6 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
|
|
|
|
var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
|
|
signingKeySettings.RootFingerprint =
|
|
senderUser.GenerateWalletResponseV.MasterHDKey.GetPublicKey().GetHDFingerPrint();
|
|
|
|
var extKey =
|
|
senderUser.GenerateWalletResponseV.MasterHDKey.Derive(signingKeySettings.GetRootedKeyPath()
|
|
.KeyPath);
|
|
|
|
|
|
var n = tester.ExplorerClient.Network.NBitcoinNetwork;
|
|
var Invoice1Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(parsedBip21.Address, parsedBip21.Amount)
|
|
.AddCoins(coin.Coin)
|
|
.AddKeys(extKey.Derive(coin.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m))
|
|
.BuildTransaction(true);
|
|
|
|
var Invoice1Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(parsedBip21.Address, parsedBip21.Amount)
|
|
.AddCoins(coin2.Coin)
|
|
.AddKeys(extKey.Derive(coin2.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m))
|
|
.BuildTransaction(true);
|
|
|
|
var Invoice2Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(secondInvoiceParsedBip21.Address, secondInvoiceParsedBip21.Amount)
|
|
.AddCoins(coin.Coin)
|
|
.AddKeys(extKey.Derive(coin.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m))
|
|
.BuildTransaction(true);
|
|
|
|
var Invoice2Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(secondInvoiceParsedBip21.Address, secondInvoiceParsedBip21.Amount)
|
|
.AddCoins(coin2.Coin)
|
|
.AddKeys(extKey.Derive(coin2.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m))
|
|
.BuildTransaction(true);
|
|
|
|
//Attempt 1: Send a signed tx to invoice 1 that does not pay the invoice at all
|
|
//Result: reject
|
|
Assert.False((await tester.PayTester.HttpClient.PostAsync(endpoint,
|
|
new StringContent(Invoice2Coin1.ToHex(), Encoding.UTF8, "text/plain"))).IsSuccessStatusCode);
|
|
|
|
//Attempt 2: Create two transactions using different inputs and send them to the same invoice.
|
|
//Result: Second Tx should be rejected.
|
|
var Invoice1Coin1Response = await tester.PayTester.HttpClient.PostAsync(endpoint,
|
|
new StringContent(Invoice1Coin1.ToHex(), Encoding.UTF8, "text/plain"));
|
|
|
|
var Invoice1Coin2Response = await tester.PayTester.HttpClient.PostAsync(endpoint,
|
|
new StringContent(Invoice1Coin2.ToHex(), Encoding.UTF8, "text/plain"));
|
|
|
|
Assert.True(Invoice1Coin1Response.IsSuccessStatusCode);
|
|
Assert.False(Invoice1Coin2Response.IsSuccessStatusCode);
|
|
var Invoice1Coin1ResponseTx =
|
|
Transaction.Parse(await Invoice1Coin1Response.Content.ReadAsStringAsync(), n);
|
|
var contributedInputsInvoice1Coin1ResponseTx =
|
|
Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut);
|
|
Assert.Single(contributedInputsInvoice1Coin1ResponseTx);
|
|
|
|
//Attempt 3: Send the same inputs from invoice 1 to invoice 2 while invoice 1 tx has not been broadcasted
|
|
//Result: Reject Tx1 but accept tx 2 as its inputs were never accepted by invoice 1
|
|
|
|
var Invoice2Coin1Response = await tester.PayTester.HttpClient.PostAsync(endpoint2,
|
|
new StringContent(Invoice2Coin1.ToHex(), Encoding.UTF8, "text/plain"));
|
|
|
|
var Invoice2Coin2Response = await tester.PayTester.HttpClient.PostAsync(endpoint2,
|
|
new StringContent(Invoice2Coin2.ToHex(), Encoding.UTF8, "text/plain"));
|
|
|
|
Assert.False(Invoice2Coin1Response.IsSuccessStatusCode);
|
|
Assert.True(Invoice2Coin2Response.IsSuccessStatusCode);
|
|
|
|
var Invoice2Coin2ResponseTx =
|
|
Transaction.Parse(await Invoice2Coin2Response.Content.ReadAsStringAsync(), n);
|
|
var contributedInputsInvoice2Coin2ResponseTx =
|
|
Invoice2Coin2ResponseTx.Inputs.Where(txin => coin2.OutPoint != txin.PrevOut);
|
|
Assert.Single(contributedInputsInvoice2Coin2ResponseTx);
|
|
|
|
//Attempt 4: Make tx that pays invoice 3 and 4 and submit to both
|
|
//Result: reject on 4: the protocol should not worry about this complexity
|
|
|
|
var invoice3 = receiverUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
|
var invoice3ParsedBip21 = new BitcoinUrlBuilder(invoice3.CryptoInfo.First().PaymentUrls.BIP21,
|
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
|
var invoice3Endpoint = invoice3ParsedBip21.UnknowParameters["bpu"];
|
|
|
|
|
|
var invoice4 = receiverUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
|
var invoice4ParsedBip21 = new BitcoinUrlBuilder(invoice4.CryptoInfo.First().PaymentUrls.BIP21,
|
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
|
var invoice4Endpoint = invoice4ParsedBip21.UnknowParameters["bpu"];
|
|
|
|
|
|
var Invoice3AndInvoice4Coin3 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(invoice3ParsedBip21.Address, invoice3ParsedBip21.Amount)
|
|
.Send(invoice4ParsedBip21.Address, invoice4ParsedBip21.Amount)
|
|
.AddCoins(coin3.Coin)
|
|
.AddKeys(extKey.Derive(coin3.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m))
|
|
.BuildTransaction(true);
|
|
|
|
var Invoice3Coin3Response = await tester.PayTester.HttpClient.PostAsync(invoice3Endpoint,
|
|
new StringContent(Invoice3AndInvoice4Coin3.ToHex(), Encoding.UTF8, "text/plain"));
|
|
|
|
var Invoice4Coin3Response = await tester.PayTester.HttpClient.PostAsync(invoice4Endpoint,
|
|
new StringContent(Invoice3AndInvoice4Coin3.ToHex(), Encoding.UTF8, "text/plain"));
|
|
|
|
Assert.True(Invoice3Coin3Response.IsSuccessStatusCode);
|
|
Assert.False(Invoice4Coin3Response.IsSuccessStatusCode);
|
|
|
|
//Attempt 5: Make tx that pays invoice 5 with 2 outputs
|
|
//Result: proposed tx consolidates the outputs
|
|
|
|
var invoice5 = receiverUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
|
var invoice5ParsedBip21 = new BitcoinUrlBuilder(invoice5.CryptoInfo.First().PaymentUrls.BIP21,
|
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
|
var invoice5Endpoint = invoice5ParsedBip21.UnknowParameters["bpu"];
|
|
|
|
var Invoice5Coin4TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(invoice5ParsedBip21.Address, invoice5ParsedBip21.Amount / 2)
|
|
.Send(invoice5ParsedBip21.Address, invoice5ParsedBip21.Amount / 2)
|
|
.AddCoins(coin4.Coin)
|
|
.AddKeys(extKey.Derive(coin4.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m));
|
|
|
|
var Invoice5Coin4 = Invoice5Coin4TxBuilder.BuildTransaction(true);
|
|
|
|
var Invoice5Coin4Response = await tester.PayTester.HttpClient.PostAsync(invoice5Endpoint,
|
|
new StringContent(Invoice5Coin4.ToHex(), Encoding.UTF8, "text/plain"));
|
|
|
|
Assert.True(Invoice5Coin4Response.IsSuccessStatusCode);
|
|
var Invoice5Coin4ResponseTx =
|
|
Transaction.Parse(await Invoice5Coin4Response.Content.ReadAsStringAsync(), n);
|
|
Assert.Single(Invoice5Coin4ResponseTx.Outputs.To(invoice5ParsedBip21.Address));
|
|
|
|
//Attempt 6: submit the same tx over and over in the hopes of getting new utxos
|
|
//Result: same tx gets sent back
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
var Invoice5Coin4Response2 = await tester.PayTester.HttpClient.PostAsync(invoice5Endpoint,
|
|
new StringContent(Invoice5Coin4.ToHex(), Encoding.UTF8, "text/plain"));
|
|
if (!Invoice5Coin4Response2.IsSuccessStatusCode)
|
|
{
|
|
Logs.Tester.LogInformation(
|
|
$"Failed on try {i + 1} with {await Invoice5Coin4Response2.Content.ReadAsStringAsync()}");
|
|
}
|
|
|
|
Assert.True(Invoice5Coin4Response2.IsSuccessStatusCode);
|
|
var Invoice5Coin4Response2Tx =
|
|
Transaction.Parse(await Invoice5Coin4Response2.Content.ReadAsStringAsync(), n);
|
|
Assert.Equal(Invoice5Coin4ResponseTx.GetHash(), Invoice5Coin4Response2Tx.GetHash());
|
|
}
|
|
|
|
//Attempt 7: send the payjoin porposed tx to the endpoint
|
|
//Result: get same tx sent back as is
|
|
Invoice5Coin4Response = await tester.PayTester.HttpClient.PostAsync(invoice5Endpoint,
|
|
new StringContent(Invoice5Coin4.ToHex(), Encoding.UTF8, "text/plain"));
|
|
Assert.True(Invoice5Coin4Response.IsSuccessStatusCode);
|
|
Assert.Equal(Invoice5Coin4ResponseTx.GetHash(),
|
|
Transaction.Parse(await Invoice5Coin4Response.Content.ReadAsStringAsync(), n).GetHash());
|
|
|
|
//Attempt 8: sign the payjoin and send it back to the endpoint
|
|
//Result: get same tx sent back as is
|
|
var Invoice5Coin4ResponseTxSigned = Invoice5Coin4TxBuilder.SignTransaction(Invoice5Coin4ResponseTx);
|
|
Invoice5Coin4Response = await tester.PayTester.HttpClient.PostAsync(invoice5Endpoint,
|
|
new StringContent(Invoice5Coin4.ToHex(), Encoding.UTF8, "text/plain"));
|
|
Assert.True(Invoice5Coin4Response.IsSuccessStatusCode);
|
|
Assert.Equal(Invoice5Coin4ResponseTxSigned.GetHash(),
|
|
Transaction.Parse(await Invoice5Coin4Response.Content.ReadAsStringAsync(), n).GetHash());
|
|
|
|
//Attempt 9: broadcast a payjoin tx, then try to submit both original tx and the payjoin itself again
|
|
//Result: fails
|
|
await tester.ExplorerClient.BroadcastAsync(Invoice5Coin4ResponseTxSigned);
|
|
|
|
Assert.False((await tester.PayTester.HttpClient.PostAsync(invoice5Endpoint,
|
|
new StringContent(Invoice5Coin4.ToHex(), Encoding.UTF8, "text/plain"))).IsSuccessStatusCode);
|
|
|
|
Assert.False((await tester.PayTester.HttpClient.PostAsync(invoice5Endpoint,
|
|
new StringContent(Invoice5Coin4.ToHex(), Encoding.UTF8, "text/plain"))).IsSuccessStatusCode);
|
|
|
|
//Attempt 10: send tx with rbf, broadcast payjoin tx, bump the rbf payjoin , attempt to submit tx again
|
|
//Result: same tx gets sent back
|
|
|
|
//give the receiver some more utxos
|
|
Assert.NotNull(await tester.ExplorerNode.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address,
|
|
new Money(0.1m, MoneyUnit.BTC)));
|
|
|
|
var invoice6 = receiverUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
|
var invoice6ParsedBip21 = new BitcoinUrlBuilder(invoice6.CryptoInfo.First().PaymentUrls.BIP21,
|
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
|
var invoice6Endpoint = invoice6ParsedBip21.UnknowParameters["bpu"];
|
|
|
|
var invoice6Coin5TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(invoice6ParsedBip21.Address, invoice6ParsedBip21.Amount)
|
|
.AddCoins(coin5.Coin)
|
|
.AddKeys(extKey.Derive(coin5.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m))
|
|
.SetLockTime(0);
|
|
|
|
var invoice6Coin5 = invoice6Coin5TxBuilder
|
|
.BuildTransaction(true);
|
|
|
|
var Invoice6Coin5Response1 = await tester.PayTester.HttpClient.PostAsync(invoice6Endpoint,
|
|
new StringContent(invoice6Coin5.ToHex(), Encoding.UTF8, "text/plain"));
|
|
Assert.True(Invoice6Coin5Response1.IsSuccessStatusCode);
|
|
var Invoice6Coin5Response1Tx =
|
|
Transaction.Parse(await Invoice6Coin5Response1.Content.ReadAsStringAsync(), n);
|
|
var Invoice6Coin5Response1TxSigned = invoice6Coin5TxBuilder.SignTransaction(Invoice6Coin5Response1Tx);
|
|
//broadcast the first payjoin
|
|
await tester.ExplorerClient.BroadcastAsync(Invoice6Coin5Response1TxSigned);
|
|
|
|
invoice6Coin5TxBuilder = invoice6Coin5TxBuilder.SendEstimatedFees(new FeeRate(100m));
|
|
var invoice6Coin5Bumpedfee = invoice6Coin5TxBuilder
|
|
.BuildTransaction(true);
|
|
|
|
var Invoice6Coin5Response3 = await tester.PayTester.HttpClient.PostAsync(invoice6Endpoint,
|
|
new StringContent(invoice6Coin5Bumpedfee.ToHex(), Encoding.UTF8, "text/plain"));
|
|
Assert.True(Invoice6Coin5Response3.IsSuccessStatusCode);
|
|
var Invoice6Coin5Response3Tx =
|
|
Transaction.Parse(await Invoice6Coin5Response3.Content.ReadAsStringAsync(), n);
|
|
Assert.True(invoice6Coin5Bumpedfee.Inputs.All(txin =>
|
|
Invoice6Coin5Response3Tx.Inputs.Any(txin2 => txin2.PrevOut == txin.PrevOut)));
|
|
|
|
//Attempt 11:
|
|
//send tx with rbt, broadcast payjoin,
|
|
//create tx spending the original tx inputs with rbf to self,
|
|
//Result: the exposed utxos are priorized in the next p2ep
|
|
|
|
//give the receiver some more utxos
|
|
Assert.NotNull(await tester.ExplorerNode.SendToAddressAsync(
|
|
(await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address,
|
|
new Money(0.1m, MoneyUnit.BTC)));
|
|
|
|
var invoice7 = receiverUser.BitPay.CreateInvoice(
|
|
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
|
var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21,
|
|
tester.ExplorerClient.Network.NBitcoinNetwork);
|
|
var invoice7Endpoint = invoice7ParsedBip21.UnknowParameters["bpu"];
|
|
|
|
var invoice7Coin6TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.Send(invoice7ParsedBip21.Address, invoice7ParsedBip21.Amount)
|
|
.AddCoins(coin6.Coin)
|
|
.AddKeys(extKey.Derive(coin6.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(100m))
|
|
.SetLockTime(0);
|
|
|
|
var invoice7Coin6Tx = invoice7Coin6TxBuilder
|
|
.BuildTransaction(true);
|
|
|
|
var invoice7Coin6Response1 = await tester.PayTester.HttpClient.PostAsync(invoice7Endpoint,
|
|
new StringContent(invoice7Coin6Tx.ToHex(), Encoding.UTF8, "text/plain"));
|
|
Assert.True(invoice7Coin6Response1.IsSuccessStatusCode);
|
|
var invoice7Coin6Response1Tx =
|
|
Transaction.Parse(await invoice7Coin6Response1.Content.ReadAsStringAsync(), n);
|
|
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 () =>
|
|
{
|
|
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
|
|
Assert.True(res.Success);
|
|
});
|
|
|
|
Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
|
|
|
|
var invoice7Coin6Tx2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
|
.SetChange(senderChange)
|
|
.AddCoins(coin6.Coin)
|
|
.SendAll(senderChange)
|
|
.SubtractFees()
|
|
.AddKeys(extKey.Derive(coin6.KeyPath))
|
|
.SendEstimatedFees(new FeeRate(200m))
|
|
.SetLockTime(0)
|
|
.BuildTransaction(true);
|
|
|
|
//broadcast the "rbf cancel" tx
|
|
await tester.WaitForEvent<InvoiceEvent>(async () =>
|
|
{
|
|
var res = (await tester.ExplorerClient.BroadcastAsync(invoice7Coin6Tx2));
|
|
Assert.True(res.Success);
|
|
});
|
|
//btcpay does not know of replaced txs where the outputs do not pay it(double spends using RBF to "cancel" a payment)
|
|
Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
|
|
|
|
//hijack our automated payjoin original broadcaster and force it to broadcast all, now
|
|
var payJoinTransactionBroadcaster = tester.PayTester.ServiceProvider.GetServices<IHostedService>()
|
|
.OfType<PayJoinTransactionBroadcaster>().First();
|
|
await payJoinTransactionBroadcaster.BroadcastStaleTransactions(TimeSpan.Zero, CancellationToken.None);
|
|
|
|
Assert.DoesNotContain(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id);
|
|
//all our failed payjoins are clear and any exposed utxo has been moved to the prioritized list
|
|
Assert.Contains(receiverWalletPayJoinState.GetExposedCoins(), receivedCoin =>
|
|
receivedCoin.OutPoint == contributedInputsInvoice7Coin6Response1TxSigned.PrevOut);
|
|
}
|
|
}
|
|
}
|
|
}
|