예제 #1
0
        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());
        }
예제 #2
0
 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());
 }
예제 #3
0
        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();
                }
            }
        }
예제 #4
0
        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;
                    }
                }
            }
        }
예제 #5
0
        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();
                }
            }
        }
예제 #6
0
        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");
        }
예제 #7
0
        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();
                }
            }
        }
예제 #8
0
        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);
                }
            }
        }
예제 #9
0
        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();
                }
            }
        }
예제 #10
0
        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;
                    }
                }
            }
        }
예제 #11
0
        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();
                    }
                }
            }
        }
예제 #12
0
        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;
                    }
                }
            }
        }