Move RBF/CPFP tests to playwright

This commit is contained in:
nicolas.dorier
2025-05-22 15:45:37 +09:00
parent 587c271086
commit b5a1de75c9
4 changed files with 174 additions and 144 deletions

View File

@@ -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();

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BTC/@EntryIndexedValue">BTC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CPFP/@EntryIndexedValue">CPFP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HWI/@EntryIndexedValue">HWI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LNURL/@EntryIndexedValue">LNURL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NB/@EntryIndexedValue">NBX</s:String>