mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Fix LN invoices (#1955)
* Fix LN invoices This commit adds more to the previous LN fix in the case of a partial payment to an invoice. While it generated a new LN invoice after 1 partial payment was made, there were some new issues uncovered: * Any other subsequent partial payments was not listened to and did not generate an invoice ( fixed by listeneing to received payment event and makng sure that the status was already set `to partialPaid`) * Any other subsequent partial payments caused a DbConcurrency error and did not generate an invoice ( Fixed in `MarkUnassigned`)
This commit is contained in:
@@ -300,32 +300,35 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(PullPaymentController.ViewPullPayment),
|
||||
"PullPayment",
|
||||
new { pullPaymentId = ppId });
|
||||
|
||||
|
||||
}
|
||||
|
||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||
{
|
||||
var model = new InvoiceDetailsModel();
|
||||
model.Archived = invoice.Archived;
|
||||
model.Payments = invoice.GetPayments();
|
||||
foreach (var data in invoice.GetPaymentMethods())
|
||||
return new InvoiceDetailsModel
|
||||
{
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
|
||||
cryptoPayment.PaymentMethodId = paymentMethodId;
|
||||
cryptoPayment.PaymentMethod = paymentMethodId.ToPrettyString();
|
||||
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
var paymentMethodDetails = data.GetPaymentMethodDetails();
|
||||
cryptoPayment.Address = paymentMethodDetails.GetPaymentDestination();
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
return model;
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(),
|
||||
CryptoPayments = invoice.GetPaymentMethods().Select(
|
||||
data =>
|
||||
{
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
return new InvoiceDetailsModel.CryptoPayment
|
||||
{
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC),
|
||||
paymentMethodId.CryptoCode),
|
||||
Paid = _CurrencyNameTable.DisplayFormatCurrency(
|
||||
accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
|
||||
paymentMethodId.CryptoCode),
|
||||
Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
|
||||
accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
|
||||
Rate = ExchangeRate(data)
|
||||
};
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("invoices/{invoiceId}/archive")]
|
||||
|
||||
@@ -29,12 +29,20 @@ namespace BTCPayServer.HostedServices
|
||||
public List<object> Events { get; set; } = new List<object>();
|
||||
|
||||
bool _Dirty = false;
|
||||
private bool _Unaffect;
|
||||
|
||||
public void MarkDirty()
|
||||
{
|
||||
_Dirty = true;
|
||||
}
|
||||
|
||||
public void UnaffectAddresses()
|
||||
{
|
||||
_Unaffect = true;
|
||||
}
|
||||
|
||||
public bool Dirty => _Dirty;
|
||||
public bool Unaffect => _Unaffect;
|
||||
}
|
||||
|
||||
readonly InvoiceRepository _InvoiceRepository;
|
||||
@@ -63,15 +71,12 @@ namespace BTCPayServer.HostedServices
|
||||
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.UnaffectAddresses();
|
||||
invoice.Status = InvoiceStatus.Expired;
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Expired));
|
||||
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
var allPaymentMethods = invoice.GetPaymentMethods();
|
||||
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting);
|
||||
if (paymentMethod == null)
|
||||
@@ -85,7 +90,7 @@ namespace BTCPayServer.HostedServices
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull));
|
||||
invoice.Status = InvoiceStatus.Paid;
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.UnaffectAddresses();
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
||||
@@ -136,14 +141,14 @@ namespace BTCPayServer.HostedServices
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.UnaffectAddresses();
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatus.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.UnaffectAddresses();
|
||||
invoice.Status = InvoiceStatus.Confirmed;
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed));
|
||||
context.MarkDirty();
|
||||
@@ -279,6 +284,10 @@ namespace BTCPayServer.HostedServices
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Unaffect)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
}
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
|
||||
@@ -25,10 +25,5 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
return 0.0m;
|
||||
}
|
||||
|
||||
public void SetPaymentDetails(IPaymentMethodDetails newPaymentMethodDetails)
|
||||
{
|
||||
BOLT11 = newPaymentMethodDetails.GetPaymentDestination();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,76 +137,32 @@ namespace BTCPayServer.Payments.Lightning
|
||||
readonly CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(inv =>
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
{
|
||||
if (inv.Name == InvoiceEvent.Created)
|
||||
{
|
||||
_CheckInvoices.Writer.TryWrite(inv.Invoice.Id);
|
||||
}
|
||||
|
||||
if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatus.New && inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
var pm = inv.Invoice.GetPaymentMethods().First();
|
||||
if (pm.Calculate().Due.GetValue(pm.Network as BTCPayNetwork) > 0m)
|
||||
{
|
||||
await CreateNewLNInvoiceForBTCPayInvoice(inv.Invoice);
|
||||
}
|
||||
}
|
||||
}));
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceDataChangedEvent>(async inv =>
|
||||
{
|
||||
if (inv.State.Status == InvoiceStatus.New &&
|
||||
inv.State.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
|
||||
var invoice = await _InvoiceRepository.GetInvoice(inv.InvoiceId);
|
||||
var paymentMethods = invoice.GetPaymentMethods()
|
||||
.Where(method => method.GetId().PaymentType == PaymentTypes.LightningLike).ToArray();
|
||||
var store = await _storeRepository.FindStore(invoice.StoreId);
|
||||
if (paymentMethods.Any())
|
||||
{
|
||||
var logs = new InvoiceLogs();
|
||||
logs.Write("Partial payment detected, attempting to update all lightning payment methods with new bolt11 with correct due amount.", InvoiceEventData.EventSeverity.Info);
|
||||
foreach (var paymentMethod in paymentMethods)
|
||||
{
|
||||
try
|
||||
{
|
||||
var supportedMethod =
|
||||
invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(
|
||||
paymentMethod.GetId()).First();
|
||||
var prepObj =
|
||||
_lightningLikePaymentHandler.PreparePayment(supportedMethod, store,
|
||||
paymentMethod.Network);
|
||||
var newPaymentMethodDetails =
|
||||
await _lightningLikePaymentHandler.CreatePaymentMethodDetails(
|
||||
logs, supportedMethod,
|
||||
paymentMethod, store, paymentMethod.Network, prepObj);
|
||||
|
||||
|
||||
var instanceListenerKey = (paymentMethod.Network.CryptoCode,
|
||||
supportedMethod.GetLightningUrl().ToString());
|
||||
if (_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener))
|
||||
{
|
||||
|
||||
await _InvoiceRepository.NewPaymentDetails(invoice.Id, newPaymentMethodDetails,
|
||||
paymentMethod.Network);
|
||||
|
||||
instanceListener.AddListenedInvoice(new ListenedInvoice()
|
||||
{
|
||||
Expiration = invoice.ExpirationTime,
|
||||
Uri = supportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
|
||||
PaymentMethodDetails = (LightningLikePaymentMethodDetails) newPaymentMethodDetails,
|
||||
SupportedPaymentMethod = supportedMethod,
|
||||
PaymentMethod = paymentMethod,
|
||||
Network = (BTCPayNetwork) paymentMethod.Network,
|
||||
InvoiceId = invoice.Id
|
||||
});
|
||||
|
||||
_Aggregator.Publish(new Events.InvoiceNewPaymentDetailsEvent(invoice.Id,
|
||||
newPaymentMethodDetails, paymentMethod.GetId()));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logs.Write($"Could not update {paymentMethod.GetId().ToPrettyString()}: {e.Message}",
|
||||
InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
await _InvoiceRepository.AddInvoiceLogs(invoice.Id, logs);
|
||||
_CheckInvoices.Writer.TryWrite(inv.InvoiceId);
|
||||
}
|
||||
await CreateNewLNInvoiceForBTCPayInvoice(invoice);
|
||||
}
|
||||
|
||||
}));
|
||||
_CheckingInvoice = CheckingInvoice(_Cts.Token);
|
||||
_ListenPoller = new Timer(async s =>
|
||||
@@ -224,6 +180,66 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice)
|
||||
{
|
||||
var paymentMethods = invoice.GetPaymentMethods()
|
||||
.Where(method => method.GetId().PaymentType == PaymentTypes.LightningLike)
|
||||
.ToArray();
|
||||
var store = await _storeRepository.FindStore(invoice.StoreId);
|
||||
if (paymentMethods.Any())
|
||||
{
|
||||
var logs = new InvoiceLogs();
|
||||
logs.Write(
|
||||
"Partial payment detected, attempting to update all lightning payment methods with new bolt11 with correct due amount.",
|
||||
InvoiceEventData.EventSeverity.Info);
|
||||
foreach (var paymentMethod in paymentMethods)
|
||||
{
|
||||
try
|
||||
{
|
||||
var supportedMethod = invoice
|
||||
.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(paymentMethod.GetId()).First();
|
||||
var prepObj =
|
||||
_lightningLikePaymentHandler.PreparePayment(supportedMethod, store, paymentMethod.Network);
|
||||
var newPaymentMethodDetails =
|
||||
(LightningLikePaymentMethodDetails)(await _lightningLikePaymentHandler
|
||||
.CreatePaymentMethodDetails(logs, supportedMethod, paymentMethod, store,
|
||||
paymentMethod.Network, prepObj));
|
||||
|
||||
var instanceListenerKey = (paymentMethod.Network.CryptoCode,
|
||||
supportedMethod.GetLightningUrl().ToString());
|
||||
if (_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener))
|
||||
{
|
||||
await _InvoiceRepository.NewPaymentDetails(invoice.Id, newPaymentMethodDetails,
|
||||
paymentMethod.Network);
|
||||
|
||||
instanceListener.AddListenedInvoice(new ListenedInvoice()
|
||||
{
|
||||
Expiration = invoice.ExpirationTime,
|
||||
Uri = supportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
|
||||
PaymentMethodDetails = newPaymentMethodDetails,
|
||||
SupportedPaymentMethod = supportedMethod,
|
||||
PaymentMethod = paymentMethod,
|
||||
Network = (BTCPayNetwork)paymentMethod.Network,
|
||||
InvoiceId = invoice.Id
|
||||
});
|
||||
|
||||
_Aggregator.Publish(new Events.InvoiceNewPaymentDetailsEvent(invoice.Id,
|
||||
newPaymentMethodDetails, paymentMethod.GetId()));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logs.Write($"Could not update {paymentMethod.GetId().ToPrettyString()}: {e.Message}",
|
||||
InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
await _InvoiceRepository.AddInvoiceLogs(invoice.Id, logs);
|
||||
_CheckInvoices.Writer.TryWrite(invoice.Id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
|
||||
@@ -260,7 +260,7 @@ retry:
|
||||
var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails();
|
||||
if (existingPaymentMethod.GetPaymentDestination() != null)
|
||||
{
|
||||
MarkUnassigned(invoiceId, invoiceEntity, context, paymentMethod.GetId());
|
||||
MarkUnassigned(invoiceId, context, paymentMethod.GetId());
|
||||
}
|
||||
paymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
|
||||
#pragma warning disable CS0618
|
||||
@@ -323,36 +323,29 @@ retry:
|
||||
catch (DbUpdateException) { } // Probably the invoice does not exists anymore
|
||||
}
|
||||
|
||||
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, PaymentMethodId paymentMethodId)
|
||||
private static void MarkUnassigned(string invoiceId, ApplicationDbContext context,
|
||||
PaymentMethodId paymentMethodId)
|
||||
{
|
||||
foreach (var address in entity.GetPaymentMethods())
|
||||
var paymentMethodIdStr = paymentMethodId?.ToString();
|
||||
var addresses = context.HistoricalAddressInvoices.Where(data =>
|
||||
(data.InvoiceDataId == invoiceId && paymentMethodIdStr == null ||
|
||||
data.CryptoCode == paymentMethodIdStr) &&
|
||||
data.UnAssigned == null);
|
||||
foreach (var historicalAddressInvoiceData in addresses)
|
||||
{
|
||||
if (paymentMethodId != null && paymentMethodId != address.GetId())
|
||||
continue;
|
||||
var historical = new HistoricalAddressInvoiceData();
|
||||
historical.InvoiceDataId = invoiceId;
|
||||
historical.SetAddress(address.GetPaymentMethodDetails().GetPaymentDestination(), address.GetId().ToString());
|
||||
historical.UnAssigned = DateTimeOffset.UtcNow;
|
||||
context.Attach(historical);
|
||||
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
|
||||
historicalAddressInvoiceData.UnAssigned = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UnaffectAddress(string invoiceId)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
MarkUnassigned(invoiceId, context, null);
|
||||
try
|
||||
{
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
var invoiceEntity = invoiceData.GetBlob(_Networks);
|
||||
MarkUnassigned(invoiceId, invoiceEntity, context, null);
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) { } //Possibly, it was unassigned before
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) { } //Possibly, it was unassigned before
|
||||
}
|
||||
|
||||
private string[] SearchInvoice(string searchTerms)
|
||||
|
||||
@@ -205,8 +205,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(Model, false)" />
|
||||
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
Reference in New Issue
Block a user