Fix: Payouts were incorrectly marked as canceled even after successful (#6365)

completion
This commit is contained in:
Nicolas Dorier 2024-11-08 16:13:13 +09:00 committed by GitHub
parent a8581510e5
commit a05dbef337
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 85 additions and 93 deletions

View File

@ -192,7 +192,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message = message
};
@ -216,7 +216,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
public string PayoutId { get; set; }
public string Destination { get; set; }
public PayResult Result { get; set; }
public bool? Success { get; set; }
public string Message { get; set; }
}

View File

@ -100,24 +100,24 @@ public class LightningPendingPayoutListener : BaseAsyncService
if (proof is not null)
payment = await client.GetPayment(proof.PaymentHash, CancellationToken);
}
catch
catch (OperationCanceledException)
{
}
if (payment is null)
{
payoutData.State = PayoutState.Cancelled;
// Do not mark as cancelled if the operation was cancelled.
// This can happen with Nostr GetPayment if the connection to relay is too slow.
continue;
}
switch (payment.Status)
payoutData.State = payment?.Status switch
{
LightningPaymentStatus.Complete => PayoutState.Completed,
LightningPaymentStatus.Failed => PayoutState.Cancelled,
LightningPaymentStatus.Unknown or LightningPaymentStatus.Pending => PayoutState.InProgress,
_ => PayoutState.Cancelled
};
if (payment is { Status: LightningPaymentStatus.Complete })
{
case LightningPaymentStatus.Complete:
payoutData.State = PayoutState.Completed;
proof.Preimage = payment.Preimage;
payoutData.SetProofBlob(proof, null);
break;
case LightningPaymentStatus.Failed:
payoutData.State = PayoutState.Cancelled;
break;
}
}

View File

@ -110,7 +110,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
result = new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message = claim.error
};
@ -118,7 +118,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
}
bool updateBlob = false;
if (result.Result is PayResult.Error or PayResult.CouldNotFindRoute && payoutData.State == PayoutState.AwaitingPayment)
if (result.Success is false && payoutData.State == PayoutState.AwaitingPayment)
{
updateBlob = true;
if (blob.IncrementErrorCount() >= 10)
@ -141,7 +141,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
new ResultVM
{
PayoutId = payoutId,
Result = PayResult.Error,
Success = false,
Message = "The payout isn't in a valid state"
};
@ -163,7 +163,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
return (null, new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message =
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
@ -183,7 +183,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message = e.Message
});
@ -204,7 +204,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutData.Amount.GetValueOrDefault()} {payoutData.Currency})",
Destination = payoutBlob.Destination
};
@ -216,41 +216,52 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Message = $"The BOLT11 invoice expiry date ({bolt11PaymentRequest.ExpiryDate}) has expired",
Destination = payoutBlob.Destination
};
}
var proofBlob = new PayoutLightningBlob { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
PayResponse pay = null;
Exception exception = null;
string errorReason = null;
string preimage = null;
bool? success = null;
LightMoney amountSent = null;
try
{
pay = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
var pay = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
new PayInvoiceParams()
{
Amount = new LightMoney((decimal)payoutData.Amount, LightMoneyUnit.BTC)
}, cancellationToken);
if (pay?.Result is PayResult.CouldNotFindRoute)
if (pay is { Result: PayResult.CouldNotFindRoute })
{
// Payment failed for sure... we can try again later!
payoutData.State = PayoutState.AwaitingPayment;
return new ResultVM
errorReason ??= $"Unable to find a route for the payment, check your channel liquidity";
success = false;
}
else if (pay is { Result: PayResult.Error })
{
PayoutId = payoutData.Id,
Result = PayResult.CouldNotFindRoute,
Message = $"Unable to find a route for the payment, check your channel liquidity",
Destination = payoutBlob.Destination
};
errorReason ??= pay.ErrorDetail;
success = false;
}
else if (pay is { Result: PayResult.Ok })
{
if (pay.Details is { } details)
{
preimage = details.Preimage?.ToString();
amountSent = details.TotalAmount;
}
success = true;
}
}
catch (Exception ex)
{
exception = ex;
errorReason ??= ex.Message;
}
if (success is null || preimage is null || amountSent is null)
{
LightningPayment payment = null;
try
{
@ -258,68 +269,49 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
}
catch (Exception ex)
{
exception = ex;
errorReason ??= ex.Message;
}
if (payment is null)
success ??= payment?.Status switch
{
payoutData.State = PayoutState.Cancelled;
var exceptionMessage = "";
if (exception is not null)
exceptionMessage = $" ({exception.Message})";
if (exceptionMessage == "")
exceptionMessage = $" ({pay?.ErrorDetail})";
return new ResultVM
LightningPaymentStatus.Complete => true,
LightningPaymentStatus.Failed => false,
_ => null
};
amountSent ??= payment?.AmountSent;
preimage ??= payment?.Preimage;
}
if (preimage is not null)
proofBlob.Preimage = preimage;
var vm = new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Message = $"Unable to confirm the payment of the invoice" + exceptionMessage,
Success = success,
Destination = payoutBlob.Destination
};
}
if (payment.Preimage is not null)
proofBlob.Preimage = payment.Preimage;
if (payment.Status == LightningPaymentStatus.Complete)
if (success is true)
{
payoutData.State = PayoutState.Completed;
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Ok,
Destination = payoutBlob.Destination,
Message = payment.AmountSent != null
? $"Paid out {payment.AmountSent.ToDecimal(LightMoneyUnit.BTC)} {payoutData.Currency}"
: "Paid out"
};
vm.Message = amountSent != null
? $"Paid out {amountSent.ToDecimal(LightMoneyUnit.BTC)} {payoutData.Currency}"
: "Paid out";
}
else if (payment.Status == LightningPaymentStatus.Failed)
else if (success is false)
{
payoutData.State = PayoutState.AwaitingPayment;
string reason = "";
if (pay?.ErrorDetail is string err)
reason = $" ({err})";
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = payoutBlob.Destination,
Message = $"The payment failed{reason}"
};
var err = errorReason is null ? "" : $" ({errorReason})";
vm.Message = $"The payment failed{err}";
}
else
{
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
payoutData.State = PayoutState.InProgress;
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Ok,
Destination = payoutBlob.Destination,
Message = "The payment has been initiated but is still in-flight."
};
vm.Message = "The payment has been initiated but is still in-flight.";
}
return vm;
}
protected override async Task<bool> ProcessShouldSave(object paymentMethodConfig, List<PayoutData> payouts)

View File

@ -8,9 +8,9 @@
<h2 class="mt-1 mb-4" text-translate="true">@ViewData["Title"]</h2>
@foreach (var item in Model)
{
<div class="alert alert-@(item.Result == PayResult.Ok ? "success" : "danger") mb-3" role="alert">
<div class="alert alert-@(item.Success is true ? "success" : "danger") mb-3" role="alert">
<h5 class="alert-heading">
@(item.Result == PayResult.Ok ? "Sent" : "Failed")
@(item.Success is true ? "Sent" : "Failed")
@if (!string.IsNullOrEmpty(item.Message))
{
<span>- @item.Message</span>