Files
btcpayserver/BTCPayServer.Tests/WalletTests.cs

170 lines
6.9 KiB
C#

using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
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);
var w = await s.GoToWalletTransactions();
await w.BumpFee(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 w.Select(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 w.Select(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 w.AssertHasLabels(cpfpTx, "CPFP");
// The CPFP should be RBF-able
Assert.DoesNotContain(cpfpTx, txs);
await w.BumpFee(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
await w.AssertNotFound(cpfpTx);
// However, the new transaction should have copied the CPFP tag from the transaction it replaced, and have a RBF label as well.
try
{
await w.AssertHasLabels(rbfTx, "CPFP");
}
catch
{
// TODO: Flaky
await s.TakeScreenshot("AssertHasLabels-Fails.png");
throw;
}
await w.AssertHasLabels(rbfTx, "RBF");
// Now, we sweep all the UTXOs to a single destination. This should be RBF-able. (Fee deducted on the lone UTXO)
var send = await s.GoToWalletSend();
await send.FillAddress(new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest));
await send.SweepBalance();
await send.SetFeeRate(20m);
await send.Sign();
await s.Page.ClickAsync("button[value=broadcast]");
// Now we RBF the sweep
await w.BumpFee();
Assert.Equal("RBF", await s.Page.Locator("#BumpMethod").InnerTextAsync());
await s.ClickPagePrimary();
await s.Page.ClickAsync("#BroadcastTransaction");
await w.AssertHasLabels("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();
}
}
[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");
for (int i = 0; i < 5; i++)
{
var txs = await s.GoToWalletTransactions();
await txs.SelectAll();
await txs.BumpFeeSelected();
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 txs.AssertHasLabels("CPFP");
}
}
}