Payment Settled Webhook event (#2944)

* Payment Settled Webhook event

resolves #2691

* Move payment methods to payment services
This commit is contained in:
Andrew Camilleri
2021-10-05 11:10:41 +02:00
committed by GitHub
parent 143d5f69c1
commit 6e3d6125c2
20 changed files with 401 additions and 224 deletions

View File

@@ -11,6 +11,7 @@ namespace BTCPayServer.Client.Models
InvoiceProcessing, InvoiceProcessing,
InvoiceExpired, InvoiceExpired,
InvoiceSettled, InvoiceSettled,
InvoiceInvalid InvoiceInvalid,
InvoicePaymentSettled,
} }
} }

View File

@@ -10,72 +10,88 @@ namespace BTCPayServer.Client.Models
{ {
public WebhookInvoiceEvent() public WebhookInvoiceEvent()
{ {
} }
public WebhookInvoiceEvent(WebhookEventType evtType) public WebhookInvoiceEvent(WebhookEventType evtType)
{ {
this.Type = evtType; this.Type = evtType;
} }
[JsonProperty(Order = 1)]
public string StoreId { get; set; } [JsonProperty(Order = 1)] public string StoreId { get; set; }
[JsonProperty(Order = 2)] [JsonProperty(Order = 2)] public string InvoiceId { get; set; }
public string InvoiceId { get; set; }
} }
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent
{ {
public WebhookInvoiceSettledEvent() public WebhookInvoiceSettledEvent()
{ {
} }
public WebhookInvoiceSettledEvent(WebhookEventType evtType) : base(evtType) public WebhookInvoiceSettledEvent(WebhookEventType evtType) : base(evtType)
{ {
} }
public bool ManuallyMarked { get; set; } public bool ManuallyMarked { get; set; }
} }
public class WebhookInvoiceInvalidEvent : WebhookInvoiceEvent public class WebhookInvoiceInvalidEvent : WebhookInvoiceEvent
{ {
public WebhookInvoiceInvalidEvent() public WebhookInvoiceInvalidEvent()
{ {
} }
public WebhookInvoiceInvalidEvent(WebhookEventType evtType) : base(evtType) public WebhookInvoiceInvalidEvent(WebhookEventType evtType) : base(evtType)
{ {
} }
public bool ManuallyMarked { get; set; } public bool ManuallyMarked { get; set; }
} }
public class WebhookInvoiceProcessingEvent : WebhookInvoiceEvent public class WebhookInvoiceProcessingEvent : WebhookInvoiceEvent
{ {
public WebhookInvoiceProcessingEvent() public WebhookInvoiceProcessingEvent()
{ {
} }
public WebhookInvoiceProcessingEvent(WebhookEventType evtType) : base(evtType) public WebhookInvoiceProcessingEvent(WebhookEventType evtType) : base(evtType)
{ {
} }
public bool OverPaid { get; set; } public bool OverPaid { get; set; }
} }
public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent
{ {
public WebhookInvoiceReceivedPaymentEvent() public WebhookInvoiceReceivedPaymentEvent()
{ {
} }
public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType) public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType)
{ {
} }
public bool AfterExpiration { get; set; } 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 class WebhookInvoiceExpiredEvent : WebhookInvoiceEvent
{ {
public WebhookInvoiceExpiredEvent() public WebhookInvoiceExpiredEvent()
{ {
} }
public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType) public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType)
{ {
} }

View File

@@ -3119,6 +3119,20 @@ namespace BTCPayServer.Tests
c => c =>
{ {
Assert.False(c.AfterExpiration); 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<WebhookInvoicePaymentSettledEvent>(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);
}); });
} }
} }

View File

