From 4063a5aaee6dad89e60c1d41fd31b0d895be160b Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 20 Jul 2023 15:05:14 +0200 Subject: [PATCH] Quality of life improvements to payout processors (#5135) * Quality of life improvements to payout processors * Allows more fleixble intervals for payout processing from 10-60 mins to 1min-24hours(requested by users) * Cancel ln payotus that expired (bolt11) * Allow cancelling of ln payotus that have failed to be paid after x attempts * Allow conifguring a threshold for when to process on-chain payouts (reduces fees) # Conflicts: # BTCPayServer.Tests/SeleniumTests.cs * Simplify the code * switch to concurrent dictionary * Allow ProcessNewPayoutsInstantly * refactor plugin hook service to have events available and change processor hooks to actions with better args * add procesor extended tests * Update BTCPayServer.Tests/GreenfieldAPITests.cs * fix concurrency issue * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs --------- Co-authored-by: nicolas.dorier --- .../Contracts/IPluginHookService.cs | 4 + .../LightningAutomatedPayoutSettings.cs | 5 + .../Models/OnChainAutomatedPayoutSettings.cs | 4 + BTCPayServer.Data/Data/PayoutProcessorData.cs | 1 + BTCPayServer.Tests/GreenfieldAPITests.cs | 126 +++++++++++++++++- ...atedLightningPayoutProcessorsController.cs | 15 ++- ...omatedOnChainPayoutProcessorsController.cs | 8 +- .../UILightningLikePayoutController.cs | 12 ++ .../PullPaymentHostedService.cs | 17 ++- BTCPayServer/Hosting/BTCPayServerServices.cs | 3 +- .../PayoutProcessors/AfterPayoutActionData.cs | 7 + .../PayoutProcessors/AfterPayoutFilterData.cs | 19 --- .../BaseAutomatedPayoutProcessor.cs | 100 ++++++++++---- .../BeforePayoutActionData.cs | 6 + .../BeforePayoutFilterData.cs | 16 --- .../Lightning/LightningAutomatedPayoutBlob.cs | 8 ++ .../LightningAutomatedPayoutProcessor.cs | 54 ++++++-- .../LightningAutomatedPayoutSenderFactory.cs | 1 + ...ningAutomatedPayoutProcessorsController.cs | 20 ++- .../OnChain/OnChainAutomatedPayoutBlob.cs | 1 + .../OnChainAutomatedPayoutProcessor.cs | 31 +++-- ...hainAutomatedPayoutProcessorsController.cs | 9 +- BTCPayServer/Plugins/PluginHookService.cs | 5 + .../Configure.cshtml | 15 +++ .../Configure.cshtml | 17 ++- .../swagger.template.payout-processors.json | 44 ++++++ 26 files changed, 441 insertions(+), 107 deletions(-) create mode 100644 BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs delete mode 100644 BTCPayServer/PayoutProcessors/AfterPayoutFilterData.cs create mode 100644 BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs delete mode 100644 BTCPayServer/PayoutProcessors/BeforePayoutFilterData.cs create mode 100644 BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutBlob.cs diff --git a/BTCPayServer.Abstractions/Contracts/IPluginHookService.cs b/BTCPayServer.Abstractions/Contracts/IPluginHookService.cs index 2c440b0de..d1b387d79 100644 --- a/BTCPayServer.Abstractions/Contracts/IPluginHookService.cs +++ b/BTCPayServer.Abstractions/Contracts/IPluginHookService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace BTCPayServer.Abstractions.Contracts @@ -6,5 +7,8 @@ namespace BTCPayServer.Abstractions.Contracts { Task ApplyAction(string hook, object args); Task ApplyFilter(string hook, object args); + + event EventHandler<(string hook, object args)> ActionInvoked; + event EventHandler<(string hook, object args)> FilterInvoked; } } diff --git a/BTCPayServer.Client/Models/LightningAutomatedPayoutSettings.cs b/BTCPayServer.Client/Models/LightningAutomatedPayoutSettings.cs index 1b34f2da6..16661e5c9 100644 --- a/BTCPayServer.Client/Models/LightningAutomatedPayoutSettings.cs +++ b/BTCPayServer.Client/Models/LightningAutomatedPayoutSettings.cs @@ -10,4 +10,9 @@ public class LightningAutomatedPayoutSettings [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] public TimeSpan IntervalSeconds { get; set; } + + public int? CancelPayoutAfterFailures { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public bool ProcessNewPayoutsInstantly { get; set; } + } diff --git a/BTCPayServer.Client/Models/OnChainAutomatedPayoutSettings.cs b/BTCPayServer.Client/Models/OnChainAutomatedPayoutSettings.cs index f0ce54830..0a8135884 100644 --- a/BTCPayServer.Client/Models/OnChainAutomatedPayoutSettings.cs +++ b/BTCPayServer.Client/Models/OnChainAutomatedPayoutSettings.cs @@ -12,4 +12,8 @@ public class OnChainAutomatedPayoutSettings public TimeSpan IntervalSeconds { get; set; } public int? FeeBlockTarget { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public decimal Threshold { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public bool ProcessNewPayoutsInstantly { get; set; } } diff --git a/BTCPayServer.Data/Data/PayoutProcessorData.cs b/BTCPayServer.Data/Data/PayoutProcessorData.cs index 0bb8e591d..2ac197a9f 100644 --- a/BTCPayServer.Data/Data/PayoutProcessorData.cs +++ b/BTCPayServer.Data/Data/PayoutProcessorData.cs @@ -8,6 +8,7 @@ namespace BTCPayServer.Data; public class AutomatedPayoutBlob { public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1); + public bool ProcessNewPayoutsInstantly { get; set; } } public class PayoutProcessorData : IHasBlobUntyped { diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 608c7716b..5c04affc4 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -17,6 +17,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors.OnChain; +using BTCPayServer.Plugins; using BTCPayServer.Services; using BTCPayServer.Services.Custodian.Client.MockCustodian; using BTCPayServer.Services.Notifications; @@ -3669,9 +3670,12 @@ namespace BTCPayServer.Tests Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress)); }); - 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); + uint256 txid = null; + await tester.WaitForEvent(async () => + { + txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, + tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee); + }, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid); await tester.PayTester.GetService().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC")); await TestUtils.EventuallyAsync(async () => { @@ -3679,6 +3683,122 @@ namespace BTCPayServer.Tests payouts = await adminClient.GetStorePayouts(admin.StoreId); Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress)); }); + + // settings that were added later + var settings = + Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")); + Assert.False( settings.ProcessNewPayoutsInstantly); + Assert.Equal(0m, settings.Threshold); + + //let's use the ProcessNewPayoutsInstantly so that it will trigger instantly + + settings.IntervalSeconds = TimeSpan.FromDays(1); + settings.ProcessNewPayoutsInstantly = true; + + await tester.WaitForEvent(async () => + { + txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, + tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(1m) + fee); + }, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid); + + await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings); + settings = + Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")); + Assert.True( settings.ProcessNewPayoutsInstantly); + + var pluginHookService = tester.PayTester.GetService(); + var beforeHookTcs = new TaskCompletionSource(); + var afterHookTcs = new TaskCompletionSource(); + pluginHookService.ActionInvoked += (sender, tuple) => + { + switch (tuple.hook) + { + case "before-automated-payout-processing": + beforeHookTcs.TrySetResult(); + break; + case "after-automated-payout-processing": + afterHookTcs.TrySetResult(); + break; + } + }; + var payoutThatShouldBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest() + { + PullPaymentId = pullPayment.Id, + Amount = 0.5m, + Approved = true, + PaymentMethod = "BTC", + Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, + }); + await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + payouts = await adminClient.GetStorePayouts(admin.StoreId); + Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id)); + + beforeHookTcs = new TaskCompletionSource(); + afterHookTcs = new TaskCompletionSource(); + //let's test the threshold limiter + settings.Threshold = 0.5m; + await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings); + + //quick test: when updating processor, it processes instantly + await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + settings = + Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")); + Assert.Equal(0.5m, settings.Threshold); + + //create a payout that should not be processed straight away due to threshold + + beforeHookTcs = new TaskCompletionSource(); + afterHookTcs = new TaskCompletionSource(); + var payoutThatShouldNotBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest() + { + Amount = 0.1m, + Approved = true, + PaymentMethod = "BTC", + Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, + }); + + await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + payouts = await adminClient.GetStorePayouts(admin.StoreId); + Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id)); + + beforeHookTcs = new TaskCompletionSource(); + afterHookTcs = new TaskCompletionSource(); + var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest() + { + Amount = 0.3m, + Approved = true, + PaymentMethod = "BTC", + Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, + }); + + await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + payouts = await adminClient.GetStorePayouts(admin.StoreId); + Assert.Equal(2, payouts.Count(data => data.State == PayoutState.AwaitingPayment && + (data.Id == payoutThatShouldNotBeProcessedStraightAway.Id || data.Id == payoutThatShouldNotBeProcessedStraightAway2.Id))); + + beforeHookTcs = new TaskCompletionSource(); + afterHookTcs = new TaskCompletionSource(); + var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest() + { + Amount = 0.3m, + Approved = true, + PaymentMethod = "BTC", + Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, + }); + + await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + payouts = await adminClient.GetStorePayouts(admin.StoreId); + Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress)); + } [Fact(Timeout = 60 * 2 * 1000)] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs index 3a32f0a8b..70020dea1 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs @@ -53,16 +53,23 @@ namespace BTCPayServer.Controllers.Greenfield private static LightningAutomatedPayoutSettings ToModel(PayoutProcessorData data) { + var blob = data.HasTypedBlob().GetBlob(); return new LightningAutomatedPayoutSettings() { PaymentMethod = data.PaymentMethod, - IntervalSeconds = data.HasTypedBlob().GetBlob()!.Interval + IntervalSeconds = blob.Interval, + CancelPayoutAfterFailures = blob.CancelPayoutAfterFailures, + ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly }; } - private static AutomatedPayoutBlob FromModel(LightningAutomatedPayoutSettings data) + private static LightningAutomatedPayoutBlob FromModel(LightningAutomatedPayoutSettings data) { - return new AutomatedPayoutBlob() { Interval = data.IntervalSeconds }; + return new LightningAutomatedPayoutBlob() { + Interval = data.IntervalSeconds, + CancelPayoutAfterFailures = data.CancelPayoutAfterFailures, + ProcessNewPayoutsInstantly = data.ProcessNewPayoutsInstantly + }; } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] @@ -84,7 +91,7 @@ namespace BTCPayServer.Controllers.Greenfield })) .FirstOrDefault(); activeProcessor ??= new PayoutProcessorData(); - activeProcessor.HasTypedBlob().SetBlob(FromModel(request)); + activeProcessor.HasTypedBlob().SetBlob(FromModel(request)); activeProcessor.StoreId = storeId; activeProcessor.PaymentMethod = paymentMethod; activeProcessor.Processor = LightningAutomatedPayoutSenderFactory.ProcessorName; diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs index 16030ca1d..fd79909d6 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedOnChainPayoutProcessorsController.cs @@ -59,7 +59,9 @@ namespace BTCPayServer.Controllers.Greenfield { FeeBlockTarget = blob.FeeTargetBlock, PaymentMethod = data.PaymentMethod, - IntervalSeconds = blob.Interval + IntervalSeconds = blob.Interval, + Threshold = blob.Threshold, + ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly }; } @@ -68,7 +70,9 @@ namespace BTCPayServer.Controllers.Greenfield return new OnChainAutomatedPayoutBlob() { FeeTargetBlock = data.FeeBlockTarget ?? 1, - Interval = data.IntervalSeconds + Interval = data.IntervalSeconds, + Threshold = data.Threshold, + ProcessNewPayoutsInstantly = data.ProcessNewPayoutsInstantly }; } diff --git a/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs b/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs index 68dc81176..0e2e9aef9 100644 --- a/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs +++ b/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs @@ -277,6 +277,18 @@ namespace BTCPayServer.Data.Payouts.LightningLike }; } + if (bolt11PaymentRequest.ExpiryDate < DateTimeOffset.Now) + { + payoutData.State = PayoutState.Cancelled; + return new ResultVM + { + PayoutId = payoutData.Id, + Result = PayResult.Error, + Message = $"The BOLT11 invoice expiry date ({bolt11PaymentRequest.ExpiryDate}) has expired", + Destination = payoutBlob.Destination + }; + } + var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() }; try { diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs index 3f94e4ad3..ff2757c28 100644 --- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs +++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs @@ -389,7 +389,7 @@ namespace BTCPayServer.HostedServices { try { - using var ctx = _dbContextFactory.CreateContext(); + await using var ctx = _dbContextFactory.CreateContext(); var payout = await ctx.Payouts.Include(p => p.PullPaymentData).Where(p => p.Id == req.PayoutId) .FirstOrDefaultAsync(); if (payout is null) @@ -440,6 +440,7 @@ namespace BTCPayServer.HostedServices payout.SetBlob(payoutBlob, _jsonSerializerSettings); await ctx.SaveChangesAsync(); + _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Approved, payout)); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount)); } catch (Exception ex) @@ -604,6 +605,8 @@ namespace BTCPayServer.HostedServices { await payoutHandler.TrackClaim(req.ClaimRequest, payout); await ctx.SaveChangesAsync(); + var response = new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout); + _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Created, payout)); if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true)) { payout.StoreData = await ctx.Stores.FindAsync(payout.StoreDataId); @@ -628,7 +631,7 @@ namespace BTCPayServer.HostedServices } } - req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout)); + req.Completion.TrySetResult(response); await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId), new PayoutNotification() { @@ -888,4 +891,14 @@ namespace BTCPayServer.HostedServices public string StoreId { get; set; } public bool? PreApprove { get; set; } } + + public record PayoutEvent(PayoutEvent.PayoutEventType Type,PayoutData Payout) + { + public enum PayoutEventType + { + Created, + Approved + } + + } } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index a69bcdeb0..f3112e32a 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -278,7 +278,8 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.AddTransient(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(provider => provider.GetService()); services.TryAddTransient(); services.TryAddTransient(); services.TryAddSingleton(o => diff --git a/BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs b/BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs new file mode 100644 index 000000000..5fa7b4eba --- /dev/null +++ b/BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using BTCPayServer.Data; + +namespace BTCPayServer.PayoutProcessors; + +public record AfterPayoutActionData(StoreData Store, PayoutProcessorData ProcessorData, + IEnumerable Payouts); diff --git a/BTCPayServer/PayoutProcessors/AfterPayoutFilterData.cs b/BTCPayServer/PayoutProcessors/AfterPayoutFilterData.cs deleted file mode 100644 index e014590d5..000000000 --- a/BTCPayServer/PayoutProcessors/AfterPayoutFilterData.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using BTCPayServer.Data; -using BTCPayServer.Payments; - -namespace BTCPayServer.PayoutProcessors; - -public class AfterPayoutFilterData -{ - private readonly StoreData _store; - private readonly ISupportedPaymentMethod _paymentMethod; - private readonly List _payoutDatas; - - public AfterPayoutFilterData(StoreData store, ISupportedPaymentMethod paymentMethod, List payoutDatas) - { - _store = store; - _paymentMethod = paymentMethod; - _payoutDatas = payoutDatas; - } -} diff --git a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs index d715713b7..719a2de84 100644 --- a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client.Models; @@ -20,89 +21,109 @@ namespace BTCPayServer.PayoutProcessors; public class AutomatedPayoutConstants { - public const double MinIntervalMinutes = 10.0; - public const double MaxIntervalMinutes = 60.0; + public const double MinIntervalMinutes = 1.0; + public const double MaxIntervalMinutes = 24 * 60; //1 day 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"); + modelState.AddModelError(parameterName, $"The minimum interval is {MinIntervalMinutes * 60} seconds"); } if (timeSpan > TimeSpan.FromMinutes(AutomatedPayoutConstants.MaxIntervalMinutes)) { - modelState.AddModelError(parameterName, $"The maximum interval is {AutomatedPayoutConstants.MaxIntervalMinutes * 60} seconds"); + modelState.AddModelError(parameterName, $"The maximum interval is {MaxIntervalMinutes * 60} seconds"); } } } public abstract class BaseAutomatedPayoutProcessor : BaseAsyncService where T : AutomatedPayoutBlob { protected readonly StoreRepository _storeRepository; - protected readonly PayoutProcessorData _PayoutProcesserSettings; + protected readonly PayoutProcessorData PayoutProcessorSettings; protected readonly ApplicationDbContextFactory _applicationDbContextFactory; - private readonly PullPaymentHostedService _pullPaymentHostedService; protected readonly BTCPayNetworkProvider _btcPayNetworkProvider; protected readonly PaymentMethodId PaymentMethodId; private readonly IPluginHookService _pluginHookService; + private readonly EventAggregator _eventAggregator; protected BaseAutomatedPayoutProcessor( ILoggerFactory logger, StoreRepository storeRepository, - PayoutProcessorData payoutProcesserSettings, + PayoutProcessorData payoutProcessorSettings, ApplicationDbContextFactory applicationDbContextFactory, - PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkProvider btcPayNetworkProvider, - IPluginHookService pluginHookService) : base(logger.CreateLogger($"{payoutProcesserSettings.Processor}:{payoutProcesserSettings.StoreId}:{payoutProcesserSettings.PaymentMethod}")) + IPluginHookService pluginHookService, + EventAggregator eventAggregator) : base(logger.CreateLogger($"{payoutProcessorSettings.Processor}:{payoutProcessorSettings.StoreId}:{payoutProcessorSettings.PaymentMethod}")) { _storeRepository = storeRepository; - _PayoutProcesserSettings = payoutProcesserSettings; - PaymentMethodId = _PayoutProcesserSettings.GetPaymentMethodId(); + PayoutProcessorSettings = payoutProcessorSettings; + PaymentMethodId = PayoutProcessorSettings.GetPaymentMethodId(); _applicationDbContextFactory = applicationDbContextFactory; - _pullPaymentHostedService = pullPaymentHostedService; _btcPayNetworkProvider = btcPayNetworkProvider; _pluginHookService = pluginHookService; + _eventAggregator = eventAggregator; this.NoLogsOnExit = true; } internal override Task[] InitializeTasks() { + _subscription = _eventAggregator.SubscribeAsync(OnPayoutEvent); return new[] { CreateLoopTask(Act) }; } + + + public override Task StopAsync(CancellationToken cancellationToken) + { + _subscription.Dispose(); + return base.StopAsync(cancellationToken); + } + + private Task OnPayoutEvent(PayoutEvent arg) + { + if (arg.Type == PayoutEvent.PayoutEventType.Approved && + PayoutProcessorSettings.StoreId == arg.Payout.StoreDataId && + arg.Payout.GetPaymentMethodId() == PaymentMethodId && + GetBlob(PayoutProcessorSettings).ProcessNewPayoutsInstantly) + { + SkipInterval(); + } + return Task.CompletedTask; + } protected abstract Task Process(ISupportedPaymentMethod paymentMethod, List payouts); private async Task Act() { - var store = await _storeRepository.FindStore(_PayoutProcesserSettings.StoreId); + _timerCTs = null; + var store = await _storeRepository.FindStore(PayoutProcessorSettings.StoreId); var paymentMethod = store?.GetEnabledPaymentMethods(_btcPayNetworkProvider)?.FirstOrDefault( method => method.PaymentId == PaymentMethodId); - var blob = GetBlob(_PayoutProcesserSettings); + var blob = GetBlob(PayoutProcessorSettings); if (paymentMethod is not null) { - - // Allow plugins to do something before the automatic payouts are executed - await _pluginHookService.ApplyFilter("before-automated-payout-processing", - new BeforePayoutFilterData(store, paymentMethod)); - await using var context = _applicationDbContextFactory.CreateContext(); var payouts = await PullPaymentHostedService.GetPayouts( new PullPaymentHostedService.PayoutQuery() { States = new[] { PayoutState.AwaitingPayment }, - PaymentMethods = new[] { _PayoutProcesserSettings.PaymentMethod }, - Stores = new[] { _PayoutProcesserSettings.StoreId } + PaymentMethods = new[] { PayoutProcessorSettings.PaymentMethod }, + Stores = new[] {PayoutProcessorSettings.StoreId} }, context, CancellationToken); + + await _pluginHookService.ApplyAction("before-automated-payout-processing", + new BeforePayoutActionData(store, PayoutProcessorSettings, payouts)); if (payouts.Any()) { - Logs.PayServer.LogInformation($"{payouts.Count} found to process. Starting (and after will sleep for {blob.Interval})"); + Logs.PayServer.LogInformation( + $"{payouts.Count} found to process. Starting (and after will sleep for {blob.Interval})"); await Process(paymentMethod, payouts); await context.SaveChangesAsync(); - - // Allow plugins do to something after automatic payout processing - await _pluginHookService.ApplyFilter("after-automated-payout-processing", - new AfterPayoutFilterData(store, paymentMethod, payouts)); } + + // Allow plugins do to something after automatic payout processing + await _pluginHookService.ApplyAction("after-automated-payout-processing", + new AfterPayoutActionData(store, PayoutProcessorSettings, payouts)); } // Clip interval @@ -110,9 +131,34 @@ public abstract class BaseAutomatedPayoutProcessor : BaseAsyncService where T blob.Interval = TimeSpan.FromMinutes(AutomatedPayoutConstants.MinIntervalMinutes); if (blob.Interval > TimeSpan.FromMinutes(AutomatedPayoutConstants.MaxIntervalMinutes)) blob.Interval = TimeSpan.FromMinutes(AutomatedPayoutConstants.MaxIntervalMinutes); - await Task.Delay(blob.Interval, CancellationToken); + + _timerCTs??= new CancellationTokenSource(); + try + { + var cts= CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, _timerCTs.Token); + await Task.Delay(blob.Interval, cts.Token); + cts.Dispose(); + } + catch (TaskCanceledException) + { + } } + private CancellationTokenSource _timerCTs; + private IEventAggregatorSubscription _subscription; + + private readonly object _intervalLock = new object(); + + public void SkipInterval() + { + lock (_intervalLock) + { + _timerCTs ??= new CancellationTokenSource(); + _timerCTs?.Cancel(); + } + } + + public static T GetBlob(PayoutProcessorData payoutProcesserSettings) { return payoutProcesserSettings.HasTypedBlob().GetBlob(); diff --git a/BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs b/BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs new file mode 100644 index 000000000..8135835fa --- /dev/null +++ b/BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; +using BTCPayServer.Data; + +namespace BTCPayServer.PayoutProcessors; + +public record BeforePayoutActionData(StoreData Store, PayoutProcessorData ProcessorData, IEnumerable Payouts); diff --git a/BTCPayServer/PayoutProcessors/BeforePayoutFilterData.cs b/BTCPayServer/PayoutProcessors/BeforePayoutFilterData.cs deleted file mode 100644 index 7b6df23e7..000000000 --- a/BTCPayServer/PayoutProcessors/BeforePayoutFilterData.cs +++ /dev/null @@ -1,16 +0,0 @@ -using BTCPayServer.Data; -using BTCPayServer.Payments; - -namespace BTCPayServer.PayoutProcessors; - -public class BeforePayoutFilterData -{ - private readonly StoreData _store; - private readonly ISupportedPaymentMethod _paymentMethod; - - public BeforePayoutFilterData(StoreData store, ISupportedPaymentMethod paymentMethod) - { - _store = store; - _paymentMethod = paymentMethod; - } -} diff --git a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutBlob.cs b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutBlob.cs new file mode 100644 index 000000000..04740f795 --- /dev/null +++ b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutBlob.cs @@ -0,0 +1,8 @@ +using BTCPayServer.Data; + +namespace BTCPayServer.PayoutProcessors.Lightning; + +public class LightningAutomatedPayoutBlob : AutomatedPayoutBlob +{ + public int? CancelPayoutAfterFailures { get; set; } = null; +} diff --git a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs index 1cd3f2d1a..d46583c8f 100644 --- a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -14,16 +15,15 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services; using BTCPayServer.Services.Stores; -using LNURL; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using NBitcoin; using PayoutData = BTCPayServer.Data.PayoutData; using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData; namespace BTCPayServer.PayoutProcessors.Lightning; -public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor +public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor { private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly LightningClientFactoryService _lightningClientFactoryService; @@ -31,6 +31,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor _options; private readonly LightningLikePayoutHandler _payoutHandler; private readonly BTCPayNetwork _network; + private readonly ConcurrentDictionary _failedPayoutCounter = new(); public LightningAutomatedPayoutProcessor( BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, @@ -38,11 +39,13 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor payoutHandlers, UserService userService, ILoggerFactory logger, IOptions options, - StoreRepository storeRepository, PayoutProcessorData payoutProcesserSettings, - ApplicationDbContextFactory applicationDbContextFactory, PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkProvider btcPayNetworkProvider, - IPluginHookService pluginHookService) : - base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, pullPaymentHostedService, - btcPayNetworkProvider, pluginHookService) + StoreRepository storeRepository, PayoutProcessorData payoutProcessorSettings, + ApplicationDbContextFactory applicationDbContextFactory, + BTCPayNetworkProvider btcPayNetworkProvider, + IPluginHookService pluginHookService, + EventAggregator eventAggregator) : + base(logger, storeRepository, payoutProcessorSettings, applicationDbContextFactory, + btcPayNetworkProvider, pluginHookService, eventAggregator) { _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _lightningClientFactoryService = lightningClientFactoryService; @@ -50,15 +53,16 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor(_PayoutProcesserSettings.GetPaymentMethodId().CryptoCode); + _network = _btcPayNetworkProvider.GetNetwork(PayoutProcessorSettings.GetPaymentMethodId().CryptoCode); } protected override async Task Process(ISupportedPaymentMethod paymentMethod, List payouts) { + var processorBlob = GetBlob(PayoutProcessorSettings); var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod; if (lightningSupportedPaymentMethod.IsInternalNode && - !(await Task.WhenAll((await _storeRepository.GetStoreUsers(_PayoutProcesserSettings.StoreId)) - .Where(user => user.StoreRole.ToPermissionSet( _PayoutProcesserSettings.StoreId).Contains(Policies.CanModifyStoreSettings, _PayoutProcesserSettings.StoreId)).Select(user => user.Id) + !(await Task.WhenAll((await _storeRepository.GetStoreUsers(PayoutProcessorSettings.StoreId)) + .Where(user => user.StoreRole.ToPermissionSet( PayoutProcessorSettings.StoreId).Contains(Policies.CanModifyStoreSettings, PayoutProcessorSettings.StoreId)).Select(user => user.Id) .Select(s => _userService.IsAdminUser(s)))).Any(b => b)) { return; @@ -70,6 +74,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor= processorBlob.CancelPayoutAfterFailures) + { + payoutData.State = PayoutState.Cancelled; + Logs.PayServer.LogError($"Payout {payoutData.Id} has failed {counter} times, cancelling it"); + } + else + { + _failedPayoutCounter.AddOrReplace(payoutData.Id, counter); + } + } + if (payoutData.State == PayoutState.Cancelled) + { + _failedPayoutCounter.TryRemove(payoutData.Id, out _); } } } diff --git a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutSenderFactory.cs b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutSenderFactory.cs index 244ba1bff..fe368e166 100644 --- a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutSenderFactory.cs +++ b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutSenderFactory.cs @@ -17,6 +17,7 @@ public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory private readonly IServiceProvider _serviceProvider; private readonly LinkGenerator _linkGenerator; + public LightningAutomatedPayoutSenderFactory(BTCPayNetworkProvider btcPayNetworkProvider, IServiceProvider serviceProvider, LinkGenerator linkGenerator) { _btcPayNetworkProvider = btcPayNetworkProvider; diff --git a/BTCPayServer/PayoutProcessors/Lightning/UILightningAutomatedPayoutProcessorsController.cs b/BTCPayServer/PayoutProcessors/Lightning/UILightningAutomatedPayoutProcessorsController.cs index 99730969b..3ace5a038 100644 --- a/BTCPayServer/PayoutProcessors/Lightning/UILightningAutomatedPayoutProcessorsController.cs +++ b/BTCPayServer/PayoutProcessors/Lightning/UILightningAutomatedPayoutProcessorsController.cs @@ -59,7 +59,7 @@ public class UILightningAutomatedPayoutProcessorsController : Controller })) .FirstOrDefault(); - return View(new LightningTransferViewModel(activeProcessor is null ? new AutomatedPayoutBlob() : OnChainAutomatedPayoutProcessor.GetBlob(activeProcessor))); + return View(new LightningTransferViewModel(activeProcessor is null ? new LightningAutomatedPayoutBlob() : LightningAutomatedPayoutProcessor.GetBlob(activeProcessor))); } [HttpPost("~/stores/{storeId}/payout-processors/lightning-automated/{cryptocode}")] @@ -92,7 +92,7 @@ public class UILightningAutomatedPayoutProcessorsController : Controller })) .FirstOrDefault(); activeProcessor ??= new PayoutProcessorData(); - activeProcessor.HasTypedBlob().SetBlob(automatedTransferBlob.ToBlob()); + activeProcessor.HasTypedBlob().SetBlob(automatedTransferBlob.ToBlob()); activeProcessor.StoreId = storeId; activeProcessor.PaymentMethod = new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(); activeProcessor.Processor = _lightningAutomatedPayoutSenderFactory.Processor; @@ -119,16 +119,26 @@ public class UILightningAutomatedPayoutProcessorsController : Controller } - public LightningTransferViewModel(AutomatedPayoutBlob blob) + public LightningTransferViewModel(LightningAutomatedPayoutBlob blob) { IntervalMinutes = blob.Interval.TotalMinutes; + CancelPayoutAfterFailures = blob.CancelPayoutAfterFailures; + ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly; } + + public bool ProcessNewPayoutsInstantly { get; set; } + + public int? CancelPayoutAfterFailures { get; set; } + [Range(AutomatedPayoutConstants.MinIntervalMinutes, AutomatedPayoutConstants.MaxIntervalMinutes)] public double IntervalMinutes { get; set; } - public AutomatedPayoutBlob ToBlob() + public LightningAutomatedPayoutBlob ToBlob() { - return new AutomatedPayoutBlob { Interval = TimeSpan.FromMinutes(IntervalMinutes) }; + return new LightningAutomatedPayoutBlob { + ProcessNewPayoutsInstantly = ProcessNewPayoutsInstantly, + Interval = TimeSpan.FromMinutes(IntervalMinutes), + CancelPayoutAfterFailures = CancelPayoutAfterFailures}; } } } diff --git a/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutBlob.cs b/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutBlob.cs index 0cc7cfeb4..1c87a201a 100644 --- a/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutBlob.cs +++ b/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutBlob.cs @@ -5,4 +5,5 @@ namespace BTCPayServer.PayoutProcessors.OnChain; public class OnChainAutomatedPayoutBlob : AutomatedPayoutBlob { public int FeeTargetBlock { get; set; } = 1; + public decimal Threshold { get; set; } = 0; } diff --git a/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs index e1db3f313..4d2744695 100644 --- a/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/OnChain/OnChainAutomatedPayoutProcessor.cs @@ -12,7 +12,6 @@ using BTCPayServer.Payments; using BTCPayServer.Services; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; @@ -45,8 +44,8 @@ namespace BTCPayServer.PayoutProcessors.OnChain BTCPayNetworkProvider btcPayNetworkProvider, IPluginHookService pluginHookService, IFeeProviderFactory feeProviderFactory) : - base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, pullPaymentHostedService, - btcPayNetworkProvider, pluginHookService) + base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, + btcPayNetworkProvider, pluginHookService, eventAggregator) { _explorerClientProvider = explorerClientProvider; _btcPayWalletProvider = btcPayWalletProvider; @@ -97,13 +96,19 @@ namespace BTCPayServer.PayoutProcessors.OnChain var changeAddress = await explorerClient.GetUnusedAsync( storePaymentMethod.AccountDerivation, DerivationFeature.Change, 0, true); - var processorBlob = GetBlob(_PayoutProcesserSettings); + var processorBlob = GetBlob(PayoutProcessorSettings); + var payoutToBlobs = payouts.ToDictionary(data => data, data => data.GetBlob(_btcPayNetworkJsonSerializerSettings)); + if (payoutToBlobs.Sum(pair => pair.Value.CryptoAmount) < processorBlob.Threshold) + { + return; + } + var feeRate = await FeeProvider.GetFeeRateAsync(Math.Max(processorBlob.FeeTargetBlock, 1)); - var transfersProcessing = new List(); - foreach (var transferRequest in payouts) + var transfersProcessing = new List>(); + foreach (var transferRequest in payoutToBlobs) { - var blob = transferRequest.GetBlob(_btcPayNetworkJsonSerializerSettings); + var blob = transferRequest.Value; if (failedAmount.HasValue && blob.CryptoAmount >= failedAmount) { continue; @@ -153,10 +158,10 @@ namespace BTCPayServer.PayoutProcessors.OnChain try { var txHash = workingTx.GetHash(); - foreach (PayoutData payoutData in transfersProcessing) + foreach (var payoutData in transfersProcessing) { - payoutData.State = PayoutState.InProgress; - _bitcoinLikePayoutHandler.SetProofBlob(payoutData, + payoutData.Key.State = PayoutState.InProgress; + _bitcoinLikePayoutHandler.SetProofBlob(payoutData.Key, new PayoutTransactionOnChainBlob() { Accounted = true, @@ -175,12 +180,12 @@ namespace BTCPayServer.PayoutProcessors.OnChain { tcs.SetResult(false); } - var walletId = new WalletId(_PayoutProcesserSettings.StoreId, PaymentMethodId.CryptoCode); - foreach (PayoutData payoutData in transfersProcessing) + var walletId = new WalletId(PayoutProcessorSettings.StoreId, PaymentMethodId.CryptoCode); + foreach (var payoutData in transfersProcessing) { await WalletRepository.AddWalletTransactionAttachment(walletId, txHash, - Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id)); + Attachment.Payout(payoutData.Key.PullPaymentDataId, payoutData.Key.Id)); } await Task.WhenAny(tcs.Task, task); } diff --git a/BTCPayServer/PayoutProcessors/OnChain/UIOnChainAutomatedPayoutProcessorsController.cs b/BTCPayServer/PayoutProcessors/OnChain/UIOnChainAutomatedPayoutProcessorsController.cs index 34cf41f2e..d55a53d41 100644 --- a/BTCPayServer/PayoutProcessors/OnChain/UIOnChainAutomatedPayoutProcessorsController.cs +++ b/BTCPayServer/PayoutProcessors/OnChain/UIOnChainAutomatedPayoutProcessorsController.cs @@ -134,12 +134,17 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller public OnChainTransferViewModel(OnChainAutomatedPayoutBlob blob) { + ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly; IntervalMinutes = blob.Interval.TotalMinutes; FeeTargetBlock = blob.FeeTargetBlock; + Threshold = blob.Threshold; } + public bool ProcessNewPayoutsInstantly { get; set; } + [Range(1, 1000)] public int FeeTargetBlock { get; set; } + public decimal Threshold { get; set; } [Range(AutomatedPayoutConstants.MinIntervalMinutes, AutomatedPayoutConstants.MaxIntervalMinutes)] public double IntervalMinutes { get; set; } @@ -148,8 +153,10 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller { return new OnChainAutomatedPayoutBlob { + ProcessNewPayoutsInstantly = ProcessNewPayoutsInstantly, FeeTargetBlock = FeeTargetBlock, - Interval = TimeSpan.FromMinutes(IntervalMinutes) + Interval = TimeSpan.FromMinutes(IntervalMinutes), + Threshold = Threshold }; } } diff --git a/BTCPayServer/Plugins/PluginHookService.cs b/BTCPayServer/Plugins/PluginHookService.cs index 650be76fe..3b61fd573 100644 --- a/BTCPayServer/Plugins/PluginHookService.cs +++ b/BTCPayServer/Plugins/PluginHookService.cs @@ -24,6 +24,7 @@ namespace BTCPayServer.Plugins // Trigger simple action hook for registered plugins public async Task ApplyAction(string hook, object args) { + ActionInvoked?.Invoke(this, (hook, args)); var filters = _actions .Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList(); foreach (IPluginHookAction pluginHookFilter in filters) @@ -42,6 +43,7 @@ namespace BTCPayServer.Plugins // Trigger hook on which registered plugins can optionally return modified args or new object back public async Task ApplyFilter(string hook, object args) { + FilterInvoked?.Invoke(this, (hook, args)); var filters = _filters .Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList(); foreach (IPluginHookFilter pluginHookFilter in filters) @@ -58,5 +60,8 @@ namespace BTCPayServer.Plugins return args; } + + public event EventHandler<(string hook, object args)> ActionInvoked; + public event EventHandler<(string hook, object args)> FilterInvoked; } } diff --git a/BTCPayServer/Views/UILightningAutomatedPayoutProcessors/Configure.cshtml b/BTCPayServer/Views/UILightningAutomatedPayoutProcessors/Configure.cshtml index a4524ba3d..fca65c4e4 100644 --- a/BTCPayServer/Views/UILightningAutomatedPayoutProcessors/Configure.cshtml +++ b/BTCPayServer/Views/UILightningAutomatedPayoutProcessors/Configure.cshtml @@ -18,6 +18,11 @@
}
+
+ + + +
@@ -26,6 +31,16 @@
+
+ +
+ + attempts + +
+
If a payout fails this many times, it will be cancelled.
+
+
diff --git a/BTCPayServer/Views/UIOnChainAutomatedPayoutProcessors/Configure.cshtml b/BTCPayServer/Views/UIOnChainAutomatedPayoutProcessors/Configure.cshtml index cb7eff7cd..a776f649d 100644 --- a/BTCPayServer/Views/UIOnChainAutomatedPayoutProcessors/Configure.cshtml +++ b/BTCPayServer/Views/UIOnChainAutomatedPayoutProcessors/Configure.cshtml @@ -5,6 +5,7 @@ ViewData["NavPartialName"] = "../UIStores/_Nav"; Layout = "../Shared/_NavLayout.cshtml"; ViewData.SetActivePage(StoreNavPages.PayoutProcessors, "On-Chain Payout Processor", Context.GetStoreData().Id); + var cryptoCode = Context.GetRouteValue("cryptocode")?.ToString(); }
@@ -17,6 +18,11 @@
}
+
+ + + +
@@ -24,7 +30,7 @@ minutes
-
+
@@ -33,6 +39,15 @@
+
+ +
+ + @cryptoCode + +
+
Only process payouts when this payout sum is reached.
+
diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payout-processors.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payout-processors.json index 6ffe17a98..04485cbed 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payout-processors.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payout-processors.json @@ -582,6 +582,16 @@ "$ref": "#/components/schemas/TimeSpanSeconds" } ] + }, + "cancelPayoutAfterFailures": { + "description": "How many failures should the processor tolerate before cancelling the payout", + "type": "number", + "nullable": true + }, + "processNewPayoutsInstantly": { + "description": "Skip the interval when ane eligible payout has been approved (or created with pre-approval)", + "type": "boolean", + "default": false } } }, @@ -600,6 +610,16 @@ "$ref": "#/components/schemas/TimeSpanSeconds" } ] + }, + "cancelPayoutAfterFailures": { + "description": "How many failures should the processor tolerate before cancelling the payout", + "type": "number", + "nullable": true + }, + "processNewPayoutsInstantly": { + "description": "Skip the interval when ane eligible payout has been approved (or created with pre-approval)", + "type": "boolean", + "default": false } } }, @@ -619,6 +639,18 @@ "$ref": "#/components/schemas/TimeSpanSeconds" } ] + }, + "threshold": { + "type": "string", + "format": "decimal", + "minimum": 0, + "description": "Only process payouts when this payout sum is reached.", + "example": "0.1" + }, + "processNewPayoutsInstantly": { + "description": "Skip the interval when ane eligible payout has been approved (or created with pre-approval)", + "type": "boolean", + "default": false } } }, @@ -641,6 +673,18 @@ "$ref": "#/components/schemas/TimeSpanSeconds" } ] + }, + "threshold": { + "type": "string", + "format": "decimal", + "minimum": 0, + "description": "Only process payouts when this payout sum is reached.", + "example": "0.1" + }, + "processNewPayoutsInstantly": { + "description": "Skip the interval when ane eligible payout has been approved (or created with pre-approval)", + "type": "boolean", + "default": false } } }