mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-02-22 22:54:23 +01:00
Move RBF/CPFP tests to playwright
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
171
BTCPayServer.Tests/WalletTests.cs
Normal file
171
BTCPayServer.Tests/WalletTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user