@@ -363,27 +363,28 @@ namespace BTCPayServer.Controllers.GreenField
PaymentLink = PaymentLink =
method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due, method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due,
Request.GetAbsoluteRoot()), Request.GetAbsoluteRoot()),
Payments = payments.Select(paymentEntity => Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList()
{
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()
}; };
}).ToArray(); }).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) private InvoiceData ToModel(InvoiceEntity entity)
{ {
return new InvoiceData() return new InvoiceData()

View File

@@ -1,5 +1,4 @@
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {

View File

@@ -7,6 +7,7 @@ namespace BTCPayServer.Events
{ {
Created = 1001, Created = 1001,
ReceivedPayment = 1002, ReceivedPayment = 1002,
PaymentSettled = 1014,
PaidInFull = 1003, PaidInFull = 1003,
Expired = 1004, Expired = 1004,
Confirmed = 1005, Confirmed = 1005,
@@ -21,6 +22,7 @@ namespace BTCPayServer.Events
{ {
public const string Created = "invoice_created"; public const string Created = "invoice_created";
public const string ReceivedPayment = "invoice_receivedPayment"; public const string ReceivedPayment = "invoice_receivedPayment";
public const string PaymentSettled = "invoice_paymentSettled";
public const string MarkedCompleted = "invoice_markedComplete"; public const string MarkedCompleted = "invoice_markedComplete";
public const string MarkedInvalid = "invoice_markedInvalid"; public const string MarkedInvalid = "invoice_markedInvalid";
public const string Expired = "invoice_expired"; public const string Expired = "invoice_expired";
@@ -36,6 +38,7 @@ namespace BTCPayServer.Events
{ {
{Created, InvoiceEventCode.Created}, {Created, InvoiceEventCode.Created},
{ReceivedPayment, InvoiceEventCode.ReceivedPayment}, {ReceivedPayment, InvoiceEventCode.ReceivedPayment},
{PaymentSettled, InvoiceEventCode.PaymentSettled},
{PaidInFull, InvoiceEventCode.PaidInFull}, {PaidInFull, InvoiceEventCode.PaidInFull},
{Expired, InvoiceEventCode.Expired}, {Expired, InvoiceEventCode.Expired},
{Confirmed, InvoiceEventCode.Confirmed}, {Confirmed, InvoiceEventCode.Confirmed},

View File

@@ -311,6 +311,11 @@ namespace BTCPayServer.HostedServices
{ {
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e => leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{ {
if (e.EventCode == InvoiceEventCode.PaymentSettled)
{
//these are greenfield specific events
return;
}
var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id); var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id);
if (invoice == null) if (invoice == null)
return; return;

View File

@@ -52,21 +52,24 @@ namespace BTCPayServer.HostedServices
} }
} }
readonly InvoiceRepository _InvoiceRepository; readonly InvoiceRepository _invoiceRepository;
readonly EventAggregator _EventAggregator; readonly EventAggregator _eventAggregator;
readonly ExplorerClientProvider _ExplorerClientProvider; readonly ExplorerClientProvider _explorerClientProvider;
private readonly NotificationSender _notificationSender; private readonly NotificationSender _notificationSender;
private readonly PaymentService _paymentService;
public InvoiceWatcher( public InvoiceWatcher(
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
EventAggregator eventAggregator, EventAggregator eventAggregator,
ExplorerClientProvider explorerClientProvider, ExplorerClientProvider explorerClientProvider,
NotificationSender notificationSender) NotificationSender notificationSender,
PaymentService paymentService)
{ {
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _invoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_ExplorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
_notificationSender = notificationSender; _notificationSender = notificationSender;
_paymentService = paymentService;
} }
readonly CompositeDisposable leases = new CompositeDisposable(); readonly CompositeDisposable leases = new CompositeDisposable();
@@ -239,7 +242,7 @@ namespace BTCPayServer.HostedServices
private async Task Wait(string invoiceId) private async Task Wait(string invoiceId)
{ {
var invoice = await _InvoiceRepository.GetInvoice(invoiceId); var invoice = await _invoiceRepository.GetInvoice(invoiceId);
try try
{ {
// add 1 second to ensure watch won't trigger moments before invoice expires // add 1 second to ensure watch won't trigger moments before invoice expires
@@ -274,11 +277,11 @@ namespace BTCPayServer.HostedServices
_Loop = StartLoop(_Cts.Token); _Loop = StartLoop(_Cts.Token);
_ = WaitPendingInvoices(); _ = WaitPendingInvoices();
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b => leases.Add(_eventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
{ {
Watch(b.InvoiceId); Watch(b.InvoiceId);
})); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b => leases.Add(_eventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{ {
if (InvoiceEventNotification.HandlesEvent(b.Name)) if (InvoiceEventNotification.HandlesEvent(b.Name))
{ {
@@ -301,7 +304,7 @@ namespace BTCPayServer.HostedServices
private async Task WaitPendingInvoices() private async Task WaitPendingInvoices()
{ {
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices()) await Task.WhenAll((await _invoiceRepository.GetPendingInvoices())
.Select(id => Wait(id)).ToArray()); .Select(id => Wait(id)).ToArray());
} }
@@ -318,28 +321,28 @@ namespace BTCPayServer.HostedServices
try try
{ {
cancellation.ThrowIfCancellationRequested(); cancellation.ThrowIfCancellationRequested();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null) if (invoice == null)
break; break;
var updateContext = new UpdateInvoiceContext(invoice); var updateContext = new UpdateInvoiceContext(invoice);
UpdateInvoice(updateContext); UpdateInvoice(updateContext);
if (updateContext.Unaffect) if (updateContext.Unaffect)
{ {
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _invoiceRepository.UnaffectAddress(invoice.Id);
} }
if (updateContext.Dirty) if (updateContext.Dirty)
{ {
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState()); await _invoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice)); updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
} }
if (updateContext.IsBlobUpdated) if (updateContext.IsBlobUpdated)
{ {
await _InvoiceRepository.UpdateInvoicePrice(invoice.Id, invoice); await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice);
} }
foreach (var evt in updateContext.Events) foreach (var evt in updateContext.Events)
{ {
_EventAggregator.Publish(evt, evt.GetType()); _eventAggregator.Publish(evt, evt.GetType());
} }
if (invoice.Status == InvoiceStatusLegacy.Complete || 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 // say user used low fee and we only got 3 confirmations right before it's time to remove
if (extendInvoiceMonitoring) 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; break;
} }
@@ -389,7 +392,7 @@ namespace BTCPayServer.HostedServices
if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted) if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) && (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; var confirmationCount = transactionResult?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount; onChainPaymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(onChainPaymentData); payment.SetCryptoPaymentData(onChainPaymentData);
@@ -408,7 +411,7 @@ namespace BTCPayServer.HostedServices
var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList(); var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList();
if (updatedPaymentData.Count > 0) if (updatedPaymentData.Count > 0)
{ {
await _InvoiceRepository.UpdatePayments(updatedPaymentData); await _paymentService.UpdatePayments(updatedPaymentData);
} }
return extendInvoiceMonitoring; return extendInvoiceMonitoring;

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Controllers.GreenField;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Logging; using BTCPayServer.Logging;
@@ -183,6 +184,8 @@ namespace BTCPayServer.HostedServices
return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated); return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated);
case WebhookEventType.InvoiceReceivedPayment: case WebhookEventType.InvoiceReceivedPayment:
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment); return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment);
case WebhookEventType.InvoicePaymentSettled:
return new WebhookInvoicePaymentSettledEvent(WebhookEventType.InvoicePaymentSettled);
case WebhookEventType.InvoiceProcessing: case WebhookEventType.InvoiceProcessing:
return new WebhookInvoiceProcessingEvent(WebhookEventType.InvoiceProcessing); return new WebhookInvoiceProcessingEvent(WebhookEventType.InvoiceProcessing);
case WebhookEventType.InvoiceExpired: case WebhookEventType.InvoiceExpired:
@@ -232,7 +235,16 @@ namespace BTCPayServer.HostedServices
case InvoiceEventCode.ReceivedPayment: case InvoiceEventCode.ReceivedPayment:
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment) 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: default:
return null; return null;

View File

@@ -102,11 +102,8 @@ namespace BTCPayServer.Hosting
services.AddStartupTask<MigrationStartupTask>(); services.AddStartupTask<MigrationStartupTask>();
// //
services.AddStartupTask<BlockExplorerLinkStartupTask>(); services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.TryAddSingleton<InvoiceRepository>(o => services.TryAddSingleton<InvoiceRepository>();
{ services.AddSingleton<PaymentService>();
var dbContext = o.GetRequiredService<ApplicationDbContextFactory>();
return new InvoiceRepository(dbContext, o.GetRequiredService<BTCPayNetworkProvider>(), o.GetService<EventAggregator>());
});
services.AddSingleton<BTCPayServerEnvironment>(); services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>(); services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<WalletRepository>(); services.TryAddSingleton<WalletRepository>();

View File

@@ -29,7 +29,7 @@ namespace BTCPayServer.Payments.Bitcoin
readonly EventAggregator _Aggregator; readonly EventAggregator _Aggregator;
private readonly PayJoinRepository _payJoinRepository; private readonly PayJoinRepository _payJoinRepository;
readonly ExplorerClientProvider _ExplorerClients; readonly ExplorerClientProvider _ExplorerClients;
readonly IHostApplicationLifetime _Lifetime; private readonly PaymentService _paymentService;
readonly InvoiceRepository _InvoiceRepository; readonly InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask; private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts; private CancellationTokenSource _Cts;
@@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
EventAggregator aggregator, EventAggregator aggregator,
PayJoinRepository payjoinRepository, PayJoinRepository payjoinRepository,
IHostApplicationLifetime lifetime) PaymentService paymentService)
{ {
PollInterval = TimeSpan.FromMinutes(1.0); PollInterval = TimeSpan.FromMinutes(1.0);
_Wallets = wallets; _Wallets = wallets;
@@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Bitcoin
_ExplorerClients = explorerClients; _ExplorerClients = explorerClients;
_Aggregator = aggregator; _Aggregator = aggregator;
_payJoinRepository = payjoinRepository; _payJoinRepository = payjoinRepository;
_Lifetime = lifetime; _paymentService = paymentService;
} }
readonly CompositeDisposable leases = new CompositeDisposable(); readonly CompositeDisposable leases = new CompositeDisposable();
@@ -167,7 +167,7 @@ namespace BTCPayServer.Payments.Bitcoin
.GetAllBitcoinPaymentData(false).Any(c => c.GetPaymentId() == paymentData.GetPaymentId()); .GetAllBitcoinPaymentData(false).Any(c => c.GetPaymentId() == paymentData.GetPaymentId());
if (!alreadyExist) if (!alreadyExist)
{ {
var payment = await _InvoiceRepository.AddPayment(invoice.Id, var payment = await _paymentService.AddPayment(invoice.Id,
DateTimeOffset.UtcNow, paymentData, network); DateTimeOffset.UtcNow, paymentData, network);
if (payment != null) if (payment != null)
await ReceivedPayment(wallet, invoice, payment, await ReceivedPayment(wallet, invoice, payment,
@@ -341,7 +341,7 @@ namespace BTCPayServer.Payments.Bitcoin
await _payJoinRepository.TryUnlock(payjoinInformation.ContributedOutPoints); await _payJoinRepository.TryUnlock(payjoinInformation.ContributedOutPoints);
} }
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities); await _paymentService.UpdatePayments(updatedPaymentEntities);
if (updatedPaymentEntities.Count != 0) if (updatedPaymentEntities.Count != 0)
_Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); _Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
return invoice; return invoice;
@@ -383,7 +383,7 @@ namespace BTCPayServer.Payments.Bitcoin
var paymentData = new BitcoinLikePaymentData(address, coin.Value, coin.OutPoint, var paymentData = new BitcoinLikePaymentData(address, coin.Value, coin.OutPoint,
transaction?.Transaction is null ? true : transaction.Transaction.RBF, coin.KeyPath); 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); alreadyAccounted.Add(coin.OutPoint);
if (payment != null) if (payment != null)
{ {

View File

@@ -31,6 +31,7 @@ namespace BTCPayServer.Payments.Lightning
private readonly LightningClientFactoryService lightningClientFactory; private readonly LightningClientFactoryService lightningClientFactory;
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler; private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly PaymentService _paymentService;
readonly Channel<string> _CheckInvoices = Channel.CreateUnbounded<string>(); readonly Channel<string> _CheckInvoices = Channel.CreateUnbounded<string>();
Task _CheckingInvoice; Task _CheckingInvoice;
readonly Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>(); readonly Dictionary<(string, string), LightningInstanceListener> _InstanceListeners = new Dictionary<(string, string), LightningInstanceListener>();
@@ -42,7 +43,8 @@ namespace BTCPayServer.Payments.Lightning
LightningClientFactoryService lightningClientFactory, LightningClientFactoryService lightningClientFactory,
LightningLikePaymentHandler lightningLikePaymentHandler, LightningLikePaymentHandler lightningLikePaymentHandler,
StoreRepository storeRepository, StoreRepository storeRepository,
IOptions<LightningNetworkOptions> options) IOptions<LightningNetworkOptions> options,
PaymentService paymentService)
{ {
_Aggregator = aggregator; _Aggregator = aggregator;
_InvoiceRepository = invoiceRepository; _InvoiceRepository = invoiceRepository;
@@ -51,6 +53,7 @@ namespace BTCPayServer.Payments.Lightning
this.lightningClientFactory = lightningClientFactory; this.lightningClientFactory = lightningClientFactory;
_lightningLikePaymentHandler = lightningLikePaymentHandler; _lightningLikePaymentHandler = lightningLikePaymentHandler;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_paymentService = paymentService;
Options = options; Options = options;
} }
@@ -67,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning
if (!_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener) || if (!_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener) ||
!instanceListener.IsListening) !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); var status = await instanceListener.PollPayment(listenedInvoice, cancellation);
if (status is null || if (status is null ||
status is LightningInvoiceStatus.Paid || status is LightningInvoiceStatus.Paid ||
@@ -309,9 +312,10 @@ namespace BTCPayServer.Payments.Lightning
public class LightningInstanceListener public class LightningInstanceListener
{ {
private readonly InvoiceRepository invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetwork network; private readonly BTCPayNetwork _network;
private readonly PaymentService _paymentService;
private readonly LightningClientFactoryService _lightningClientFactory; private readonly LightningClientFactoryService _lightningClientFactory;
public LightningConnectionString ConnectionString { get; } public LightningConnectionString ConnectionString { get; }
@@ -320,13 +324,15 @@ namespace BTCPayServer.Payments.Lightning
EventAggregator eventAggregator, EventAggregator eventAggregator,
LightningClientFactoryService lightningClientFactory, LightningClientFactoryService lightningClientFactory,
BTCPayNetwork network, BTCPayNetwork network,
LightningConnectionString connectionString) LightningConnectionString connectionString,
PaymentService paymentService)
{ {
if (connectionString == null) if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString)); throw new ArgumentNullException(nameof(connectionString));
this.invoiceRepository = invoiceRepository; this._invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
this.network = network; this._network = network;
_paymentService = paymentService;
_lightningClientFactory = lightningClientFactory; _lightningClientFactory = lightningClientFactory;
ConnectionString = connectionString; ConnectionString = connectionString;
} }
@@ -337,12 +343,12 @@ namespace BTCPayServer.Payments.Lightning
internal async Task<LightningInvoiceStatus?> PollPayment(ListenedInvoice listenedInvoice, CancellationToken cancellation) internal async Task<LightningInvoiceStatus?> 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); LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId);
if (lightningInvoice?.Status is LightningInvoiceStatus.Paid && if (lightningInvoice?.Status is LightningInvoiceStatus.Paid &&
await AddPayment(lightningInvoice, listenedInvoice.InvoiceId)) 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; return lightningInvoice?.Status;
} }
@@ -360,17 +366,17 @@ namespace BTCPayServer.Payments.Lightning
public CancellationTokenSource StopListeningCancellationTokenSource; public CancellationTokenSource StopListeningCancellationTokenSource;
async Task Listen(CancellationToken cancellation) 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 try
{ {
var lightningClient = _lightningClientFactory.Create(ConnectionString, network); var lightningClient = _lightningClientFactory.Create(ConnectionString, _network);
using (var session = await lightningClient.Listen(cancellation)) using (var session = await lightningClient.Listen(cancellation))
{ {
// Just in case the payment arrived after our last poll but before we listened. // Just in case the payment arrived after our last poll but before we listened.
await PollAllListenedInvoices(cancellation); await PollAllListenedInvoices(cancellation);
if (_ErrorAlreadyLogged) 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; _ErrorAlreadyLogged = false;
while (!_ListenedInvoices.IsEmpty) while (!_ListenedInvoices.IsEmpty)
@@ -386,7 +392,7 @@ namespace BTCPayServer.Payments.Lightning
{ {
if (await AddPayment(notification, listenedInvoice.InvoiceId)) 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 _); _ListenedInvoices.TryRemove(notification.Id, out var _);
} }
@@ -401,12 +407,12 @@ namespace BTCPayServer.Payments.Lightning
catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged) catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged)
{ {
_ErrorAlreadyLogged = true; _ErrorAlreadyLogged = true;
Logs.PayServer.LogError(ex, $"{network.CryptoCode} (Lightning): Error while contacting {ConnectionString.BaseUri}"); Logs.PayServer.LogError(ex, $"{_network.CryptoCode} (Lightning): Error while contacting {ConnectionString.BaseUri}");
Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Stop listening {ConnectionString.BaseUri}"); Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Stop listening {ConnectionString.BaseUri}");
} }
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
if (_ListenedInvoices.IsEmpty) 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; } public DateTimeOffset? LastFullPoll { get; set; }
@@ -433,15 +439,15 @@ namespace BTCPayServer.Payments.Lightning
public async Task<bool> AddPayment(LightningInvoice notification, string invoiceId) public async Task<bool> 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, 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 Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable
}, network, accounted: true); }, _network, accounted: true);
if (payment != null) if (payment != null)
{ {
var invoice = await invoiceRepository.GetInvoice(invoiceId); var invoice = await _invoiceRepository.GetInvoice(invoiceId);
if (invoice != null) if (invoice != null)
_eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); _eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
} }

