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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 401 additions and 224 deletions

View File

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

View File

@ -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)
{
}

View File

@ -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<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 =
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()

View File

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

View File

@ -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},

View File

@ -311,6 +311,11 @@ namespace BTCPayServer.HostedServices
{
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);
if (invoice == null)
return;

View File

@ -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<Events.InvoiceNeedUpdateEvent>(b =>
leases.Add(_eventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
{
Watch(b.InvoiceId);
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
leases.Add(_eventAggregator.Subscribe<Events.InvoiceEvent>(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;

View File

@ -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;

View File

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

View File

@ -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<bool> _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)
{

View File

@ -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<string> _CheckInvoices = Channel.CreateUnbounded<string>();
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<LightningNetworkOptions> options)
IOptions<LightningNetworkOptions> 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<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);
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<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,
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 });
}

View File

@ -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",

View File

@ -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<int, EthereumWatcher> _chainHostedServices = new Dictionary<int, EthereumWatcher>();
private readonly Dictionary<int, CancellationTokenSource> _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);
}

View File

@ -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<PaymentMethodId> 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<PaymentEntity>() {existingPayment});
await _paymentService.UpdatePayments(new List<PaymentEntity>() {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<PaymentEntity>() {existingPayment});
await _paymentService.UpdatePayments(new List<PaymentEntity>() {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<PaymentEntity>() {existingPayment});
await _paymentService.UpdatePayments(new List<PaymentEntity>() {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))

View File

@ -27,6 +27,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly ILogger<MoneroListener> _logger;
private readonly PaymentService _paymentService;
private readonly CompositeDisposable leases = new CompositeDisposable();
private readonly Queue<Func<CancellationToken, Task>> taskQueue = new Queue<Func<CancellationToken, Task>>();
private CancellationTokenSource _Cts;
@ -36,7 +37,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
MoneroRPCProvider moneroRpcProvider,
MoneroLikeConfiguration moneroLikeConfiguration,
BTCPayNetworkProvider networkProvider,
ILogger<MoneroListener> logger)
ILogger<MoneroListener> 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<MoneroLikeSpecificBtcPayNetwork>(cryptoCode), true);
if (payment != null)
await ReceivedPayment(invoice, payment);

View File

@ -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<Data.WebhookDeliveryData> 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<bool> 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<IEnumerable<InvoiceEntity>> 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<string[]> 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<List<Data.WebhookDeliveryData>> 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<string>();
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<PaymentEntity>();
#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<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();
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<Data.InvoiceData>(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<Data.InvoiceData>(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<InvoiceData>(invoiceId).ConfigureAwait(false);
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)
{
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<bool> 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<InvoiceEntity> 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<InvoiceEntity[]> GetInvoices(string[] invoiceIds)
{
var invoiceIdSet = invoiceIds.ToHashSet();
using (var context = _ContextFactory.CreateContext())
using (var context = _applicationDbContextFactory.CreateContext())
{
IQueryable<Data.InvoiceData> 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<int> 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<InvoiceEntity[]> 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;
}
/// <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)
{
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)
internal static byte[] ToBytes<T>(T obj, BTCPayNetworkBase network = null)
{
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 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),

View File

@ -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",