private async Task <IEnumerable <NetworkCoins> > GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies) { var getCoinsResponsesAsync = strategies .Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network))) .ToArray(); await Task.WhenAll(getCoinsResponsesAsync); var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray(); foreach (var response in getCoinsResponses) { response.TimestampedCoins = response.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray(); } return(getCoinsResponses.Where(s => s.TimestampedCoins.Length != 0).ToArray()); }
private IEnumerable <Task <NetworkCoins> > GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies) { return(strategies .Select(d => (Wallet: _WalletProvider.IsAvailable(d.Network) ? _WalletProvider.GetWallet(d.Network) : null, Network: d.Network, Strategy: d.DerivationStrategyBase)) .Where(d => d.Wallet != null) .Select(d => (Network: d.Network, Coins: d.Wallet.GetCoins(d.Strategy, context.KnownStates.TryGet(d.Network)))) .Select(async d => { var coins = await d.Coins; // Keep only coins from the invoice coins.TimestampedCoins = coins.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + d.Network.CryptoCode)).ToArray(); return coins; }) .ToArray()); }
private async Task UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime < DateTimeOffset.UtcNow) { context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); invoice.Status = InvoiceStatus.Expired; context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired)); if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) { context.Events.Add(new InvoiceEvent(invoice, 2000, 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) { return; } if (invoice.Status == InvoiceStatus.New || invoice.Status == InvoiceStatus.Expired) { if (accounting.Paid >= accounting.MinimumTotalDue) { if (invoice.Status == InvoiceStatus.New) { context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull)); invoice.Status = InvoiceStatus.Paid; invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate; context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration)); context.MarkDirty(); } } if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial; context.MarkDirty(); } } // Just make sure RBF did not cancelled a payment if (invoice.Status == InvoiceStatus.Paid) { if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver) { invoice.ExceptionStatus = InvoiceExceptionStatus.None; context.MarkDirty(); } if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver; context.MarkDirty(); } if (accounting.Paid < accounting.MinimumTotalDue) { invoice.Status = InvoiceStatus.New; invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial; context.MarkDirty(); } } if (invoice.Status == InvoiceStatus.Paid) { var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed (confirmedAccounting.Paid < accounting.MinimumTotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm)); invoice.Status = InvoiceStatus.Invalid; context.MarkDirty(); } else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); invoice.Status = InvoiceStatus.Confirmed; context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed)); context.MarkDirty(); } } if (invoice.Status == InvoiceStatus.Confirmed) { var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)); if (completedAccounting.Paid >= accounting.MinimumTotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed)); invoice.Status = InvoiceStatus.Complete; context.MarkDirty(); } } }
async Task StartLoop(CancellationToken cancellation) { Logs.PayServer.LogInformation("Start watching invoices"); while (await _WatchRequests.Reader.WaitToReadAsync(cancellation) && _WatchRequests.Reader.TryRead(out var invoiceId)) { int maxLoop = 5; int loopCount = -1; while (loopCount < maxLoop) { loopCount++; try { cancellation.ThrowIfCancellationRequested(); var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); if (invoice == null) { break; } var updateContext = new UpdateInvoiceContext(invoice); await UpdateInvoice(updateContext); if (updateContext.Dirty) { await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState()); updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); } foreach (var evt in updateContext.Events) { _EventAggregator.Publish(evt, evt.GetType()); } if (invoice.Status == InvoiceStatus.Complete || ((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { 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) { await _InvoiceRepository.ExtendInvoiceMonitor(invoice.Id); } else if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id)) { _EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id)); } break; } 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; } } } }
private async Task UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired")); invoice.Status = "expired"; } var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray(); var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider); var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting, _NetworkProvider); if (paymentMethod == null) { return; } var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); if (invoice.Status == "new" || invoice.Status == "expired") { if (accounting.Paid >= accounting.TotalDue) { if (invoice.Status == "new") { context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull")); invoice.Status = "paid"; invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate") { invoice.ExceptionStatus = "paidLate"; context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration")); context.MarkDirty(); } } if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") { invoice.ExceptionStatus = "paidPartial"; context.MarkDirty(); } } // Just make sure RBF did not cancelled a payment if (invoice.Status == "paid") { if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver") { invoice.ExceptionStatus = null; context.MarkDirty(); } if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver") { invoice.ExceptionStatus = "paidOver"; context.MarkDirty(); } if (accounting.Paid < accounting.TotalDue) { invoice.Status = "new"; invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial"; context.MarkDirty(); } } if (invoice.Status == "paid") { var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network)); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed (confirmedAccounting.Paid < accounting.TotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm")); invoice.Status = "invalid"; context.MarkDirty(); } else if (confirmedAccounting.Paid >= accounting.TotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed")); invoice.Status = "confirmed"; context.MarkDirty(); } } if (invoice.Status == "confirmed") { var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network)); if (completedAccounting.Paid >= accounting.TotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); invoice.Status = "complete"; context.MarkDirty(); } } }
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 try { foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation)) { int maxLoop = 5; int loopCount = -1; while (!cancellation.IsCancellationRequested && loopCount < maxLoop) { loopCount++; try { var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true); if (invoice == null) { break; } var updateContext = new UpdateInvoiceContext(invoice); await UpdateInvoice(updateContext); if (updateContext.Dirty) { await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus); updateContext.Events.Add(new InvoiceDataChangedEvent(invoice)); } foreach (var evt in updateContext.Events) { _EventAggregator.Publish(evt, evt.GetType()); } if (invoice.Status == "complete" || ((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id)) { _EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id)); } break; } if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested) { break; } } catch (OperationCanceledException) when(cancellation.IsCancellationRequested) { break; } catch (Exception ex) { Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Task.Delay(10000, cancellation) .ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed break; } } } } catch when(cancellation.IsCancellationRequested) { } Logs.PayServer.LogInformation("Stop watching invoices"); }
private void UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; if (invoice.Status == InvoiceStatusLegacy.New && invoice.ExpirationTime <= DateTimeOffset.UtcNow) { context.MarkDirty(); context.UnaffectAddresses(); invoice.Status = InvoiceStatusLegacy.Expired; var paidPartial = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial; context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Expired) { PaidPartial = paidPartial }); if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) { context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial) { PaidPartial = paidPartial }); } } var allPaymentMethods = invoice.GetPaymentMethods(); var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting); if (allPaymentMethods.Any() && paymentMethod == null) { return; } 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 }; } if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired) { var isPaid = invoice.IsUnsetTopUp() ? accounting.Paid > Money.Zero : accounting.Paid >= accounting.MinimumTotalDue; if (isPaid) { if (invoice.Status == InvoiceStatusLegacy.New) { context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull)); invoice.Status = InvoiceStatusLegacy.Paid; 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; } context.UnaffectAddresses(); context.MarkDirty(); } else if (invoice.Status == InvoiceStatusLegacy.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate; context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidAfterExpiration)); context.MarkDirty(); } } if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments(true).Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial; context.MarkDirty(); } } // Just make sure RBF did not cancelled a payment if (invoice.Status == InvoiceStatusLegacy.Paid) { if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver) { invoice.ExceptionStatus = InvoiceExceptionStatus.None; context.MarkDirty(); } if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver; context.MarkDirty(); } if (accounting.Paid < accounting.MinimumTotalDue) { invoice.Status = InvoiceStatusLegacy.New; invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial; context.MarkDirty(); } } if (invoice.Status == InvoiceStatusLegacy.Paid) { var confirmedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ?? accounting; if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed (confirmedAccounting.Paid < accounting.MinimumTotalDue)) { context.UnaffectAddresses(); context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm)); invoice.Status = InvoiceStatusLegacy.Invalid; context.MarkDirty(); } else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue) { context.UnaffectAddresses(); invoice.Status = InvoiceStatusLegacy.Confirmed; context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed)); context.MarkDirty(); } } if (invoice.Status == InvoiceStatusLegacy.Confirmed) { var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ?? accounting; if (completedAccounting.Paid >= accounting.MinimumTotalDue) { context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed)); invoice.Status = InvoiceStatusLegacy.Complete; context.MarkDirty(); } } }
private async Task UpdateInvoice(string invoiceId) { Dictionary <BTCPayNetwork, KnownState> changes = new Dictionary <BTCPayNetwork, KnownState>(); while (true) { try { var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false); if (invoice == null) { break; } var stateBefore = invoice.Status; var updateContext = new UpdateInvoiceContext() { Invoice = invoice, KnownStates = changes }; await UpdateInvoice(updateContext).ConfigureAwait(false); if (updateContext.Dirty) { await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false); _EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id }); } var changed = stateBefore != invoice.Status; foreach (var evt in updateContext.Events) { _EventAggregator.Publish(evt, evt.GetType()); } foreach (var modifiedKnownState in updateContext.ModifiedKnownStates) { changes.AddOrReplace(modifiedKnownState.Key, modifiedKnownState.Value); } if (invoice.Status == "complete" || ((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false)) { Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); } break; } if (!changed || _Cts.Token.IsCancellationRequested) { break; } } catch (OperationCanceledException) when(_Cts.Token.IsCancellationRequested) { break; } catch (Exception ex) { Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId); await Task.Delay(10000, _Cts.Token).ConfigureAwait(false); } } }
private async Task UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; //Fetch unknown payments var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray(); var getCoinsResponsesAsync = strategies .Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network), _Cts.Token)) .ToArray(); await Task.WhenAll(getCoinsResponsesAsync); var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray(); foreach (var response in getCoinsResponses) { response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray(); } var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault(); bool dirtyAddress = false; if (coins != null) { context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State); var alreadyAccounted = new HashSet <OutPoint>(invoice.Payments.Select(p => p.Outpoint)); foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint))) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false); invoice.Payments.Add(payment); context.Events.Add(new InvoicePaymentEvent(invoice.Id)); dirtyAddress = true; } } ////// var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key); var cryptoData = invoice.GetCryptoData(network); var cryptoDataAll = invoice.GetCryptoData(); var accounting = cryptoData.Calculate(); if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired")); invoice.Status = "expired"; } if (invoice.Status == "new" || invoice.Status == "expired") { var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalPaid >= accounting.TotalDue) { if (invoice.Status == "new") { context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid")); invoice.Status = "paid"; invoice.ExceptionStatus = null; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } else if (invoice.Status == "expired") { invoice.ExceptionStatus = "paidLate"; context.MarkDirty(); } } if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver") { invoice.ExceptionStatus = "paidOver"; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") { invoice.ExceptionStatus = "paidPartial"; context.MarkDirty(); if (dirtyAddress) { var address = await _Wallet.ReserveAddressAsync(coins.Strategy); Logs.PayServer.LogInformation("Generate new " + address); await _InvoiceRepository.NewAddress(invoice.Id, address, network); } } } if (invoice.Status == "paid") { var transactions = await GetPaymentsWithTransaction(network, invoice); if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) { transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF); } 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); } var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed (totalConfirmed < accounting.TotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid")); invoice.Status = "invalid"; context.MarkDirty(); } else if (totalConfirmed >= accounting.TotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed")); invoice.Status = "confirmed"; context.MarkDirty(); } } if (invoice.Status == "confirmed") { var transactions = await GetPaymentsWithTransaction(network, invoice); transactions = transactions.Where(t => t.Confirmations >= 6); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalConfirmed >= accounting.TotalDue) { context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete")); invoice.Status = "complete"; context.MarkDirty(); } } }
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 foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation)) { int maxLoop = 5; int loopCount = -1; while (loopCount < maxLoop) { loopCount++; try { cancellation.ThrowIfCancellationRequested(); var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); if (invoice == null) { break; } var updateContext = new UpdateInvoiceContext(invoice); await UpdateInvoice(updateContext); if (updateContext.Dirty) { await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState()); updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); } foreach (var evt in updateContext.Events) { _EventAggregator.Publish(evt, evt.GetType()); } if (invoice.Status == InvoiceStatus.Complete || ((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id)) { _EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id)); } break; } 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; } } } }
private async Task UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired")); invoice.Status = "expired"; } var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray(); var payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); foreach (Task <NetworkCoins> coinsAsync in GetCoinsPerNetwork(context, invoice, derivationStrategies)) { var coins = await coinsAsync; if (coins.TimestampedCoins.Length == 0) { continue; } bool dirtyAddress = false; if (coins.State != null) { context.ModifiedKnownStates.AddOrReplace(coins.Wallet.Network, coins.State); } var alreadyAccounted = new HashSet <OutPoint>(invoice.GetPayments(coins.Wallet.Network).Select(p => p.Outpoint)); foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint))) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Wallet.Network.CryptoCode).ConfigureAwait(false); #pragma warning disable CS0618 invoice.Payments.Add(payment); #pragma warning restore CS0618 alreadyAccounted.Add(coin.Coin.Outpoint); context.Events.Add(new InvoiceEvent(invoice, 1002, "invoice_receivedPayment")); dirtyAddress = true; } if (dirtyAddress) { payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); } var network = coins.Wallet.Network; var cryptoData = invoice.GetCryptoData(network, _NetworkProvider); var cryptoDataAll = invoice.GetCryptoData(_NetworkProvider); var accounting = cryptoData.Calculate(); if (invoice.Status == "new" || invoice.Status == "expired") { var totalPaid = payments.Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalPaid >= accounting.TotalDue) { if (invoice.Status == "new") { context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull")); invoice.Status = "paid"; invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null; await _InvoiceRepository.UnaffectAddress(invoice.Id); context.MarkDirty(); } else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate") { invoice.ExceptionStatus = "paidLate"; context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration")); context.MarkDirty(); } } if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial") { invoice.ExceptionStatus = "paidPartial"; context.MarkDirty(); if (dirtyAddress) { var address = await coins.Wallet.ReserveAddressAsync(coins.Strategy); Logs.PayServer.LogInformation("Generate new " + address); await _InvoiceRepository.NewAddress(invoice.Id, address, network); } } } if (invoice.Status == "paid") { IEnumerable <AccountedPaymentEntity> transactions = payments; if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) { transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF); } 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); } var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed (totalConfirmed < accounting.TotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm")); invoice.Status = "invalid"; context.MarkDirty(); } else if (totalConfirmed >= accounting.TotalDue) { await _InvoiceRepository.UnaffectAddress(invoice.Id); context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed")); invoice.Status = "confirmed"; context.MarkDirty(); } } if (invoice.Status == "confirmed") { IEnumerable <AccountedPaymentEntity> transactions = payments; transactions = transactions.Where(t => t.Confirmations >= 6); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalConfirmed >= accounting.TotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); invoice.Status = "complete"; context.MarkDirty(); } } } }
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 foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation)) { int maxLoop = 5; int loopCount = -1; while (loopCount < maxLoop) { loopCount++; try { cancellation.ThrowIfCancellationRequested(); var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); if (invoice == null) { break; } var updateContext = new UpdateInvoiceContext(invoice); await UpdateInvoice(updateContext); if (updateContext.Dirty) { await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState()); updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); } foreach (var evt in updateContext.Events) { _EventAggregator.Publish(evt, evt.GetType()); } if (invoice.Status == InvoiceStatus.Complete || ((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { var updateConfirmationCountIfNeeded = invoice .GetPayments() .Select <PaymentEntity, Task>(async payment => { var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode()); var paymentData = payment.GetCryptoPaymentData(); if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData) { // Do update if confirmation count in the paymentData is not up to date if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted) && (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { var transactionResult = await _ExplorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash); var confirmationCount = transactionResult?.Confirmations ?? 0; onChainPaymentData.ConfirmationCount = confirmationCount; payment.SetCryptoPaymentData(onChainPaymentData); await _InvoiceRepository.UpdatePayments(new List <PaymentEntity> { payment }); } } }) .ToArray(); await Task.WhenAll(updateConfirmationCountIfNeeded); if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id)) { _EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id)); } break; } 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; } } } }