From 6e3d6125c2dfdb5eec6bd8485d4ebee6b970c89c Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 5 Oct 2021 11:10:41 +0200 Subject: [PATCH] Payment Settled Webhook event (#2944) * Payment Settled Webhook event resolves #2691 * Move payment methods to payment services --- .../Models/WebhookEventType.cs | 3 +- .../Models/WebhookInvoiceEvent.cs | 36 ++-- BTCPayServer.Tests/UnitTest1.cs | 14 ++ .../GreenField/InvoiceController.cs | 37 ++-- BTCPayServer/Data/InvoiceDataExtensions.cs | 1 - BTCPayServer/Events/InvoiceEvent.cs | 3 + .../InvoiceNotificationManager.cs | 5 + BTCPayServer/HostedServices/InvoiceWatcher.cs | 45 ++--- .../WebhookNotificationManager.cs | 14 +- BTCPayServer/Hosting/BTCPayServerServices.cs | 7 +- .../Payments/Bitcoin/NBXplorerListener.cs | 12 +- .../Payments/Lightning/LightningListener.cs | 46 ++--- .../PayJoin/PayJoinEndpointController.cs | 7 +- .../Ethereum/Services/EthereumService.cs | 7 +- .../Ethereum/Services/EthereumWatcher.cs | 14 +- .../Monero/Services/MoneroListener.cs | 11 +- .../Services/Invoices/InvoiceRepository.cs | 162 +++++------------- .../Services/Invoices/PaymentService.cs | 129 ++++++++++++++ .../Views/Stores/ModifyWebhook.cshtml | 1 + .../swagger/v1/swagger.template.webhooks.json | 71 +++++++- 20 files changed, 401 insertions(+), 224 deletions(-) create mode 100644 BTCPayServer/Services/Invoices/PaymentService.cs diff --git a/BTCPayServer.Client/Models/WebhookEventType.cs b/BTCPayServer.Client/Models/WebhookEventType.cs index e7b622e56..e4b6e6458 100644 --- a/BTCPayServer.Client/Models/WebhookEventType.cs +++ b/BTCPayServer.Client/Models/WebhookEventType.cs @@ -11,6 +11,7 @@ namespace BTCPayServer.Client.Models InvoiceProcessing, InvoiceExpired, InvoiceSettled, - InvoiceInvalid + InvoiceInvalid, + InvoicePaymentSettled, } } diff --git a/BTCPayServer.Client/Models/WebhookInvoiceEvent.cs b/BTCPayServer.Client/Models/WebhookInvoiceEvent.cs index 3f12e2935..29a81078e 100644 --- a/BTCPayServer.Client/Models/WebhookInvoiceEvent.cs +++ b/BTCPayServer.Client/Models/WebhookInvoiceEvent.cs @@ -10,72 +10,88 @@ namespace BTCPayServer.Client.Models { public WebhookInvoiceEvent() { - } + public WebhookInvoiceEvent(WebhookEventType evtType) { this.Type = evtType; } - [JsonProperty(Order = 1)] - public string StoreId { get; set; } - [JsonProperty(Order = 2)] - public string InvoiceId { get; set; } + + [JsonProperty(Order = 1)] public string StoreId { get; set; } + [JsonProperty(Order = 2)] public string InvoiceId { get; set; } } public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent { public WebhookInvoiceSettledEvent() { - } + public WebhookInvoiceSettledEvent(WebhookEventType evtType) : base(evtType) { } public bool ManuallyMarked { get; set; } } + public class WebhookInvoiceInvalidEvent : WebhookInvoiceEvent { public WebhookInvoiceInvalidEvent() { - } + public WebhookInvoiceInvalidEvent(WebhookEventType evtType) : base(evtType) { } public bool ManuallyMarked { get; set; } } + public class WebhookInvoiceProcessingEvent : WebhookInvoiceEvent { public WebhookInvoiceProcessingEvent() { - } + public WebhookInvoiceProcessingEvent(WebhookEventType evtType) : base(evtType) { } public bool OverPaid { get; set; } } + public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent { public WebhookInvoiceReceivedPaymentEvent() { - } + public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType) { } public bool AfterExpiration { get; set; } + public string PaymentMethod { get; set; } + public InvoicePaymentMethodDataModel.Payment Payment { get; set; } } + + public class WebhookInvoicePaymentSettledEvent : WebhookInvoiceReceivedPaymentEvent + { + public WebhookInvoicePaymentSettledEvent() + { + } + + public WebhookInvoicePaymentSettledEvent(WebhookEventType evtType) : base(evtType) + { + } + } + public class WebhookInvoiceExpiredEvent : WebhookInvoiceEvent { public WebhookInvoiceExpiredEvent() { - } + public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType) { } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 8b2d4d39a..5f50323c1 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -3119,6 +3119,20 @@ namespace BTCPayServer.Tests c => { Assert.False(c.AfterExpiration); + Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(),c.PaymentMethod); + Assert.NotNull(c.Payment); + Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); + Assert.StartsWith(txId.ToString(), c.Payment.Id); + + }); + user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled, + c => + { + Assert.False(c.AfterExpiration); + Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(),c.PaymentMethod); + Assert.NotNull(c.Payment); + Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); + Assert.StartsWith(txId.ToString(), c.Payment.Id); }); } } diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index f237c11a2..6a5c509ae 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -363,27 +363,28 @@ namespace BTCPayServer.Controllers.GreenField PaymentLink = method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due, Request.GetAbsoluteRoot()), - Payments = payments.Select(paymentEntity => - { - var data = paymentEntity.GetCryptoPaymentData(); - return new InvoicePaymentMethodDataModel.Payment() - { - Destination = data.GetDestination(), - Id = data.GetPaymentId(), - Status = !paymentEntity.Accounted - ? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid - : data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) || - data.PaymentCompleted(paymentEntity) - ? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled - : InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing, - Fee = paymentEntity.NetworkFee, - Value = data.GetValue(), - ReceivedDate = paymentEntity.ReceivedTime.DateTime - }; - }).ToList() + Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList() }; }).ToArray(); } + + public static InvoicePaymentMethodDataModel.Payment ToPaymentModel(InvoiceEntity entity, PaymentEntity paymentEntity) + { + var data = paymentEntity.GetCryptoPaymentData(); + return new InvoicePaymentMethodDataModel.Payment() + { + Destination = data.GetDestination(), + Id = data.GetPaymentId(), + Status = !paymentEntity.Accounted + ? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid + : data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) || data.PaymentCompleted(paymentEntity) + ? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled + : InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing, + Fee = paymentEntity.NetworkFee, + Value = data.GetValue(), + ReceivedDate = paymentEntity.ReceivedTime.DateTime + }; + } private InvoiceData ToModel(InvoiceEntity entity) { return new InvoiceData() diff --git a/BTCPayServer/Data/InvoiceDataExtensions.cs b/BTCPayServer/Data/InvoiceDataExtensions.cs index 16bb3fd25..d1c865a80 100644 --- a/BTCPayServer/Data/InvoiceDataExtensions.cs +++ b/BTCPayServer/Data/InvoiceDataExtensions.cs @@ -1,5 +1,4 @@ using BTCPayServer.Services.Invoices; -using Microsoft.AspNetCore.Authorization.Infrastructure; namespace BTCPayServer.Data { diff --git a/BTCPayServer/Events/InvoiceEvent.cs b/BTCPayServer/Events/InvoiceEvent.cs index 783f2e0be..1d3f75e2a 100644 --- a/BTCPayServer/Events/InvoiceEvent.cs +++ b/BTCPayServer/Events/InvoiceEvent.cs @@ -7,6 +7,7 @@ namespace BTCPayServer.Events { Created = 1001, ReceivedPayment = 1002, + PaymentSettled = 1014, PaidInFull = 1003, Expired = 1004, Confirmed = 1005, @@ -21,6 +22,7 @@ namespace BTCPayServer.Events { public const string Created = "invoice_created"; public const string ReceivedPayment = "invoice_receivedPayment"; + public const string PaymentSettled = "invoice_paymentSettled"; public const string MarkedCompleted = "invoice_markedComplete"; public const string MarkedInvalid = "invoice_markedInvalid"; public const string Expired = "invoice_expired"; @@ -36,6 +38,7 @@ namespace BTCPayServer.Events { {Created, InvoiceEventCode.Created}, {ReceivedPayment, InvoiceEventCode.ReceivedPayment}, + {PaymentSettled, InvoiceEventCode.PaymentSettled}, {PaidInFull, InvoiceEventCode.PaidInFull}, {Expired, InvoiceEventCode.Expired}, {Confirmed, InvoiceEventCode.Confirmed}, diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 14d9aa88f..a13913f9f 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -311,6 +311,11 @@ namespace BTCPayServer.HostedServices { leases.Add(_EventAggregator.Subscribe(async e => { + if (e.EventCode == InvoiceEventCode.PaymentSettled) + { + //these are greenfield specific events + return; + } var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id); if (invoice == null) return; diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 299300c2d..e993637c1 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -52,21 +52,24 @@ namespace BTCPayServer.HostedServices } } - readonly InvoiceRepository _InvoiceRepository; - readonly EventAggregator _EventAggregator; - readonly ExplorerClientProvider _ExplorerClientProvider; + readonly InvoiceRepository _invoiceRepository; + readonly EventAggregator _eventAggregator; + readonly ExplorerClientProvider _explorerClientProvider; private readonly NotificationSender _notificationSender; + private readonly PaymentService _paymentService; public InvoiceWatcher( InvoiceRepository invoiceRepository, EventAggregator eventAggregator, ExplorerClientProvider explorerClientProvider, - NotificationSender notificationSender) + NotificationSender notificationSender, + PaymentService paymentService) { - _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); - _EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); - _ExplorerClientProvider = explorerClientProvider; + _invoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _explorerClientProvider = explorerClientProvider; _notificationSender = notificationSender; + _paymentService = paymentService; } readonly CompositeDisposable leases = new CompositeDisposable(); @@ -239,7 +242,7 @@ namespace BTCPayServer.HostedServices private async Task Wait(string invoiceId) { - var invoice = await _InvoiceRepository.GetInvoice(invoiceId); + var invoice = await _invoiceRepository.GetInvoice(invoiceId); try { // add 1 second to ensure watch won't trigger moments before invoice expires @@ -274,11 +277,11 @@ namespace BTCPayServer.HostedServices _Loop = StartLoop(_Cts.Token); _ = WaitPendingInvoices(); - leases.Add(_EventAggregator.Subscribe(b => + leases.Add(_eventAggregator.Subscribe(b => { Watch(b.InvoiceId); })); - leases.Add(_EventAggregator.Subscribe(async b => + leases.Add(_eventAggregator.Subscribe(async b => { if (InvoiceEventNotification.HandlesEvent(b.Name)) { @@ -301,7 +304,7 @@ namespace BTCPayServer.HostedServices private async Task WaitPendingInvoices() { - await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices()) + await Task.WhenAll((await _invoiceRepository.GetPendingInvoices()) .Select(id => Wait(id)).ToArray()); } @@ -318,28 +321,28 @@ namespace BTCPayServer.HostedServices try { cancellation.ThrowIfCancellationRequested(); - var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); + var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); if (invoice == null) break; var updateContext = new UpdateInvoiceContext(invoice); UpdateInvoice(updateContext); if (updateContext.Unaffect) { - await _InvoiceRepository.UnaffectAddress(invoice.Id); + await _invoiceRepository.UnaffectAddress(invoice.Id); } if (updateContext.Dirty) { - await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState()); + await _invoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState()); updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); } if (updateContext.IsBlobUpdated) { - await _InvoiceRepository.UpdateInvoicePrice(invoice.Id, invoice); + await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice); } foreach (var evt in updateContext.Events) { - _EventAggregator.Publish(evt, evt.GetType()); + _eventAggregator.Publish(evt, evt.GetType()); } if (invoice.Status == InvoiceStatusLegacy.Complete || @@ -351,11 +354,11 @@ namespace BTCPayServer.HostedServices // 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); + await _invoiceRepository.ExtendInvoiceMonitor(invoice.Id); } - else if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id)) + else if (await _invoiceRepository.RemovePendingInvoice(invoice.Id)) { - _EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id)); + _eventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id)); } break; } @@ -389,7 +392,7 @@ namespace BTCPayServer.HostedServices if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted) && (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { - var transactionResult = await _ExplorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash); + var transactionResult = await _explorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash); var confirmationCount = transactionResult?.Confirmations ?? 0; onChainPaymentData.ConfirmationCount = confirmationCount; payment.SetCryptoPaymentData(onChainPaymentData); @@ -408,7 +411,7 @@ namespace BTCPayServer.HostedServices var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList(); if (updatedPaymentData.Count > 0) { - await _InvoiceRepository.UpdatePayments(updatedPaymentData); + await _paymentService.UpdatePayments(updatedPaymentData); } return extendInvoiceMonitoring; diff --git a/BTCPayServer/HostedServices/WebhookNotificationManager.cs b/BTCPayServer/HostedServices/WebhookNotificationManager.cs index 1bf72cbd9..0b3ccdf10 100644 --- a/BTCPayServer/HostedServices/WebhookNotificationManager.cs +++ b/BTCPayServer/HostedServices/WebhookNotificationManager.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using BTCPayServer.Client.Models; +using BTCPayServer.Controllers.GreenField; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Logging; @@ -183,6 +184,8 @@ namespace BTCPayServer.HostedServices return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated); case WebhookEventType.InvoiceReceivedPayment: return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment); + case WebhookEventType.InvoicePaymentSettled: + return new WebhookInvoicePaymentSettledEvent(WebhookEventType.InvoicePaymentSettled); case WebhookEventType.InvoiceProcessing: return new WebhookInvoiceProcessingEvent(WebhookEventType.InvoiceProcessing); case WebhookEventType.InvoiceExpired: @@ -232,7 +235,16 @@ namespace BTCPayServer.HostedServices case InvoiceEventCode.ReceivedPayment: return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment) { - AfterExpiration = invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Expired || invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Invalid + AfterExpiration = invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Expired || invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Invalid, + PaymentMethod = invoiceEvent.Payment.GetPaymentMethodId().ToStringNormalized(), + Payment = GreenFieldInvoiceController.ToPaymentModel(invoiceEvent.Invoice, invoiceEvent.Payment) + }; + case InvoiceEventCode.PaymentSettled: + return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoicePaymentSettled) + { + AfterExpiration = invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Expired || invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Invalid, + PaymentMethod = invoiceEvent.Payment.GetPaymentMethodId().ToStringNormalized(), + Payment = GreenFieldInvoiceController.ToPaymentModel(invoiceEvent.Invoice, invoiceEvent.Payment) }; default: return null; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index d8242ce42..239b9e6a9 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -102,11 +102,8 @@ namespace BTCPayServer.Hosting services.AddStartupTask(); // services.AddStartupTask(); - services.TryAddSingleton(o => - { - var dbContext = o.GetRequiredService(); - return new InvoiceRepository(dbContext, o.GetRequiredService(), o.GetService()); - }); + services.TryAddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 5179a7a1d..65c2338f2 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -29,7 +29,7 @@ namespace BTCPayServer.Payments.Bitcoin readonly EventAggregator _Aggregator; private readonly PayJoinRepository _payJoinRepository; readonly ExplorerClientProvider _ExplorerClients; - readonly IHostApplicationLifetime _Lifetime; + private readonly PaymentService _paymentService; readonly InvoiceRepository _InvoiceRepository; private TaskCompletionSource _RunningTask; private CancellationTokenSource _Cts; @@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin InvoiceRepository invoiceRepository, EventAggregator aggregator, PayJoinRepository payjoinRepository, - IHostApplicationLifetime lifetime) + PaymentService paymentService) { PollInterval = TimeSpan.FromMinutes(1.0); _Wallets = wallets; @@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Bitcoin _ExplorerClients = explorerClients; _Aggregator = aggregator; _payJoinRepository = payjoinRepository; - _Lifetime = lifetime; + _paymentService = paymentService; } readonly CompositeDisposable leases = new CompositeDisposable(); @@ -167,7 +167,7 @@ namespace BTCPayServer.Payments.Bitcoin .GetAllBitcoinPaymentData(false).Any(c => c.GetPaymentId() == paymentData.GetPaymentId()); if (!alreadyExist) { - var payment = await _InvoiceRepository.AddPayment(invoice.Id, + var payment = await _paymentService.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); if (payment != null) await ReceivedPayment(wallet, invoice, payment, @@ -341,7 +341,7 @@ namespace BTCPayServer.Payments.Bitcoin await _payJoinRepository.TryUnlock(payjoinInformation.ContributedOutPoints); } - await _InvoiceRepository.UpdatePayments(updatedPaymentEntities); + await _paymentService.UpdatePayments(updatedPaymentEntities); if (updatedPaymentEntities.Count != 0) _Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); return invoice; @@ -383,7 +383,7 @@ namespace BTCPayServer.Payments.Bitcoin var paymentData = new BitcoinLikePaymentData(address, coin.Value, coin.OutPoint, transaction?.Transaction is null ? true : transaction.Transaction.RBF, coin.KeyPath); - var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false); + var payment = await _paymentService.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false); alreadyAccounted.Add(coin.OutPoint); if (payment != null) { diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index 7cb133162..98480c95c 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -31,6 +31,7 @@ namespace BTCPayServer.Payments.Lightning private readonly LightningClientFactoryService lightningClientFactory; private readonly LightningLikePaymentHandler _lightningLikePaymentHandler; private readonly StoreRepository _storeRepository; + private readonly PaymentService _paymentService; readonly Channel _CheckInvoices = Channel.CreateUnbounded(); Task _CheckingInvoice; readonly Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>(); @@ -42,7 +43,8 @@ namespace BTCPayServer.Payments.Lightning LightningClientFactoryService lightningClientFactory, LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository, - IOptions options) + IOptions options, + PaymentService paymentService) { _Aggregator = aggregator; _InvoiceRepository = invoiceRepository; @@ -51,6 +53,7 @@ namespace BTCPayServer.Payments.Lightning this.lightningClientFactory = lightningClientFactory; _lightningLikePaymentHandler = lightningLikePaymentHandler; _storeRepository = storeRepository; + _paymentService = paymentService; Options = options; } @@ -67,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning if (!_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener) || !instanceListener.IsListening) { - instanceListener ??= new LightningInstanceListener(_InvoiceRepository, _Aggregator, lightningClientFactory, listenedInvoice.Network, GetLightningUrl(listenedInvoice.SupportedPaymentMethod)); + instanceListener ??= new LightningInstanceListener(_InvoiceRepository, _Aggregator, lightningClientFactory, listenedInvoice.Network, GetLightningUrl(listenedInvoice.SupportedPaymentMethod), _paymentService); var status = await instanceListener.PollPayment(listenedInvoice, cancellation); if (status is null || status is LightningInvoiceStatus.Paid || @@ -309,9 +312,10 @@ namespace BTCPayServer.Payments.Lightning public class LightningInstanceListener { - private readonly InvoiceRepository invoiceRepository; + private readonly InvoiceRepository _invoiceRepository; private readonly EventAggregator _eventAggregator; - private readonly BTCPayNetwork network; + private readonly BTCPayNetwork _network; + private readonly PaymentService _paymentService; private readonly LightningClientFactoryService _lightningClientFactory; public LightningConnectionString ConnectionString { get; } @@ -320,13 +324,15 @@ namespace BTCPayServer.Payments.Lightning EventAggregator eventAggregator, LightningClientFactoryService lightningClientFactory, BTCPayNetwork network, - LightningConnectionString connectionString) + LightningConnectionString connectionString, + PaymentService paymentService) { if (connectionString == null) throw new ArgumentNullException(nameof(connectionString)); - this.invoiceRepository = invoiceRepository; + this._invoiceRepository = invoiceRepository; _eventAggregator = eventAggregator; - this.network = network; + this._network = network; + _paymentService = paymentService; _lightningClientFactory = lightningClientFactory; ConnectionString = connectionString; } @@ -337,12 +343,12 @@ namespace BTCPayServer.Payments.Lightning internal async Task PollPayment(ListenedInvoice listenedInvoice, CancellationToken cancellation) { - var client = _lightningClientFactory.Create(ConnectionString, network); + var client = _lightningClientFactory.Create(ConnectionString, _network); LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId); if (lightningInvoice?.Status is LightningInvoiceStatus.Paid && await AddPayment(lightningInvoice, listenedInvoice.InvoiceId)) { - Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}"); + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}"); } return lightningInvoice?.Status; } @@ -360,17 +366,17 @@ namespace BTCPayServer.Payments.Lightning public CancellationTokenSource StopListeningCancellationTokenSource; async Task Listen(CancellationToken cancellation) { - Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Start listening {ConnectionString.BaseUri}"); + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Start listening {ConnectionString.BaseUri}"); try { - var lightningClient = _lightningClientFactory.Create(ConnectionString, network); + var lightningClient = _lightningClientFactory.Create(ConnectionString, _network); using (var session = await lightningClient.Listen(cancellation)) { // Just in case the payment arrived after our last poll but before we listened. await PollAllListenedInvoices(cancellation); if (_ErrorAlreadyLogged) { - Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Could reconnect successfully to {ConnectionString.BaseUri}"); + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Could reconnect successfully to {ConnectionString.BaseUri}"); } _ErrorAlreadyLogged = false; while (!_ListenedInvoices.IsEmpty) @@ -386,7 +392,7 @@ namespace BTCPayServer.Payments.Lightning { if (await AddPayment(notification, listenedInvoice.InvoiceId)) { - Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})"); + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})"); } _ListenedInvoices.TryRemove(notification.Id, out var _); } @@ -401,12 +407,12 @@ namespace BTCPayServer.Payments.Lightning catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged) { _ErrorAlreadyLogged = true; - Logs.PayServer.LogError(ex, $"{network.CryptoCode} (Lightning): Error while contacting {ConnectionString.BaseUri}"); - Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Stop listening {ConnectionString.BaseUri}"); + Logs.PayServer.LogError(ex, $"{_network.CryptoCode} (Lightning): Error while contacting {ConnectionString.BaseUri}"); + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Stop listening {ConnectionString.BaseUri}"); } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { } if (_ListenedInvoices.IsEmpty) - Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): No more invoice to listen on {ConnectionString.BaseUri}, releasing the connection."); + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): No more invoice to listen on {ConnectionString.BaseUri}, releasing the connection."); } public DateTimeOffset? LastFullPoll { get; set; } @@ -433,15 +439,15 @@ namespace BTCPayServer.Payments.Lightning public async Task AddPayment(LightningInvoice notification, string invoiceId) { - var payment = await invoiceRepository.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData() + var payment = await _paymentService.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData() { BOLT11 = notification.BOLT11, - PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash, + PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, _network.NBitcoinNetwork).PaymentHash, Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable - }, network, accounted: true); + }, _network, accounted: true); if (payment != null) { - var invoice = await invoiceRepository.GetInvoice(invoiceId); + var invoice = await _invoiceRepository.GetInvoice(invoiceId); if (invoice != null) _eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); } diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index efb322f41..c90e4c311 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -91,6 +91,7 @@ namespace BTCPayServer.Payments.PayJoin private readonly BTCPayServerEnvironment _env; private readonly WalletReceiveService _walletReceiveService; private readonly StoreRepository _storeRepository; + private readonly PaymentService _paymentService; public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider, InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider, @@ -101,7 +102,8 @@ namespace BTCPayServer.Payments.PayJoin DelayedTransactionBroadcaster broadcaster, BTCPayServerEnvironment env, WalletReceiveService walletReceiveService, - StoreRepository storeRepository) + StoreRepository storeRepository, + PaymentService paymentService) { _btcPayNetworkProvider = btcPayNetworkProvider; _invoiceRepository = invoiceRepository; @@ -114,6 +116,7 @@ namespace BTCPayServer.Payments.PayJoin _env = env; _walletReceiveService = walletReceiveService; _storeRepository = storeRepository; + _paymentService = paymentService; } [HttpPost("")] @@ -487,7 +490,7 @@ namespace BTCPayServer.Payments.PayJoin }; if (invoice != null) { - var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); + var payment = await _paymentService.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { return UnprocessableEntity(CreatePayjoinError("already-paid", diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumService.cs b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumService.cs index 8360e233b..ac90b9404 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumService.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumService.cs @@ -29,6 +29,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services private readonly SettingsRepository _settingsRepository; private readonly InvoiceRepository _invoiceRepository; private readonly IConfiguration _configuration; + private readonly PaymentService _paymentService; private readonly Dictionary _chainHostedServices = new Dictionary(); private readonly Dictionary _chainHostedServiceCancellationTokenSources = @@ -41,7 +42,8 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services BTCPayNetworkProvider btcPayNetworkProvider, SettingsRepository settingsRepository, InvoiceRepository invoiceRepository, - IConfiguration configuration) : base( + IConfiguration configuration, + PaymentService paymentService) : base( eventAggregator) { _httpClientFactory = httpClientFactory; @@ -51,6 +53,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services _settingsRepository = settingsRepository; _invoiceRepository = invoiceRepository; _configuration = configuration; + _paymentService = paymentService; } public override async Task StartAsync(CancellationToken cancellationToken) @@ -186,7 +189,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services _chainHostedServiceCancellationTokenSources.AddOrReplace(ethereumLikeConfiguration.ChainId, cts); _chainHostedServices.AddOrReplace(ethereumLikeConfiguration.ChainId, new EthereumWatcher(ethereumLikeConfiguration.ChainId, ethereumLikeConfiguration, - _btcPayNetworkProvider, _eventAggregator, _invoiceRepository)); + _btcPayNetworkProvider, _eventAggregator, _invoiceRepository, _paymentService)); await _chainHostedServices[ethereumLikeConfiguration.ChainId].StartAsync(CancellationTokenSource .CreateLinkedTokenSource(cancellationToken, cts.Token).Token); } diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs index a96a5908e..5e97d2850 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs @@ -24,6 +24,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services { private readonly EventAggregator _eventAggregator; private readonly InvoiceRepository _invoiceRepository; + private readonly PaymentService _paymentService; private int ChainId { get; } private readonly HashSet PaymentMethods; @@ -113,7 +114,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services AccountIndex = response.PaymentMethodDetails.Index, XPub = response.PaymentMethodDetails.XPub }; - var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, + var payment = await _paymentService.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network, true); if (payment != null) ReceivedPayment(invoice, payment); } @@ -125,7 +126,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services { existingPayment.Accounted = false; - await _invoiceRepository.UpdatePayments(new List() {existingPayment}); + await _paymentService.UpdatePayments(new List() {existingPayment}); if (response.Amount > 0) { var paymentData = new EthereumLikePaymentData() @@ -148,7 +149,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services AccountIndex = cd.AccountIndex, XPub = cd.XPub }; - var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, + var payment = await _paymentService.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network, true); if (payment != null) ReceivedPayment(invoice, payment); } @@ -163,7 +164,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services cd.BlockNumber = (long?)response.BlockParameter.BlockNumber.Value; existingPayment.SetCryptoPaymentData(cd); - await _invoiceRepository.UpdatePayments(new List() {existingPayment}); + await _paymentService.UpdatePayments(new List() {existingPayment}); _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); } @@ -183,7 +184,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services } existingPayment.SetCryptoPaymentData(cd); - await _invoiceRepository.UpdatePayments(new List() {existingPayment}); + await _paymentService.UpdatePayments(new List() {existingPayment}); _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); } @@ -345,11 +346,12 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services public EthereumWatcher(int chainId, EthereumLikeConfiguration config, BTCPayNetworkProvider btcPayNetworkProvider, - EventAggregator eventAggregator, InvoiceRepository invoiceRepository) : + EventAggregator eventAggregator, InvoiceRepository invoiceRepository, PaymentService paymentService) : base(eventAggregator) { _eventAggregator = eventAggregator; _invoiceRepository = invoiceRepository; + _paymentService = paymentService; ChainId = chainId; AuthenticationHeaderValue headerValue = null; if (!string.IsNullOrEmpty(config.Web3ProviderUsername)) diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index 2b946abdd..cf3e448f0 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -27,6 +27,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly BTCPayNetworkProvider _networkProvider; private readonly ILogger _logger; + private readonly PaymentService _paymentService; private readonly CompositeDisposable leases = new CompositeDisposable(); private readonly Queue> taskQueue = new Queue>(); private CancellationTokenSource _Cts; @@ -36,7 +37,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services MoneroRPCProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, BTCPayNetworkProvider networkProvider, - ILogger logger) + ILogger logger, + PaymentService paymentService) { _invoiceRepository = invoiceRepository; _eventAggregator = eventAggregator; @@ -44,6 +46,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services _MoneroLikeConfiguration = moneroLikeConfiguration; _networkProvider = networkProvider; _logger = logger; + _paymentService = paymentService; } public Task StartAsync(CancellationToken cancellationToken) @@ -243,7 +246,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services } transferProcessingTasks.Add( - _invoiceRepository.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList())); + _paymentService.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList())); await Task.WhenAll(transferProcessingTasks); foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2)) { @@ -304,7 +307,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services if (paymentsToUpdate.Any()) { - await _invoiceRepository.UpdatePayments(paymentsToUpdate.Select(tuple => tuple.Payment).ToList()); + await _paymentService.UpdatePayments(paymentsToUpdate.Select(tuple => tuple.Payment).ToList()); foreach (var valueTuples in paymentsToUpdate.GroupBy(entity => entity.invoice)) { if (valueTuples.Any()) @@ -341,7 +344,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services //if it doesnt, add it and assign a new monerolike address to the system if a balance is still due if (alreadyExistingPaymentThatMatches.Payment == null) { - var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, + var payment = await _paymentService.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, _networkProvider.GetNetwork(cryptoCode), true); if (payment != null) await ReceivedPayment(invoice, payment); diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 4557e45d6..c12c4c2eb 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -29,21 +29,21 @@ namespace BTCPayServer.Services.Invoices NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings); } - private readonly ApplicationDbContextFactory _ContextFactory; + private readonly ApplicationDbContextFactory _applicationDbContextFactory; private readonly EventAggregator _eventAggregator; - private readonly BTCPayNetworkProvider _Networks; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; public InvoiceRepository(ApplicationDbContextFactory contextFactory, BTCPayNetworkProvider networks, EventAggregator eventAggregator) { - _ContextFactory = contextFactory; - _Networks = networks; + _applicationDbContextFactory = contextFactory; + _btcPayNetworkProvider = networks; _eventAggregator = eventAggregator; } public async Task GetWebhookDelivery(string invoiceId, string deliveryId) { - using var ctx = _ContextFactory.CreateContext(); + using var ctx = _applicationDbContextFactory.CreateContext(); return await ctx.InvoiceWebhookDeliveries .Where(d => d.InvoiceId == invoiceId && d.DeliveryId == deliveryId) .Select(d => d.Delivery) @@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Invoices { return new InvoiceEntity() { - Networks = _Networks, + Networks = _btcPayNetworkProvider, Version = InvoiceEntity.Lastest_Version, InvoiceTime = DateTimeOffset.UtcNow, Metadata = new InvoiceMetadata() @@ -64,7 +64,7 @@ namespace BTCPayServer.Services.Invoices public async Task RemovePendingInvoice(string invoiceId) { Logs.PayServer.LogInformation($"Remove pending invoice {invoiceId}"); - using (var ctx = _ContextFactory.CreateContext()) + using (var ctx = _applicationDbContextFactory.CreateContext()) { ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId }); try @@ -78,7 +78,7 @@ namespace BTCPayServer.Services.Invoices public async Task> GetInvoicesFromAddresses(string[] addresses) { - using (var db = _ContextFactory.CreateContext()) + using (var db = _applicationDbContextFactory.CreateContext()) { return (await db.AddressInvoices .Include(a => a.InvoiceData.Payments) @@ -92,7 +92,7 @@ namespace BTCPayServer.Services.Invoices public async Task GetPendingInvoices() { - using (var ctx = _ContextFactory.CreateContext()) + using (var ctx = _applicationDbContextFactory.CreateContext()) { return await ctx.PendingInvoices.AsQueryable().Select(data => data.Id).ToArrayAsync(); } @@ -100,7 +100,7 @@ namespace BTCPayServer.Services.Invoices public async Task> GetWebhookDeliveries(string invoiceId) { - using var ctx = _ContextFactory.CreateContext(); + using var ctx = _applicationDbContextFactory.CreateContext(); return await ctx.InvoiceWebhookDeliveries .Where(s => s.InvoiceId == invoiceId) .Select(s => s.Delivery) @@ -112,7 +112,7 @@ namespace BTCPayServer.Services.Invoices { if (storeId == null) throw new ArgumentNullException(nameof(storeId)); - using (var ctx = _ContextFactory.CreateContext()) + using (var ctx = _applicationDbContextFactory.CreateContext()) { return await ctx.Apps.Where(a => a.StoreDataId == storeId && a.TagAllInvoices).ToArrayAsync(); } @@ -120,7 +120,7 @@ namespace BTCPayServer.Services.Invoices public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data) { - using (var ctx = _ContextFactory.CreateContext()) + using (var ctx = _applicationDbContextFactory.CreateContext()) { var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) @@ -136,11 +136,11 @@ namespace BTCPayServer.Services.Invoices public async Task ExtendInvoiceMonitor(string invoiceId) { - using (var ctx = _ContextFactory.CreateContext()) + using (var ctx = _applicationDbContextFactory.CreateContext()) { var invoiceData = await ctx.Invoices.FindAsync(invoiceId); - var invoice = invoiceData.GetBlob(_Networks); + var invoice = invoiceData.GetBlob(_btcPayNetworkProvider); invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1); invoiceData.Blob = ToBytes(invoice, null); @@ -152,13 +152,13 @@ namespace BTCPayServer.Services.Invoices { var textSearch = new HashSet(); invoice = Clone(invoice); - invoice.Networks = _Networks; + invoice.Networks = _btcPayNetworkProvider; invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); #pragma warning disable CS0618 invoice.Payments = new List(); #pragma warning restore CS0618 invoice.StoreId = storeId; - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = new Data.InvoiceData() { @@ -229,12 +229,12 @@ namespace BTCPayServer.Services.Invoices { var temp = new InvoiceData(); temp.Blob = ToBytes(invoice); - return temp.GetBlob(_Networks); + return temp.GetBlob(_btcPayNetworkProvider); } public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs) { - await using var context = _ContextFactory.CreateContext(); + await using var context = _applicationDbContextFactory.CreateContext(); foreach (var log in logs.ToList()) { await context.InvoiceEvents.AddAsync(new InvoiceEventData() @@ -269,12 +269,12 @@ namespace BTCPayServer.Services.Invoices public async Task NewPaymentDetails(string invoiceId, IPaymentMethodDetails paymentMethodDetails, BTCPayNetworkBase network) { - await using var context = _ContextFactory.CreateContext(); + await using var context = _applicationDbContextFactory.CreateContext(); var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault(); if (invoice == null) return false; - var invoiceEntity = invoice.GetBlob(_Networks); + var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType()); if (paymentMethod == null) return false; @@ -313,13 +313,13 @@ namespace BTCPayServer.Services.Invoices public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var invoice = await context.Invoices.FindAsync(invoiceId); if (invoice == null) return; var network = paymentMethod.Network; - var invoiceEntity = invoice.GetBlob(_Networks); + var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); var newDetails = paymentMethod.GetPaymentMethodDetails(); var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId()); if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated) @@ -346,7 +346,7 @@ namespace BTCPayServer.Services.Invoices public async Task AddPendingInvoiceIfNotPresent(string invoiceId) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { if (!context.PendingInvoices.Any(a => a.Id == invoiceId)) { @@ -362,7 +362,7 @@ namespace BTCPayServer.Services.Invoices public async Task AddInvoiceEvent(string invoiceId, object evt, InvoiceEventData.EventSeverity severity) { - await using var context = _ContextFactory.CreateContext(); + await using var context = _applicationDbContextFactory.CreateContext(); await context.InvoiceEvents.AddAsync(new InvoiceEventData() { Severity = severity, @@ -396,7 +396,7 @@ namespace BTCPayServer.Services.Invoices public async Task UnaffectAddress(string invoiceId) { - await using var context = _ContextFactory.CreateContext(); + await using var context = _applicationDbContextFactory.CreateContext(); MarkUnassigned(invoiceId, context, null); try { @@ -416,7 +416,7 @@ namespace BTCPayServer.Services.Invoices public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) @@ -430,12 +430,12 @@ namespace BTCPayServer.Services.Invoices { if (invoice.Type != InvoiceType.TopUp) throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice)); - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null) return; - var blob = invoiceData.GetBlob(_Networks); + var blob = invoiceData.GetBlob(_btcPayNetworkProvider); blob.Price = invoice.Price; AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) }); invoiceData.Blob = ToBytes(blob, null); @@ -445,7 +445,7 @@ namespace BTCPayServer.Services.Invoices public async Task MassArchive(string[] invoiceIds) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var items = context.Invoices.Where(a => invoiceIds.Contains(a.Id)); if (items == null) @@ -464,7 +464,7 @@ namespace BTCPayServer.Services.Invoices public async Task ToggleInvoiceArchival(string invoiceId, bool archived, string storeId = null) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null || invoiceData.Archived == archived || @@ -477,14 +477,14 @@ namespace BTCPayServer.Services.Invoices } public async Task UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = await GetInvoiceRaw(invoiceId, context); if (invoiceData == null || (storeId != null && !invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase))) return null; - var blob = invoiceData.GetBlob(_Networks); + var blob = invoiceData.GetBlob(_btcPayNetworkProvider); blob.Metadata = InvoiceMetadata.FromJObject(metadata); invoiceData.Blob = ToBytes(blob); await context.SaveChangesAsync().ConfigureAwait(false); @@ -493,7 +493,7 @@ namespace BTCPayServer.Services.Invoices } public async Task MarkInvoiceStatus(string invoiceId, InvoiceStatus status) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = await GetInvoiceRaw(invoiceId, context); if (invoiceData == null) @@ -538,7 +538,7 @@ namespace BTCPayServer.Services.Invoices public async Task GetInvoice(string id, bool inludeAddressData = false) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var res = await GetInvoiceRaw(id, context, inludeAddressData); return res == null ? null : ToEntity(res); @@ -547,7 +547,7 @@ namespace BTCPayServer.Services.Invoices public async Task GetInvoices(string[] invoiceIds) { var invoiceIdSet = invoiceIds.ToHashSet(); - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { IQueryable query = context @@ -578,12 +578,12 @@ namespace BTCPayServer.Services.Invoices private InvoiceEntity ToEntity(Data.InvoiceData invoice) { - var entity = invoice.GetBlob(_Networks); + var entity = invoice.GetBlob(_btcPayNetworkProvider); PaymentMethodDictionary paymentMethods = null; #pragma warning disable CS0618 entity.Payments = invoice.Payments.Select(p => { - var paymentEntity = p.GetBlob(_Networks); + var paymentEntity = p.GetBlob(_btcPayNetworkProvider); if (paymentEntity is null) return null; // PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee. @@ -707,7 +707,7 @@ namespace BTCPayServer.Services.Invoices public async Task GetInvoicesTotal(InvoiceQuery queryObject) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var query = GetInvoiceQuery(context, queryObject); return await query.CountAsync(); @@ -716,7 +716,7 @@ namespace BTCPayServer.Services.Invoices public async Task GetInvoices(InvoiceQuery queryObject) { - using (var context = _ContextFactory.CreateContext()) + using (var context = _applicationDbContextFactory.CreateContext()) { var query = GetInvoiceQuery(context, queryObject); query = query.Include(o => o.Payments); @@ -752,89 +752,7 @@ namespace BTCPayServer.Services.Invoices return status; } - /// - /// Add a payment to an invoice - /// - /// - /// - /// - /// - /// - /// The PaymentEntity or null if already added - public async Task AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetworkBase network, bool accounted = false) - { - using (var context = _ContextFactory.CreateContext()) - { - var invoice = context.Invoices.Find(invoiceId); - if (invoice == null) - return null; - InvoiceEntity invoiceEntity = invoice.GetBlob(_Networks); - PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType())); - IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); - PaymentEntity entity = new PaymentEntity - { - Version = 1, -#pragma warning disable CS0618 - CryptoCode = network.CryptoCode, -#pragma warning restore CS0618 - ReceivedTime = date.UtcDateTime, - Accounted = accounted, - NetworkFee = paymentMethodDetails.GetNextNetworkFee(), - Network = network - }; - entity.SetCryptoPaymentData(paymentData); - //TODO: abstract - if (paymentMethodDetails is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod && - bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly && - bitcoinPaymentMethod.NextNetworkFee == Money.Zero) - { - bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.NetworkFeeRate.GetFee(100); // assume price for 100 bytes - paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod); - invoiceEntity.SetPaymentMethod(paymentMethod); - invoice.Blob = ToBytes(invoiceEntity, network); - } - PaymentData data = new PaymentData - { - Id = paymentData.GetPaymentId(), - Blob = ToBytes(entity, entity.Network), - InvoiceDataId = invoiceId, - Accounted = accounted - }; - - await context.Payments.AddAsync(data); - - AddToTextSearch(context, invoice, paymentData.GetSearchTerms()); - try - { - await context.SaveChangesAsync().ConfigureAwait(false); - } - catch (DbUpdateException) { return null; } // Already exists - return entity; - } - } - - public async Task UpdatePayments(List payments) - { - if (payments.Count == 0) - return; - using (var context = _ContextFactory.CreateContext()) - { - foreach (var payment in payments) - { - var paymentData = payment.GetCryptoPaymentData(); - var data = new PaymentData(); - data.Id = paymentData.GetPaymentId(); - data.Accounted = payment.Accounted; - data.Blob = ToBytes(payment, payment.Network); - context.Attach(data); - context.Entry(data).Property(o => o.Accounted).IsModified = true; - context.Entry(data).Property(o => o.Blob).IsModified = true; - } - await context.SaveChangesAsync().ConfigureAwait(false); - } - } - - private static byte[] ToBytes(T obj, BTCPayNetworkBase network = null) + internal static byte[] ToBytes(T obj, BTCPayNetworkBase network = null) { return ZipUtils.Zip(ToJsonString(obj, network)); } diff --git a/BTCPayServer/Services/Invoices/PaymentService.cs b/BTCPayServer/Services/Invoices/PaymentService.cs new file mode 100644 index 000000000..027cb1c0d --- /dev/null +++ b/BTCPayServer/Services/Invoices/PaymentService.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Payments; +using Microsoft.EntityFrameworkCore; +using NBitcoin; + +namespace BTCPayServer.Services.Invoices +{ + public class PaymentService + { + private readonly ApplicationDbContextFactory _applicationDbContextFactory; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly EventAggregator _eventAggregator; + + public PaymentService(EventAggregator eventAggregator, ApplicationDbContextFactory applicationDbContextFactory, BTCPayNetworkProvider btcPayNetworkProvider) + { + _applicationDbContextFactory = applicationDbContextFactory; + _btcPayNetworkProvider = btcPayNetworkProvider; + _eventAggregator = eventAggregator; + } + /// + /// Add a payment to an invoice + /// + /// + /// + /// + /// + /// + /// The PaymentEntity or null if already added + public async Task AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetworkBase network, bool accounted = false) + { + await using var context = _applicationDbContextFactory.CreateContext(); + var invoice = await context.Invoices.FindAsync(invoiceId); + if (invoice == null) + return null; + InvoiceEntity invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); + PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType())); + IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); + PaymentEntity entity = new PaymentEntity + { + Version = 1, +#pragma warning disable CS0618 + CryptoCode = network.CryptoCode, +#pragma warning restore CS0618 + ReceivedTime = date.UtcDateTime, + Accounted = accounted, + NetworkFee = paymentMethodDetails.GetNextNetworkFee(), + Network = network + }; + entity.SetCryptoPaymentData(paymentData); + //TODO: abstract + if (paymentMethodDetails is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod && + bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly && + bitcoinPaymentMethod.NextNetworkFee == Money.Zero) + { + bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.NetworkFeeRate.GetFee(100); // assume price for 100 bytes + paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod); + invoiceEntity.SetPaymentMethod(paymentMethod); + invoice.Blob = InvoiceRepository.ToBytes(invoiceEntity, network); + } + PaymentData data = new PaymentData + { + Id = paymentData.GetPaymentId(), + Blob = InvoiceRepository.ToBytes(entity, entity.Network), + InvoiceDataId = invoiceId, + Accounted = accounted + }; + + await context.Payments.AddAsync(data); + + InvoiceRepository.AddToTextSearch(context, invoice, paymentData.GetSearchTerms()); + var alreadyExists = false; + try + { + await context.SaveChangesAsync().ConfigureAwait(false); + } + catch (DbUpdateException) { alreadyExists = true; } + + if (alreadyExists) + { + return null; + } + + if (paymentData.PaymentConfirmed(entity, invoiceEntity.SpeedPolicy)) + { + _eventAggregator.Publish(new InvoiceEvent(invoiceEntity, InvoiceEvent.PaymentSettled) { Payment = entity }); + } + return entity; + } + + public async Task UpdatePayments(List payments) + { + if (payments.Count == 0) + return; + await using var context = _applicationDbContextFactory.CreateContext(); + var paymentsDict = payments + .Select(entity => (entity, entity.GetCryptoPaymentData())) + .ToDictionary(tuple => tuple.Item2.GetPaymentId()); + var paymentIds = paymentsDict.Keys.ToArray(); + var dbPayments = await context.Payments + .Include(data => data.InvoiceData) + .Where(data => paymentIds.Contains(data.Id)).ToDictionaryAsync(data => data.Id); + var eventsToSend = new List(); + foreach (KeyValuePair payment in paymentsDict) + { + var dbPayment = dbPayments[payment.Key]; + var invBlob = dbPayment.InvoiceData.GetBlob(_btcPayNetworkProvider); + var dbPaymentEntity = dbPayment.GetBlob(_btcPayNetworkProvider); + var wasConfirmed = dbPayment.GetBlob(_btcPayNetworkProvider).GetCryptoPaymentData() + .PaymentConfirmed(dbPaymentEntity, invBlob.SpeedPolicy); + if (!wasConfirmed && payment.Value.Item2.PaymentConfirmed(payment.Value.entity, invBlob.SpeedPolicy)) + { + eventsToSend.Add(new InvoiceEvent(invBlob, InvoiceEvent.PaymentSettled) { Payment = payment.Value.entity }); + } + + dbPayment.Accounted = payment.Value.entity.Accounted; + dbPayment.Blob = InvoiceRepository.ToBytes(payment.Value.entity, payment.Value.entity.Network); + } + await context.SaveChangesAsync().ConfigureAwait(false); + eventsToSend.ForEach(_eventAggregator.Publish); + } + + } +} diff --git a/BTCPayServer/Views/Stores/ModifyWebhook.cshtml b/BTCPayServer/Views/Stores/ModifyWebhook.cshtml index df215123b..fd8290293 100644 --- a/BTCPayServer/Views/Stores/ModifyWebhook.cshtml +++ b/BTCPayServer/Views/Stores/ModifyWebhook.cshtml @@ -53,6 +53,7 @@ { ("A new invoice has been created", WebhookEventType.InvoiceCreated), ("A new payment has been received", WebhookEventType.InvoiceReceivedPayment), + ("A payment has been settled", WebhookEventType.InvoicePaymentSettled), ("An invoice is processing", WebhookEventType.InvoiceProcessing), ("An invoice has expired", WebhookEventType.InvoiceExpired), ("An invoice has been settled", WebhookEventType.InvoiceSettled), diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json index b9dcd3d79..c6b5e62ee 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json @@ -41,7 +41,9 @@ ] }, "post": { - "tags": [ "Webhooks" ], + "tags": [ + "Webhooks" + ], "summary": "Create a new webhook", "description": "Create a new webhook", "requestBody": { @@ -141,7 +143,9 @@ ] }, "put": { - "tags": [ "Webhooks" ], + "tags": [ + "Webhooks" + ], "summary": "Update a webhook", "description": "Update a webhook", "requestBody": { @@ -187,7 +191,9 @@ ] }, "delete": { - "tags": [ "Webhooks" ], + "tags": [ + "Webhooks" + ], "summary": "Delete a webhook", "description": "Delete a webhook", "requestBody": { @@ -483,7 +489,11 @@ "timestamp": { "nullable": false, "description": "Timestamp of when the delivery got broadcasted", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}] + "allOf": [ + { + "$ref": "#/components/schemas/UnixTimestamp" + } + ] }, "httpCode": { "type": "number", @@ -619,7 +629,11 @@ }, "timestamp": { "description": "The timestamp when this delivery has been created", - "allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}] + "allOf": [ + { + "$ref": "#/components/schemas/UnixTimestamp" + } + ] } } }, @@ -712,11 +726,28 @@ "type": "boolean", "description": "Whether this payment has been sent after expiration of the invoice", "nullable": false + }, + "paymentMethod": { + "type": "string", + "description": "What payment method was used for this payment", + "nullable": false + }, + "payment": { + "description": "Details about the payment", + "$ref": "#/components/schemas/InvoicePaymentMethodDataModel" } } } ] }, + "WebhookInvoicePaymentSettledEvent": { + "description": "Callback sent if the `type` is `InvoicePaymentSettled`", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookInvoiceReceivedPaymentEvent" + } + ] + }, "WebhookInvoiceExpiredEvent": { "description": "Callback sent if the `type` is `InvoiceExpired`", "allOf": [ @@ -828,6 +859,36 @@ } } }, + "InvoicePaymentSettled": { + "post": { + "summary": "InvoicePaymentSettled", + "description": "An payment relating to an invoice has settled", + "parameters": [ + { + "in": "header", + "name": "BTCPay-Sig", + "required": true, + "description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`", + "schema": { + "type": "string", + "example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9" + } + } + ], + "tags": [ + "Webhooks" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookInvoicePaymentSettledEvent" + } + } + } + } + } + }, "InvoicePaidInFull": { "post": { "summary": "InvoicePaidInFull",