2017-09-13 08:47:34 +02:00
|
|
|
|
using NBXplorer;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using NBXplorer.DerivationStrategy;
|
|
|
|
|
using NBXplorer.Models;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using NBitcoin;
|
|
|
|
|
using BTCPayServer.Logging;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
|
using System.Collections.Concurrent;
|
2017-09-25 18:31:43 +02:00
|
|
|
|
using Hangfire;
|
2017-10-06 03:37:38 +02:00
|
|
|
|
using BTCPayServer.Services.Wallets;
|
2017-12-17 06:17:42 +01:00
|
|
|
|
using BTCPayServer.Controllers;
|
|
|
|
|
using BTCPayServer.Events;
|
2018-01-06 18:16:42 +01:00
|
|
|
|
using Microsoft.AspNetCore.Hosting;
|
2018-01-07 18:36:41 +01:00
|
|
|
|
using BTCPayServer.Services.Invoices;
|
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
|
|
|
|
|
{
|
|
|
|
|
public UpdateInvoiceContext()
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Dictionary<BTCPayNetwork, KnownState> KnownStates { get; set; }
|
|
|
|
|
public Dictionary<BTCPayNetwork, KnownState> ModifiedKnownStates { get; set; } = new Dictionary<BTCPayNetwork, KnownState>();
|
|
|
|
|
public InvoiceEntity Invoice { get; set; }
|
|
|
|
|
public List<object> Events { get; set; } = new List<object>();
|
|
|
|
|
|
|
|
|
|
bool _Dirty = false;
|
|
|
|
|
public void MarkDirty()
|
|
|
|
|
{
|
|
|
|
|
_Dirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool Dirty => _Dirty;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
InvoiceRepository _InvoiceRepository;
|
2017-12-17 06:17:42 +01:00
|
|
|
|
EventAggregator _EventAggregator;
|
2017-10-27 10:53:04 +02:00
|
|
|
|
BTCPayWallet _Wallet;
|
2017-12-21 07:52:04 +01:00
|
|
|
|
BTCPayNetworkProvider _NetworkProvider;
|
2017-10-27 10:53:04 +02:00
|
|
|
|
|
2018-01-06 18:16:42 +01:00
|
|
|
|
public InvoiceWatcher(
|
|
|
|
|
IHostingEnvironment env,
|
2017-12-21 07:52:04 +01:00
|
|
|
|
BTCPayNetworkProvider networkProvider,
|
2017-10-27 10:53:04 +02:00
|
|
|
|
InvoiceRepository invoiceRepository,
|
2017-12-17 06:17:42 +01:00
|
|
|
|
EventAggregator eventAggregator,
|
2018-01-07 18:36:41 +01:00
|
|
|
|
BTCPayWallet wallet)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
PollInterval = TimeSpan.FromMinutes(1.0);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
|
|
|
|
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
2017-12-17 06:17:42 +01:00
|
|
|
|
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
2017-12-21 07:52:04 +01:00
|
|
|
|
_NetworkProvider = networkProvider;
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2017-12-17 06:17:42 +01:00
|
|
|
|
CompositeDisposable leases = new CompositeDisposable();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
|
2018-01-08 17:56:37 +01:00
|
|
|
|
async Task NotifyReceived(Script scriptPubKey, BTCPayNetwork network)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-08 17:56:37 +01:00
|
|
|
|
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (invoice != null)
|
|
|
|
|
_WatchRequests.Add(invoice);
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-17 06:17:42 +01:00
|
|
|
|
async Task NotifyBlock()
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
|
|
|
|
|
{
|
|
|
|
|
_WatchRequests.Add(invoice);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-09 18:07:42 +01:00
|
|
|
|
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
|
2018-01-09 18:07:42 +01:00
|
|
|
|
while (!cancellation.IsCancellationRequested)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2017-11-06 09:31:02 +01:00
|
|
|
|
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (invoice == null)
|
|
|
|
|
break;
|
|
|
|
|
var stateBefore = invoice.Status;
|
2018-01-06 18:16:42 +01:00
|
|
|
|
var updateContext = new UpdateInvoiceContext()
|
|
|
|
|
{
|
|
|
|
|
Invoice = invoice,
|
|
|
|
|
KnownStates = changes
|
|
|
|
|
};
|
|
|
|
|
await UpdateInvoice(updateContext).ConfigureAwait(false);
|
|
|
|
|
if (updateContext.Dirty)
|
|
|
|
|
{
|
2017-10-27 10:53:04 +02:00
|
|
|
|
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
|
2017-12-17 06:17:42 +01:00
|
|
|
|
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
|
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
|
|
|
|
|
var changed = stateBefore != invoice.Status;
|
2017-12-17 06:17:42 +01:00
|
|
|
|
|
2018-01-06 18:16:42 +01:00
|
|
|
|
foreach (var evt in updateContext.Events)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
_EventAggregator.Publish(evt, evt.GetType());
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2018-01-06 18:16:42 +01:00
|
|
|
|
|
2018-01-07 20:14:35 +01:00
|
|
|
|
foreach (var modifiedKnownState in updateContext.ModifiedKnownStates)
|
2018-01-06 18:16:42 +01:00
|
|
|
|
{
|
|
|
|
|
changes.AddOrReplace(modifiedKnownState.Key, modifiedKnownState.Value);
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (invoice.Status == "complete" ||
|
2017-12-03 06:43:52 +01:00
|
|
|
|
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
|
|
|
|
|
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-09 18:07:42 +01:00
|
|
|
|
if (!changed || cancellation.IsCancellationRequested)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
2018-01-09 18:07:42 +01:00
|
|
|
|
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
2018-01-09 18:07:42 +01:00
|
|
|
|
await Task.Delay(10000, cancellation).ConfigureAwait(false);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-01-06 18:16:42 +01:00
|
|
|
|
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
var invoice = context.Invoice;
|
2017-10-27 10:53:04 +02:00
|
|
|
|
//Fetch unknown payments
|
2018-01-06 18:16:42 +01:00
|
|
|
|
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
|
|
|
|
var getCoinsResponsesAsync = strategies
|
2018-01-09 18:07:42 +01:00
|
|
|
|
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network)))
|
2018-01-06 18:16:42 +01:00
|
|
|
|
.ToArray();
|
|
|
|
|
await Task.WhenAll(getCoinsResponsesAsync);
|
|
|
|
|
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
|
|
|
|
|
foreach (var response in getCoinsResponses)
|
|
|
|
|
{
|
2018-01-08 17:56:37 +01:00
|
|
|
|
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
|
2018-01-06 18:16:42 +01:00
|
|
|
|
}
|
|
|
|
|
var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
bool dirtyAddress = false;
|
2018-01-06 18:16:42 +01:00
|
|
|
|
if (coins != null)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
if (coins.State != null)
|
|
|
|
|
context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State);
|
2018-01-06 18:16:42 +01:00
|
|
|
|
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
|
|
|
|
foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
|
|
|
|
{
|
2018-01-08 18:57:06 +01:00
|
|
|
|
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
|
2018-01-06 18:16:42 +01:00
|
|
|
|
invoice.Payments.Add(payment);
|
|
|
|
|
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
|
|
|
|
|
dirtyAddress = true;
|
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
//////
|
2018-01-07 18:36:41 +01:00
|
|
|
|
var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key);
|
2017-12-21 07:52:04 +01:00
|
|
|
|
var cryptoData = invoice.GetCryptoData(network);
|
|
|
|
|
var cryptoDataAll = invoice.GetCryptoData();
|
2017-12-21 10:01:26 +01:00
|
|
|
|
var accounting = cryptoData.Calculate();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
2017-12-17 06:33:38 +01:00
|
|
|
|
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
|
2017-10-27 10:53:04 +02:00
|
|
|
|
invoice.Status = "expired";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (invoice.Status == "new" || invoice.Status == "expired")
|
|
|
|
|
{
|
2018-01-07 18:36:41 +01:00
|
|
|
|
var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
2017-12-21 10:01:26 +01:00
|
|
|
|
if (totalPaid >= accounting.TotalDue)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
if (invoice.Status == "new")
|
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
|
2017-10-27 10:53:04 +02:00
|
|
|
|
invoice.Status = "paid";
|
|
|
|
|
invoice.ExceptionStatus = null;
|
|
|
|
|
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
else if (invoice.Status == "expired")
|
|
|
|
|
{
|
|
|
|
|
invoice.ExceptionStatus = "paidLate";
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-21 10:01:26 +01:00
|
|
|
|
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
invoice.ExceptionStatus = "paidOver";
|
|
|
|
|
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-12-21 10:01:26 +01:00
|
|
|
|
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
invoice.ExceptionStatus = "paidPartial";
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (dirtyAddress)
|
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
Logs.PayServer.LogInformation("Generate new " + address);
|
2017-12-21 07:52:04 +01:00
|
|
|
|
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (invoice.Status == "paid")
|
|
|
|
|
{
|
2018-01-07 18:36:41 +01:00
|
|
|
|
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
2017-12-03 06:43:52 +01:00
|
|
|
|
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-07 20:14:35 +01:00
|
|
|
|
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
|
2017-12-03 06:43:52 +01:00
|
|
|
|
}
|
|
|
|
|
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
|
|
|
|
{
|
|
|
|
|
transactions = transactions.Where(t => t.Confirmations >= 1);
|
|
|
|
|
}
|
|
|
|
|
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
|
|
|
|
{
|
|
|
|
|
transactions = transactions.Where(t => t.Confirmations >= 6);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 20:14:35 +01:00
|
|
|
|
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
2017-12-03 06:43:52 +01:00
|
|
|
|
|
|
|
|
|
if (// Is after the monitoring deadline
|
|
|
|
|
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
|
|
|
|
&&
|
|
|
|
|
// And not enough amount confirmed
|
2018-01-07 20:14:35 +01:00
|
|
|
|
(totalConfirmed < accounting.TotalDue))
|
2017-12-03 06:43:52 +01:00
|
|
|
|
{
|
|
|
|
|
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
|
2017-12-03 06:43:52 +01:00
|
|
|
|
invoice.Status = "invalid";
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.MarkDirty();
|
2017-12-03 06:43:52 +01:00
|
|
|
|
}
|
2018-01-07 20:14:35 +01:00
|
|
|
|
else if (totalConfirmed >= accounting.TotalDue)
|
2017-12-03 06:43:52 +01:00
|
|
|
|
{
|
2018-01-07 20:14:35 +01:00
|
|
|
|
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
|
|
|
|
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
|
|
|
|
|
invoice.Status = "confirmed";
|
|
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (invoice.Status == "confirmed")
|
|
|
|
|
{
|
2018-01-07 18:36:41 +01:00
|
|
|
|
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
2017-11-06 09:31:02 +01:00
|
|
|
|
transactions = transactions.Where(t => t.Confirmations >= 6);
|
2017-12-21 07:52:04 +01:00
|
|
|
|
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
2017-12-21 10:01:26 +01:00
|
|
|
|
if (totalConfirmed >= accounting.TotalDue)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
|
2017-10-27 10:53:04 +02:00
|
|
|
|
invoice.Status = "complete";
|
2018-01-06 18:16:42 +01:00
|
|
|
|
context.MarkDirty();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 18:36:41 +01:00
|
|
|
|
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-07 18:36:41 +01:00
|
|
|
|
var transactions = await _Wallet.GetTransactions(network, invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
|
2017-11-06 09:31:02 +01:00
|
|
|
|
|
|
|
|
|
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
|
|
|
|
|
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
|
|
|
|
|
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
|
|
|
|
|
foreach (var payment in invoice.Payments)
|
|
|
|
|
{
|
|
|
|
|
TransactionResult tx;
|
|
|
|
|
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
|
|
|
|
|
{
|
|
|
|
|
result.Remove(payment.Outpoint);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
|
|
|
|
|
{
|
|
|
|
|
Confirmations = tx.Confirmations,
|
|
|
|
|
Transaction = tx.Transaction,
|
|
|
|
|
Payment = payment
|
|
|
|
|
};
|
|
|
|
|
payments.Add(accountedPayment);
|
|
|
|
|
foreach (var txin in tx.Transaction.Inputs)
|
|
|
|
|
{
|
|
|
|
|
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
|
|
|
|
|
{
|
|
|
|
|
//We get a double spend
|
|
|
|
|
var existing = spentTxIn[txin.PrevOut];
|
|
|
|
|
|
|
|
|
|
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
|
|
|
|
|
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
|
|
|
|
|
{
|
|
|
|
|
spentTxIn[txin.PrevOut] = accountedPayment;
|
|
|
|
|
result.Remove(existing.Payment.Outpoint);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<PaymentEntity> updated = new List<PaymentEntity>();
|
|
|
|
|
var accountedPayments = payments.Where(p =>
|
|
|
|
|
{
|
|
|
|
|
var accounted = result.Contains(p.Payment.Outpoint);
|
|
|
|
|
if (p.Payment.Accounted != accounted)
|
|
|
|
|
{
|
|
|
|
|
p.Payment.Accounted = accounted;
|
|
|
|
|
updated.Add(p.Payment);
|
|
|
|
|
}
|
|
|
|
|
return accounted;
|
|
|
|
|
}).ToArray();
|
|
|
|
|
|
|
|
|
|
await _InvoiceRepository.UpdatePayments(payments);
|
|
|
|
|
return accountedPayments;
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TimeSpan _PollInterval;
|
|
|
|
|
public TimeSpan PollInterval
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _PollInterval;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_PollInterval = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 18:36:41 +01:00
|
|
|
|
private void Watch(string invoiceId)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
if (invoiceId == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(invoiceId));
|
|
|
|
|
_WatchRequests.Add(invoiceId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
|
|
|
|
|
2018-01-09 18:07:42 +01:00
|
|
|
|
Task _Poller;
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
_Poller = StartPoller(_Cts.Token);
|
|
|
|
|
_Loop = StartLoop(_Cts.Token);
|
2017-12-17 06:17:42 +01:00
|
|
|
|
|
2018-01-04 14:43:28 +01:00
|
|
|
|
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
|
2018-01-08 17:56:37 +01:00
|
|
|
|
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
|
2018-01-07 18:36:41 +01:00
|
|
|
|
leases.Add(_EventAggregator.Subscribe<Events.InvoiceCreatedEvent>(b => { Watch(b.InvoiceId); }));
|
2017-12-17 06:17:42 +01:00
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-09 18:07:42 +01:00
|
|
|
|
|
|
|
|
|
private async Task StartPoller(CancellationToken cancellation)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
while (!cancellation.IsCancellationRequested)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
_WatchRequests.Add(pending);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2018-01-09 18:07:42 +01:00
|
|
|
|
await Task.Delay(PollInterval, cancellation);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2018-01-09 18:07:42 +01:00
|
|
|
|
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
Logs.PayServer.LogError(ex, $"Unhandled exception in InvoiceWatcher poller");
|
|
|
|
|
await Task.Delay(PollInterval, cancellation);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-09 18:07:42 +01:00
|
|
|
|
catch when (cancellation.IsCancellationRequested) { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task StartLoop(CancellationToken cancellation)
|
|
|
|
|
{
|
|
|
|
|
Logs.PayServer.LogInformation("Start watching invoices");
|
|
|
|
|
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
|
|
|
|
ConcurrentDictionary<string, Task> executing = new ConcurrentDictionary<string, Task>();
|
|
|
|
|
try
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
foreach (var item in _WatchRequests.GetConsumingEnumerable(cancellation))
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
var task = executing.GetOrAdd(item, async i =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await UpdateInvoice(i, cancellation);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
|
|
|
|
|
await Task.Delay(2000);
|
|
|
|
|
}
|
|
|
|
|
finally { executing.TryRemove(item, out Task useless); }
|
|
|
|
|
});
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2018-01-09 18:07:42 +01:00
|
|
|
|
}
|
|
|
|
|
catch when (cancellation.IsCancellationRequested)
|
|
|
|
|
{
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2018-01-09 18:07:42 +01:00
|
|
|
|
await Task.WhenAll(executing.Values);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
2018-01-09 18:07:42 +01:00
|
|
|
|
Logs.PayServer.LogInformation("Stop watching invoices");
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
2017-12-17 06:17:42 +01:00
|
|
|
|
leases.Dispose();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
_Cts.Cancel();
|
2018-01-09 18:07:42 +01:00
|
|
|
|
return Task.WhenAll(_Poller, _Loop);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
}
|