Move some tests in PullPaymentsTests, fix warnings
What changed, and why it matters
This commit is purely a test-code refactor. It moves existing pull-payment and coin-selection tests into a new dedicated test file, removes duplicate copies from other test files, adds a couple of shared assertion helpers, and fixes compiler/style warnings. No production application code is changed, so it cannot affect the security of a running BTCPay Server instance.
No security action required. Treat as normal maintenance/refactoring commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff shows only changes under BTCPayServer.Tests/ and btcpayserver.sln.DotSettings. Existing tests (CanTopUpPullPayment, CanMigratePayoutsAndPullPayments, CanUsePullPaymentViaAPI, CanEditPullPaymentUI, CanUseCoinSelection) are relocated or duplicated into PullPaymentsTests.cs and WalletTests.cs, with old copies removed from UnitTest1.cs, DatabaseTests.cs, GreenfieldAPITests.cs, and PlaywrightTests.cs. AssertEx.cs gains two shared helpers (AssertPermissionError, AssertApiError) and GreenfieldAPITests.cs now delegates to them. There are no changes to controllers, services, data access, cryptography, or any runtime code path.
Changed components
BTCPayServer.Tests/AssertEx.csBTCPayServer.Tests/DatabaseTests.csBTCPayServer.Tests/GreenfieldAPITests.csBTCPayServer.Tests/PlaywrightTests.csBTCPayServer.Tests/PullPaymentsTests.cs (new)BTCPayServer.Tests/TestUtils.csBTCPayServer.Tests/UnitTest1.csBTCPayServer.Tests/WalletTests.csbtcpayserver.sln.DotSettingsInspect captured patch +680 / −651
diff --git a/BTCPayServer.Tests/AssertEx.cs b/BTCPayServer.Tests/AssertEx.cs
index 7b5face..7fbf4f8 100644
--- a/BTCPayServer.Tests/AssertEx.cs
+++ b/BTCPayServer.Tests/AssertEx.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
+using BTCPayServer.Client.Models;
using Xunit;
namespace BTCPayServer.Tests;
@@ -18,6 +19,14 @@ public class AssertEx
return ex;
}
+ public static async Task<GreenfieldAPIException> AssertPermissionError(string expectedPermission, Func<Task> act)
+ {
+ var err = await Assert.ThrowsAsync<GreenfieldAPIException>(async () => await act());
+ var err2 = Assert.IsType<GreenfieldPermissionAPIError>(err.APIError);
+ Assert.Equal(expectedPermission, err2.MissingPermission);
+ return err;
+ }
+
public static async Task AssertHttpError(int code, Func<Task> act)
{
var ex = await Assert.ThrowsAsync<GreenfieldAPIException>(act);
@@ -29,4 +38,11 @@ public class AssertEx
Assert.Equal(httpStatus, ex.HttpCode);
Assert.Equal(errorCode, ex.APIError.Code);
}
+
+ public static async Task<GreenfieldAPIException> AssertApiError(string expectedError, Func<Task> act)
+ {
+ var err = await Assert.ThrowsAsync<GreenfieldAPIException>(async () => await act());
+ Assert.Equal(expectedError, err.APIError.Code);
+ return err;
+ }
}
diff --git a/BTCPayServer.Tests/DatabaseTests.cs b/BTCPayServer.Tests/DatabaseTests.cs
index 0d38fdd..a813828 100644
--- a/BTCPayServer.Tests/DatabaseTests.cs
+++ b/BTCPayServer.Tests/DatabaseTests.cs
@@ -4,9 +4,6 @@ using BTCPayServer.Payments;
using BTCPayServer.Services;
using Dapper;
using Microsoft.EntityFrameworkCore;
-using NBitcoin;
-using NBitcoin.Altcoins;
-using NBitpayClient;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
@@ -14,13 +11,8 @@ using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Trait("Integration", "Integration")]
- public class DatabaseTests : UnitTestBase
+ public class DatabaseTests(ITestOutputHelper helper) : UnitTestBase(helper)
{
-
- public DatabaseTests(ITestOutputHelper helper):base(helper)
- {
- }
-
[Fact]
public async Task CanConcurrentlyModifyWalletObject()
{
@@ -31,7 +23,7 @@ namespace BTCPayServer.Tests
var wid = new WalletObjectId(new WalletId("AAA", "ddd"), "a", "b");
var all = Enumerable.Range(0, 10)
#pragma warning disable CS0618 // Type or member is obsolete
- .Select(i => walletRepo.ModifyWalletObjectData(wid, (o) => { o["idx"] = i; }))
+ .Select(i => walletRepo.ModifyWalletObjectData(wid, o => { o["idx"] = i; }))
#pragma warning restore CS0618 // Type or member is obsolete
.ToArray();
foreach (var task in all)
@@ -46,12 +38,12 @@ namespace BTCPayServer.Tests
var tester = CreateDBTester();
await tester.MigrateUntil();
var invoiceRepository = tester.GetInvoiceRepository();
- using var ctx = tester.CreateContext();
+ await using var ctx = tester.CreateContext();
var conn = ctx.Database.GetDbConnection();
async Task AddPrompt(string invoiceId, string paymentMethodId, bool activated = true)
{
- JObject prompt = new JObject();
+ var prompt = new JObject();
if (!activated)
prompt["inactive"] = true;
prompt["currency"] = "USD";
@@ -72,13 +64,13 @@ namespace BTCPayServer.Tests
('LTCAndBTC', NOW(), 'New', 'USD'),
('LTCAndBTCLazy', NOW(), 'New', 'USD')
""");
- foreach (var invoiceId in new string[] { "LTCOnly", "LTCAndBTCLazy", "LTCAndBTC" })
+ foreach (var invoiceId in new[] { "LTCOnly", "LTCAndBTCLazy", "LTCAndBTC" })
{
- await AddPrompt(invoiceId, "LTC-CHAIN", true);
+ await AddPrompt(invoiceId, "LTC-CHAIN");
}
- foreach (var invoiceId in new string[] { "BTCOnly", "LTCAndBTC" })
+ foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC" })
{
- await AddPrompt(invoiceId, "BTC-CHAIN", true);
+ await AddPrompt(invoiceId, "BTC-CHAIN");
}
await AddPrompt("LTCAndBTCLazy", "BTC-CHAIN", false);
@@ -149,7 +141,7 @@ namespace BTCPayServer.Tests
{
var tester = CreateDBTester();
await tester.MigrateUntil("20240919085726_refactorinvoiceaddress");
- using var ctx = tester.CreateContext();
+ await using var ctx = tester.CreateContext();
var conn = ctx.Database.GetDbConnection();
await conn.ExecuteAsync("INSERT INTO \"Invoices\" (\"Id\", \"Created\") VALUES ('i', NOW())");
await conn.ExecuteAsync(
@@ -164,74 +156,6 @@ namespace BTCPayServer.Tests
Assert.False(notok);
}
- [Fact]
- public async Task CanMigratePayoutsAndPullPayments()
- {
- var tester = CreateDBTester();
- await tester.MigrateUntil("20240827034505_migratepayouts");
- using var ctx = tester.CreateContext();
- var conn = ctx.Database.GetDbConnection();
- await conn.ExecuteAsync("INSERT INTO \"Stores\"(\"Id\", \"SpeedPolicy\") VALUES (@store, 0)", new { store = "store1" });
- var param = new
- {
- Id = "pp1",
- StoreId = "store1",
- Blob = "{\"Name\": \"CoinLottery\", \"View\": {\"Email\": null, \"Title\": \"\", \"Description\": \"\", \"EmbeddedCSS\": null, \"CustomCSSLink\": null}, \"Limit\": \"10.00\", \"Period\": null, \"Currency\": \"GBP\", \"Description\": \"\", \"Divisibility\": 0, \"MinimumClaim\": \"0\", \"AutoApproveClaims\": false, \"SupportedPaymentMethods\": [\"BTC\", \"BTC_LightningLike\"]}"
- };
- await conn.ExecuteAsync("INSERT INTO \"PullPayments\"(\"Id\", \"StoreId\", \"Blob\", \"StartDate\", \"Archived\") VALUES (@Id, @StoreId, @Blob::JSONB, NOW(), 'f')", param);
- var parameters = new[]
- {
- new
- {
- Id = "p1",
- StoreId = "store1",
- PullPaymentDataId = "pp1",
- PaymentMethodId = "BTC",
- Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": \"0.00012225\", \"MinimumConfirmation\": 1}"
- },
- new
- {
- Id = "p2",
- StoreId = "store1",
- PullPaymentDataId = "pp1",
- PaymentMethodId = "BTC_LightningLike",
- Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
- },
- new
- {
- Id = "p3",
- StoreId = "store1",
- PullPaymentDataId = null as string,
- PaymentMethodId = "BTC_LightningLike",
- Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
- },
- new
- {
- Id = "p4",
- StoreId = "store1",
- PullPaymentDataId = null as string,
- PaymentMethodId = "BTC_LightningLike",
- Blob = "{\"Amount\": \"-10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
- }
- };
- await conn.ExecuteAsync("INSERT INTO \"Payouts\"(\"Id\", \"StoreDataId\", \"PullPaymentDataId\", \"PaymentMethodId\", \"Blob\", \"State\", \"Date\") VALUES (@Id, @StoreId, @PullPaymentDataId, @PaymentMethodId, @Blob::JSONB, 'state', NOW())", parameters);
- await tester.CompleteMigrations();
-
- var migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"PullPayments\" WHERE \"Id\"='pp1' AND \"Limit\"=10.0 AND \"Currency\"='GBP' AND \"Blob\"->>'SupportedPayoutMethods'='[\"BTC-CHAIN\", \"BTC-LN\"]'");
- Assert.True(migrated);
-
- migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p1' AND \"Amount\"= 0.00012225 AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-CHAIN'");
- Assert.True(migrated);
-
- migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p2' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-LN'");
- Assert.True(migrated);
-
- migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p3' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='BTC'");
- Assert.True(migrated);
-
- migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p4' AND \"Amount\" IS NULL AND \"OriginalAmount\"=-10.0 AND \"OriginalCurrency\"='BTC' AND \"PayoutMethodId\"='TOPUP'");
- Assert.True(migrated);
- }
}
}
diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs
index 5492f5b..249537c 100644
--- a/BTCPayServer.Tests/GreenfieldAPITests.cs
+++ b/BTCPayServer.Tests/GreenfieldAPITests.cs
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Linq;
using System.Net.Http;
-using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
@@ -13,26 +11,21 @@ using BTCPayServer.Controllers;
using BTCPayServer.Events;
using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
-using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.Plugins.PointOfSale.Controllers;
-using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Plugins.Webhooks.HostedServices;
-using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
-using BTCPayServer.Plugins.Emails.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
-using NBitcoin.DataEncoders;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -1122,388 +1115,6 @@ namespace BTCPayServer.Tests
Assert.Equal("avatar.jpg", changed.ImageUrl);
}
- [Fact]
- [Trait("Integration", "Integration")]
- public async Task CanUsePullPaymentViaAPI()
- {
- using var tester = CreateServerTester();
- tester.ActivateLightning();
- await tester.StartAsync();
- await tester.EnsureChannelsSetup();
- var acc = tester.NewAccount();
- await acc.GrantAccessAsync(true);
- acc.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
- var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
- var client = await acc.CreateClient();
- var result = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
- {
- Name = "Test",
- Description = "Test description",
- Amount = 12.3m,
- Currency = "BTC",
- PayoutMethods = new[] { "BTC" }
- });
-
- void VerifyResult()
- {
- Assert.Equal("Test", result.Name);
- Assert.Equal("Test description", result.Description);
- // If it contains ? it means that we are resolving an unknown route with the link generator
- Assert.DoesNotContain("?", result.ViewLink);
- Assert.False(result.Archived);
- Assert.Equal("BTC", result.Currency);
- Assert.Equal(12.3m, result.Amount);
- }
- VerifyResult();
-
- var unauthenticated = new BTCPayServerClient(tester.PayTester.ServerUri);
- result = await unauthenticated.GetPullPayment(result.Id);
- VerifyResult();
- await AssertHttpError(404, async () => await unauthenticated.GetPullPayment("lol"));
- // Can't list pull payments unauthenticated
- await AssertHttpError(401, async () => await unauthenticated.GetPullPayments(storeId));
-
- var pullPayments = await client.GetPullPayments(storeId);
- result = Assert.Single(pullPayments);
- VerifyResult();
-
- var test2 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Test 2",
- Amount = 12.3m,
- Currency = "BTC",
- PayoutMethods = new[] { "BTC" },
- BOLT11Expiration = TimeSpan.FromDays(31.0)
- });
- Assert.Equal(TimeSpan.FromDays(31.0), test2.BOLT11Expiration);
-
- TestLogs.LogInformation("Can't archive without knowing the walletId");
- var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
- Assert.Equal("btcpay.store.canarchivepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
- TestLogs.LogInformation("Can't archive without permission");
- await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
- await client.ArchivePullPayment(storeId, result.Id);
- result = await unauthenticated.GetPullPayment(result.Id);
- Assert.Equal(TimeSpan.FromDays(30.0), result.BOLT11Expiration);
- Assert.True(result.Archived);
- var pps = await client.GetPullPayments(storeId);
- result = Assert.Single(pps);
- Assert.Equal("Test 2", result.Name);
- pps = await client.GetPullPayments(storeId, true);
- Assert.Equal(2, pps.Length);
- Assert.Equal("Test 2", pps[0].Name);
- Assert.Equal("Test", pps[1].Name);
-
- var payouts = await unauthenticated.GetPayouts(pps[0].Id);
- Assert.Empty(payouts);
-
- var destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
- await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
- {
- Destination = destination,
- Amount = 1_000_000m,
- PayoutMethodId = "BTC",
- }));
-
- await this.AssertAPIError("archived", async () => await unauthenticated.CreatePayout(pps[1].Id, new CreatePayoutRequest()
- {
- Destination = destination,
- PayoutMethodId = "BTC"
- }));
-
- var payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
- {
- Destination = destination,
- PayoutMethodId = "BTC"
- });
-
- payouts = await unauthenticated.GetPayouts(pps[0].Id);
- var payout2 = Assert.Single(payouts);
- Assert.Equal(payout.OriginalAmount, payout2.OriginalAmount);
- Assert.Equal(payout.Id, payout2.Id);
- Assert.Equal(destination, payout2.Destination);
- Assert.Equal(PayoutState.AwaitingApproval, payout.State);
- Assert.Equal("BTC-CHAIN", payout2.PayoutMethodId);
- Assert.Equal("BTC", payout2.PayoutCurrency);
- Assert.Null(payout.PayoutAmount);
-
- TestLogs.LogInformation("Can't overdraft");
-
- var destination2 = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
- await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
- {
- Destination = destination2,
- Amount = 0.00001m,
- PayoutMethodId = "BTC"
- }));
-
- TestLogs.LogInformation("Can't create too low payout");
- await this.AssertAPIError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
- {
- Destination = destination2,
- PayoutMethodId = "BTC"
- }));
-
- TestLogs.LogInformation("Can archive payout");
- await client.CancelPayout(storeId, payout.Id);
- payouts = await unauthenticated.GetPayouts(pps[0].Id);
- Assert.Empty(payouts);
-
- payouts = await client.GetPayouts(pps[0].Id, true);
- payout = Assert.Single(payouts);
- Assert.Equal(PayoutState.Cancelled, payout.State);
-
- TestLogs.LogInformation("Can create payout after cancelling");
- payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
- {
- Destination = destination,
- PayoutMethodId = "BTC"
- });
-
- var start = RoundSeconds(DateTimeOffset.Now + TimeSpan.FromDays(7.0));
- var inFuture = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Starts in the future",
- Amount = 12.3m,
- StartsAt = start,
- Currency = "BTC",
- PayoutMethods = new[] { "BTC" }
- });
- Assert.Equal(start, inFuture.StartsAt);
- Assert.Null(inFuture.ExpiresAt);
- await this.AssertAPIError("not-started", async () => await unauthenticated.CreatePayout(inFuture.Id, new CreatePayoutRequest()
- {
- Amount = 1.0m,
- Destination = destination,
- PayoutMethodId = "BTC"
- }));
-
- var expires = RoundSeconds(DateTimeOffset.Now - TimeSpan.FromDays(7.0));
- var inPast = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Will expires",
- Amount = 12.3m,
- ExpiresAt = expires,
- Currency = "BTC",
- PayoutMethods = new[] { "BTC" }
- });
- await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest()
- {
- Amount = 1.0m,
- Destination = destination,
- PayoutMethodId = "BTC"
- }));
-
- await this.AssertValidationError(new[] { "ExpiresAt" }, async () => await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Test 2",
- Amount = 12.3m,
- StartsAt = DateTimeOffset.UtcNow,
- ExpiresAt = DateTimeOffset.UtcNow - TimeSpan.FromDays(1)
- }));
-
-
- TestLogs.LogInformation("Create a pull payment with USD");
- var pp = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Test USD",
- Amount = 5000m,
- Currency = "USD",
- PayoutMethods = new[] { "BTC" }
- });
-
- await this.AssertAPIError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id));
-
- destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
- TestLogs.LogInformation("Try to pay it in BTC");
- payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest()
- {
- Destination = destination,
- PayoutMethodId = "BTC"
- });
- await this.AssertAPIError("old-revision", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
- {
- Revision = -1
- }));
- await this.AssertAPIError("rate-unavailable", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
- {
- RateRule = "DONOTEXIST(BTC_USD)"
- }));
- payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
- {
- Revision = payout.Revision
- });
- Assert.Equal(PayoutState.AwaitingPayment, payout.State);
- Assert.NotNull(payout.PayoutAmount);
- Assert.Equal(1.0m, payout.PayoutAmount); // 1 BTC == 5000 USD in tests
- await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
- {
- Revision = payout.Revision
- }));
-
- // Create one pull payment with an amount of 9 decimals
- var test3 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Test 2",
- Amount = 12.303228134m,
- Currency = "BTC",
- PayoutMethods = new[] { "BTC" }
- });
- destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
- payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
- {
- Destination = destination,
- PayoutMethodId = "BTC"
- });
- payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
- // The payout should round the value of the payment down to the network of the payment method
- Assert.Equal(12.30322814m, payout.PayoutAmount);
- Assert.Equal(12.303228134m, payout.OriginalAmount);
-
- await client.MarkPayoutPaid(storeId, payout.Id);
- payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
- Assert.Equal(PayoutState.Completed, payout.State);
- await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
-
- // Test LNURL values
- var test4 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Test 3",
- Amount = 12.303228134m,
- Currency = "BTC",
- PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
- });
- var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
- Assert.IsType<string>(lnrURLs.LNURLBech32);
- Assert.IsType<string>(lnrURLs.LNURLUri);
- Assert.Equal(12.303228134m, test4.Amount);
- Assert.Equal("BTC", test4.Currency);
-
- // Check we can register Boltcard
- var uid = new byte[7];
- RandomNumberGenerator.Fill(uid);
- var card = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- UID = uid
- });
- Assert.Equal(0, card.Version);
- var card1keys = new[] { card.K0, card.K1, card.K2, card.K3, card.K4 };
- Assert.DoesNotContain(null, card1keys);
-
- var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- UID = uid
- });
- Assert.Equal(0, card2.Version);
- card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- UID = uid,
- OnExisting = OnExistingBehavior.UpdateVersion
- });
- Assert.Equal(1, card2.Version);
- Assert.StartsWith("lnurlw://", card2.LNURLW);
- Assert.EndsWith("/boltcard", card2.LNURLW);
- var card2keys = new[] { card2.K0, card2.K1, card2.K2, card2.K3, card2.K4 };
- Assert.DoesNotContain(null, card2keys);
- for (int i = 0; i < card1keys.Length; i++)
- {
- if (i == 1)
- Assert.Contains(card1keys[i], card2keys);
- else
- Assert.DoesNotContain(card1keys[i], card2keys);
- }
- var card3 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- UID = uid,
- OnExisting = OnExistingBehavior.KeepVersion
- });
- Assert.Equal(card2.Version, card3.Version);
- var p = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[8]).ToArray();
- var card4 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- OnExisting = OnExistingBehavior.KeepVersion,
- LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
- });
- Assert.Equal(card2.Version, card4.Version);
- Assert.Equal(card2.K4, card4.K4);
- // Can't define both properties
- await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- OnExisting = OnExistingBehavior.KeepVersion,
- UID = uid,
- LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
- }));
- // p is malformed
- await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- OnExisting = OnExistingBehavior.KeepVersion,
- UID = uid,
- LNURLW = card2.LNURLW + $"?p=lol"
- }));
- // p is invalid
- p[0] = 0;
- await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
- {
- OnExisting = OnExistingBehavior.KeepVersion,
- LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
- }));
- // Test with SATS denomination values
- var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
- {
- Name = "Test SATS",
- Amount = 21000,
- Currency = "SATS",
- PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
- });
- lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
- Assert.IsType<string>(lnrURLs.LNURLBech32);
- Assert.IsType<string>(lnrURLs.LNURLUri);
- Assert.Equal(21000, testSats.Amount);
- Assert.Equal("SATS", testSats.Currency);
-
- //permission test around auto approved pps and payouts
- var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
- var approved = await acc.CreateClient(Policies.CanCreatePullPayments);
- await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
- {
- await nonApproved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
- {
- Amount = 100,
- Currency = "USD",
- Name = "pull payment",
- PayoutMethods = new[] { "BTC" },
- AutoApproveClaims = true
- });
- });
- await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
- {
- await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
- {
- Amount = 100,
- PayoutMethodId = "BTC",
- Approved = true,
- Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
- });
- });
-
- await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
- {
- Amount = 100,
- Currency = "USD",
- Name = "pull payment",
- PayoutMethods = new[] { "BTC" },
- AutoApproveClaims = true
- });
-
- await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
- {
- Amount = 100,
- PayoutMethodId = "BTC",
- Approved = true,
- Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
- });
- }
-
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanProcessPayoutsExternally()
@@ -1511,7 +1122,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
- acc.Register();
+ await acc.RegisterAsync();
await acc.CreateStoreAsync();
var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
var client = await acc.CreateClient();
@@ -1616,23 +1227,12 @@ namespace BTCPayServer.Tests
}
private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset)
- {
- return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset);
- }
+ => TestUtils.RoundSeconds(dateTimeOffset);
- private async Task<GreenfieldAPIException> AssertAPIError(string expectedError, Func<Task> act)
- {
- var err = await Assert.ThrowsAsync<GreenfieldAPIException>(async () => await act());
- Assert.Equal(expectedError, err.APIError.Code);
- return err;
- }
- private async Task<GreenfieldAPIException> AssertPermissionError(string expectedPermission, Func<Task> act)
- {
- var err = await Assert.ThrowsAsync<GreenfieldAPIException>(async () => await act());
- var err2 = Assert.IsType<GreenfieldPermissionAPIError>(err.APIError);
- Assert.Equal(expectedPermission, err2.MissingPermission);
- return err;
- }
+ private Task<GreenfieldAPIException> AssertAPIError(string expectedError, Func<Task> act)
+ => AssertEx.AssertApiError(expectedError, act);
+ private Task<GreenfieldAPIException> AssertPermissionError(string expectedPermission, Func<Task> act)
+ => AssertEx.AssertPermissionError(expectedPermission, act);
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
diff --git a/BTCPayServer.Tests/PlaywrightTests.cs b/BTCPayServer.Tests/PlaywrightTests.cs
index f041a34..70ca0b1 100644
--- a/BTCPayServer.Tests/PlaywrightTests.cs
+++ b/BTCPayServer.Tests/PlaywrightTests.cs
@@ -1823,58 +1823,6 @@ namespace BTCPayServer.Tests
await Expect(newPage.Locator("#LightningNodeUrlClearnet")).ToBeHiddenAsync();
}
- [Fact]
- [Trait("Playwright", "Playwright")]
- public async Task CanEditPullPaymentUI()
- {
- await using var s = CreatePlaywrightTester();
- await s.StartAsync();
- await s.RegisterNewUser(true);
- await s.CreateNewStore();
- await s.GenerateWallet("BTC", "", true, true);
- await s.Server.ExplorerNode.GenerateAsync(1);
- await s.FundStoreWallet(denomination: 50.0m);
-
- await s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
-
- await s.ClickPagePrimary();
- await s.Page.FillAsync("#Name", "PP1");
- await s.Page.Locator("#Amount").ClearAsync();
- await s.Page.FillAsync("#Amount", "99.0");
- await s.ClickPagePrimary();
-
- var opening = s.Page.Context.WaitForPageAsync();
- await s.Page.ClickAsync("text=View");
- var newPage = await opening;
- await Expect(newPage.Locator("body")).ToContainTextAsync("PP1");
- await newPage.CloseAsync();
-
- await s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
-
- await s.Page.ClickAsync("text=PP1");
- await s.Page.FillAsync(".note-editable", "Description Edit");
- await s.Page.FillAsync("#Name", "PP1 Edited");
- await s.ClickPagePrimary();
-
- await s.FindAlertMessage();
-
- opening = s.Page.Context.WaitForPageAsync();
- await s.Page.ClickAsync("text=View");
- await using (await s.SwitchPage(opening))
- {
- try
- {
- await Expect(s.Page.GetByTestId("description")).ToContainTextAsync("Description Edit");
- await Expect(s.Page.GetByTestId("title")).ToContainTextAsync("PP1 Edited");
- }
- catch
- {
- await s.TakeScreenshot("Flaky-CanEditPullPaymentUI.png");
- throw;
- }
- }
- }
-
[Fact]
public async Task CookieReflectProperPermissions()
{
@@ -2511,77 +2459,6 @@ namespace BTCPayServer.Tests
Assert.Contains("The store has been unarchived and will appear in the stores list by default again.", await (await s.FindAlertMessage()).InnerTextAsync());
}
- [Fact]
- public async Task CanUseCoinSelection()
- {
- await using var s = CreatePlaywrightTester();
- await s.StartAsync();
- await s.RegisterNewUser(true);
- var (_, storeId) = await s.CreateNewStore();
- await s.GenerateWallet("BTC", "", false, true);
- var walletId = new WalletId(storeId, "BTC");
- await s.GoToWallet(walletId, WalletsNavPages.Receive);
- var addressStr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
- var address = BitcoinAddress.Create(addressStr!, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
- await s.Server.ExplorerNode.GenerateAsync(1);
- for (int i = 0; i < 6; i++)
- {
- await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m));
- }
- var handlers = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>();
- var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m));
- var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx);
- var spentOutpoint = new OutPoint(targetTx, tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
- var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
- await TestUtils.EventuallyAsync(async () =>
- {
- var store = await s.Server.PayTester.StoreRepository.FindStore(storeId);
- var x = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
- var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode);
- wallet.InvalidateCache(x.AccountDerivation);
- Assert.Contains(
- await wallet.GetUnspentCoins(x.AccountDerivation),
- coin => coin.OutPoint == spentOutpoint);
- });
- await s.Server.ExplorerNode.GenerateAsync(1);
- await s.GoToWallet(walletId, WalletsNavPages.Send);
- await s.Page.Locator("#toggleInputSelection").ClickAsync();
- await s.Page.Locator($"[id='{spentOutpoint}']").WaitForAsync();
- Assert.Equal("true", (await s.Page.Locator("[name='InputSelection']").InputValueAsync()).ToLowerInvariant());
-
- // Select All test
- await s.Page.Locator("#select-all-checkbox").ClickAsync();
- var selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
- var listItems = await s.Page.Locator("li.list-group-item").AllAsync();
- Assert.Equal(listItems.Count, selectedOptions.Count);
- await s.Page.Locator("#select-all-checkbox").ClickAsync();
- selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
- Assert.Empty(selectedOptions);
-
- await s.Page.Locator($"[id='{spentOutpoint}']").ClickAsync();
- selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
- Assert.Single(selectedOptions);
-
- var bob = new NBitcoin.Key().PubKey.Hash.GetAddress(NBitcoin.Network.RegTest);
- await s.Page.Locator("[name='Outputs[0].DestinationAddress']").FillAsync(bob.ToString());
- var amountInput = s.Page.Locator("[name='Outputs[0].Amount']");
- await amountInput.FillAsync("0.3");
- var checkboxElement = s.Page.Locator("input[type='checkbox'][name='Outputs[0].SubtractFeesFromOutput']");
- if (!await checkboxElement.IsCheckedAsync())
- {
- await checkboxElement.ClickAsync();
- }
- await s.Page.Locator("#SignTransaction").ClickAsync();
- await s.Page.Locator("button[value='broadcast']").ClickAsync();
- var happyElement = await s.FindAlertMessage();
- var happyText = await happyElement.InnerTextAsync();
- var txid = System.Text.RegularExpressions.Regex.Match(happyText, @"\((.*)\)").Groups[1].Value;
-
- tx = await s.Server.ExplorerNode.GetRawTransactionAsync(new uint256(txid));
- Assert.Single(tx.Inputs);
- Assert.Equal(spentOutpoint, tx.Inputs[0].PrevOut);
- }
-
[Fact]
[Trait("Lightning", "Lightning")]
public async Task CanAccessUserStoreAsAdmin()
diff --git a/BTCPayServer.Tests/PullPaymentsTests.cs b/BTCPayServer.Tests/PullPaymentsTests.cs
new file mode 100644
index 0000000..af7a089
--- /dev/null
+++ b/BTCPayServer.Tests/PullPaymentsTests.cs
@@ -0,0 +1,564 @@
+using System;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+using BTCPayServer.Client;
+using BTCPayServer.Client.Models;
+using BTCPayServer.Controllers;
+using BTCPayServer.HostedServices;
+using BTCPayServer.Lightning;
+using BTCPayServer.NTag424;
+using BTCPayServer.Views.Stores;
+using Dapper;
+using Microsoft.EntityFrameworkCore;
+using NBitcoin;
+using NBitcoin.DataEncoders;
+using Xunit;
+using Xunit.Abstractions;
+using static Microsoft.Playwright.Assertions;
+
+namespace BTCPayServer.Tests;
+
+[Collection(nameof(NonParallelizableCollectionDefinition))]
+public class PullPaymentsTests(ITestOutputHelper helper) : UnitTestBase(helper)
+{
+ [Fact]
+ [Trait("Integration", "Integration")]
+ public async Task CanTopUpPullPayment()
+ {
+ using var tester = CreateServerTester();
+ await tester.StartAsync();
+ var user = tester.NewAccount();
+ await user.GrantAccessAsync(true);
+ await user.RegisterDerivationSchemeAsync("BTC");
+ var client = await user.CreateClient();
+ var pp = await client.CreatePullPayment(user.StoreId, new()
+ {
+ Currency = "BTC",
+ Amount = 1.0m,
+ PayoutMethods = [ "BTC-CHAIN" ]
+ });
+ var controller = user.GetController<UIInvoiceController>();
+ var invoice = await controller.CreateInvoiceCoreRaw(new()
+ {
+ Amount = 0.5m,
+ Currency = "BTC",
+ }, controller.HttpContext.GetStoreData(), controller.Url.Link(null, null)!, [PullPaymentHostedService.GetInternalTag(pp.Id)]);
+ await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new() { Status = InvoiceStatus.Settled });
+
+ await TestUtils.EventuallyAsync(async () =>
+ {
+ var payouts = await client.GetPayouts(pp.Id);
+ var payout = Assert.Single(payouts);
+ Assert.Equal("TOPUP", payout.PayoutMethodId);
+ Assert.Equal(invoice.Id, payout.Destination);
+ Assert.Equal(-0.5m, payout.OriginalAmount);
+ });
+ }
+
+ [Fact]
+ public async Task CanMigratePayoutsAndPullPayments()
+ {
+ var tester = CreateDBTester();
+ await tester.MigrateUntil("20240827034505_migratepayouts");
+
+ await using var ctx = tester.CreateContext();
+ var conn = ctx.Database.GetDbConnection();
+ await conn.ExecuteAsync("INSERT INTO \"Stores\"(\"Id\", \"SpeedPolicy\") VALUES (@store, 0)", new { store = "store1" });
+ var param = new
+ {
+ Id = "pp1",
+ StoreId = "store1",
+ Blob = "{\"Name\": \"CoinLottery\", \"View\": {\"Email\": null, \"Title\": \"\", \"Description\": \"\", \"EmbeddedCSS\": null, \"CustomCSSLink\": null}, \"Limit\": \"10.00\", \"Period\": null, \"Currency\": \"GBP\", \"Description\": \"\", \"Divisibility\": 0, \"MinimumClaim\": \"0\", \"AutoApproveClaims\": false, \"SupportedPaymentMethods\": [\"BTC\", \"BTC_LightningLike\"]}"
+ };
+ await conn.ExecuteAsync("INSERT INTO \"PullPayments\"(\"Id\", \"StoreId\", \"Blob\", \"StartDate\", \"Archived\") VALUES (@Id, @StoreId, @Blob::JSONB, NOW(), 'f')", param);
+ var parameters = new[]
+ {
+ new
+ {
+ Id = "p1",
+ StoreId = "store1",
+ PullPaymentDataId = "pp1",
+ PaymentMethodId = "BTC",
+ Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": \"0.00012225\", \"MinimumConfirmation\": 1}"
+ },
+ new
+ {
+ Id = "p2",
+ StoreId = "store1",
+ PullPaymentDataId = "pp1",
+ PaymentMethodId = "BTC_LightningLike",
+ Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
+ },
+ new
+ {
+ Id = "p3",
+ StoreId = "store1",
+ PullPaymentDataId = null as string,
+ PaymentMethodId = "BTC_LightningLike",
+ Blob = "{\"Amount\": \"10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
+ },
+ new
+ {
+ Id = "p4",
+ StoreId = "store1",
+ PullPaymentDataId = null as string,
+ PaymentMethodId = "BTC_LightningLike",
+ Blob = "{\"Amount\": \"-10.0\", \"Revision\": 0, \"Destination\": \"address\", \"CryptoAmount\": null, \"MinimumConfirmation\": 1}"
+ }
+ };
+ await conn.ExecuteAsync("INSERT INTO \"Payouts\"(\"Id\", \"StoreDataId\", \"PullPaymentDataId\", \"PaymentMethodId\", \"Blob\", \"State\", \"Date\") VALUES (@Id, @StoreId, @PullPaymentDataId, @PaymentMethodId, @Blob::JSONB, 'state', NOW())", parameters);
+ await tester.CompleteMigrations();
+
+ var migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"PullPayments\" WHERE \"Id\"='pp1' AND \"Limit\"=10.0 AND \"Currency\"='GBP' AND \"Blob\"->>'SupportedPayoutMethods'='[\"BTC-CHAIN\", \"BTC-LN\"]'");
+ Assert.True(migrated);
+
+ migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p1' AND \"Amount\"= 0.00012225 AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-CHAIN'");
+ Assert.True(migrated);
+
+ migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p2' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='GBP' AND \"PayoutMethodId\"='BTC-LN'");
+ Assert.True(migrated);
+
+ migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p3' AND \"Amount\" IS NULL AND \"OriginalAmount\"=10.0 AND \"OriginalCurrency\"='BTC'");
+ Assert.True(migrated);
+
+ migrated = await conn.ExecuteScalarAsync<bool>("SELECT 't'::BOOLEAN FROM \"Payouts\" WHERE \"Id\"='p4' AND \"Amount\" IS NULL AND \"OriginalAmount\"=-10.0 AND \"OriginalCurrency\"='BTC' AND \"PayoutMethodId\"='TOPUP'");
+ Assert.True(migrated);
+ }
+
+ [Fact]
+ [Trait("Integration", "Integration")]
+ public async Task CanUsePullPaymentViaAPI()
+ {
+ using var tester = CreateServerTester();
+ tester.ActivateLightning();
+ await tester.StartAsync();
+ await tester.EnsureChannelsSetup();
+ var acc = tester.NewAccount();
+ await acc.GrantAccessAsync(true);
+ acc.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
+ var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
+ var client = await acc.CreateClient();
+ var result = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Test",
+ Description = "Test description",
+ Amount = 12.3m,
+ Currency = "BTC",
+ PayoutMethods = new[] { "BTC" }
+ });
+
+ void VerifyResult()
+ {
+ Assert.Equal("Test", result.Name);
+ Assert.Equal("Test description", result.Description);
+ // If it contains ? it means that we are resolving an unknown route with the link generator
+ Assert.DoesNotContain("?", result.ViewLink);
+ Assert.False(result.Archived);
+ Assert.Equal("BTC", result.Currency);
+ Assert.Equal(12.3m, result.Amount);
+ }
+
+ VerifyResult();
+
+ var unauthenticated = new BTCPayServerClient(tester.PayTester.ServerUri);
+ result = await unauthenticated.GetPullPayment(result.Id);
+ VerifyResult();
+ await AssertEx.AssertHttpError(404, async () => await unauthenticated.GetPullPayment("lol"));
+ // Can't list pull payments unauthenticated
+ await AssertEx.AssertHttpError(401, async () => await unauthenticated.GetPullPayments(storeId));
+
+ var pullPayments = await client.GetPullPayments(storeId);
+ result = Assert.Single(pullPayments);
+ VerifyResult();
+
+ var test2 = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Test 2",
+ Amount = 12.3m,
+ Currency = "BTC",
+ PayoutMethods = new[] { "BTC" },
+ BOLT11Expiration = TimeSpan.FromDays(31.0)
+ });
+ Assert.Equal(TimeSpan.FromDays(31.0), test2.BOLT11Expiration);
+
+ TestLogs.LogInformation("Can't archive without knowing the walletId");
+ var ex = await AssertEx.AssertApiError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
+ Assert.Equal("btcpay.store.canarchivepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
+ TestLogs.LogInformation("Can't archive without permission");
+ await AssertEx.AssertApiError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
+ await client.ArchivePullPayment(storeId, result.Id);
+ result = await unauthenticated.GetPullPayment(result.Id);
+ Assert.Equal(TimeSpan.FromDays(30.0), result.BOLT11Expiration);
+ Assert.True(result.Archived);
+ var pps = await client.GetPullPayments(storeId);
+ result = Assert.Single(pps);
+ Assert.Equal("Test 2", result.Name);
+ pps = await client.GetPullPayments(storeId, true);
+ Assert.Equal(2, pps.Length);
+ Assert.Equal("Test 2", pps[0].Name);
+ Assert.Equal("Test", pps[1].Name);
+
+ var payouts = await unauthenticated.GetPayouts(pps[0].Id);
+ Assert.Empty(payouts);
+
+ var destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
+ await AssertEx.AssertApiError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
+ {
+ Destination = destination,
+ Amount = 1_000_000m,
+ PayoutMethodId = "BTC",
+ }));
+
+ await AssertEx.AssertApiError("archived", async () => await unauthenticated.CreatePayout(pps[1].Id, new CreatePayoutRequest()
+ {
+ Destination = destination,
+ PayoutMethodId = "BTC"
+ }));
+
+ var payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
+ {
+ Destination = destination,
+ PayoutMethodId = "BTC"
+ });
+
+ payouts = await unauthenticated.GetPayouts(pps[0].Id);
+ var payout2 = Assert.Single(payouts);
+ Assert.Equal(payout.OriginalAmount, payout2.OriginalAmount);
+ Assert.Equal(payout.Id, payout2.Id);
+ Assert.Equal(destination, payout2.Destination);
+ Assert.Equal(PayoutState.AwaitingApproval, payout.State);
+ Assert.Equal("BTC-CHAIN", payout2.PayoutMethodId);
+ Assert.Equal("BTC", payout2.PayoutCurrency);
+ Assert.Null(payout.PayoutAmount);
+
+ TestLogs.LogInformation("Can't overdraft");
+
+ var destination2 = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
+ await AssertEx.AssertApiError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
+ {
+ Destination = destination2,
+ Amount = 0.00001m,
+ PayoutMethodId = "BTC"
+ }));
+
+ TestLogs.LogInformation("Can't create too low payout");
+ await AssertEx.AssertApiError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
+ {
+ Destination = destination2,
+ PayoutMethodId = "BTC"
+ }));
+
+ TestLogs.LogInformation("Can archive payout");
+ await client.CancelPayout(storeId, payout.Id);
+ payouts = await unauthenticated.GetPayouts(pps[0].Id);
+ Assert.Empty(payouts);
+
+ payouts = await client.GetPayouts(pps[0].Id, true);
+ payout = Assert.Single(payouts);
+ Assert.Equal(PayoutState.Cancelled, payout.State);
+
+ TestLogs.LogInformation("Can create payout after cancelling");
+ await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest()
+ {
+ Destination = destination,
+ PayoutMethodId = "BTC"
+ });
+
+ var start = TestUtils.RoundSeconds(DateTimeOffset.Now + TimeSpan.FromDays(7.0));
+ var inFuture = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Starts in the future",
+ Amount = 12.3m,
+ StartsAt = start,
+ Currency = "BTC",
+ PayoutMethods = new[] { "BTC" }
+ });
+ Assert.Equal(start, inFuture.StartsAt);
+ Assert.Null(inFuture.ExpiresAt);
+ await AssertEx.AssertApiError("not-started", async () => await unauthenticated.CreatePayout(inFuture.Id, new CreatePayoutRequest()
+ {
+ Amount = 1.0m,
+ Destination = destination,
+ PayoutMethodId = "BTC"
+ }));
+
+ var expires = TestUtils.RoundSeconds(DateTimeOffset.Now - TimeSpan.FromDays(7.0));
+ var inPast = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Will expires",
+ Amount = 12.3m,
+ ExpiresAt = expires,
+ Currency = "BTC",
+ PayoutMethods = new[] { "BTC" }
+ });
+ await AssertEx.AssertApiError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest()
+ {
+ Amount = 1.0m,
+ Destination = destination,
+ PayoutMethodId = "BTC"
+ }));
+
+ await AssertEx.AssertValidationError(new[] { "ExpiresAt" }, async () => await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Test 2",
+ Amount = 12.3m,
+ StartsAt = DateTimeOffset.UtcNow,
+ ExpiresAt = DateTimeOffset.UtcNow - TimeSpan.FromDays(1)
+ }));
+
+
+ TestLogs.LogInformation("Create a pull payment with USD");
+ var pp = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Test USD",
+ Amount = 5000m,
+ Currency = "USD",
+ PayoutMethods = new[] { "BTC" }
+ });
+
+ await AssertEx.AssertApiError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id));
+
+ destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
+ TestLogs.LogInformation("Try to pay it in BTC");
+ payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest()
+ {
+ Destination = destination,
+ PayoutMethodId = "BTC"
+ });
+ await AssertEx.AssertApiError("old-revision", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
+ {
+ Revision = -1
+ }));
+ await AssertEx.AssertApiError("rate-unavailable", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
+ {
+ RateRule = "DONOTEXIST(BTC_USD)"
+ }));
+ payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
+ {
+ Revision = payout.Revision
+ });
+ Assert.Equal(PayoutState.AwaitingPayment, payout.State);
+ Assert.NotNull(payout.PayoutAmount);
+ Assert.Equal(1.0m, payout.PayoutAmount); // 1 BTC == 5000 USD in tests
+ await AssertEx.AssertApiError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
+ {
+ Revision = payout.Revision
+ }));
+
+ // Create one pull payment with an amount of 9 decimals
+ var test3 = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Test 2",
+ Amount = 12.303228134m,
+ Currency = "BTC",
+ PayoutMethods = new[] { "BTC" }
+ });
+ destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
+ payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
+ {
+ Destination = destination,
+ PayoutMethodId = "BTC"
+ });
+ payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
+ // The payout should round the value of the payment down to the network of the payment method
+ Assert.Equal(12.30322814m, payout.PayoutAmount);
+ Assert.Equal(12.303228134m, payout.OriginalAmount);
+
+ await client.MarkPayoutPaid(storeId, payout.Id);
+ payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
+ Assert.Equal(PayoutState.Completed, payout.State);
+ await AssertEx.AssertApiError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
+
+ // Test LNURL values
+ var test4 = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Test 3",
+ Amount = 12.303228134m,
+ Currency = "BTC",
+ PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
+ });
+ var lnrUrLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
+ Assert.IsType<string>(lnrUrLs.LNURLBech32);
+ Assert.IsType<string>(lnrUrLs.LNURLUri);
+ Assert.Equal(12.303228134m, test4.Amount);
+ Assert.Equal("BTC", test4.Currency);
+
+ // Check we can register Boltcard
+ var uid = new byte[7];
+ RandomNumberGenerator.Fill(uid);
+ var card = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ UID = uid
+ });
+ Assert.Equal(0, card.Version);
+ var card1Keys = new[] { card.K0, card.K1, card.K2, card.K3, card.K4 };
+ Assert.DoesNotContain(null, card1Keys);
+
+ var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ UID = uid
+ });
+ Assert.Equal(0, card2.Version);
+ card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ UID = uid,
+ OnExisting = OnExistingBehavior.UpdateVersion
+ });
+ Assert.Equal(1, card2.Version);
+ Assert.StartsWith("lnurlw://", card2.LNURLW);
+ Assert.EndsWith("/boltcard", card2.LNURLW);
+ var card2Keys = new[] { card2.K0, card2.K1, card2.K2, card2.K3, card2.K4 };
+ Assert.DoesNotContain(null, card2Keys);
+ for (var i = 0; i < card1Keys.Length; i++)
+ {
+ if (i == 1)
+ Assert.Contains(card1Keys[i], card2Keys);
+ else
+ Assert.DoesNotContain(card1Keys[i], card2Keys);
+ }
+
+ var card3 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ UID = uid,
+ OnExisting = OnExistingBehavior.KeepVersion
+ });
+ Assert.Equal(card2.Version, card3.Version);
+ var p = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[8]).ToArray();
+ var card4 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ OnExisting = OnExistingBehavior.KeepVersion,
+ LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
+ });
+ Assert.Equal(card2.Version, card4.Version);
+ Assert.Equal(card2.K4, card4.K4);
+ // Can't define both properties
+ await AssertEx.AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ OnExisting = OnExistingBehavior.KeepVersion,
+ UID = uid,
+ LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
+ }));
+ // p is malformed
+ await AssertEx.AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ OnExisting = OnExistingBehavior.KeepVersion,
+ UID = uid,
+ LNURLW = card2.LNURLW + $"?p=lol"
+ }));
+ // p is invalid
+ p[0] = 0;
+ await AssertEx.AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
+ {
+ OnExisting = OnExistingBehavior.KeepVersion,
+ LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
+ }));
+ // Test with SATS denomination values
+ var testSats = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest()
+ {
+ Name = "Test SATS",
+ Amount = 21000,
+ Currency = "SATS",
+ PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
+ });
+ lnrUrLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
+ Assert.IsType<string>(lnrUrLs.LNURLBech32);
+ Assert.IsType<string>(lnrUrLs.LNURLUri);
+ Assert.Equal(21000, testSats.Amount);
+ Assert.Equal("SATS", testSats.Currency);
+
+ //permission test around auto approved pps and payouts
+ var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
+ var approved = await acc.CreateClient(Policies.CanCreatePullPayments);
+ await AssertEx.AssertPermissionError(Policies.CanCreatePullPayments, async () =>
+ {
+ await nonApproved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
+ {
+ Amount = 100,
+ Currency = "USD",
+ Name = "pull payment",
+ PayoutMethods = new[] { "BTC" },
+ AutoApproveClaims = true
+ });
+ });
+ await AssertEx.AssertPermissionError(Policies.CanCreatePullPayments, async () =>
+ {
+ await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
+ {
+ Amount = 100,
+ PayoutMethodId = "BTC",
+ Approved = true,
+ Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
+ });
+ });
+
+ await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
+ {
+ Amount = 100,
+ Currency = "USD",
+ Name = "pull payment",
+ PayoutMethods = new[] { "BTC" },
+ AutoApproveClaims = true
+ });
+
+ await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
+ {
+ Amount = 100,
+ PayoutMethodId = "BTC",
+ Approved = true,
+ Destination = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, Network.RegTest).ToString()
+ });
+ }
+
+ [Fact]
+ [Trait("Playwright", "Playwright-2")]
+ public async Task CanEditPullPaymentUI()
+ {
+ await using var s = CreatePlaywrightTester();
+ await s.StartAsync();
+ await s.RegisterNewUser(true);
+ await s.CreateNewStore();
+ await s.GenerateWallet("BTC", "", true, true);
+ await s.Server.ExplorerNode.GenerateAsync(1);
+ await s.FundStoreWallet(denomination: 50.0m);
+
+ await s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
+
+ await s.ClickPagePrimary();
+ await s.Page.FillAsync("#Name", "PP1");
+ await s.Page.Locator("#Amount").ClearAsync();
+ await s.Page.FillAsync("#Amount", "99.0");
+ await s.ClickPagePrimary();
+
+ var opening = s.Page.Context.WaitForPageAsync();
+ await s.Page.ClickAsync("text=View");
+ var newPage = await opening;
+ await Expect(newPage.Locator("body")).ToContainTextAsync("PP1");
+ await newPage.CloseAsync();
+
+ await s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
+
+ await s.Page.ClickAsync("text=PP1");
+ await s.Page.FillAsync(".note-editable", "Description Edit");
+ await s.Page.FillAsync("#Name", "PP1 Edited");
+ await s.ClickPagePrimary();
+
+ await s.FindAlertMessage();
+
+ opening = s.Page.Context.WaitForPageAsync();
+ await s.Page.ClickAsync("text=View");
+ await using (await s.SwitchPage(opening))
+ {
+ try
+ {
+ await Expect(s.Page.GetByTestId("description")).ToContainTextAsync("Description Edit");
+ await Expect(s.Page.GetByTestId("title")).ToContainTextAsync("PP1 Edited");
+ }
+ catch
+ {
+ await s.TakeScreenshot("Flaky-CanEditPullPaymentUI.png");
+ throw;
+ }
+ }
+ }
+}
diff --git a/BTCPayServer.Tests/TestUtils.cs b/BTCPayServer.Tests/TestUtils.cs
index 198a3e3..65ed36d 100644
--- a/BTCPayServer.Tests/TestUtils.cs
+++ b/BTCPayServer.Tests/TestUtils.cs
@@ -47,6 +47,9 @@ namespace BTCPayServer.Tests
return Path.Combine(directory.FullName, "TestData", relativeFilePath);
}
+ public static DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset)
+ => new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset);
+
public static T AssertType<T>(this object obj)
=> Assert.IsType<T>(obj);
diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs
index b7191f4..b667486 100644
--- a/BTCPayServer.Tests/UnitTest1.cs
+++ b/BTCPayServer.Tests/UnitTest1.cs
@@ -1570,41 +1570,6 @@ namespace BTCPayServer.Tests
await tester.CreateInvoice(currency: "JPY", amount: 700000m, expectedSeverity: StatusMessageModel.StatusSeverity.Error);
}
-
- [Fact]
- [Trait("Integration", "Integration")]
- public async Task CanTopUpPullPayment()
- {
- using var tester = CreateServerTester();
- await tester.StartAsync();
- var user = tester.NewAccount();
- await user.GrantAccessAsync(true);
- await user.RegisterDerivationSchemeAsync("BTC");
- var client = await user.CreateClient();
- var pp = await client.CreatePullPayment(user.StoreId, new()
- {
- Currency = "BTC",
- Amount = 1.0m,
- PayoutMethods = [ "BTC-CHAIN" ]
- });
- var controller = user.GetController<UIInvoiceController>();
- var invoice = await controller.CreateInvoiceCoreRaw(new()
- {
- Amount = 0.5m,
- Currency = "BTC",
- }, controller.HttpContext.GetStoreData(), controller.Url.Link(null, null), [PullPaymentHostedService.GetInternalTag(pp.Id)]);
- await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new() { Status = InvoiceStatus.Settled });
-
- await TestUtils.EventuallyAsync(async () =>
- {
- var payouts = await client.GetPayouts(pp.Id);
- var payout = Assert.Single(payouts);
- Assert.Equal("TOPUP", payout.PayoutMethodId);
- Assert.Equal(invoice.Id, payout.Destination);
- Assert.Equal(-0.5m, payout.OriginalAmount);
- });
- }
-
[Fact]
[Trait("FastTest", "FastTest")]
public void TestMailTemplate()
diff --git a/BTCPayServer.Tests/WalletTests.cs b/BTCPayServer.Tests/WalletTests.cs
index a78b9ba..9652845 100644
--- a/BTCPayServer.Tests/WalletTests.cs
+++ b/BTCPayServer.Tests/WalletTests.cs
@@ -2,6 +2,11 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
+using BTCPayServer.Data;
+using BTCPayServer.Payments;
+using BTCPayServer.Services.Invoices;
+using BTCPayServer.Services.Wallets;
+using BTCPayServer.Views.Wallets;
using NBitcoin;
using Xunit;
using Xunit.Abstractions;
@@ -114,6 +119,77 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
}
}
+ [Fact]
+ [Trait("Playwright", "Playwright-2")]
+ public async Task CanUseCoinSelection()
+ {
+ await using var s = CreatePlaywrightTester();
+ await s.StartAsync();
+ await s.RegisterNewUser(true);
+ var (_, storeId) = await s.CreateNewStore();
+ await s.GenerateWallet("BTC", "", false, true);
+ var walletId = new WalletId(storeId, "BTC");
+ await s.GoToWallet(walletId, WalletsNavPages.Receive);
+ var addressStr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
+ var address = BitcoinAddress.Create(addressStr!, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
+ await s.Server.ExplorerNode.GenerateAsync(1);
+ for (var i = 0; i < 6; i++)
+ {
+ await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m));
+ }
+ var handlers = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>();
+ var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m));
+ var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx);
+ var spentOutpoint = new OutPoint(targetTx, tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
+ var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
+ await TestUtils.EventuallyAsync(async () =>
+ {
+ var store = await s.Server.PayTester.StoreRepository.FindStore(storeId);
+ var x = store!.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
+ var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode);
+ wallet.InvalidateCache(x!.AccountDerivation);
+ Assert.Contains(
+ await wallet.GetUnspentCoins(x.AccountDerivation),
+ coin => coin.OutPoint == spentOutpoint);
+ });
+ await s.Server.ExplorerNode.GenerateAsync(1);
+ await s.GoToWallet(walletId, WalletsNavPages.Send);
+ await s.Page.Locator("#toggleInputSelection").ClickAsync();
+ await s.Page.Locator($"[id='{spentOutpoint}']").WaitForAsync();
+ Assert.Equal("true", (await s.Page.Locator("[name='InputSelection']").InputValueAsync()).ToLowerInvariant());
+
+ // Select All test
+ await s.Page.Locator("#select-all-checkbox").ClickAsync();
+ var selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
+ var listItems = await s.Page.Locator("li.list-group-item").AllAsync();
+ Assert.Equal(listItems.Count, selectedOptions.Count);
+ await s.Page.Locator("#select-all-checkbox").ClickAsync();
+ selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
+ Assert.Empty(selectedOptions);
+
+ await s.Page.Locator($"[id='{spentOutpoint}']").ClickAsync();
+ selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
+ Assert.Single(selectedOptions);
+
+ var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
+ await s.Page.Locator("[name='Outputs[0].DestinationAddress']").FillAsync(bob.ToString());
+ var amountInput = s.Page.Locator("[name='Outputs[0].Amount']");
+ await amountInput.FillAsync("0.3");
+ var checkboxElement = s.Page.Locator("input[type='checkbox'][name='Outputs[0].SubtractFeesFromOutput']");
+ if (!await checkboxElement.IsCheckedAsync())
+ {
+ await checkboxElement.ClickAsync();
+ }
+ await s.Page.Locator("#SignTransaction").ClickAsync();
+ await s.Page.Locator("button[value='broadcast']").ClickAsync();
+ var happyElement = await s.FindAlertMessage();
+ var happyText = await happyElement.InnerTextAsync();
+ var txid = System.Text.RegularExpressions.Regex.Match(happyText, @"\((.*)\)").Groups[1].Value;
+
+ tx = await s.Server.ExplorerNode.GetRawTransactionAsync(new uint256(txid));
+ Assert.Single(tx.Inputs);
+ Assert.Equal(spentOutpoint, tx.Inputs[0].PrevOut);
+ }
[Fact]
[Trait("Playwright", "Playwright-2")]
@@ -143,7 +219,7 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
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++)
+ for (var i = 0; i < 5; i++)
{
var txs = await s.GoToWalletTransactions(s.WalletId);
await txs.SelectAll();
diff --git a/btcpayserver.sln.DotSettings b/btcpayserver.sln.DotSettings
index e3065fa..e5c808b 100644
--- a/btcpayserver.sln.DotSettings
+++ b/btcpayserver.sln.DotSettings
@@ -14,4 +14,8 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SMTP/@EntryIndexedValue">SMTP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SSH/@EntryIndexedValue">SSH</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=TX/@EntryIndexedValue">TX</s:String>
- <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String></wpf:ResourceDictionary>
\ No newline at end of file
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=btcpay/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=lnurlw/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=Sats/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=sats/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.