2020-06-29 04:44:35 +02:00
|
|
|
using System;
|
2017-09-13 08:47:34 +02:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Threading;
|
2020-06-28 10:55:27 +02:00
|
|
|
using System.Threading.Channels;
|
|
|
|
using System.Threading.Tasks;
|
2020-07-24 09:40:37 +02:00
|
|
|
using BTCPayServer.Client.Models;
|
2017-12-17 06:17:42 +01:00
|
|
|
using BTCPayServer.Events;
|
2020-06-28 10:55:27 +02:00
|
|
|
using BTCPayServer.Logging;
|
2018-01-07 18:36:41 +01:00
|
|
|
using BTCPayServer.Services.Invoices;
|
2020-06-22 09:32:51 +02:00
|
|
|
using BTCPayServer.Services.Notifications;
|
|
|
|
using BTCPayServer.Services.Notifications.Blobs;
|
2020-06-28 10:55:27 +02:00
|
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
using NBitcoin;
|
|
|
|
using NBXplorer;
|
2017-09-13 08:47:34 +02:00
|
|
|
|
2018-01-07 18:36:41 +01:00
|
|
|
namespace BTCPayServer.HostedServices
|
2017-09-13 08:47:34 +02:00
|
|
|
{
|
2017-10-27 10:53:04 +02:00
|
|
|
public class InvoiceWatcher : IHostedService
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
class UpdateInvoiceContext
|
|
|
|
{
|
2018-02-17 05:18:16 +01:00
|
|
|
public UpdateInvoiceContext(InvoiceEntity invoice)
|
2018-01-06 18:16:42 +01:00
|
|
|
{
|
2018-02-17 05:18:16 +01:00
|
|
|
Invoice = invoice;
|
2018-01-06 18:16:42 +01:00
|
|
|
}
|
|
|
|
public InvoiceEntity Invoice { get; set; }
|
|
|
|
public List<object> Events { get; set; } = new List<object>();
|
|
|
|
|
|
|
|
bool _Dirty = false;
|
2020-10-17 08:57:21 +02:00
|
|
|
private bool _Unaffect;
|
|
|
|
|
2018-01-06 18:16:42 +01:00
|
|
|
public void MarkDirty()
|
|
|
|
{
|
|
|
|
_Dirty = true;
|
|
|
|
}
|
|
|
|
|
2020-10-17 08:57:21 +02:00
|
|
|
public void UnaffectAddresses()
|
|
|
|
{
|
|
|
|
_Unaffect = true;
|
|
|
|
}
|
|
|
|
|
2018-01-06 18:16:42 +01:00
|
|
|
public bool Dirty => _Dirty;
|
2020-10-17 08:57:21 +02:00
|
|
|
public bool Unaffect => _Unaffect;
|
2021-08-03 10:03:00 +02:00
|
|
|
|
|
|
|
bool _IsBlobUpdated;
|
|
|
|
public bool IsBlobUpdated => _IsBlobUpdated;
|
|
|
|
public void BlobUpdated()
|
|
|
|
{
|
|
|
|
_IsBlobUpdated = true;
|
|
|
|
}
|
2018-01-06 18:16:42 +01:00
|
|
|
}
|
|
|
|
|
2021-10-05 11:10:41 +02:00
|
|
|
readonly InvoiceRepository _invoiceRepository;
|
|
|
|
readonly EventAggregator _eventAggregator;
|
|
|
|
readonly ExplorerClientProvider _explorerClientProvider;
|
2020-06-22 09:32:51 +02:00
|
|
|
private readonly NotificationSender _notificationSender;
|
2021-10-05 11:10:41 +02:00
|
|
|
private readonly PaymentService _paymentService;
|
2021-11-22 09:16:08 +01:00
|
|
|
public Logs Logs { get; }
|
2017-10-27 10:53:04 +02:00
|
|
|
|
2018-01-06 18:16:42 +01:00
|
|
|
public InvoiceWatcher(
|
2017-10-27 10:53:04 +02:00
|
|
|
InvoiceRepository invoiceRepository,
|
2019-05-07 18:37:57 +02:00
|
|
|
EventAggregator eventAggregator,
|
2020-06-28 10:55:27 +02:00
|
|
|
ExplorerClientProvider explorerClientProvider,
|
2021-10-05 11:10:41 +02:00
|
|
|
NotificationSender notificationSender,
|
2021-11-22 09:16:08 +01:00
|
|
|
PaymentService paymentService,
|
|
|
|
Logs logs)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
_invoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
|
|
|
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
|
|
|
_explorerClientProvider = explorerClientProvider;
|
2020-06-22 09:32:51 +02:00
|
|
|
_notificationSender = notificationSender;
|
2021-10-05 11:10:41 +02:00
|
|
|
_paymentService = paymentService;
|
2021-11-22 09:16:08 +01:00
|
|
|
this.Logs = logs;
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
2020-06-29 05:07:48 +02:00
|
|
|
|
|
|
|
readonly CompositeDisposable leases = new CompositeDisposable();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
|
|
|
|
2020-10-26 06:18:38 +01:00
|
|
|
private void UpdateInvoice(UpdateInvoiceContext context)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
var invoice = context.Invoice;
|
2020-11-23 07:57:05 +01:00
|
|
|
if (invoice.Status == InvoiceStatusLegacy.New && invoice.ExpirationTime <= DateTimeOffset.UtcNow)
|
2018-01-06 18:16:42 +01:00
|
|
|
{
|
2018-01-10 07:43:07 +01:00
|
|
|
context.MarkDirty();
|
2020-10-17 08:57:21 +02:00
|
|
|
context.UnaffectAddresses();
|
2020-11-23 07:57:05 +01:00
|
|
|
invoice.Status = InvoiceStatusLegacy.Expired;
|
2021-10-27 12:27:19 +02:00
|
|
|
var paidPartial = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial;
|
|
|
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Expired) { PaidPartial = paidPartial });
|
2019-02-19 04:06:13 +01:00
|
|
|
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
2021-10-27 12:27:19 +02:00
|
|
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial) { PaidPartial = paidPartial });
|
2018-01-06 18:16:42 +01:00
|
|
|
}
|
2019-05-24 15:22:38 +02:00
|
|
|
var allPaymentMethods = invoice.GetPaymentMethods();
|
2019-06-07 06:34:38 +02:00
|
|
|
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting);
|
2021-09-06 17:23:41 +02:00
|
|
|
if (allPaymentMethods.Any() && paymentMethod == null)
|
2018-02-19 10:54:21 +01:00
|
|
|
return;
|
2021-09-06 17:23:41 +02:00
|
|
|
if (accounting is null && invoice.Price is 0m)
|
|
|
|
{
|
|
|
|
accounting = new PaymentMethodAccounting()
|
|
|
|
{
|
|
|
|
Due = Money.Zero,
|
|
|
|
Paid = Money.Zero,
|
|
|
|
CryptoPaid = Money.Zero,
|
|
|
|
DueUncapped = Money.Zero,
|
|
|
|
NetworkFee = Money.Zero,
|
|
|
|
TotalDue = Money.Zero,
|
|
|
|
TxCount = 0,
|
|
|
|
TxRequired = 0,
|
|
|
|
MinimumTotalDue = Money.Zero,
|
|
|
|
NetworkFeeAlreadyPaid = Money.Zero
|
|
|
|
};
|
|
|
|
}
|
2020-11-23 07:57:05 +01:00
|
|
|
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2021-08-03 10:03:00 +02:00
|
|
|
var isPaid = invoice.IsUnsetTopUp() ?
|
|
|
|
accounting.Paid > Money.Zero :
|
|
|
|
accounting.Paid >= accounting.MinimumTotalDue;
|
|
|
|
if (isPaid)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2020-11-23 07:57:05 +01:00
|
|
|
if (invoice.Status == InvoiceStatusLegacy.New)
|
2018-01-10 07:43:07 +01:00
|
|
|
{
|
2020-07-24 09:40:37 +02:00
|
|
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull));
|
2020-11-23 07:57:05 +01:00
|
|
|
invoice.Status = InvoiceStatusLegacy.Paid;
|
2021-08-03 10:03:00 +02:00
|
|
|
if (invoice.IsUnsetTopUp())
|
|
|
|
{
|
|
|
|
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
|
|
|
invoice.Price = (accounting.Paid - accounting.NetworkFeeAlreadyPaid).ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate;
|
|
|
|
accounting = paymentMethod.Calculate();
|
|
|
|
context.BlobUpdated();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
|
|
|
}
|
2020-10-17 08:57:21 +02:00
|
|
|
context.UnaffectAddresses();
|
2018-02-19 10:54:21 +01:00
|
|
|
context.MarkDirty();
|
2018-01-10 07:43:07 +01:00
|
|
|
}
|
2020-11-23 07:57:05 +01:00
|
|
|
else if (invoice.Status == InvoiceStatusLegacy.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2018-12-10 13:48:28 +01:00
|
|
|
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
|
2020-07-24 09:40:37 +02:00
|
|
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidAfterExpiration));
|
2018-01-06 18:16:42 +01:00
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-14 09:16:19 +02:00
|
|
|
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments(true).Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
2018-02-19 07:09:05 +01:00
|
|
|
{
|
2019-04-08 06:28:13 +02:00
|
|
|
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
|
|
|
context.MarkDirty();
|
2018-02-19 10:54:21 +01:00
|
|
|
}
|
|
|
|
}
|
2018-02-19 07:09:05 +01:00
|
|
|
|
2018-02-19 10:54:21 +01:00
|
|
|
// Just make sure RBF did not cancelled a payment
|
2020-11-23 07:57:05 +01:00
|
|
|
if (invoice.Status == InvoiceStatusLegacy.Paid)
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
2018-12-10 13:48:28 +01:00
|
|
|
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
2018-12-10 13:48:28 +01:00
|
|
|
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
2018-02-19 10:54:21 +01:00
|
|
|
context.MarkDirty();
|
|
|
|
}
|
2018-02-19 07:09:05 +01:00
|
|
|
|
2018-12-10 13:48:28 +01:00
|
|
|
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
2018-12-10 13:48:28 +01:00
|
|
|
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver;
|
2018-02-19 10:54:21 +01:00
|
|
|
context.MarkDirty();
|
2018-02-19 07:09:05 +01:00
|
|
|
}
|
|
|
|
|
2018-05-05 16:07:22 +02:00
|
|
|
if (accounting.Paid < accounting.MinimumTotalDue)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2020-11-23 07:57:05 +01:00
|
|
|
invoice.Status = InvoiceStatusLegacy.New;
|
2018-12-10 13:48:28 +01:00
|
|
|
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial;
|
2018-02-19 10:54:21 +01:00
|
|
|
context.MarkDirty();
|
|
|
|
}
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
2020-11-23 07:57:05 +01:00
|
|
|
if (invoice.Status == InvoiceStatusLegacy.Paid)
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
2021-09-06 17:23:41 +02:00
|
|
|
var confirmedAccounting =
|
|
|
|
paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ??
|
|
|
|
accounting;
|
2018-05-05 16:07:22 +02:00
|
|
|
|
2018-02-19 10:54:21 +01:00
|
|
|
if (// Is after the monitoring deadline
|
|
|
|
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
|
|
|
&&
|
|
|
|
// And not enough amount confirmed
|
2018-05-05 16:07:22 +02:00
|
|
|
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
2020-10-17 08:57:21 +02:00
|
|
|
context.UnaffectAddresses();
|
2020-07-24 09:40:37 +02:00
|
|
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm));
|
2020-11-23 07:57:05 +01:00
|
|
|
invoice.Status = InvoiceStatusLegacy.Invalid;
|
2018-02-19 10:54:21 +01:00
|
|
|
context.MarkDirty();
|
|
|
|
}
|
2018-05-05 16:07:22 +02:00
|
|
|
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
2020-10-17 08:57:21 +02:00
|
|
|
context.UnaffectAddresses();
|
2020-11-23 07:57:05 +01:00
|
|
|
invoice.Status = InvoiceStatusLegacy.Confirmed;
|
2020-07-24 09:40:37 +02:00
|
|
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed));
|
2018-02-19 10:54:21 +01:00
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
2018-02-19 10:54:21 +01:00
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
2020-11-23 07:57:05 +01:00
|
|
|
if (invoice.Status == InvoiceStatusLegacy.Confirmed)
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
2021-09-06 17:23:41 +02:00
|
|
|
var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ??
|
|
|
|
accounting;
|
2018-05-05 16:07:22 +02:00
|
|
|
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
2017-12-03 06:43:52 +01:00
|
|
|
{
|
2020-07-24 09:40:37 +02:00
|
|
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));
|
2020-11-23 07:57:05 +01:00
|
|
|
invoice.Status = InvoiceStatusLegacy.Complete;
|
2018-02-19 10:54:21 +01:00
|
|
|
context.MarkDirty();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-06-07 06:34:38 +02:00
|
|
|
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting)
|
2018-02-19 10:54:21 +01:00
|
|
|
{
|
|
|
|
PaymentMethod result = null;
|
|
|
|
accounting = null;
|
|
|
|
decimal nearestToZero = 0.0m;
|
|
|
|
foreach (var paymentMethod in allPaymentMethods)
|
|
|
|
{
|
|
|
|
var currentAccounting = paymentMethod.Calculate();
|
|
|
|
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
|
|
|
|
if (result == null || distanceFromZero < nearestToZero)
|
|
|
|
{
|
|
|
|
result = paymentMethod;
|
|
|
|
nearestToZero = distanceFromZero;
|
|
|
|
accounting = currentAccounting;
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
|
|
|
}
|
2018-02-19 10:54:21 +01:00
|
|
|
return result;
|
2018-01-10 07:43:07 +01:00
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
2018-02-16 17:34:40 +01:00
|
|
|
private void Watch(string invoiceId)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
|
|
|
if (invoiceId == null)
|
|
|
|
throw new ArgumentNullException(nameof(invoiceId));
|
2020-06-28 10:55:27 +02:00
|
|
|
|
2020-02-20 23:54:18 +01:00
|
|
|
if (!_WatchRequests.Writer.TryWrite(invoiceId))
|
|
|
|
{
|
|
|
|
Logs.PayServer.LogWarning($"Failed to write invoice {invoiceId} into WatchRequests channel");
|
|
|
|
}
|
2018-02-16 17:34:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private async Task Wait(string invoiceId)
|
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
var invoice = await _invoiceRepository.GetInvoice(invoiceId);
|
2018-01-18 04:45:39 +01:00
|
|
|
try
|
|
|
|
{
|
2020-02-21 09:14:49 +01:00
|
|
|
// add 1 second to ensure watch won't trigger moments before invoice expires
|
|
|
|
var delay = invoice.ExpirationTime.AddSeconds(1) - DateTimeOffset.UtcNow;
|
2018-04-07 04:53:33 +02:00
|
|
|
if (delay > TimeSpan.Zero)
|
2018-01-18 04:45:39 +01:00
|
|
|
{
|
2018-04-07 04:53:33 +02:00
|
|
|
await Task.Delay(delay, _Cts.Token);
|
2018-01-18 04:45:39 +01:00
|
|
|
}
|
2018-02-17 05:18:16 +01:00
|
|
|
Watch(invoiceId);
|
2020-02-21 09:14:49 +01:00
|
|
|
|
|
|
|
// add 1 second to ensure watch won't trigger moments before monitoring expires
|
|
|
|
delay = invoice.MonitoringExpiration.AddSeconds(1) - DateTimeOffset.UtcNow;
|
2018-04-07 04:53:33 +02:00
|
|
|
if (delay > TimeSpan.Zero)
|
2018-02-16 17:34:40 +01:00
|
|
|
{
|
2018-04-07 04:53:33 +02:00
|
|
|
await Task.Delay(delay, _Cts.Token);
|
2018-02-16 17:34:40 +01:00
|
|
|
}
|
2018-02-17 05:18:16 +01:00
|
|
|
Watch(invoiceId);
|
2018-01-18 04:45:39 +01:00
|
|
|
}
|
|
|
|
catch when (_Cts.IsCancellationRequested)
|
|
|
|
{ }
|
2018-02-16 17:34:40 +01:00
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
|
|
|
|
2020-06-29 05:07:48 +02:00
|
|
|
readonly Channel<string> _WatchRequests = Channel.CreateUnbounded<string>();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
2018-01-09 18:07:42 +01:00
|
|
|
Task _Loop;
|
2017-10-27 10:53:04 +02:00
|
|
|
CancellationTokenSource _Cts;
|
|
|
|
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
|
|
{
|
|
|
|
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
2018-01-09 18:07:42 +01:00
|
|
|
_Loop = StartLoop(_Cts.Token);
|
2019-04-08 06:28:13 +02:00
|
|
|
_ = WaitPendingInvoices();
|
2017-12-17 06:17:42 +01:00
|
|
|
|
2021-10-05 11:10:41 +02:00
|
|
|
leases.Add(_eventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
|
2018-02-16 17:34:40 +01:00
|
|
|
{
|
2018-02-17 18:40:53 +01:00
|
|
|
Watch(b.InvoiceId);
|
2018-02-16 17:34:40 +01:00
|
|
|
}));
|
2021-10-06 06:22:55 +02:00
|
|
|
leases.Add(_eventAggregator.SubscribeAsync<Events.InvoiceEvent>(async b =>
|
2018-01-18 12:56:55 +01:00
|
|
|
{
|
2020-06-22 09:32:51 +02:00
|
|
|
if (InvoiceEventNotification.HandlesEvent(b.Name))
|
|
|
|
{
|
|
|
|
await _notificationSender.SendNotification(new StoreScope(b.Invoice.StoreId),
|
|
|
|
new InvoiceEventNotification(b.Invoice.Id, b.Name));
|
|
|
|
}
|
2019-01-06 10:12:45 +01:00
|
|
|
if (b.Name == InvoiceEvent.Created)
|
2018-01-19 09:39:15 +01:00
|
|
|
{
|
2018-06-21 07:15:36 +02:00
|
|
|
Watch(b.Invoice.Id);
|
2019-04-08 06:28:13 +02:00
|
|
|
_ = Wait(b.Invoice.Id);
|
2018-01-18 12:56:55 +01:00
|
|
|
}
|
2017-12-17 06:17:42 +01:00
|
|
|
|
2019-01-06 10:12:45 +01:00
|
|
|
if (b.Name == InvoiceEvent.ReceivedPayment)
|
2018-02-16 17:34:40 +01:00
|
|
|
{
|
2018-06-21 07:15:36 +02:00
|
|
|
Watch(b.Invoice.Id);
|
2018-02-16 17:34:40 +01:00
|
|
|
}
|
|
|
|
}));
|
2017-10-27 10:53:04 +02:00
|
|
|
return Task.CompletedTask;
|
|
|
|
}
|
|
|
|
|
2018-02-16 17:34:40 +01:00
|
|
|
private async Task WaitPendingInvoices()
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
await Task.WhenAll((await _invoiceRepository.GetPendingInvoices())
|
2018-02-16 17:34:40 +01:00
|
|
|
.Select(id => Wait(id)).ToArray());
|
2018-01-09 18:07:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async Task StartLoop(CancellationToken cancellation)
|
|
|
|
{
|
|
|
|
Logs.PayServer.LogInformation("Start watching invoices");
|
2019-11-16 11:05:30 +01:00
|
|
|
while (await _WatchRequests.Reader.WaitToReadAsync(cancellation) && _WatchRequests.Reader.TryRead(out var invoiceId))
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2019-04-08 06:28:13 +02:00
|
|
|
int maxLoop = 5;
|
|
|
|
int loopCount = -1;
|
|
|
|
while (loopCount < maxLoop)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2019-04-08 06:28:13 +02:00
|
|
|
loopCount++;
|
|
|
|
try
|
2018-01-09 18:07:42 +01:00
|
|
|
{
|
2019-04-08 06:28:13 +02:00
|
|
|
cancellation.ThrowIfCancellationRequested();
|
2021-10-05 11:10:41 +02:00
|
|
|
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
2019-04-08 06:28:13 +02:00
|
|
|
if (invoice == null)
|
|
|
|
break;
|
|
|
|
var updateContext = new UpdateInvoiceContext(invoice);
|
2020-10-26 06:18:38 +01:00
|
|
|
UpdateInvoice(updateContext);
|
2020-10-17 08:57:21 +02:00
|
|
|
if (updateContext.Unaffect)
|
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
await _invoiceRepository.UnaffectAddress(invoice.Id);
|
2020-10-17 08:57:21 +02:00
|
|
|
}
|
2019-04-08 06:28:13 +02:00
|
|
|
if (updateContext.Dirty)
|
2018-01-09 18:07:42 +01:00
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
await _invoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
2019-04-08 06:28:13 +02:00
|
|
|
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
2018-01-09 18:07:42 +01:00
|
|
|
}
|
2021-08-03 10:03:00 +02:00
|
|
|
if (updateContext.IsBlobUpdated)
|
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice);
|
2021-08-03 10:03:00 +02:00
|
|
|
}
|
2019-04-08 06:28:13 +02:00
|
|
|
|
|
|
|
foreach (var evt in updateContext.Events)
|
2018-01-09 18:07:42 +01:00
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
_eventAggregator.Publish(evt, evt.GetType());
|
2018-02-17 05:18:16 +01:00
|
|
|
}
|
2019-04-08 06:28:13 +02:00
|
|
|
|
2020-11-23 07:57:05 +01:00
|
|
|
if (invoice.Status == InvoiceStatusLegacy.Complete ||
|
|
|
|
((invoice.Status == InvoiceStatusLegacy.Invalid || invoice.Status == InvoiceStatusLegacy.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
2018-02-17 05:18:16 +01:00
|
|
|
{
|
2019-05-26 00:20:17 +02:00
|
|
|
var extendInvoiceMonitoring = await UpdateConfirmationCount(invoice);
|
|
|
|
|
|
|
|
// we extend monitor time if we haven't reached max confirmation count
|
|
|
|
// say user used low fee and we only got 3 confirmations right before it's time to remove
|
|
|
|
if (extendInvoiceMonitoring)
|
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
await _invoiceRepository.ExtendInvoiceMonitor(invoice.Id);
|
2019-05-26 00:20:17 +02:00
|
|
|
}
|
2021-10-05 11:10:41 +02:00
|
|
|
else if (await _invoiceRepository.RemovePendingInvoice(invoice.Id))
|
2019-05-26 00:20:17 +02:00
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
_eventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
2019-05-26 00:20:17 +02:00
|
|
|
}
|
2018-02-17 18:40:53 +01:00
|
|
|
break;
|
2018-01-18 10:33:26 +01:00
|
|
|
}
|
2019-04-08 06:28:13 +02:00
|
|
|
|
|
|
|
if (updateContext.Events.Count == 0)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
|
|
|
{
|
|
|
|
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
|
|
|
_ = Task.Delay(10000, cancellation)
|
|
|
|
.ContinueWith(t => Watch(invoiceId), TaskScheduler.Default);
|
|
|
|
break;
|
2018-01-18 10:53:11 +01:00
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
2018-01-09 18:07:42 +01:00
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
|
|
|
|
2019-05-26 00:20:17 +02:00
|
|
|
private async Task<bool> UpdateConfirmationCount(InvoiceEntity invoice)
|
|
|
|
{
|
|
|
|
bool extendInvoiceMonitoring = false;
|
|
|
|
var updateConfirmationCountIfNeeded = invoice
|
2021-05-14 09:16:19 +02:00
|
|
|
.GetPayments(false)
|
2019-05-26 00:20:17 +02:00
|
|
|
.Select<PaymentEntity, Task<PaymentEntity>>(async payment =>
|
|
|
|
{
|
|
|
|
var paymentData = payment.GetCryptoPaymentData();
|
|
|
|
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
|
|
|
{
|
2019-09-21 16:39:44 +02:00
|
|
|
var network = payment.Network as BTCPayNetwork;
|
2019-05-26 00:20:17 +02:00
|
|
|
// Do update if confirmation count in the paymentData is not up to date
|
2019-09-21 16:39:44 +02:00
|
|
|
if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted)
|
2019-05-26 00:20:17 +02:00
|
|
|
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
var transactionResult = await _explorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
2019-05-26 00:20:17 +02:00
|
|
|
var confirmationCount = transactionResult?.Confirmations ?? 0;
|
|
|
|
onChainPaymentData.ConfirmationCount = confirmationCount;
|
|
|
|
payment.SetCryptoPaymentData(onChainPaymentData);
|
|
|
|
|
|
|
|
// we want to extend invoice monitoring until we reach max confirmations on all onchain payment methods
|
2019-09-21 16:39:44 +02:00
|
|
|
if (confirmationCount < network.MaxTrackedConfirmation)
|
2019-05-26 00:20:17 +02:00
|
|
|
extendInvoiceMonitoring = true;
|
2020-06-28 10:55:27 +02:00
|
|
|
|
2019-05-26 00:20:17 +02:00
|
|
|
return payment;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
})
|
|
|
|
.ToArray();
|
|
|
|
await Task.WhenAll(updateConfirmationCountIfNeeded);
|
|
|
|
var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList();
|
|
|
|
if (updatedPaymentData.Count > 0)
|
|
|
|
{
|
2021-10-05 11:10:41 +02:00
|
|
|
await _paymentService.UpdatePayments(updatedPaymentData);
|
2019-05-26 00:20:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return extendInvoiceMonitoring;
|
|
|
|
}
|
|
|
|
|
2019-04-08 06:28:13 +02:00
|
|
|
public async Task StopAsync(CancellationToken cancellationToken)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2019-03-27 10:56:43 +01:00
|
|
|
if (_Cts == null)
|
2019-04-08 06:28:13 +02:00
|
|
|
return;
|
2017-12-17 06:17:42 +01:00
|
|
|
leases.Dispose();
|
2017-10-27 10:53:04 +02:00
|
|
|
_Cts.Cancel();
|
2019-04-08 06:28:13 +02:00
|
|
|
try
|
|
|
|
{
|
|
|
|
await _Loop;
|
|
|
|
}
|
|
|
|
catch { }
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
Logs.PayServer.LogInformation("Stop watching invoices");
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
}
|