mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-02-23 07:04:26 +01:00
Refactor confirmation count tracking (#6215)
This commit is contained in:
@@ -39,7 +39,6 @@ namespace BTCPayServer.Data
|
||||
public DbSet<PaymentRequestData> PaymentRequests { get; set; }
|
||||
public DbSet<PaymentData> Payments { get; set; }
|
||||
public DbSet<PayoutData> Payouts { get; set; }
|
||||
public DbSet<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
public DbSet<PlannedTransaction> PlannedTransactions { get; set; }
|
||||
public DbSet<PullPaymentData> PullPayments { get; set; }
|
||||
public DbSet<RefundData> Refunds { get; set; }
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
<ItemGroup>
|
||||
<None Remove="DBScripts\001.InvoiceFunctions.sql" />
|
||||
<None Remove="DBScripts\002.RefactorPayouts.sql" />
|
||||
<None Remove="DBScripts\003.RefactorPendingInvoicesPayments.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE OR REPLACE FUNCTION is_pending(status TEXT)
|
||||
RETURNS BOOLEAN AS $$
|
||||
SELECT status = 'Processing' OR status = 'New';
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
CREATE INDEX "IX_Invoices_Pending" ON "Invoices"((1)) WHERE is_pending("Status");
|
||||
CREATE INDEX "IX_Payments_Pending" ON "Payments"((1)) WHERE is_pending("Status");
|
||||
|
||||
DROP TABLE "PendingInvoices";
|
||||
@@ -32,8 +32,9 @@ namespace BTCPayServer.Data
|
||||
|
||||
public static string GetOrderId(string blob) => throw new NotSupportedException();
|
||||
public static string GetItemCode(string blob) => throw new NotSupportedException();
|
||||
public static bool IsPending(string status) => throw new NotSupportedException();
|
||||
|
||||
[Timestamp]
|
||||
[Timestamp]
|
||||
// With this, update of InvoiceData will fail if the row was modified by another process
|
||||
public uint XMin { get; set; }
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
@@ -51,7 +52,7 @@ namespace BTCPayServer.Data
|
||||
.HasColumnType("NUMERIC");
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetOrderId), new[] { typeof(string) }), b => b.HasName("get_orderid"));
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(GetItemCode), new[] { typeof(string) }), b => b.HasName("get_itemcode"));
|
||||
|
||||
}
|
||||
builder.HasDbFunction(typeof(InvoiceData).GetMethod(nameof(IsPending), new[] { typeof(string) }), b => b.HasName("is_pending"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace BTCPayServer.Data
|
||||
[Obsolete("Use Status instead")]
|
||||
public bool? Accounted { get; set; }
|
||||
public PaymentStatus? Status { get; set; }
|
||||
|
||||
public static bool IsPending(PaymentStatus? status) => throw new NotSupportedException();
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<PaymentData>()
|
||||
@@ -48,6 +48,7 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PaymentData>()
|
||||
.Property(o => o.Amount)
|
||||
.HasColumnType("NUMERIC");
|
||||
builder.HasDbFunction(typeof(PaymentData).GetMethod(nameof(IsPending), new[] { typeof(PaymentStatus?) }), b => b.HasName("is_pending"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240913034505_refactorpendinginvoicespayments")]
|
||||
[DBScript("003.RefactorPendingInvoicesPayments.sql")]
|
||||
public partial class refactorpendinginvoicespayments : DBScriptsMigration
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -926,15 +926,6 @@ namespace BTCPayServer.Controllers
|
||||
NetworkFeeMode.Never => 0,
|
||||
_ => throw new NotImplementedException()
|
||||
},
|
||||
RequiredConfirmations = invoice.SpeedPolicy switch
|
||||
{
|
||||
SpeedPolicy.HighSpeed => 0,
|
||||
SpeedPolicy.MediumSpeed => 1,
|
||||
SpeedPolicy.LowMediumSpeed => 2,
|
||||
SpeedPolicy.LowSpeed => 6,
|
||||
_ => null
|
||||
},
|
||||
ReceivedConfirmations = handler is BitcoinLikePaymentHandler bh ? invoice.GetAllBitcoinPaymentData(bh, false).FirstOrDefault()?.ConfirmationCount : null,
|
||||
Status = invoice.Status.ToString(),
|
||||
// The Tweak is part of the PaymentMethodFee, but let's not show it in the UI as it's negligible.
|
||||
NetworkFee = prompt.PaymentMethodFee - prompt.TweakFee,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceStopWatchedEvent : IHasInvoiceId
|
||||
{
|
||||
public InvoiceStopWatchedEvent(string invoiceId)
|
||||
{
|
||||
this.InvoiceId = invoiceId;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} is not monitored anymore.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,8 +323,8 @@ namespace BTCPayServer
|
||||
public static IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(this InvoiceEntity invoice, BitcoinLikePaymentHandler handler, bool accountedOnly)
|
||||
{
|
||||
return invoice.GetPayments(accountedOnly)
|
||||
.Where(p => p.PaymentMethodId == handler.PaymentMethodId)
|
||||
.Select(p => handler.ParsePaymentDetails(p.Details));
|
||||
.Select(p => p.GetDetails<BitcoinLikePaymentData>(handler))
|
||||
.Where(p => p is not null);
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, bool includeOffchain = false, CancellationToken cts = default(CancellationToken))
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
Subscribe<InvoiceDataChangedEvent>();
|
||||
Subscribe<InvoiceStopWatchedEvent>();
|
||||
Subscribe<InvoiceIPNEvent>();
|
||||
}
|
||||
|
||||
|
||||
@@ -191,9 +191,9 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Wait(string invoiceId)
|
||||
private async Task Wait(string invoiceId) => await Wait(await _invoiceRepository.GetInvoice(invoiceId));
|
||||
private async Task Wait(InvoiceEntity invoice)
|
||||
{
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId);
|
||||
try
|
||||
{
|
||||
// add 1 second to ensure watch won't trigger moments before invoice expires
|
||||
@@ -202,7 +202,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
Watch(invoice.Id);
|
||||
|
||||
// add 1 second to ensure watch won't trigger moments before monitoring expires
|
||||
delay = invoice.MonitoringExpiration.AddSeconds(1) - DateTimeOffset.UtcNow;
|
||||
@@ -210,7 +210,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
Watch(invoice.Id);
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{ }
|
||||
@@ -255,8 +255,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private async Task WaitPendingInvoices()
|
||||
{
|
||||
await Task.WhenAll((await _invoiceRepository.GetPendingInvoiceIds())
|
||||
.Select(id => Wait(id)).ToArray());
|
||||
await Task.WhenAll((await _invoiceRepository.GetPendingInvoices())
|
||||
.Select(i => Wait(i)).ToArray());
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
@@ -292,24 +292,6 @@ namespace BTCPayServer.HostedServices
|
||||
_eventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatus.Settled ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
var extendInvoiceMonitoring = await UpdateConfirmationCount(invoice);
|
||||
|
||||
// we extend monitor time if we haven't reached max confirmation count
|
||||
// say user used low fee and we only got 3 confirmations right before it's time to remove
|
||||
if (extendInvoiceMonitoring)
|
||||
{
|
||||
await _invoiceRepository.ExtendInvoiceMonitor(invoice.Id);
|
||||
}
|
||||
else if (await _invoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
{
|
||||
_eventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateContext.Events.Count == 0)
|
||||
break;
|
||||
}
|
||||
@@ -324,48 +306,6 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move that in the NBXplorerListener
|
||||
private async Task<bool> UpdateConfirmationCount(InvoiceEntity invoice)
|
||||
{
|
||||
bool extendInvoiceMonitoring = false;
|
||||
var updateConfirmationCountIfNeeded = invoice
|
||||
.GetPayments(true)
|
||||
.Select<PaymentEntity, Task<PaymentEntity>>(async payment =>
|
||||
{
|
||||
if (!_handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not Payments.Bitcoin.BitcoinLikePaymentHandler handler)
|
||||
return null;
|
||||
|
||||
var onChainPaymentData = handler.ParsePaymentDetails(payment.Details);
|
||||
var network = handler.Network;
|
||||
// Do update if confirmation count in the paymentData is not up to date
|
||||
if (onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation)
|
||||
{
|
||||
var client = _explorerClientProvider.GetExplorerClient(payment.Currency);
|
||||
var transactionResult = client is null ? null : await client.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
||||
var confirmationCount = transactionResult?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.Status = NBXplorerListener.IsSettled(invoice, onChainPaymentData) ? PaymentStatus.Settled : PaymentStatus.Processing;
|
||||
payment.SetDetails(handler, onChainPaymentData);
|
||||
|
||||
// we want to extend invoice monitoring until we reach max confirmations on all onchain payment methods
|
||||
if (confirmationCount < network.MaxTrackedConfirmation)
|
||||
extendInvoiceMonitoring = true;
|
||||
|
||||
return payment;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(updateConfirmationCountIfNeeded);
|
||||
var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList();
|
||||
if (updatedPaymentData.Count > 0)
|
||||
{
|
||||
await _paymentService.UpdatePayments(updatedPaymentData);
|
||||
}
|
||||
|
||||
return extendInvoiceMonitoring;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
|
||||
@@ -85,6 +85,13 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
|
||||
if (context.Model.Activated)
|
||||
{
|
||||
var paymentData = context.InvoiceEntity.GetAllBitcoinPaymentData(handler, true)?.MinBy(o => o.ConfirmationCount);
|
||||
if (paymentData is not null)
|
||||
{
|
||||
context.Model.RequiredConfirmations = NBXplorerListener.ConfirmationRequired(context.InvoiceEntity, paymentData);
|
||||
context.Model.ReceivedConfirmations = paymentData.ConfirmationCount;
|
||||
}
|
||||
|
||||
// We're leading the way in Bitcoin community with adding UPPERCASE Bech32 addresses in QR Code
|
||||
//
|
||||
// Correct casing: Addresses in payment URI need to be …
|
||||
|
||||
@@ -237,7 +237,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
|
||||
async Task UpdatePaymentStates(BTCPayWallet wallet)
|
||||
{
|
||||
var invoices = await _InvoiceRepository.GetPendingInvoices(skipNoPaymentInvoices: true);
|
||||
var invoices = await _InvoiceRepository.GetInvoicesWithPendingPayments(PaymentTypes.CHAIN.GetPaymentMethodId(wallet.Network.CryptoCode));
|
||||
await Task.WhenAll(invoices.Select(i => UpdatePaymentStates(wallet, i)).ToArray());
|
||||
}
|
||||
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId, bool fireEvents = true)
|
||||
@@ -348,10 +348,6 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
paymentEntitiesByPrevOut.TryAdd(prevout, payment);
|
||||
}
|
||||
|
||||
// if needed add invoice back to pending to track number of confirmations
|
||||
if (paymentData.ConfirmationCount < wallet.Network.MaxTrackedConfirmation)
|
||||
await _InvoiceRepository.AddPendingInvoiceIfNotPresent(invoice.Id);
|
||||
|
||||
if (updated)
|
||||
updatedPaymentEntities.Add(payment);
|
||||
}
|
||||
@@ -372,32 +368,25 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public static bool IsSettled(InvoiceEntity invoice, BitcoinLikePaymentData paymentData)
|
||||
public static int ConfirmationRequired(InvoiceEntity invoice, BitcoinLikePaymentData paymentData)
|
||||
=> (invoice, paymentData) switch
|
||||
{
|
||||
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
return paymentData.ConfirmationCount >= 1 || !paymentData.RBF;
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
return paymentData.ConfirmationCount >= 1;
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed)
|
||||
{
|
||||
return paymentData.ConfirmationCount >= 2;
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
return paymentData.ConfirmationCount >= 6;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
({ SpeedPolicy: SpeedPolicy.HighSpeed }, { RBF: true }) => 1,
|
||||
({ SpeedPolicy: SpeedPolicy.HighSpeed }, _) => 0,
|
||||
({ SpeedPolicy: SpeedPolicy.MediumSpeed }, _) => 1,
|
||||
({ SpeedPolicy: SpeedPolicy.LowMediumSpeed }, _) => 2,
|
||||
({ SpeedPolicy: SpeedPolicy.LowSpeed }, _) => 6,
|
||||
_ => 6,
|
||||
};
|
||||
|
||||
static bool IsSettled(InvoiceEntity invoice, BitcoinLikePaymentData paymentData)
|
||||
=> ConfirmationRequired(invoice, paymentData) <= paymentData.ConfirmationCount;
|
||||
|
||||
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
|
||||
{
|
||||
var handler = _handlers.GetBitcoinHandler(wallet.Network);
|
||||
int totalPayment = 0;
|
||||
var invoices = await _InvoiceRepository.GetPendingInvoices(true);
|
||||
var invoices = await _InvoiceRepository.GetInvoicesWithPendingPayments(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), true);
|
||||
var coinsPerDerivationStrategy =
|
||||
new Dictionary<DerivationStrategyBase, ReceivedCoin[]>();
|
||||
foreach (var i in invoices)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services.Altcoins.Monero.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
@@ -8,15 +10,18 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
public class MoneroPaymentModelExtension : IPaymentModelExtension
|
||||
{
|
||||
private readonly BTCPayNetworkBase _network;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly IPaymentLinkExtension paymentLinkExtension;
|
||||
|
||||
public MoneroPaymentModelExtension(
|
||||
PaymentMethodId paymentMethodId,
|
||||
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
|
||||
BTCPayNetworkBase network)
|
||||
BTCPayNetworkBase network,
|
||||
PaymentMethodHandlerDictionary handlers)
|
||||
{
|
||||
PaymentMethodId = paymentMethodId;
|
||||
_network = network;
|
||||
_handlers = handlers;
|
||||
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
|
||||
}
|
||||
public PaymentMethodId PaymentMethodId { get; }
|
||||
@@ -30,6 +35,18 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
{
|
||||
if (context.Model.Activated)
|
||||
{
|
||||
if (_handlers.TryGetValue(PaymentMethodId, out var handler))
|
||||
{
|
||||
var details = context.InvoiceEntity.GetPayments(true)
|
||||
.Select(p => p.GetDetails<MoneroLikePaymentData>(handler))
|
||||
.Where(p => p is not null)
|
||||
.FirstOrDefault();
|
||||
if (details is not null)
|
||||
{
|
||||
context.Model.ReceivedConfirmations = details.ConfirmationCount;
|
||||
context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy);
|
||||
}
|
||||
}
|
||||
context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
|
||||
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl;
|
||||
}
|
||||
|
||||
@@ -369,34 +369,27 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
}
|
||||
|
||||
private bool GetStatus(MoneroLikePaymentData details, SpeedPolicy speedPolicy)
|
||||
{
|
||||
if (details.ConfirmationCount < details.LockTime)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (details.InvoiceSettledConfirmationThreshold is { } v)
|
||||
return details.ConfirmationCount >= v;
|
||||
switch (speedPolicy)
|
||||
{
|
||||
case SpeedPolicy.HighSpeed:
|
||||
return details.ConfirmationCount >= 0;
|
||||
case SpeedPolicy.MediumSpeed:
|
||||
return details.ConfirmationCount >= 1;
|
||||
case SpeedPolicy.LowMediumSpeed:
|
||||
return details.ConfirmationCount >= 2;
|
||||
case SpeedPolicy.LowSpeed:
|
||||
return details.ConfirmationCount >= 6;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
=> ConfirmationsRequired(details, speedPolicy) <= details.ConfirmationCount;
|
||||
|
||||
public static long ConfirmationsRequired(MoneroLikePaymentData details, SpeedPolicy speedPolicy)
|
||||
=> (details, speedPolicy) switch
|
||||
{
|
||||
(_, _) when details.ConfirmationCount < details.LockTime => details.LockTime - details.ConfirmationCount,
|
||||
({ InvoiceSettledConfirmationThreshold: long v }, _) => v,
|
||||
(_, SpeedPolicy.HighSpeed) => 0,
|
||||
(_, SpeedPolicy.MediumSpeed) => 1,
|
||||
(_, SpeedPolicy.LowMediumSpeed) => 2,
|
||||
(_, SpeedPolicy.LowSpeed) => 6,
|
||||
_ => 6,
|
||||
};
|
||||
|
||||
|
||||
private async Task UpdateAnyPendingMoneroLikePayment(string cryptoCode)
|
||||
{
|
||||
var invoices = await _invoiceRepository.GetPendingInvoices();
|
||||
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
var invoices = await _invoiceRepository.GetInvoicesWithPendingPayments(paymentMethodId);
|
||||
if (!invoices.Any())
|
||||
return;
|
||||
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
invoices = invoices.Where(entity => entity.GetPaymentPrompt(paymentMethodId)?.Activated is true).ToArray();
|
||||
await UpdatePaymentStates(cryptoCode, invoices);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Altcoins.Zcash.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
|
||||
@@ -8,15 +9,18 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
|
||||
public class ZcashPaymentModelExtension : IPaymentModelExtension
|
||||
{
|
||||
private readonly BTCPayNetworkBase _network;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly IPaymentLinkExtension paymentLinkExtension;
|
||||
|
||||
public ZcashPaymentModelExtension(
|
||||
PaymentMethodId paymentMethodId,
|
||||
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
|
||||
BTCPayNetworkBase network)
|
||||
BTCPayNetworkBase network,
|
||||
PaymentMethodHandlerDictionary handlers)
|
||||
{
|
||||
PaymentMethodId = paymentMethodId;
|
||||
_network = network;
|
||||
_handlers = handlers;
|
||||
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
|
||||
}
|
||||
public PaymentMethodId PaymentMethodId { get; }
|
||||
@@ -30,6 +34,18 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
|
||||
{
|
||||
if (context.Model.Activated)
|
||||
{
|
||||
if (_handlers.TryGetValue(PaymentMethodId, out var handler))
|
||||
{
|
||||
var details = context.InvoiceEntity.GetPayments(true)
|
||||
.Select(p => p.GetDetails<ZcashLikePaymentData>(handler))
|
||||
.Where(p => p is not null)
|
||||
.FirstOrDefault();
|
||||
if (details is not null)
|
||||
{
|
||||
context.Model.ReceivedConfirmations = details.ConfirmationCount;
|
||||
context.Model.RequiredConfirmations = (int)ZcashListener.ConfirmationsRequired(context.InvoiceEntity.SpeedPolicy);
|
||||
}
|
||||
}
|
||||
context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
|
||||
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl;
|
||||
}
|
||||
|
||||
@@ -363,28 +363,23 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
|
||||
}
|
||||
|
||||
private bool GetStatus(ZcashLikePaymentData details, SpeedPolicy speedPolicy)
|
||||
=> details.ConfirmationCount >= ConfirmationsRequired(speedPolicy);
|
||||
public static int ConfirmationsRequired(SpeedPolicy speedPolicy)
|
||||
=> speedPolicy switch
|
||||
{
|
||||
switch (speedPolicy)
|
||||
{
|
||||
case SpeedPolicy.HighSpeed:
|
||||
return details.ConfirmationCount >= 0;
|
||||
case SpeedPolicy.MediumSpeed:
|
||||
return details.ConfirmationCount >= 1;
|
||||
case SpeedPolicy.LowMediumSpeed:
|
||||
return details.ConfirmationCount >= 2;
|
||||
case SpeedPolicy.LowSpeed:
|
||||
return details.ConfirmationCount >= 6;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
SpeedPolicy.HighSpeed => 0,
|
||||
SpeedPolicy.MediumSpeed => 1,
|
||||
SpeedPolicy.LowMediumSpeed => 2,
|
||||
SpeedPolicy.LowSpeed => 6,
|
||||
_ => 6,
|
||||
};
|
||||
|
||||
private async Task UpdateAnyPendingZcashLikePayment(string cryptoCode)
|
||||
{
|
||||
var invoices = await _invoiceRepository.GetPendingInvoices();
|
||||
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
var invoices = await _invoiceRepository.GetInvoicesWithPendingPayments(paymentMethodId);
|
||||
if (!invoices.Any())
|
||||
return;
|
||||
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
invoices = invoices.Where(entity => entity.GetPaymentPrompt(paymentMethodId).Activated).ToArray();
|
||||
await UpdatePaymentStates(cryptoCode, invoices);
|
||||
}
|
||||
|
||||
@@ -67,18 +67,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePendingInvoice(string invoiceId)
|
||||
{
|
||||
using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException) { return false; }
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<InvoiceEntity>> GetInvoicesFromAddresses(string[] addresses)
|
||||
{
|
||||
if (addresses.Length is 0)
|
||||
@@ -103,23 +91,31 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity[]> GetPendingInvoices(bool includeAddressData = false, bool skipNoPaymentInvoices = false, CancellationToken cancellationToken = default)
|
||||
public async Task<InvoiceEntity[]> GetInvoicesWithPendingPayments(PaymentMethodId paymentMethodId, bool includeAddresses = false)
|
||||
{
|
||||
using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
var q = ctx.PendingInvoices.AsQueryable();
|
||||
q = q.Include(o => o.InvoiceData)
|
||||
.ThenInclude(o => o.Payments);
|
||||
if (includeAddressData)
|
||||
q = q.Include(o => o.InvoiceData)
|
||||
.ThenInclude(o => o.AddressInvoices);
|
||||
if (skipNoPaymentInvoices)
|
||||
q = q.Where(i => i.InvoiceData.Payments.Any());
|
||||
return (await q.Select(o => o.InvoiceData).ToArrayAsync(cancellationToken)).Select(ToEntity).ToArray();
|
||||
var invoiceIds = (await ctx.Payments.Where(p => PaymentData.IsPending(p.Status)).Select(p => p.InvoiceDataId).ToArrayAsync()).Distinct().ToArray();
|
||||
if (invoiceIds.Length is 0)
|
||||
return Array.Empty<InvoiceEntity>();
|
||||
return await GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = invoiceIds,
|
||||
IncludeAddresses = true
|
||||
});
|
||||
}
|
||||
public async Task<string[]> GetPendingInvoiceIds()
|
||||
public async Task<InvoiceEntity[]> GetPendingInvoices(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
return await ctx.PendingInvoices.AsQueryable().Select(data => data.Id).ToArrayAsync();
|
||||
var rows = await ctx.Invoices.Where(i => InvoiceData.IsPending(i.Status))
|
||||
.Include(i => i.Payments)
|
||||
.Select(o => o).ToArrayAsync();
|
||||
return rows.Select(ToEntity).ToArray();
|
||||
}
|
||||
public async Task<InvoiceEntity[]> GetPendingInvoices()
|
||||
{
|
||||
using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
return (await ctx.Invoices.Where(i => InvoiceData.IsPending(i.Status)).Select(i => i).ToArrayAsync())
|
||||
.Select(i => ToEntity(i)).ToArray();
|
||||
}
|
||||
|
||||
public async Task<List<Data.WebhookDeliveryData>> GetWebhookDeliveries(string invoiceId)
|
||||
@@ -170,27 +166,6 @@ retry:
|
||||
_eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoiceId));
|
||||
}
|
||||
|
||||
public async Task ExtendInvoiceMonitor(string invoiceId)
|
||||
{
|
||||
retry:
|
||||
using (var ctx = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
|
||||
|
||||
var invoice = invoiceData.GetBlob();
|
||||
invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1);
|
||||
invoiceData.SetBlob(invoice);
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateInvoiceAsync(InvoiceCreationContext creationContext)
|
||||
{
|
||||
var invoice = creationContext.InvoiceEntity;
|
||||
@@ -225,7 +200,6 @@ retry:
|
||||
if (prompt.Activated)
|
||||
textSearch.Add(prompt.Calculate().TotalDue.ToString());
|
||||
}
|
||||
await context.PendingInvoices.AddAsync(new PendingInvoiceData() { Id = invoice.Id });
|
||||
|
||||
textSearch.Add(invoice.Id);
|
||||
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
|
||||
@@ -357,20 +331,6 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddPendingInvoiceIfNotPresent(string invoiceId)
|
||||
{
|
||||
using var context = _applicationDbContextFactory.CreateContext();
|
||||
if (!context.PendingInvoices.Any(a => a.Id == invoiceId))
|
||||
{
|
||||
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId });
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) { } // Already exists
|
||||
}
|
||||
}
|
||||
|
||||
const string InsertInvoiceEvent = "INSERT INTO \"InvoiceEvents\" (\"InvoiceDataId\", \"Severity\", \"Message\", \"Timestamp\") VALUES (@InvoiceDataId, @Severity, @Message, @Timestamp)";
|
||||
|
||||
public async Task AddInvoiceEvent(string invoiceId, object evt, InvoiceEventData.EventSeverity severity)
|
||||
|
||||
@@ -19,5 +19,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
Details = JToken.FromObject(details, handler.Serializer);
|
||||
return this;
|
||||
}
|
||||
public T GetDetails<T>(IPaymentMethodHandler handler) where T : class
|
||||
{
|
||||
if (handler.Id != handler.Id)
|
||||
return null;
|
||||
return handler.ParsePaymentDetails(Details) as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,10 @@
|
||||
m.Crypto = network.CryptoCode;
|
||||
m.DepositAddress = BitcoinAddress.Create(payment.Destination, network.NBitcoinNetwork);
|
||||
|
||||
var confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
if (confirmationCount >= network.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (network.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
var confReq = NBXplorerListener.ConfirmationRequired(payment.InvoiceEntity, onChainPaymentData);
|
||||
var confCount = onChainPaymentData.ConfirmationCount;
|
||||
confCount = Math.Min(confReq, confCount);
|
||||
m.Confirmations = $"{confCount} / {confReq}";
|
||||
if (onChainPaymentData.PayjoinInformation is PayjoinInformation pj)
|
||||
{
|
||||
payjoinInformation = pj;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@using BTCPayServer.Plugins.Altcoins;
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
@using BTCPayServer.Services.Altcoins.Monero.Services
|
||||
@using BTCPayServer.Services.Altcoins.Monero.UI
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@@ -19,15 +20,11 @@
|
||||
m.Crypto = handler.Network.CryptoCode;
|
||||
m.DepositAddress = payment.Destination;
|
||||
m.Amount = payment.Value.ToString(CultureInfo.InvariantCulture);
|
||||
var confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
if (confirmationCount >= handler.Network.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (handler.Network.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var confReq = MoneroListener.ConfirmationsRequired(onChainPaymentData, payment.InvoiceEntity.SpeedPolicy);
|
||||
var confCount = onChainPaymentData.ConfirmationCount;
|
||||
confCount = Math.Min(confReq, confCount);
|
||||
m.Confirmations = $"{confCount} / {confReq}";
|
||||
|
||||
m.TransactionId = onChainPaymentData.TransactionId;
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using BTCPayServer.Components.TruncateCenter
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Altcoins.Zcash.Payments
|
||||
@using BTCPayServer.Services.Altcoins.Zcash.Services
|
||||
@using BTCPayServer.Services.Altcoins.Zcash.UI
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@@ -20,15 +21,11 @@
|
||||
m.Crypto = handler.Network.CryptoCode;
|
||||
m.DepositAddress = payment.Destination;
|
||||
m.Amount = payment.Value.ToString(CultureInfo.InvariantCulture);
|
||||
var confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
if (confirmationCount >= handler.Network.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (handler.Network.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var confReq = ZcashListener.ConfirmationsRequired(payment.InvoiceEntity.SpeedPolicy);
|
||||
var confCount = onChainPaymentData.ConfirmationCount;
|
||||
confCount = Math.Min(confReq, confCount);
|
||||
m.Confirmations = $"{confCount} / {confReq}";
|
||||
|
||||
m.TransactionId = onChainPaymentData.TransactionId;
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
|
||||
Reference in New Issue
Block a user