diff --git a/BTCPayServer.Client/BTCPayServerClient.StorePayoutProcessors.cs b/BTCPayServer.Client/BTCPayServerClient.StorePayoutProcessors.cs index 175f717b2..663e344f8 100644 --- a/BTCPayServer.Client/BTCPayServerClient.StorePayoutProcessors.cs +++ b/BTCPayServer.Client/BTCPayServerClient.StorePayoutProcessors.cs @@ -19,14 +19,14 @@ public partial class BTCPayServerClient await SendHttpRequest($"api/v1/stores/{storeId}/payout-processors/{processor}/{paymentMethod}", null, HttpMethod.Delete, token); } - public virtual async Task> GetStoreLightningAutomatedPayoutProcessors(string storeId, string? paymentMethod = null, CancellationToken token = default) + public virtual async Task> GetStoreLightningAutomatedPayoutProcessors(string storeId, string? payoutMethodId = null, CancellationToken token = default) { - return await SendHttpRequest>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory{(paymentMethod is null ? string.Empty : $"/{paymentMethod}")}", null, HttpMethod.Get, token); + return await SendHttpRequest>($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory{(payoutMethodId is null ? string.Empty : $"/{payoutMethodId}")}", null, HttpMethod.Get, token); } - public virtual async Task UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string paymentMethod, LightningAutomatedPayoutSettings request, CancellationToken token = default) + public virtual async Task UpdateStoreLightningAutomatedPayoutProcessors(string storeId, string payoutMethodId, LightningAutomatedPayoutSettings request, CancellationToken token = default) { - return await SendHttpRequest($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}", request, HttpMethod.Put, token); + return await SendHttpRequest($"api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{payoutMethodId}", request, HttpMethod.Put, token); } public virtual async Task UpdateStoreOnChainAutomatedPayoutProcessors(string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request, CancellationToken token = default) diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 0e673a263..044d856b9 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -183,15 +183,17 @@ retry: Driver.AssertNoError(); CreatedUser = usr; Password = "123456"; + IsAdmin = isAdmin; return usr; } string CreatedUser; public string Password { get; private set; } + public bool IsAdmin { get; private set; } public TestAccount AsTestAccount() { - return new TestAccount(Server) { RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser } }; + return new TestAccount(Server) { StoreId = StoreId, Email = CreatedUser, Password = Password, RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser }, IsAdmin = IsAdmin }; } public (string storeName, string storeId) CreateNewStore(bool keepId = true) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index aae22c9b0..d8ffb365a 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -2487,7 +2487,16 @@ namespace BTCPayServer.Tests $"LNurl w payout test {DateTime.UtcNow.Ticks}", TimeSpan.FromHours(1), CancellationToken.None)); var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null); - await TestUtils.EventuallyAsync(async () => + // Oops! + Assert.Equal("The request has been approved. The sender needs to send the payment manually. (Or activate the lightning automated payment processor)", response.Reason); + var account = await s.AsTestAccount().CreateClient(); + await account.UpdateStoreLightningAutomatedPayoutProcessors(s.StoreId, "BTC-LN", new() + { + ProcessNewPayoutsInstantly = true, + IntervalSeconds = TimeSpan.FromSeconds(60) + }); + // Now it should process to complete + await TestUtils.EventuallyAsync(async () => { s.Driver.Navigate().Refresh(); Assert.Contains(bolt2.BOLT11, s.Driver.PageSource); @@ -2577,7 +2586,9 @@ namespace BTCPayServer.Tests $"LNurl w payout test {DateTime.UtcNow.Ticks}", TimeSpan.FromHours(1), CancellationToken.None)); response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null); - TestUtils.Eventually(() => + // Nope, you need to approve the claim automatically + Assert.Equal("The request has been recorded, but still need to be approved before execution.", response.Reason); + TestUtils.Eventually(() => { s.Driver.Navigate().Refresh(); Assert.Contains(bolt2.BOLT11, s.Driver.PageSource); diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 3c73e59ba..d4c367cf2 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -19,6 +19,8 @@ using BTCPayServer.HostedServices; using BTCPayServer.Lightning; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.PayoutProcessors; +using BTCPayServer.PayoutProcessors.Lightning; using BTCPayServer.Payouts; using BTCPayServer.Plugins; using BTCPayServer.Plugins.Crowdfund; @@ -60,11 +62,13 @@ namespace BTCPayServer private readonly IPluginHookService _pluginHookService; private readonly InvoiceActivator _invoiceActivator; private readonly PaymentMethodHandlerDictionary _handlers; + private readonly PayoutProcessorService _payoutProcessorService; public UILNURLController(InvoiceRepository invoiceRepository, EventAggregator eventAggregator, PayoutMethodHandlerDictionary payoutHandlers, PaymentMethodHandlerDictionary handlers, + PayoutProcessorService payoutProcessorService, StoreRepository storeRepository, AppService appService, UIInvoiceController invoiceController, @@ -79,6 +83,7 @@ namespace BTCPayServer _eventAggregator = eventAggregator; _payoutHandlers = payoutHandlers; _handlers = handlers; + _payoutProcessorService = payoutProcessorService; _storeRepository = storeRepository; _appService = appService; _invoiceController = invoiceController; @@ -151,6 +156,7 @@ namespace BTCPayServer if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable) return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" }); + var store = await _storeRepository.FindStore(pp.StoreId); var pm = store!.GetPaymentMethodConfig(paymentMethodId, _handlers); if (pm is null) @@ -158,74 +164,83 @@ namespace BTCPayServer return NotFound(); } - var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest + var processors = await _payoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery() + { + Stores = [pp.StoreId], + PayoutMethods = [pmi], + Processors = [LightningAutomatedPayoutSenderFactory.ProcessorName] + }); + var processorBlob = processors.FirstOrDefault()?.HasTypedBlob().GetBlob(); + var instantProcessing = processorBlob?.ProcessNewPayoutsInstantly is true; + var interval = processorBlob?.Interval.TotalMinutes; + var autoApprove = pp.GetBlob().AutoApproveClaims; + var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest { Destination = new BoltInvoiceClaimDestination(pr, result), PayoutMethodId = pmi, PullPaymentId = pullPaymentId, StoreId = pp.StoreId, - Value = result.MinimumAmount.ToDecimal(unit) + Value = result.MinimumAmount.ToDecimal(unit), }); if (claimResponse.Result != ClaimRequest.ClaimResult.Ok) return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" }); - - var lightningHandler = _handlers.GetLightningHandler(network); - switch (claimResponse.PayoutData.State) + var payout = claimResponse.PayoutData; + DateTimeOffset since = DateTimeOffset.UtcNow; + while (true) { - case PayoutState.AwaitingPayment: - { - var client = - lightningHandler.CreateLightningClient(pm); - var payResult = await UILightningLikePayoutController.TrypayBolt(client, - claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings), - claimResponse.PayoutData, result, cancellationToken); - - switch (payResult.Result) - { - case PayResult.Ok: - case PayResult.Unknown: - await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest - { - PayoutId = claimResponse.PayoutData.Id, - State = claimResponse.PayoutData.State, - Proof = claimResponse.PayoutData.GetProofBlobJson() - }); - - return Ok(new LNUrlStatusResponse - { - Status = "OK", - Reason = payResult.Message - }); - case PayResult.CouldNotFindRoute: - case PayResult.Error: - default: - await _pullPaymentHostedService.Cancel( - new PullPaymentHostedService.CancelRequest(new[] - { claimResponse.PayoutData.Id }, null)); - - return BadRequest(new LNUrlStatusResponse - { - Status = "ERROR", - Reason = payResult.Message ?? payResult.Result.ToString() - }); - } - } - case PayoutState.AwaitingApproval: - return Ok(new LNUrlStatusResponse - { - Status = "OK", - Reason = - "The payment request has been recorded, but still needs to be approved before execution." - }); - case PayoutState.InProgress: - case PayoutState.Completed: - return Ok(new LNUrlStatusResponse { Status = "OK" }); - case PayoutState.Cancelled: - return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" }); + switch (payout.State) + { + case PayoutState.Completed: + return Ok(new LNUrlStatusResponse { Status = "OK" }); + case PayoutState.Cancelled: + return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" }); + case PayoutState.AwaitingApproval when !autoApprove: + return Ok(new LNUrlStatusResponse + { + Status = "OK", + Reason = + "The request has been recorded, but still need to be approved before execution." + }); + } + if (instantProcessing) + { + if (DateTimeOffset.UtcNow - since > TimeSpan.FromSeconds(10.0)) + return Ok(new LNUrlStatusResponse + { + Status = "OK", + Reason = $"The payment is in pending state and should be completed shortly. ({payout.State})" + }); + await WaitPayoutChanged(claimResponse.PayoutData.Id, cancellationToken); + payout = (await _pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery() + { + PayoutIds = [claimResponse.PayoutData.Id] + })).Single(); + } + else + { + var message = interval switch + { + double intervalMinutes => $"The payment will be sent after {intervalMinutes} minutes.", + null => "The sender needs to send the payment manually. (Or activate the lightning automated payment processor)" + }; + return Ok(new LNUrlStatusResponse + { + Status = "OK", + Reason = $"The request has been approved. {message}" + }); + } } + } - return Ok(request); + private async Task WaitPayoutChanged(string payoutId, CancellationToken cancellationToken) + { + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + // We also wait delay, in case we missed the event + var delay = Task.Delay(1000, cts.Token); + var payoutEvent = _eventAggregator.WaitNext(o => o.Payout.Id == payoutId, cts.Token); + await Task.WhenAny(delay, payoutEvent); + cts.Cancel(); } private BTCPayNetwork GetNetwork(string cryptoCode) diff --git a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs index eff8ab0ab..3332b9060 100644 --- a/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs @@ -22,6 +22,7 @@ namespace BTCPayServer.PayoutProcessors; public class AutomatedPayoutConstants { public const double MinIntervalMinutes = 1.0; + public const double DefaultIntervalMinutes = 60.0; public const double MaxIntervalMinutes = 24 * 60; //1 day public static void ValidateInterval(ModelStateDictionary modelState, TimeSpan timeSpan, string parameterName) { diff --git a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs index d604fb77c..ac57a58b7 100644 --- a/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs +++ b/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs @@ -82,10 +82,10 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor