Adapt ln payouts to handle unknown status (#4382)

Co-authored-by: d11n <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2022-12-04 13:23:59 +01:00
committed by GitHub
parent 8894d14130
commit 2fd9eb6c68
6 changed files with 51 additions and 41 deletions

View File

@@ -460,12 +460,12 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(approvePayoutRequest.RateRule), "Invalid RateRule"); ModelState.AddModelError(nameof(approvePayoutRequest.RateRule), "Invalid RateRule");
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
} }
var result = await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval() var result = (await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
{ {
PayoutId = payoutId, PayoutId = payoutId,
Revision = revision!.Value, Revision = revision!.Value,
Rate = rateResult.BidAsk.Ask Rate = rateResult.BidAsk.Ask
}); })).Result;
var errorMessage = PullPaymentHostedService.PayoutApproval.GetErrorMessage(result); var errorMessage = PullPaymentHostedService.PayoutApproval.GetErrorMessage(result);
switch (result) switch (result)
{ {

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
@@ -20,6 +21,7 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
@@ -51,6 +53,7 @@ namespace BTCPayServer
private readonly LightningAddressService _lightningAddressService; private readonly LightningAddressService _lightningAddressService;
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler; private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
public UILNURLController(InvoiceRepository invoiceRepository, public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator, EventAggregator eventAggregator,
@@ -62,7 +65,8 @@ namespace BTCPayServer
LinkGenerator linkGenerator, LinkGenerator linkGenerator,
LightningAddressService lightningAddressService, LightningAddressService lightningAddressService,
LightningLikePayoutHandler lightningLikePayoutHandler, LightningLikePayoutHandler lightningLikePayoutHandler,
PullPaymentHostedService pullPaymentHostedService) PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
{ {
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@@ -75,11 +79,12 @@ namespace BTCPayServer
_lightningAddressService = lightningAddressService; _lightningAddressService = lightningAddressService;
_lightningLikePayoutHandler = lightningLikePayoutHandler; _lightningLikePayoutHandler = lightningLikePayoutHandler;
_pullPaymentHostedService = pullPaymentHostedService; _pullPaymentHostedService = pullPaymentHostedService;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
} }
[HttpGet("withdraw/pp/{pullPaymentId}")] [HttpGet("withdraw/pp/{pullPaymentId}")]
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr) public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken)
{ {
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode); var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
@@ -155,25 +160,28 @@ namespace BTCPayServer
{ {
var client = var client =
_lightningLikePaymentHandler.CreateLightningClient(pm, network); _lightningLikePaymentHandler.CreateLightningClient(pm, network);
PayResponse payResult; var payResult = await UILightningLikePayoutController.TrypayBolt(client,
try claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
{ claimResponse.PayoutData, result, pmi, cancellationToken);
payResult = await client.Pay(pr);
}
catch (Exception e)
{
payResult = new PayResponse(PayResult.Error, e.Message);
}
switch (payResult.Result) switch (payResult.Result)
{ {
case PayResult.Ok: case PayResult.Ok:
case PayResult.Unknown:
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest() await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
{ {
PayoutId = claimResponse.PayoutData.Id, State = PayoutState.Completed PayoutId = claimResponse.PayoutData.Id,
State = claimResponse.PayoutData.State,
Proof = claimResponse.PayoutData.GetProofBlobJson()
}); });
return Ok(new LNUrlStatusResponse {Status = "OK"}); return Ok(new LNUrlStatusResponse
{
Status = "OK",
Reason = payResult.Message
});
case PayResult.CouldNotFindRoute:
case PayResult.Error:
default: default:
await _pullPaymentHostedService.Cancel( await _pullPaymentHostedService.Cancel(
new PullPaymentHostedService.CancelRequest(new string[] new PullPaymentHostedService.CancelRequest(new string[]
@@ -184,7 +192,7 @@ namespace BTCPayServer
return Ok(new LNUrlStatusResponse return Ok(new LNUrlStatusResponse
{ {
Status = "ERROR", Status = "ERROR",
Reason = $"Pr could not be paid because {payResult.ErrorDetail}" Reason = payResult.Message
}); });
} }
} }

View File

@@ -338,11 +338,11 @@ namespace BTCPayServer.Controllers
Revision = payout.GetBlob(_jsonSerializerSettings).Revision, Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
Rate = rateResult.BidAsk.Ask Rate = rateResult.BidAsk.Ask
}); });
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok) if (approveResult.Result != PullPaymentHostedService.PayoutApproval.Result.Ok)
{ {
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult), Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult.Result),
Severity = StatusMessageModel.StatusSeverity.Error Severity = StatusMessageModel.StatusSeverity.Error
}); });
failed = true; failed = true;

View File

@@ -257,9 +257,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
}); });
} }
} }
public static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(20);
public static async Task<ResultVM> TrypayBolt( public static async Task<ResultVM> TrypayBolt(
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest,
PaymentMethodId pmi, CancellationToken cancellationToken) PaymentMethodId pmi, CancellationToken cancellationToken)
@@ -281,17 +279,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() }; var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
try try
{ {
// TODO: Incorporate the changes from this PR here:
// https://github.com/btcpayserver/BTCPayServer.Lightning/pull/106
using var timeout = new CancellationTokenSource(SendTimeout);
using var c = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(), var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
new PayInvoiceParams() new PayInvoiceParams()
{ {
Amount = bolt11PaymentRequest.MinimumAmount == LightMoney.Zero Amount = bolt11PaymentRequest.MinimumAmount == LightMoney.Zero
? new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC) ? new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
: null : null
}, c.Token); }, cancellationToken);
string message = null; string message = null;
if (result.Result == PayResult.Ok) if (result.Result == PayResult.Ok)
{ {
@@ -309,6 +303,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
// ignored // ignored
} }
} }
else if(result.Result == PayResult.Unknown)
{
payoutData.State = PayoutState.InProgress;
message = "The payment has been initiated but is still in-flight.";
}
payoutData.SetProofBlob(proofBlob, null); payoutData.SetProofBlob(proofBlob, null);
return new ResultVM return new ResultVM

View File

@@ -79,10 +79,12 @@ namespace BTCPayServer.HostedServices
OldRevision OldRevision
} }
public record ApprovalResult(Result Result, decimal? CryptoAmount);
public string PayoutId { get; set; } public string PayoutId { get; set; }
public int Revision { get; set; } public int Revision { get; set; }
public decimal Rate { get; set; } public decimal Rate { get; set; }
internal TaskCompletionSource<Result> Completion { get; set; } internal TaskCompletionSource<ApprovalResult> Completion { get; set; }
public static string GetErrorMessage(Result result) public static string GetErrorMessage(Result result)
{ {
@@ -333,10 +335,10 @@ namespace BTCPayServer.HostedServices
return _rateFetcher.FetchRate(rule, cancellationToken); return _rateFetcher.FetchRate(rule, cancellationToken);
} }
public Task<PayoutApproval.Result> Approve(PayoutApproval approval) public Task<PayoutApproval.ApprovalResult> Approve(PayoutApproval approval)
{ {
approval.Completion = approval.Completion =
new TaskCompletionSource<PayoutApproval.Result>(TaskCreationOptions.RunContinuationsAsynchronously); new TaskCompletionSource<PayoutApproval.ApprovalResult>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!_Channel.Writer.TryWrite(approval)) if (!_Channel.Writer.TryWrite(approval))
throw new ObjectDisposedException(nameof(PullPaymentHostedService)); throw new ObjectDisposedException(nameof(PullPaymentHostedService));
return approval.Completion.Task; return approval.Completion.Task;
@@ -351,26 +353,26 @@ namespace BTCPayServer.HostedServices
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (payout is null) if (payout is null)
{ {
req.Completion.SetResult(PayoutApproval.Result.NotFound); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
return; return;
} }
if (payout.State != PayoutState.AwaitingApproval) if (payout.State != PayoutState.AwaitingApproval)
{ {
req.Completion.SetResult(PayoutApproval.Result.InvalidState); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.InvalidState, null));
return; return;
} }
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings); var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
if (payoutBlob.Revision != req.Revision) if (payoutBlob.Revision != req.Revision)
{ {
req.Completion.SetResult(PayoutApproval.Result.OldRevision); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.OldRevision, null));
return; return;
} }
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod)) if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
{ {
req.Completion.SetResult(PayoutApproval.Result.NotFound); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
return; return;
} }
@@ -388,7 +390,7 @@ namespace BTCPayServer.HostedServices
await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination); await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
if (cryptoAmount < minimumCryptoAmount) if (cryptoAmount < minimumCryptoAmount)
{ {
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount); req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null));
return; return;
} }
@@ -397,7 +399,7 @@ namespace BTCPayServer.HostedServices
payout.SetBlob(payoutBlob, _jsonSerializerSettings); payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
req.Completion.SetResult(PayoutApproval.Result.Ok); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -566,18 +568,20 @@ namespace BTCPayServer.HostedServices
var rateResult = await GetRate(payout, null, CancellationToken.None); var rateResult = await GetRate(payout, null, CancellationToken.None);
if (rateResult.BidAsk != null) if (rateResult.BidAsk != null)
{ {
var approveResult = new TaskCompletionSource<PayoutApproval.Result>(); var approveResultTask = new TaskCompletionSource<PayoutApproval.ApprovalResult>();
await HandleApproval(new PayoutApproval() await HandleApproval(new PayoutApproval()
{ {
PayoutId = payout.Id, PayoutId = payout.Id,
Revision = payoutBlob.Revision, Revision = payoutBlob.Revision,
Rate = rateResult.BidAsk.Ask, Rate = rateResult.BidAsk.Ask,
Completion = approveResult Completion = approveResultTask
}); });
var approveResult = await approveResultTask.Task;
if ((await approveResult.Task) == PayoutApproval.Result.Ok) if (approveResult.Result == PayoutApproval.Result.Ok)
{ {
payout.State = PayoutState.AwaitingPayment; payout.State = PayoutState.AwaitingPayment;
payoutBlob.CryptoAmount = approveResult.CryptoAmount;
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
} }
} }
} }

View File

@@ -14,7 +14,6 @@ public static class PayoutProcessorsExtensions
serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<OnChainAutomatedPayoutSenderFactory>()); serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<OnChainAutomatedPayoutSenderFactory>());
serviceCollection.AddSingleton<LightningAutomatedPayoutSenderFactory>(); serviceCollection.AddSingleton<LightningAutomatedPayoutSenderFactory>();
serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<LightningAutomatedPayoutSenderFactory>()); serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<LightningAutomatedPayoutSenderFactory>());
serviceCollection.AddHostedService<PayoutProcessorService>();
serviceCollection.AddSingleton<PayoutProcessorService>(); serviceCollection.AddSingleton<PayoutProcessorService>();
serviceCollection.AddHostedService(s=> s.GetRequiredService<PayoutProcessorService>()); serviceCollection.AddHostedService(s=> s.GetRequiredService<PayoutProcessorService>());
} }