View File

@@ -91,6 +91,7 @@ namespace BTCPayServer.Payments.PayJoin
private readonly BTCPayServerEnvironment _env; private readonly BTCPayServerEnvironment _env;
private readonly WalletReceiveService _walletReceiveService; private readonly WalletReceiveService _walletReceiveService;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly PaymentService _paymentService;
public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider, public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider,
InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider, InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider,
@@ -101,7 +102,8 @@ namespace BTCPayServer.Payments.PayJoin
DelayedTransactionBroadcaster broadcaster, DelayedTransactionBroadcaster broadcaster,
BTCPayServerEnvironment env, BTCPayServerEnvironment env,
WalletReceiveService walletReceiveService, WalletReceiveService walletReceiveService,
StoreRepository storeRepository) StoreRepository storeRepository,
PaymentService paymentService)
{ {
_btcPayNetworkProvider = btcPayNetworkProvider; _btcPayNetworkProvider = btcPayNetworkProvider;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
@@ -114,6 +116,7 @@ namespace BTCPayServer.Payments.PayJoin
_env = env; _env = env;
_walletReceiveService = walletReceiveService; _walletReceiveService = walletReceiveService;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_paymentService = paymentService;
} }
[HttpPost("")] [HttpPost("")]
@@ -487,7 +490,7 @@ namespace BTCPayServer.Payments.PayJoin
}; };
if (invoice != null) 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) if (payment is null)
{ {
return UnprocessableEntity(CreatePayjoinError("already-paid", return UnprocessableEntity(CreatePayjoinError("already-paid",

View File

@@ -29,6 +29,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
private readonly SettingsRepository _settingsRepository; private readonly SettingsRepository _settingsRepository;
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly PaymentService _paymentService;
private readonly Dictionary<int, EthereumWatcher> _chainHostedServices = new Dictionary<int, EthereumWatcher>(); private readonly Dictionary<int, EthereumWatcher> _chainHostedServices = new Dictionary<int, EthereumWatcher>();
private readonly Dictionary<int, CancellationTokenSource> _chainHostedServiceCancellationTokenSources = private readonly Dictionary<int, CancellationTokenSource> _chainHostedServiceCancellationTokenSources =
@@ -41,7 +42,8 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayNetworkProvider btcPayNetworkProvider,
SettingsRepository settingsRepository, SettingsRepository settingsRepository,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
IConfiguration configuration) : base( IConfiguration configuration,
PaymentService paymentService) : base(
eventAggregator) eventAggregator)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
@@ -51,6 +53,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_configuration = configuration; _configuration = configuration;
_paymentService = paymentService;
} }
public override async Task StartAsync(CancellationToken cancellationToken) public override async Task StartAsync(CancellationToken cancellationToken)
@@ -186,7 +189,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
_chainHostedServiceCancellationTokenSources.AddOrReplace(ethereumLikeConfiguration.ChainId, cts); _chainHostedServiceCancellationTokenSources.AddOrReplace(ethereumLikeConfiguration.ChainId, cts);
_chainHostedServices.AddOrReplace(ethereumLikeConfiguration.ChainId, _chainHostedServices.AddOrReplace(ethereumLikeConfiguration.ChainId,
new EthereumWatcher(ethereumLikeConfiguration.ChainId, ethereumLikeConfiguration, new EthereumWatcher(ethereumLikeConfiguration.ChainId, ethereumLikeConfiguration,
_btcPayNetworkProvider, _eventAggregator, _invoiceRepository)); _btcPayNetworkProvider, _eventAggregator, _invoiceRepository, _paymentService));
await _chainHostedServices[ethereumLikeConfiguration.ChainId].StartAsync(CancellationTokenSource await _chainHostedServices[ethereumLikeConfiguration.ChainId].StartAsync(CancellationTokenSource
.CreateLinkedTokenSource(cancellationToken, cts.Token).Token); .CreateLinkedTokenSource(cancellationToken, cts.Token).Token);
} }

