mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-11 01:35:22 +01:00
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 <nicolas.dorier@gmail.com>
This commit is contained in:
parent
b1c81b696f
commit
4063a5aaee
26 changed files with 441 additions and 107 deletions
|
@ -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<object> ApplyFilter(string hook, object args);
|
||||
|
||||
event EventHandler<(string hook, object args)> ActionInvoked;
|
||||
event EventHandler<(string hook, object args)> FilterInvoked;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
uint256 txid = null;
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(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<PayoutProcessorService>().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<NewOnChainTransactionEvent>(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<IPluginHookService>();
|
||||
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)]
|
||||
|
|
|
@ -53,16 +53,23 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
|
||||
private static LightningAutomatedPayoutSettings ToModel(PayoutProcessorData data)
|
||||
{
|
||||
var blob = data.HasTypedBlob<LightningAutomatedPayoutBlob>().GetBlob();
|
||||
return new LightningAutomatedPayoutSettings()
|
||||
{
|
||||
PaymentMethod = data.PaymentMethod,
|
||||
IntervalSeconds = data.HasTypedBlob<AutomatedPayoutBlob>().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<AutomatedPayoutBlob>().SetBlob(FromModel(request));
|
||||
activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(FromModel(request));
|
||||
activeProcessor.StoreId = storeId;
|
||||
activeProcessor.PaymentMethod = paymentMethod;
|
||||
activeProcessor.Processor = LightningAutomatedPayoutSenderFactory.ProcessorName;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -278,7 +278,8 @@ namespace BTCPayServer.Hosting
|
|||
|
||||
services.TryAddSingleton<AppService>();
|
||||
services.AddTransient<PluginService>();
|
||||
services.AddSingleton<IPluginHookService, PluginHookService>();
|
||||
services.AddSingleton<PluginHookService>();
|
||||
services.AddSingleton<IPluginHookService, PluginHookService>(provider => provider.GetService<PluginHookService>());
|
||||
services.TryAddTransient<Safe>();
|
||||
services.TryAddTransient<DisplayFormatter>();
|
||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||
|
|
7
BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs
Normal file
7
BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
public record AfterPayoutActionData(StoreData Store, PayoutProcessorData ProcessorData,
|
||||
IEnumerable<PayoutData> Payouts);
|
|
@ -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<PayoutData> _payoutDatas;
|
||||
|
||||
public AfterPayoutFilterData(StoreData store, ISupportedPaymentMethod paymentMethod, List<PayoutData> payoutDatas)
|
||||
{
|
||||
_store = store;
|
||||
_paymentMethod = paymentMethod;
|
||||
_payoutDatas = payoutDatas;
|
||||
}
|
||||
}
|
|
@ -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<T> : 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<PayoutEvent>(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<PayoutData> 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<T> : 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<T>().GetBlob();
|
||||
|
|
6
BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs
Normal file
6
BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
public record BeforePayoutActionData(StoreData Store, PayoutProcessorData ProcessorData, IEnumerable<PayoutData> Payouts);
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors.Lightning;
|
||||
|
||||
public class LightningAutomatedPayoutBlob : AutomatedPayoutBlob
|
||||
{
|
||||
public int? CancelPayoutAfterFailures { get; set; } = null;
|
||||
}
|
|
@ -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<AutomatedPayoutBlob>
|
||||
public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<LightningAutomatedPayoutBlob>
|
||||
{
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly LightningClientFactoryService _lightningClientFactoryService;
|
||||
|
@ -31,6 +31,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
private readonly IOptions<LightningNetworkOptions> _options;
|
||||
private readonly LightningLikePayoutHandler _payoutHandler;
|
||||
private readonly BTCPayNetwork _network;
|
||||
private readonly ConcurrentDictionary<string, int> _failedPayoutCounter = new();
|
||||
|
||||
public LightningAutomatedPayoutProcessor(
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
|
@ -38,11 +39,13 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
UserService userService,
|
||||
ILoggerFactory logger, IOptions<LightningNetworkOptions> 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<Au
|
|||
_options = options;
|
||||
_payoutHandler = (LightningLikePayoutHandler)payoutHandlers.FindPayoutHandler(PaymentMethodId);
|
||||
|
||||
_network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(_PayoutProcesserSettings.GetPaymentMethodId().CryptoCode);
|
||||
_network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(PayoutProcessorSettings.GetPaymentMethodId().CryptoCode);
|
||||
}
|
||||
|
||||
protected override async Task Process(ISupportedPaymentMethod paymentMethod, List<PayoutData> 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<Au
|
|||
foreach (var payoutData in payouts)
|
||||
{
|
||||
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||
var failed = false;
|
||||
var claim = await _payoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination, CancellationToken);
|
||||
try
|
||||
{
|
||||
|
@ -83,17 +88,40 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||
{
|
||||
continue;
|
||||
}
|
||||
await TrypayBolt(client, blob, payoutData,
|
||||
failed = await TrypayBolt(client, blob, payoutData,
|
||||
lnurlResult.Item1);
|
||||
break;
|
||||
case BoltInvoiceClaimDestination item1:
|
||||
await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
|
||||
failed = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}");
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (failed && processorBlob.CancelPayoutAfterFailures is not null)
|
||||
{
|
||||
if (!_failedPayoutCounter.TryGetValue(payoutData.Id, out int counter))
|
||||
{
|
||||
counter = 0;
|
||||
}
|
||||
counter++;
|
||||
if(counter >= 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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<AutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob());
|
||||
activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,4 +5,5 @@ namespace BTCPayServer.PayoutProcessors.OnChain;
|
|||
public class OnChainAutomatedPayoutBlob : AutomatedPayoutBlob
|
||||
{
|
||||
public int FeeTargetBlock { get; set; } = 1;
|
||||
public decimal Threshold { get; set; } = 0;
|
||||
}
|
||||
|
|
|
@ -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<PayoutData>();
|
||||
foreach (var transferRequest in payouts)
|
||||
var transfersProcessing = new List<KeyValuePair<PayoutData, PayoutBlob>>();
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<object> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
}
|
||||
<form method="post">
|
||||
<div class="form-check">
|
||||
<input asp-for="ProcessNewPayoutsInstantly" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="ProcessNewPayoutsInstantly" class="form-check-label">Process approved payouts instantly</label>
|
||||
<span asp-validation-for="ProcessNewPayoutsInstantly" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="IntervalMinutes" class="form-label" data-required>Interval</label>
|
||||
<div class="input-group">
|
||||
|
@ -26,6 +31,16 @@
|
|||
<span asp-validation-for="IntervalMinutes" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CancelPayoutAfterFailures" class="form-label">Max Payout Failure Attempts</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="CancelPayoutAfterFailures" min="1" class="form-control" inputmode="numeric" style="max-width:10ch;">
|
||||
<span class="input-group-text">attempts</span>
|
||||
<span asp-validation-for="IntervalMinutes" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-text">If a payout fails this many times, it will be cancelled.</div>
|
||||
</div>
|
||||
|
||||
<button name="command" type="submit" class="btn btn-primary mt-2" value="Save" id="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
|
@ -17,6 +18,11 @@
|
|||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
}
|
||||
<form method="post">
|
||||
<div class="form-check">
|
||||
<input asp-for="ProcessNewPayoutsInstantly" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="ProcessNewPayoutsInstantly" class="form-check-label">Process approved payouts instantly</label>
|
||||
<span asp-validation-for="ProcessNewPayoutsInstantly" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="IntervalMinutes" class="form-label" data-required>Interval</label>
|
||||
<div class="input-group">
|
||||
|
@ -24,7 +30,7 @@
|
|||
<span class="input-group-text">minutes</span>
|
||||
<span asp-validation-for="IntervalMinutes" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="FeeTargetBlock" class="form-label" data-required>Fee block target</label>
|
||||
<div class="input-group">
|
||||
|
@ -33,6 +39,15 @@
|
|||
<span asp-validation-for="FeeTargetBlock" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Threshold" class="form-label" data-required>Threshold</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="Threshold" class="form-control" min="0" inputmode="numeric" style="max-width:10ch;">
|
||||
<span class="input-group-text">@cryptoCode</span>
|
||||
<span asp-validation-for="FeeTargetBlock" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-text">Only process payouts when this payout sum is reached.</div>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary mt-2" value="Save" id="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue