diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 5d08d3571..9e573f920 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -15,12 +15,15 @@ using BTCPayServer.Lightning; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.PayoutProcessors; +using BTCPayServer.PayoutProcessors.OnChain; using BTCPayServer.Services; using BTCPayServer.Services.Custodian.Client.MockCustodian; using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; using NBitcoin; using NBitpayClient; using Newtonsoft.Json; @@ -3495,8 +3498,8 @@ namespace BTCPayServer.Tests Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId)); await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", - new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(100000) }); - Assert.Equal(100000, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds); + new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(3600) }); + Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds); var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId)); Assert.Equal("BTC", Assert.Single(tpGen.PaymentMethods)); @@ -3509,8 +3512,10 @@ namespace BTCPayServer.Tests Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")); Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId)); + // Send just enough money to cover the smallest of the payouts. + var fee = (await tester.ExplorerClient.GetFeeRateAsync(1000)).FeeRate.GetFee(150); await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, - tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.000012m)); + tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.00001m) + fee); await tester.ExplorerNode.GenerateAsync(1); await TestUtils.EventuallyAsync(async () => { @@ -3520,8 +3525,9 @@ namespace BTCPayServer.Tests Assert.Equal(3, payouts.Length); }); await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", - new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(5) }); - Assert.Equal(5, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds); + new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600), FeeBlockTarget = 1000 }); + Assert.Equal(600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds); + await TestUtils.EventuallyAsync(async () => { Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count()); @@ -3529,8 +3535,10 @@ namespace BTCPayServer.Tests Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress)); }); - await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, - tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m)); + var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, + tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee); + await tester.WaitForEvent(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid); + await tester.PayTester.GetService().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC")); await TestUtils.EventuallyAsync(async () => { Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count()); diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 82d6cf1e6..92a4ca143 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -194,7 +194,8 @@ namespace BTCPayServer.Tests tcs.TrySetResult(evt); } }); - await action.Invoke(); + if (action != null) + await action.Invoke(); var result = await tcs.Task; sub.Dispose(); return result; diff --git a/BTCPayServer.Tests/TestUtils.cs b/BTCPayServer.Tests/TestUtils.cs index f08b259ba..6c8afe167 100644 --- a/BTCPayServer.Tests/TestUtils.cs +++ b/BTCPayServer.Tests/TestUtils.cs @@ -112,7 +112,14 @@ namespace BTCPayServer.Tests } catch (XunitException) when (!cts.Token.IsCancellationRequested) { - await Task.Delay(500, cts.Token); + bool timeout =false; + try + { + await Task.Delay(500, cts.Token); + } + catch { timeout = true; } + if (timeout) + throw; } } } diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs index 940c05779..3a32f0a8b 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; @@ -69,6 +70,9 @@ namespace BTCPayServer.Controllers.Greenfield public async Task UpdateStoreLightningAutomatedPayoutProcessor( string storeId, string paymentMethod, LightningAutomatedPayoutSettings request) { + AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds)); + if (!ModelState.IsValid) + return this.CreateValidationError(ModelState); paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString(); var activeProcessor = (await _payoutProcessorService.GetProcessors( diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs index ff5571e21..16030ca1d 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs @@ -1,7 +1,9 @@ #nullable enable +using System; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; @@ -75,6 +77,11 @@ namespace BTCPayServer.Controllers.Greenfield public async Task UpdateStoreOnchainAutomatedPayoutProcessor( string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request) { + AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds)); + if (request.FeeBlockTarget is int t && (t < 1 || t > 1000)) + ModelState.AddModelError(nameof(request.FeeBlockTarget), "The feeBlockTarget should be between 1 and 1000"); + if (!ModelState.IsValid) + return this.CreateValidationError(ModelState); paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString(); var activeProcessor = (await _payoutProcessorService.GetProcessors( diff --git a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs index cf9e0f7b9..d715713b7 100644 --- a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs @@ -9,8 +9,10 @@ using BTCPayServer.HostedServices; using BTCPayServer.Payments; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using NBitcoin.Protocol; using PayoutData = BTCPayServer.Data.PayoutData; using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData; @@ -20,6 +22,17 @@ public class AutomatedPayoutConstants { public const double MinIntervalMinutes = 10.0; public const double MaxIntervalMinutes = 60.0; + public static void ValidateInterval(ModelStateDictionary modelState, TimeSpan timeSpan, string parameterName) + { + if (timeSpan < TimeSpan.FromMinutes(AutomatedPayoutConstants.MinIntervalMinutes)) + { + modelState.AddModelError(parameterName, $"The minimum interval is {AutomatedPayoutConstants.MinIntervalMinutes * 60} seconds"); + } + if (timeSpan > TimeSpan.FromMinutes(AutomatedPayoutConstants.MaxIntervalMinutes)) + { + modelState.AddModelError(parameterName, $"The maximum interval is {AutomatedPayoutConstants.MaxIntervalMinutes * 60} seconds"); + } + } } public abstract class BaseAutomatedPayoutProcessor : BaseAsyncService where T : AutomatedPayoutBlob { diff --git a/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs index 745b9c2fe..c4d078cef 100644 --- a/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs @@ -95,7 +95,7 @@ namespace BTCPayServer.PayoutProcessors.OnChain storePaymentMethod.AccountDerivation, DerivationFeature.Change, 0, true); var processorBlob = GetBlob(_PayoutProcesserSettings); - var feeRate = await explorerClient.GetFeeRateAsync(processorBlob.FeeTargetBlock, new FeeRate(1m)); + var feeRate = await explorerClient.GetFeeRateAsync(Math.Max(processorBlob.FeeTargetBlock, 1), new FeeRate(1m)); var transfersProcessing = new List(); foreach (var transferRequest in payouts) diff --git a/BTCPayServer/PayoutProcessors/PayoutProcessorService.cs b/BTCPayServer/PayoutProcessors/PayoutProcessorService.cs index 036e633b9..ccd4a9266 100644 --- a/BTCPayServer/PayoutProcessors/PayoutProcessorService.cs +++ b/BTCPayServer/PayoutProcessors/PayoutProcessorService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -43,6 +44,15 @@ public class PayoutProcessorService : EventHostedServiceBase public class PayoutProcessorQuery { + public PayoutProcessorQuery() + { + + } + public PayoutProcessorQuery(string storeId, string paymentMethod) + { + Stores = new[] { storeId }; + PaymentMethods = new[] { paymentMethod }; + } public string[] Stores { get; set; } public string[] Processors { get; set; } public string[] PaymentMethods { get; set; } @@ -166,4 +176,12 @@ public class PayoutProcessorService : EventHostedServiceBase processorUpdated.Processed?.SetResult(); } } + + internal async Task Restart(PayoutProcessorQuery payoutProcessorQuery) + { + foreach (var data in await GetProcessors(payoutProcessorQuery)) + { + await StartOrUpdateProcessor(data, default); + } + } }