View File

@@ -24,6 +24,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
{ {
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly PaymentService _paymentService;
private int ChainId { get; } private int ChainId { get; }
private readonly HashSet<PaymentMethodId> PaymentMethods; private readonly HashSet<PaymentMethodId> PaymentMethods;
@@ -113,7 +114,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
AccountIndex = response.PaymentMethodDetails.Index, AccountIndex = response.PaymentMethodDetails.Index,
XPub = response.PaymentMethodDetails.XPub 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); paymentData, network, true);
if (payment != null) ReceivedPayment(invoice, payment); if (payment != null) ReceivedPayment(invoice, payment);
} }
@@ -125,7 +126,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
{ {
existingPayment.Accounted = false; existingPayment.Accounted = false;
await _invoiceRepository.UpdatePayments(new List<PaymentEntity>() {existingPayment}); await _paymentService.UpdatePayments(new List<PaymentEntity>() {existingPayment});
if (response.Amount > 0) if (response.Amount > 0)
{ {
var paymentData = new EthereumLikePaymentData() var paymentData = new EthereumLikePaymentData()
@@ -148,7 +149,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
AccountIndex = cd.AccountIndex, AccountIndex = cd.AccountIndex,
XPub = cd.XPub XPub = cd.XPub
}; };
var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, var payment = await _paymentService.AddPayment(invoice.Id, DateTimeOffset.UtcNow,
paymentData, network, true); paymentData, network, true);
if (payment != null) ReceivedPayment(invoice, payment); if (payment != null) ReceivedPayment(invoice, payment);
} }
@@ -163,7 +164,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
cd.BlockNumber = (long?)response.BlockParameter.BlockNumber.Value; cd.BlockNumber = (long?)response.BlockParameter.BlockNumber.Value;
existingPayment.SetCryptoPaymentData(cd); existingPayment.SetCryptoPaymentData(cd);
await _invoiceRepository.UpdatePayments(new List<PaymentEntity>() {existingPayment}); await _paymentService.UpdatePayments(new List<PaymentEntity>() {existingPayment});
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
} }
@@ -183,7 +184,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
} }
existingPayment.SetCryptoPaymentData(cd); existingPayment.SetCryptoPaymentData(cd);
await _invoiceRepository.UpdatePayments(new List<PaymentEntity>() {existingPayment}); await _paymentService.UpdatePayments(new List<PaymentEntity>() {existingPayment});
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id)); _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
} }
@@ -345,11 +346,12 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
public EthereumWatcher(int chainId, EthereumLikeConfiguration config, public EthereumWatcher(int chainId, EthereumLikeConfiguration config,
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayNetworkProvider btcPayNetworkProvider,
EventAggregator eventAggregator, InvoiceRepository invoiceRepository) : EventAggregator eventAggregator, InvoiceRepository invoiceRepository, PaymentService paymentService) :
base(eventAggregator) base(eventAggregator)
{ {
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_paymentService = paymentService;
ChainId = chainId; ChainId = chainId;
AuthenticationHeaderValue headerValue = null; AuthenticationHeaderValue headerValue = null;
if (!string.IsNullOrEmpty(config.Web3ProviderUsername)) if (!string.IsNullOrEmpty(config.Web3ProviderUsername))

View File

@@ -27,6 +27,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly ILogger<MoneroListener> _logger; private readonly ILogger<MoneroListener> _logger;
private readonly PaymentService _paymentService;
private readonly CompositeDisposable leases = new CompositeDisposable(); private readonly CompositeDisposable leases = new CompositeDisposable();
private readonly Queue<Func<CancellationToken, Task>> taskQueue = new Queue<Func<CancellationToken, Task>>(); private readonly Queue<Func<CancellationToken, Task>> taskQueue = new Queue<Func<CancellationToken, Task>>();
private CancellationTokenSource _Cts; private CancellationTokenSource _Cts;
@@ -36,7 +37,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
MoneroRPCProvider moneroRpcProvider, MoneroRPCProvider moneroRpcProvider,
MoneroLikeConfiguration moneroLikeConfiguration, MoneroLikeConfiguration moneroLikeConfiguration,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
ILogger<MoneroListener> logger) ILogger<MoneroListener> logger,
PaymentService paymentService)
{ {
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@@ -44,6 +46,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
_MoneroLikeConfiguration = moneroLikeConfiguration; _MoneroLikeConfiguration = moneroLikeConfiguration;
_networkProvider = networkProvider; _networkProvider = networkProvider;
_logger = logger; _logger = logger;
_paymentService = paymentService;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -243,7 +246,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
} }
transferProcessingTasks.Add( transferProcessingTasks.Add(
_invoiceRepository.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList())); _paymentService.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList()));
await Task.WhenAll(transferProcessingTasks); await Task.WhenAll(transferProcessingTasks);
foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2)) foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2))
{ {
@@ -304,7 +307,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
if (paymentsToUpdate.Any()) 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)) foreach (var valueTuples in paymentsToUpdate.GroupBy(entity => entity.invoice))
{ {
if (valueTuples.Any()) 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 it doesnt, add it and assign a new monerolike address to the system if a balance is still due
if (alreadyExistingPaymentThatMatches.Payment == null) 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<MoneroLikeSpecificBtcPayNetwork>(cryptoCode), true); paymentData, _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(cryptoCode), true);
if (payment != null) if (payment != null)
await ReceivedPayment(invoice, payment); await ReceivedPayment(invoice, payment);

