diff --git a/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs b/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs index f1871aa28..3f4f9ffdf 100644 --- a/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningPendingPayoutListener.cs @@ -68,6 +68,7 @@ public class LightningPendingPayoutListener : BaseAsyncService foreach (IGrouping payoutByStore in payouts.GroupBy(data => data.StoreDataId)) { + //this should never happen if (!stores.TryGetValue(payoutByStore.Key, out var store)) { foreach (PayoutData payoutData in payoutByStore) diff --git a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs index 630d45674..2501b2d3b 100644 --- a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs @@ -87,7 +87,15 @@ public abstract class BaseAutomatedPayoutProcessor : BaseAsyncService where T return Task.CompletedTask; } - protected abstract Task Process(ISupportedPaymentMethod paymentMethod, List payouts); + protected virtual Task Process(ISupportedPaymentMethod paymentMethod, List payouts) => + throw new NotImplementedException(); + + protected virtual async Task ProcessShouldSave(ISupportedPaymentMethod paymentMethod, + List payouts) + { + await Process(paymentMethod, payouts); + return true; + } private async Task Act() { @@ -114,14 +122,16 @@ public abstract class BaseAutomatedPayoutProcessor : BaseAsyncService where T { Logs.PayServer.LogInformation( $"{payouts.Count} found to process. Starting (and after will sleep for {blob.Interval})"); - await Process(paymentMethod, payouts); - - await context.SaveChangesAsync(); - - foreach (var payoutData in payouts.Where(payoutData => payoutData.State != PayoutState.AwaitingPayment)) + if (await ProcessShouldSave(paymentMethod, payouts)) { - _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payoutData)); + await context.SaveChangesAsync(); + + foreach (var payoutData in payouts.Where(payoutData => payoutData.State != PayoutState.AwaitingPayment)) + { + _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payoutData)); + } } + } // Allow plugins do to something after automatic payout processing diff --git a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs index 4bdf41fe3..54a507ff5 100644 --- a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs @@ -15,9 +15,11 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services; using BTCPayServer.Services.Stores; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NBitcoin; +using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; using PayoutData = BTCPayServer.Data.PayoutData; using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData; @@ -29,9 +31,9 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor
  • _options; + private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly LightningLikePayoutHandler _payoutHandler; private readonly BTCPayNetwork _network; - private readonly ConcurrentDictionary _failedPayoutCounter = new(); public LightningAutomatedPayoutProcessor( BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, @@ -40,10 +42,11 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor
  • options, StoreRepository storeRepository, PayoutProcessorData payoutProcessorSettings, - ApplicationDbContextFactory applicationDbContextFactory, + ApplicationDbContextFactory applicationDbContextFactory, BTCPayNetworkProvider btcPayNetworkProvider, IPluginHookService pluginHookService, - EventAggregator eventAggregator) : + EventAggregator eventAggregator, + PullPaymentHostedService pullPaymentHostedService) : base(logger, storeRepository, payoutProcessorSettings, applicationDbContextFactory, btcPayNetworkProvider, pluginHookService, eventAggregator) { @@ -51,86 +54,91 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor
  • (PayoutProcessorSettings.GetPaymentMethodId().CryptoCode); + _network = _btcPayNetworkProvider.GetNetwork(PayoutProcessorSettings.GetPaymentMethodId() + .CryptoCode); } - protected override async Task Process(ISupportedPaymentMethod paymentMethod, List payouts) + private async Task HandlePayout(PayoutData payoutData, ILightningClient lightningClient) + { + if (payoutData.State != PayoutState.AwaitingPayment) + return; + var res = await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest() + { + State = PayoutState.InProgress, PayoutId = payoutData.Id, Proof = null + }); + if (res != MarkPayoutRequest.PayoutPaidResult.Ok) + { + return; + } + + var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings); + var claim = await _payoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination, CancellationToken); + try + { + switch (claim.destination) + { + case LNURLPayClaimDestinaton lnurlPayClaimDestinaton: + var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData, + _payoutHandler, blob, + lnurlPayClaimDestinaton, _network.NBitcoinNetwork, CancellationToken); + if (lnurlResult.Item2 is null) + { + await TrypayBolt(lightningClient, blob, payoutData, + lnurlResult.Item1); + } + break; + case BoltInvoiceClaimDestination item1: + await TrypayBolt(lightningClient, blob, payoutData, item1.PaymentRequest); + break; + } + } + catch (Exception e) + { + Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}"); + } + + if (payoutData.State != PayoutState.InProgress || payoutData.Proof is not null) + { + await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest() + { + State = payoutData.State, PayoutId = payoutData.Id, Proof = payoutData.GetProofBlobJson() + }); + } + } + + protected override async TaskProcessShouldSave(ISupportedPaymentMethod paymentMethod, List payouts) { var processorBlob = GetBlob(PayoutProcessorSettings); var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod; if (lightningSupportedPaymentMethod.IsInternalNode && !(await Task.WhenAll((await _storeRepository.GetStoreUsers(PayoutProcessorSettings.StoreId)) - .Where(user => user.StoreRole.ToPermissionSet( PayoutProcessorSettings.StoreId).Contains(Policies.CanModifyStoreSettings, PayoutProcessorSettings.StoreId)).Select(user => user.Id) + .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; + return false; } + var client = lightningSupportedPaymentMethod.CreateLightningClient(_network, _options.Value, _lightningClientFactoryService); + await Task.WhenAll(payouts.Select(data => HandlePayout(data, client))); - foreach (var payoutData in payouts) - { - var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings); - var failed = false; - var claim = await _payoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination, CancellationToken); - try - { - switch (claim.destination) - { - case LNURLPayClaimDestinaton lnurlPayClaimDestinaton: - var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData, - _payoutHandler, blob, - lnurlPayClaimDestinaton, _network.NBitcoinNetwork, CancellationToken); - if (lnurlResult.Item2 is not null) - { - continue; - } - failed = !await TrypayBolt(client, blob, payoutData, - lnurlResult.Item1); - break; - case BoltInvoiceClaimDestination item1: - 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 _); - } - } + //we return false because this processor handles db updates on its own + return false; } //we group per store and init the transfers by each async Task TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest) { - return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData, bolt11PaymentRequest, - payoutData.GetPaymentMethodId(), CancellationToken)).Result == PayResult.Ok; + return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData, + bolt11PaymentRequest, + payoutData.GetPaymentMethodId(), CancellationToken)).Result is PayResult.Ok ; } }