From b5a1de75c9c68a74f040cfb0d8200e4c6f7ea6c1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 22 May 2025 15:45:37 +0900 Subject: [PATCH] Move RBF/CPFP tests to playwright --- BTCPayServer.Tests/PlaywrightTester.cs | 2 + BTCPayServer.Tests/SeleniumTests.cs | 144 --------------------- BTCPayServer.Tests/WalletTests.cs | 171 +++++++++++++++++++++++++ btcpayserver.sln.DotSettings | 1 + 4 files changed, 174 insertions(+), 144 deletions(-) create mode 100644 BTCPayServer.Tests/WalletTests.cs diff --git a/BTCPayServer.Tests/PlaywrightTester.cs b/BTCPayServer.Tests/PlaywrightTester.cs index 77302b99e..64d4fd112 100644 --- a/BTCPayServer.Tests/PlaywrightTester.cs +++ b/BTCPayServer.Tests/PlaywrightTester.cs @@ -506,6 +506,8 @@ namespace BTCPayServer.Tests } await Page.ClickAsync("#FakePayment"); await Page.Locator("#CheatSuccessMessage").WaitForAsync(); + await Page.Locator("text=Payment Received").WaitForAsync(); + if (mine) { await MineBlockOnInvoiceCheckout(); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 5668c12ce..65a273d4c 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -51,150 +51,6 @@ namespace BTCPayServer.Tests { } - [Fact(Timeout = TestTimeout)] - public async Task CanUseBumpFee() - { - using var s = CreateSeleniumTester(); - await s.StartAsync(); - await s.Server.ExplorerNode.GenerateAsync(1); - s.RegisterNewUser(true); - s.CreateNewStore(); - s.GenerateWallet(isHotWallet: true); - - for (int i = 0; i < 3; i++) - { - s.CreateInvoice(); - s.GoToInvoiceCheckout(); - s.PayInvoice(); - s.GoToInvoices(s.StoreId); - } - var client = await s.AsTestAccount().CreateClient(); - var txs = (await client.ShowOnChainWalletTransactions(s.StoreId, "BTC")).Select(t => t.TransactionHash).ToArray(); - Assert.Equal(3, txs.Length); - - s.GoToWallet(navPages: WalletsNavPages.Transactions); - ClickBumpFee(s, txs[0]); - - // Because a single transaction is selected, we should be able to select CPFP only (Because no change are available, we can't do RBF) - s.Driver.FindElement(By.Name("txId")); - Assert.Equal("true", s.Driver.FindElement(By.Id("BumpMethod")).GetAttribute("disabled")); - Assert.Equal("CPFP", new SelectElement(s.Driver.FindElement(By.Id("BumpMethod"))).SelectedOption.Text); - s.ClickCancel(); - - // Same but using mass action - SelectTransactions(s, txs[0]); - s.Driver.FindElement(By.Id("BumpFee")).Click(); - s.Driver.FindElement(By.Name("txId")); - s.ClickCancel(); - - // Because two transactions are select we can only mass bump on CPFP - SelectTransactions(s, txs[0], txs[1]); - s.Driver.FindElement(By.Id("BumpFee")).Click(); - s.Driver.ElementDoesNotExist(By.Name("txId")); - Assert.Equal("true", s.Driver.FindElement(By.Id("BumpMethod")).GetAttribute("disabled")); - Assert.Equal("CPFP", new SelectElement(s.Driver.FindElement(By.Id("BumpMethod"))).SelectedOption.Text); - - var newExpectedEffectiveFeeRate = decimal.Parse(s.Driver.FindElement(By.Name("FeeSatoshiPerByte")).GetAttribute("value"), CultureInfo.InvariantCulture); - - s.ClickPagePrimary(); - s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); - Assert.Contains("Transaction broadcasted successfully", s.FindAlertMessage().Text); - - // The CPFP tag should be applied to the new tx - s.Driver.Navigate().Refresh(); - s.Driver.WaitWalletTransactionsLoaded(); - var cpfpTx = (await client.ShowOnChainWalletTransactions(s.StoreId, "BTC")).Select(t => t.TransactionHash).ToArray()[0]; - - // The CPFP should be RBF-able - Assert.DoesNotContain(cpfpTx, txs); - s.Driver.FindElement(By.CssSelector($"{TxRowSelector(cpfpTx)} .transaction-label[data-value=\"CPFP\"]")); - ClickBumpFee(s, cpfpTx); - Assert.Null(s.Driver.FindElement(By.Id("BumpMethod")).GetAttribute("disabled")); - Assert.Equal("RBF", new SelectElement(s.Driver.FindElement(By.Id("BumpMethod"))).SelectedOption.Text); - - var currentEffectiveFeeRate = decimal.Parse(s.Driver.FindElement(By.Name("CurrentFeeSatoshiPerByte")).GetAttribute("value"), CultureInfo.InvariantCulture); - - // We CPFP'd two transactions with a newExpectedEffectiveFeeRate of 20.0 - // When we want to RBF the previous CPFP, the currentEffectiveFeeRate should be coherent with our ealier choice - Assert.Equal(newExpectedEffectiveFeeRate, currentEffectiveFeeRate, 0); - - s.ClickPagePrimary(); - s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); - - s.Driver.Navigate().Refresh(); - s.Driver.WaitWalletTransactionsLoaded(); - var rbfTx = (await client.ShowOnChainWalletTransactions(s.StoreId, "BTC")).Select(t => t.TransactionHash).ToArray()[0]; - - // CPFP has been replaced, so it should not be found - s.Driver.AssertElementNotFound(By.CssSelector(TxRowSelector(cpfpTx))); - - // However, the new transaction should have copied the CPFP tag from the transaction it replaced, and have a RBF label as well. - s.Driver.FindElement(By.CssSelector($"{TxRowSelector(rbfTx)} .transaction-label[data-value=\"CPFP\"]")); - s.Driver.FindElement(By.CssSelector($"{TxRowSelector(rbfTx)} .transaction-label[data-value=\"RBF\"]")); - } - static string TxRowSelector(uint256 txId) => $".transaction-row[data-value=\"{txId}\"]"; - - private void SelectTransactions(SeleniumTester s, params uint256[] txs) - { - s.Driver.WaitWalletTransactionsLoaded(); - foreach (var txId in txs) - { - s.Driver.SetCheckbox(By.CssSelector($"{TxRowSelector(txId)} .mass-action-select"), true); - } - } - - private static void ClickBumpFee(SeleniumTester s, uint256 txId) - { - s.Driver.WaitWalletTransactionsLoaded(); - s.Driver.FindElement(By.CssSelector($"{TxRowSelector(txId)} .bumpFee-btn")).Click(); - } - - [Fact(Timeout = TestTimeout)] - public async Task CanUseCPFP() - { - using var s = CreateSeleniumTester(); - await s.StartAsync(); - s.RegisterNewUser(true); - s.CreateNewStore(); - s.GenerateWallet(isHotWallet: true); - await s.FundStoreWallet(); - for (int i = 0; i < 3; i++) - { - s.CreateInvoice(); - s.GoToInvoiceCheckout(); - s.PayInvoice(); - s.GoToInvoices(s.StoreId); - } - // Let's CPFP from the invoices page - s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true); - s.Driver.FindElement(By.Id("BumpFee")).Click(); - s.ClickPagePrimary(); - s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); - s.FindAlertMessage(); - Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url); - - // CPFP again should fail because all invoices got bumped - s.GoToInvoices(); - s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true); - s.Driver.FindElement(By.Id("BumpFee")).Click(); - Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url); - Assert.Contains("No UTXOs available", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text); - - // But we should be able to bump from the wallet's page - s.GoToWallet(navPages: WalletsNavPages.Transactions); - s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true); - s.Driver.FindElement(By.Id("BumpFee")).Click(); - s.ClickPagePrimary(); - s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); - Assert.Contains($"/wallets/{s.WalletId}", s.Driver.Url); - Assert.Contains("Transaction broadcasted successfully", s.FindAlertMessage().Text); - - // The CPFP tag should be applied to the new tx - s.Driver.Navigate().Refresh(); - s.Driver.WaitWalletTransactionsLoaded(); - s.Driver.FindElement(By.CssSelector(".transaction-label[data-value=\"CPFP\"]")); - } - [Fact(Timeout = TestTimeout)] [Trait("Lightning", "Lightning")] public async Task CanUseLndSeedBackup() diff --git a/BTCPayServer.Tests/WalletTests.cs b/BTCPayServer.Tests/WalletTests.cs new file mode 100644 index 000000000..40fab4ed0 --- /dev/null +++ b/BTCPayServer.Tests/WalletTests.cs @@ -0,0 +1,171 @@ +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Views.Wallets; +using NBitcoin; +using Xunit; +using Xunit.Abstractions; + +namespace BTCPayServer.Tests; +public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper) +{ + [Fact] + [Trait("Playwright", "Playwright")] + public async Task CanUseBumpFee() + { + await using var s = CreatePlaywrightTester(); + await s.StartAsync(); + await s.Server.ExplorerNode.GenerateAsync(1); + await s.RegisterNewUser(true); + await s.CreateNewStore(); + await s.GenerateWallet(isHotWallet: true); + await CreateInvoices(s); + + var client = await s.AsTestAccount().CreateClient(); + var txs = (await client.ShowOnChainWalletTransactions(s.StoreId, "BTC")).Select(t => t.TransactionHash).ToArray(); + Assert.Equal(3, txs.Length); + + await s.GoToWallet(navPages: WalletsNavPages.Transactions); + await ClickBumpFee(s, txs[0]); + + // Because a single transaction is selected, we should be able to select CPFP only (Because no change are available, we can't do RBF) + await s.Page.Locator("[name='txId']").WaitForAsync(); + Assert.Equal("disabled", await s.Page.GetAttributeAsync("#BumpMethod", "disabled")); + Assert.Equal("CPFP", await s.Page.Locator("#BumpMethod").InnerTextAsync()); + await s.ClickCancel(); + + // Same but using mass action + await SelectTransactions(s, txs[0]); + await s.Page.ClickAsync("#BumpFee"); + await s.Page.Locator("[name='txId']").WaitForAsync(); + await s.ClickCancel(); + + // Because two transactions are select we can only mass bump on CPFP + await SelectTransactions(s, txs[0], txs[1]); + await s.Page.ClickAsync("#BumpFee"); + Assert.False(await s.Page.Locator("[name='txId']").IsVisibleAsync()); + Assert.Equal("disabled", await s.Page.GetAttributeAsync("#BumpMethod", "disabled")); + Assert.Equal("CPFP", await s.Page.Locator("#BumpMethod").InnerTextAsync()); + + var newExpectedEffectiveFeeRate = decimal.Parse(await s.Page.GetAttributeAsync("[name='FeeSatoshiPerByte']", "value") ?? string.Empty, CultureInfo.InvariantCulture); + + await s.ClickPagePrimary(); + await s.Page.ClickAsync("#BroadcastTransaction"); + await s.FindAlertMessage(partialText: "Transaction broadcasted successfully"); + + // The CPFP tag should be applied to the new tx + var cpfpTx = (await client.ShowOnChainWalletTransactions(s.StoreId, "BTC")).Select(t => t.TransactionHash).ToArray()[0]; + await AssertHasLabels(s, cpfpTx, "CPFP"); + + // The CPFP should be RBF-able + Assert.DoesNotContain(cpfpTx, txs); + + await ClickBumpFee(s, cpfpTx); + Assert.Null(await s.Page.GetAttributeAsync("#BumpMethod", "disabled")); + Assert.Equal("RBF", await s.Page.Locator("#BumpMethod option:checked").InnerTextAsync()); + + var currentEffectiveFeeRate = decimal.Parse( + await s.Page.GetAttributeAsync("[name='CurrentFeeSatoshiPerByte']", "value") ?? string.Empty, + CultureInfo.InvariantCulture); + + // We CPFP'd two transactions with a newExpectedEffectiveFeeRate of 20.0 + // When we want to RBF the previous CPFP, the currentEffectiveFeeRate should be coherent with our ealier choice + Assert.Equal(newExpectedEffectiveFeeRate, currentEffectiveFeeRate, 0); + + await s.ClickPagePrimary(); + await s.Page.ClickAsync("#BroadcastTransaction"); + + await s.Page.ReloadAsync(); + var rbfTx = (await client.ShowOnChainWalletTransactions(s.StoreId, "BTC")).Select(t => t.TransactionHash).ToArray()[0]; + + // CPFP has been replaced, so it should not be found + Assert.False(await s.Page.Locator(TxRowSelector(cpfpTx)).IsVisibleAsync()); + + // However, the new transaction should have copied the CPFP tag from the transaction it replaced, and have a RBF label as well. + await AssertHasLabels(s, rbfTx, "CPFP"); + await AssertHasLabels(s, rbfTx, "RBF"); + } + + private async Task CreateInvoices(PlaywrightTester tester) + { + var client = await tester.AsTestAccount().CreateClient(); + var creating = Enumerable.Range(0, 3) + .Select(_ => client.CreateInvoice(tester.StoreId, new() { Amount = 10m })); + foreach (var c in creating) + { + var created = await c; + await tester.GoToUrl($"i/{created.Id}"); + await tester.PayInvoice(); + } + } + + private async Task AssertHasLabels(PlaywrightTester s, uint256 txId, string label) + { + await s.Page.ReloadAsync(); + await s.Page.Locator($"{TxRowSelector(txId)} .transaction-label[data-value=\"{label}\"]").WaitForAsync(); + } + private async Task AssertHasLabels(PlaywrightTester s, string label) + { + await s.Page.ReloadAsync(); + await s.Page.Locator($".transaction-label[data-value=\"{label}\"]").First.WaitForAsync(); + } + + static string TxRowSelector(uint256 txId) => $".transaction-row[data-value=\"{txId}\"]"; + + private async Task SelectTransactions(PlaywrightTester s, params uint256[] txs) + { + foreach (var txId in txs) + { + await s.Page.SetCheckedAsync($"{TxRowSelector(txId)} .mass-action-select", true); + } + } + + private async Task ClickBumpFee(PlaywrightTester s, uint256 txId) + { + await s.Page.ClickAsync($"{TxRowSelector(txId)} .bumpFee-btn"); + } + + [Fact] + [Trait("Playwright", "Playwright")] + public async Task CanUseCPFP() + { + await using var s = CreatePlaywrightTester(); + await s.StartAsync(); + await s.RegisterNewUser(true); + await s.CreateNewStore(); + await s.GenerateWallet(isHotWallet: true); + await s.FundStoreWallet(); + await CreateInvoices(s); + + // Let's CPFP from the invoices page + await s.GoToInvoices(s.StoreId); + await s.Page.SetCheckedAsync(".mass-action-select-all", true); + await s.Page.ClickAsync("#BumpFee"); + await s.ClickPagePrimary(); + await s.Page.ClickAsync("#BroadcastTransaction"); + await s.FindAlertMessage(); + Assert.Contains($"/stores/{s.StoreId}/invoices", s.Page.Url); + + // CPFP again should fail because all invoices got bumped + await s.GoToInvoices(); + await s.Page.SetCheckedAsync(".mass-action-select-all", true); + await s.Page.ClickAsync("#BumpFee"); + Assert.Contains($"/stores/{s.StoreId}/invoices", s.Page.Url); + await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error, partialText: "No UTXOs available"); + + // But we should be able to bump from the wallet's page + await s.GoToWallet(navPages: WalletsNavPages.Transactions); + await s.Page.SetCheckedAsync(".mass-action-select-all", true); + await s.Page.ClickAsync("#BumpFee"); + await s.ClickPagePrimary(); + await s.Page.ClickAsync("#BroadcastTransaction"); + Assert.Contains($"/wallets/{s.WalletId}", s.Page.Url); + await s.FindAlertMessage(partialText: "Transaction broadcasted successfully"); + + // The CPFP tag should be applied to the new tx + await AssertHasLabels(s, "CPFP"); + } +} diff --git a/btcpayserver.sln.DotSettings b/btcpayserver.sln.DotSettings index 4b842eab1..bfdba3a71 100644 --- a/btcpayserver.sln.DotSettings +++ b/btcpayserver.sln.DotSettings @@ -1,5 +1,6 @@  BTC + CPFP HWI LNURL NBX