View File

@@ -29,21 +29,21 @@ namespace BTCPayServer.Services.Invoices
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings); NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
} }
private readonly ApplicationDbContextFactory _ContextFactory; private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _Networks; private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, public InvoiceRepository(ApplicationDbContextFactory contextFactory,
BTCPayNetworkProvider networks, EventAggregator eventAggregator) BTCPayNetworkProvider networks, EventAggregator eventAggregator)
{ {
_ContextFactory = contextFactory; _applicationDbContextFactory = contextFactory;
_Networks = networks; _btcPayNetworkProvider = networks;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
} }
public async Task<Data.WebhookDeliveryData> GetWebhookDelivery(string invoiceId, string deliveryId) public async Task<Data.WebhookDeliveryData> GetWebhookDelivery(string invoiceId, string deliveryId)
{ {
using var ctx = _ContextFactory.CreateContext(); using var ctx = _applicationDbContextFactory.CreateContext();
return await ctx.InvoiceWebhookDeliveries return await ctx.InvoiceWebhookDeliveries
.Where(d => d.InvoiceId == invoiceId && d.DeliveryId == deliveryId) .Where(d => d.InvoiceId == invoiceId && d.DeliveryId == deliveryId)
.Select(d => d.Delivery) .Select(d => d.Delivery)
@@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Invoices
{ {
return new InvoiceEntity() return new InvoiceEntity()
{ {
Networks = _Networks, Networks = _btcPayNetworkProvider,
Version = InvoiceEntity.Lastest_Version, Version = InvoiceEntity.Lastest_Version,
InvoiceTime = DateTimeOffset.UtcNow, InvoiceTime = DateTimeOffset.UtcNow,
Metadata = new InvoiceMetadata() Metadata = new InvoiceMetadata()
@@ -64,7 +64,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<bool> RemovePendingInvoice(string invoiceId) public async Task<bool> RemovePendingInvoice(string invoiceId)
{ {
Logs.PayServer.LogInformation($"Remove pending invoice {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 }); ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
try try
@@ -78,7 +78,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<IEnumerable<InvoiceEntity>> GetInvoicesFromAddresses(string[] addresses) public async Task<IEnumerable<InvoiceEntity>> GetInvoicesFromAddresses(string[] addresses)
{ {
using (var db = _ContextFactory.CreateContext()) using (var db = _applicationDbContextFactory.CreateContext())
{ {
return (await db.AddressInvoices return (await db.AddressInvoices
.Include(a => a.InvoiceData.Payments) .Include(a => a.InvoiceData.Payments)
@@ -92,7 +92,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<string[]> GetPendingInvoices() public async Task<string[]> GetPendingInvoices()
{ {
using (var ctx = _ContextFactory.CreateContext()) using (var ctx = _applicationDbContextFactory.CreateContext())
{ {
return await ctx.PendingInvoices.AsQueryable().Select(data => data.Id).ToArrayAsync(); return await ctx.PendingInvoices.AsQueryable().Select(data => data.Id).ToArrayAsync();
} }
@@ -100,7 +100,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<List<Data.WebhookDeliveryData>> GetWebhookDeliveries(string invoiceId) public async Task<List<Data.WebhookDeliveryData>> GetWebhookDeliveries(string invoiceId)
{ {
using var ctx = _ContextFactory.CreateContext(); using var ctx = _applicationDbContextFactory.CreateContext();
return await ctx.InvoiceWebhookDeliveries return await ctx.InvoiceWebhookDeliveries
.Where(s => s.InvoiceId == invoiceId) .Where(s => s.InvoiceId == invoiceId)
.Select(s => s.Delivery) .Select(s => s.Delivery)
@@ -112,7 +112,7 @@ namespace BTCPayServer.Services.Invoices
{ {
if (storeId == null) if (storeId == null)
throw new ArgumentNullException(nameof(storeId)); 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(); 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) 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); var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
if (invoiceData == null) if (invoiceData == null)
@@ -136,11 +136,11 @@ namespace BTCPayServer.Services.Invoices
public async Task ExtendInvoiceMonitor(string invoiceId) public async Task ExtendInvoiceMonitor(string invoiceId)
{ {
using (var ctx = _ContextFactory.CreateContext()) using (var ctx = _applicationDbContextFactory.CreateContext())
{ {
var invoiceData = await ctx.Invoices.FindAsync(invoiceId); var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
var invoice = invoiceData.GetBlob(_Networks); var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1); invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1);
invoiceData.Blob = ToBytes(invoice, null); invoiceData.Blob = ToBytes(invoice, null);
@@ -152,13 +152,13 @@ namespace BTCPayServer.Services.Invoices
{ {
var textSearch = new HashSet<string>(); var textSearch = new HashSet<string>();
invoice = Clone(invoice); invoice = Clone(invoice);
invoice.Networks = _Networks; invoice.Networks = _btcPayNetworkProvider;
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
#pragma warning disable CS0618 #pragma warning disable CS0618
invoice.Payments = new List<PaymentEntity>(); invoice.Payments = new List<PaymentEntity>();
#pragma warning restore CS0618 #pragma warning restore CS0618
invoice.StoreId = storeId; invoice.StoreId = storeId;
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var invoiceData = new Data.InvoiceData() var invoiceData = new Data.InvoiceData()
{ {
@@ -229,12 +229,12 @@ namespace BTCPayServer.Services.Invoices
{ {
var temp = new InvoiceData(); var temp = new InvoiceData();
temp.Blob = ToBytes(invoice); temp.Blob = ToBytes(invoice);
return temp.GetBlob(_Networks); return temp.GetBlob(_btcPayNetworkProvider);
} }
public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs) 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()) foreach (var log in logs.ToList())
{ {
await context.InvoiceEvents.AddAsync(new InvoiceEventData() await context.InvoiceEvents.AddAsync(new InvoiceEventData()
@@ -269,12 +269,12 @@ namespace BTCPayServer.Services.Invoices
public async Task<bool> NewPaymentDetails(string invoiceId, IPaymentMethodDetails paymentMethodDetails, BTCPayNetworkBase network) public async Task<bool> 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(); var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
if (invoice == null) if (invoice == null)
return false; return false;
var invoiceEntity = invoice.GetBlob(_Networks); var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType()); var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType());
if (paymentMethod == null) if (paymentMethod == null)
return false; return false;
@@ -313,13 +313,13 @@ namespace BTCPayServer.Services.Invoices
public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod) 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); var invoice = await context.Invoices.FindAsync(invoiceId);
if (invoice == null) if (invoice == null)
return; return;
var network = paymentMethod.Network; var network = paymentMethod.Network;
var invoiceEntity = invoice.GetBlob(_Networks); var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var newDetails = paymentMethod.GetPaymentMethodDetails(); var newDetails = paymentMethod.GetPaymentMethodDetails();
var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId()); var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId());
if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated) if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated)
@@ -346,7 +346,7 @@ namespace BTCPayServer.Services.Invoices
public async Task AddPendingInvoiceIfNotPresent(string invoiceId) public async Task AddPendingInvoiceIfNotPresent(string invoiceId)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
if (!context.PendingInvoices.Any(a => a.Id == invoiceId)) 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) 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() await context.InvoiceEvents.AddAsync(new InvoiceEventData()
{ {
Severity = severity, Severity = severity,
@@ -396,7 +396,7 @@ namespace BTCPayServer.Services.Invoices
public async Task UnaffectAddress(string invoiceId) public async Task UnaffectAddress(string invoiceId)
{ {
await using var context = _ContextFactory.CreateContext(); await using var context = _applicationDbContextFactory.CreateContext();
MarkUnassigned(invoiceId, context, null); MarkUnassigned(invoiceId, context, null);
try try
{ {
@@ -416,7 +416,7 @@ namespace BTCPayServer.Services.Invoices
public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState) public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false); var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null) if (invoiceData == null)
@@ -430,12 +430,12 @@ namespace BTCPayServer.Services.Invoices
{ {
if (invoice.Type != InvoiceType.TopUp) if (invoice.Type != InvoiceType.TopUp)
throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice)); 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<Data.InvoiceData>(invoiceId).ConfigureAwait(false); var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null) if (invoiceData == null)
return; return;
var blob = invoiceData.GetBlob(_Networks); var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
blob.Price = invoice.Price; blob.Price = invoice.Price;
AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) }); AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) });
invoiceData.Blob = ToBytes(blob, null); invoiceData.Blob = ToBytes(blob, null);
@@ -445,7 +445,7 @@ namespace BTCPayServer.Services.Invoices
public async Task MassArchive(string[] invoiceIds) 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)); var items = context.Invoices.Where(a => invoiceIds.Contains(a.Id));
if (items == null) if (items == null)
@@ -464,7 +464,7 @@ namespace BTCPayServer.Services.Invoices
public async Task ToggleInvoiceArchival(string invoiceId, bool archived, string storeId = null) 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<InvoiceData>(invoiceId).ConfigureAwait(false); var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null || invoiceData.Archived == archived || if (invoiceData == null || invoiceData.Archived == archived ||
@@ -477,14 +477,14 @@ namespace BTCPayServer.Services.Invoices
} }
public async Task<InvoiceEntity> UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata) public async Task<InvoiceEntity> UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var invoiceData = await GetInvoiceRaw(invoiceId, context); var invoiceData = await GetInvoiceRaw(invoiceId, context);
if (invoiceData == null || (storeId != null && if (invoiceData == null || (storeId != null &&
!invoiceData.StoreDataId.Equals(storeId, !invoiceData.StoreDataId.Equals(storeId,
StringComparison.InvariantCultureIgnoreCase))) StringComparison.InvariantCultureIgnoreCase)))
return null; return null;
var blob = invoiceData.GetBlob(_Networks); var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
blob.Metadata = InvoiceMetadata.FromJObject(metadata); blob.Metadata = InvoiceMetadata.FromJObject(metadata);
invoiceData.Blob = ToBytes(blob); invoiceData.Blob = ToBytes(blob);
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);
@@ -493,7 +493,7 @@ namespace BTCPayServer.Services.Invoices
} }
public async Task<bool> MarkInvoiceStatus(string invoiceId, InvoiceStatus status) public async Task<bool> MarkInvoiceStatus(string invoiceId, InvoiceStatus status)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var invoiceData = await GetInvoiceRaw(invoiceId, context); var invoiceData = await GetInvoiceRaw(invoiceId, context);
if (invoiceData == null) if (invoiceData == null)
@@ -538,7 +538,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<InvoiceEntity> GetInvoice(string id, bool inludeAddressData = false) public async Task<InvoiceEntity> GetInvoice(string id, bool inludeAddressData = false)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var res = await GetInvoiceRaw(id, context, inludeAddressData); var res = await GetInvoiceRaw(id, context, inludeAddressData);
return res == null ? null : ToEntity(res); return res == null ? null : ToEntity(res);
@@ -547,7 +547,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<InvoiceEntity[]> GetInvoices(string[] invoiceIds) public async Task<InvoiceEntity[]> GetInvoices(string[] invoiceIds)
{ {
var invoiceIdSet = invoiceIds.ToHashSet(); var invoiceIdSet = invoiceIds.ToHashSet();
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
IQueryable<Data.InvoiceData> query = IQueryable<Data.InvoiceData> query =
context context
@@ -578,12 +578,12 @@ namespace BTCPayServer.Services.Invoices
private InvoiceEntity ToEntity(Data.InvoiceData invoice) private InvoiceEntity ToEntity(Data.InvoiceData invoice)
{ {
var entity = invoice.GetBlob(_Networks); var entity = invoice.GetBlob(_btcPayNetworkProvider);
PaymentMethodDictionary paymentMethods = null; PaymentMethodDictionary paymentMethods = null;
#pragma warning disable CS0618 #pragma warning disable CS0618
entity.Payments = invoice.Payments.Select(p => entity.Payments = invoice.Payments.Select(p =>
{ {
var paymentEntity = p.GetBlob(_Networks); var paymentEntity = p.GetBlob(_btcPayNetworkProvider);
if (paymentEntity is null) if (paymentEntity is null)
return null; return null;
// PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee. // 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<int> GetInvoicesTotal(InvoiceQuery queryObject) public async Task<int> GetInvoicesTotal(InvoiceQuery queryObject)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var query = GetInvoiceQuery(context, queryObject); var query = GetInvoiceQuery(context, queryObject);
return await query.CountAsync(); return await query.CountAsync();
@@ -716,7 +716,7 @@ namespace BTCPayServer.Services.Invoices
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject) public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var query = GetInvoiceQuery(context, queryObject); var query = GetInvoiceQuery(context, queryObject);
query = query.Include(o => o.Payments); query = query.Include(o => o.Payments);
@@ -752,89 +752,7 @@ namespace BTCPayServer.Services.Invoices
return status; return status;
} }
/// <summary> internal static byte[] ToBytes<T>(T obj, BTCPayNetworkBase network = null)
/// Add a payment to an invoice
/// </summary>
/// <param name="invoiceId"></param>
/// <param name="date"></param>
/// <param name="paymentData"></param>
/// <param name="cryptoCode"></param>
/// <param name="accounted"></param>
/// <returns>The PaymentEntity or null if already added</returns>
public async Task<PaymentEntity> 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<PaymentEntity> 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>(T obj, BTCPayNetworkBase network = null)
{ {
return ZipUtils.Zip(ToJsonString(obj, network)); return ZipUtils.Zip(ToJsonString(obj, network));
} }

View File

@@ -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;
}
/// <summary>
/// Add a payment to an invoice
/// </summary>
/// <param name="invoiceId"></param>
/// <param name="date"></param>
/// <param name="paymentData"></param>
/// <param name="cryptoCode"></param>
/// <param name="accounted"></param>
/// <returns>The PaymentEntity or null if already added</returns>
public async Task<PaymentEntity> 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<PaymentEntity> 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<InvoiceEvent>();
foreach (KeyValuePair<string,(PaymentEntity entity, CryptoPaymentData)> 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);
}
}
}

View File

@@ -53,6 +53,7 @@
{ {
("A new invoice has been created", WebhookEventType.InvoiceCreated), ("A new invoice has been created", WebhookEventType.InvoiceCreated),
("A new payment has been received", WebhookEventType.InvoiceReceivedPayment), ("A new payment has been received", WebhookEventType.InvoiceReceivedPayment),
("A payment has been settled", WebhookEventType.InvoicePaymentSettled),
("An invoice is processing", WebhookEventType.InvoiceProcessing), ("An invoice is processing", WebhookEventType.InvoiceProcessing),
("An invoice has expired", WebhookEventType.InvoiceExpired), ("An invoice has expired", WebhookEventType.InvoiceExpired),
("An invoice has been settled", WebhookEventType.InvoiceSettled), ("An invoice has been settled", WebhookEventType.InvoiceSettled),

View File

@@ -41,7 +41,9 @@
] ]
}, },
"post": { "post": {
"tags": [ "Webhooks" ], "tags": [
"Webhooks"
],
"summary": "Create a new webhook", "summary": "Create a new webhook",
"description": "Create a new webhook", "description": "Create a new webhook",
"requestBody": { "requestBody": {
@@ -141,7 +143,9 @@
] ]
}, },
"put": { "put": {
"tags": [ "Webhooks" ], "tags": [
"Webhooks"
],
"summary": "Update a webhook", "summary": "Update a webhook",
"description": "Update a webhook", "description": "Update a webhook",
"requestBody": { "requestBody": {
@@ -187,7 +191,9 @@
] ]
}, },
"delete": { "delete": {
"tags": [ "Webhooks" ], "tags": [
"Webhooks"
],
"summary": "Delete a webhook", "summary": "Delete a webhook",
"description": "Delete a webhook", "description": "Delete a webhook",
"requestBody": { "requestBody": {
@@ -483,7 +489,11 @@
"timestamp": { "timestamp": {
"nullable": false, "nullable": false,
"description": "Timestamp of when the delivery got broadcasted", "description": "Timestamp of when the delivery got broadcasted",
"allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}] "allOf": [
{
"$ref": "#/components/schemas/UnixTimestamp"
}
]
}, },
"httpCode": { "httpCode": {
"type": "number", "type": "number",
@@ -619,7 +629,11 @@
}, },
"timestamp": { "timestamp": {
"description": "The timestamp when this delivery has been created", "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", "type": "boolean",
"description": "Whether this payment has been sent after expiration of the invoice", "description": "Whether this payment has been sent after expiration of the invoice",
"nullable": false "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": { "WebhookInvoiceExpiredEvent": {
"description": "Callback sent if the `type` is `InvoiceExpired`", "description": "Callback sent if the `type` is `InvoiceExpired`",
"allOf": [ "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": { "InvoicePaidInFull": {
"post": { "post": {
"summary": "InvoicePaidInFull", "summary": "InvoicePaidInFull",