diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 1025bf314..7aab999ad 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.0.21 + 1.0.0.22 diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 6f31d20ef..023b2e654 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -75,8 +75,9 @@ namespace BTCPayServer.Controllers _FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider)); } - internal async Task> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15) + internal async Task> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60) { + //TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level var derivationStrategy = store.DerivationStrategy; var entity = new InvoiceEntity { @@ -88,6 +89,7 @@ namespace BTCPayServer.Controllers notificationUri = null; EmailAddressAttribute emailValidator = new EmailAddressAttribute(); entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes); + entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes); entity.OrderId = invoice.OrderId; entity.ServerUrl = serverUrl; entity.FullNotifications = invoice.FullNotifications; diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 2c690bdde..67ae72691 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -248,6 +248,11 @@ namespace BTCPayServer.Services.Invoices get; set; } + public DateTimeOffset? MonitoringExpiration + { + get; + set; + } public bool IsExpired() { diff --git a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs index 8a57e29d0..1cae8f1e0 100644 --- a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs +++ b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs @@ -80,7 +80,9 @@ namespace BTCPayServer.Services.Invoices Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}"); } - if(invoice.Status == "complete" || invoice.Status == "invalid") + var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60); + if(invoice.Status == "complete" || + ((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow)) { if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false)) Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); @@ -106,56 +108,56 @@ namespace BTCPayServer.Services.Invoices private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice) { bool needSave = false; - + //Fetch unknown payments + var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); + changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false); + + var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); + var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray(); + utxos = + utxos + .Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id) + .ToArray(); + + List receivedCoins = new List(); + foreach(var received in utxos) + if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey) + receivedCoins.Add(new Coin(received.Outpoint, received.Output)); + + var alreadyAccounted = new HashSet(invoice.Payments.Select(p => p.Outpoint)); + foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint))) + { + var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false); + invoice.Payments.Add(payment); + } + ////// + if(invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { needSave = true; invoice.Status = "expired"; } - if(invoice.Status == "expired" || invoice.Status == "new" || invoice.Status == "invalid") - { - var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); - changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false); - - var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); - var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray(); - utxos = - utxos - .Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id) - .ToArray(); - - List receivedCoins = new List(); - foreach(var received in utxos) - if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey) - receivedCoins.Add(new Coin(received.Outpoint, received.Output)); - - var alreadyAccounted = new HashSet(invoice.Payments.Select(p => p.Outpoint)); - foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint))) - { - var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false); - invoice.Payments.Add(payment); - if(invoice.Status == "expired") - { - if(invoice.ExceptionStatus == null) - invoice.ExceptionStatus = "paidLate"; - needSave = true; - } - } - } - - if(invoice.Status == "new") + if(invoice.Status == "new" || invoice.Status == "expired") { var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum(); if(totalPaid >= invoice.GetTotalCryptoDue()) { - invoice.Status = "paid"; - if(invoice.FullNotifications) + if(invoice.Status == "new") { - _NotificationManager.Notify(invoice); + invoice.Status = "paid"; + if(invoice.FullNotifications) + { + _NotificationManager.Notify(invoice); + } + invoice.ExceptionStatus = null; + needSave = true; + } + else if(invoice.Status == "expired") + { + invoice.ExceptionStatus = "paidLate"; + needSave = true; } - invoice.ExceptionStatus = null; - needSave = true; } if(totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver") @@ -173,25 +175,33 @@ namespace BTCPayServer.Services.Invoices if(invoice.Status == "paid") { - var transactions = await GetPaymentsWithTransaction(invoice); - if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed) + if(!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow) { - transactions = transactions.Where(t => !t.Transaction.Transaction.RBF); - } - else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed) - { - transactions = transactions.Where(t => t.Transaction.Confirmations >= 1); - } - else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed) - { - transactions = transactions.Where(t => t.Transaction.Confirmations >= 6); - } + var transactions = await GetPaymentsWithTransaction(invoice); + if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed) + { + transactions = transactions.Where(t => !t.Transaction.Transaction.RBF); + } + else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed) + { + transactions = transactions.Where(t => t.Transaction.Confirmations >= 1); + } + else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed) + { + transactions = transactions.Where(t => t.Transaction.Confirmations >= 6); + } - var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); - if(totalConfirmed >= invoice.GetTotalCryptoDue()) + var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); + if(totalConfirmed >= invoice.GetTotalCryptoDue()) + { + invoice.Status = "confirmed"; + _NotificationManager.Notify(invoice); + needSave = true; + } + } + else { - invoice.Status = "confirmed"; - _NotificationManager.Notify(invoice); + invoice.Status = "invalid"; needSave = true; } }