Fix monero payments

This commit is contained in:
nicolas.dorier 2024-10-16 23:15:27 +09:00
parent d7fd90c4c3
commit b7affb1d34
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
5 changed files with 106 additions and 146 deletions

View File

@ -727,5 +727,39 @@
},
"version": 2
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": null,
"accounted": true,
"cryptoCode": "XMR",
"networkFee": 0.0000000019,
"receivedTimeMs": 1705500405468,
"cryptoPaymentData": "{\"Amount\":62700000000,\"Address\":\"85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t\",\"SubaddressIndex\":23,\"SubaccountIndex\":0,\"BlockHeight\":3063946,\"ConfirmationCount\":10,\"TransactionId\":\"cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06\",\"LockTime\":0}",
"cryptoPaymentDataType": "MoneroLike"
},
"expected": {
"divisibility": 12,
"destination": "85CjjvQyW7PjNmiFRKZuEHKzZjiB3rjSu6n8zPzji4PtQxw1CyEY5H5FBge6GRUMJqR7FqsgBHU7H1FpEppvZXS6HGpFF6t",
"details": {
"blockHeight": 3063946,
"confirmationCount": 10,
"lockTime": 0,
"subaccountIndex": 0,
"subaddressIndex": 23,
"transactionId": "cc2e9ef03864c6af5e0d6f1c730ba142144f6588c50035b2996a59a6f3771b06"
},
"version": 2
},
"expectedProperties": {
"Amount": "0.0627",
"PaymentMethodId": "XMR-CHAIN",
"Currency": "XMR",
"Status": "Settled",
"Accounted": null
}
}
]

View File

@ -430,7 +430,7 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork);
amount ??= bip21.Amount.GetValue(network);
amount ??= bip21.Amount?.GetValue(network);
if (bip21.Address is null)
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
"This BIP21 destination is missing a bitcoin address", this);

View File

@ -3,10 +3,12 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Altcoins.Monero.Configuration;
@ -24,7 +26,7 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Altcoins.Monero.Services
{
public class MoneroListener : IHostedService
public class MoneroListener : EventHostedServiceBase
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
@ -35,9 +37,6 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly InvoiceActivator _invoiceActivator;
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;
public MoneroListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
@ -47,7 +46,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
ILogger<MoneroListener> logger,
PaymentMethodHandlerDictionary handlers,
InvoiceActivator invoiceActivator,
PaymentService paymentService)
PaymentService paymentService) : base(eventAggregator, logger)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
@ -60,70 +59,40 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
_paymentService = paymentService;
}
public Task StartAsync(CancellationToken cancellationToken)
protected override void SubscribeToEvents()
{
if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
{
return Task.CompletedTask;
base.SubscribeToEvents();
Subscribe<MoneroEvent>();
Subscribe<MoneroRPCProvider.MoneroDaemonStateChange>();
}
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_eventAggregator.Subscribe<MoneroEvent>(OnMoneroEvent));
leases.Add(_eventAggregator.Subscribe<MoneroRPCProvider.MoneroDaemonStateChange>(e =>
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (_moneroRpcProvider.IsAvailable(e.CryptoCode))
if (evt is MoneroRPCProvider.MoneroDaemonStateChange stateChange)
{
_logger.LogInformation($"{e.CryptoCode} just became available");
_ = UpdateAnyPendingMoneroLikePayment(e.CryptoCode);
if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode))
{
_logger.LogInformation($"{stateChange.CryptoCode} just became available");
_ = UpdateAnyPendingMoneroLikePayment(stateChange.CryptoCode);
}
else
{
_logger.LogInformation($"{e.CryptoCode} just became unavailable");
}
}));
_ = WorkThroughQueue(_Cts.Token);
return Task.CompletedTask;
}
private async Task WorkThroughQueue(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
if (taskQueue.TryDequeue(out var t))
{
try
{
await t.Invoke(token);
}
catch (Exception e)
{
_logger.LogError($"error with queue item", e);
_logger.LogInformation($"{stateChange.CryptoCode} just became unavailable");
}
}
else
{
await Task.Delay(TimeSpan.FromSeconds(1), token);
}
}
}
private void OnMoneroEvent(MoneroEvent obj)
{
if (!_moneroRpcProvider.IsAvailable(obj.CryptoCode))
else if (evt is MoneroEvent moneroEvent)
{
if (!_moneroRpcProvider.IsAvailable(moneroEvent.CryptoCode))
return;
}
if (!string.IsNullOrEmpty(obj.BlockHash))
if (!string.IsNullOrEmpty(moneroEvent.BlockHash))
{
taskQueue.Enqueue(token => OnNewBlock(obj.CryptoCode));
await OnNewBlock(moneroEvent.CryptoCode);
}
if (!string.IsNullOrEmpty(obj.TransactionHash))
if (!string.IsNullOrEmpty(moneroEvent.TransactionHash))
{
taskQueue.Enqueue(token => OnTransactionUpdated(obj.CryptoCode, obj.TransactionHash));
await OnTransactionUpdated(moneroEvent.CryptoCode, moneroEvent.TransactionHash);
}
}
}
@ -204,7 +173,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
var transferProcessingTasks = new List<Task>();
var updatedPaymentEntities = new BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)>();
var updatedPaymentEntities = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
foreach (var keyValuePair in tasks)
{
var transfers = keyValuePair.Value.Result.In;
@ -256,14 +225,6 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts?.Cancel();
return Task.CompletedTask;
}
private async Task OnNewBlock(string cryptoCode)
{
await UpdateAnyPendingMoneroLikePayment(cryptoCode);
@ -278,7 +239,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
"get_transfer_by_txid",
new GetTransferByTransactionIdRequest() { TransactionId = transactionHash });
var paymentsToUpdate = new BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)>();
var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
//group all destinations of the tx together and loop through the sets
foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address))
@ -317,7 +278,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex,
long subaddressIndex,
string txId, long confirmations, long blockHeight, long locktime, InvoiceEntity invoice,
BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
List<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
{
var network = _networkProvider.GetNetwork(cryptoCode);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
@ -333,9 +294,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
LockTime = locktime,
InvoiceSettledConfirmationThreshold = promptDetails.InvoiceSettledConfirmationThreshold
};
var status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing;
var paymentData = new Data.PaymentData()
{
Status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing,
Status = status,
Amount = MoneroMoney.Convert(totalAmount),
Created = DateTimeOffset.UtcNow,
Id = $"{txId}#{subaccountIndex}#{subaddressIndex}",
@ -346,11 +308,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
//check if this tx exists as a payment to this invoice already
var alreadyExistingPaymentThatMatches = GetAllMoneroLikePayments(invoice, cryptoCode)
.Select(entity => (Payment: entity, PaymentData: handler.ParsePaymentDetails(entity.Details)))
.SingleOrDefault(c => c.Payment.PaymentMethodId == pmi);
.SingleOrDefault(c => c.Id == paymentData.Id && c.PaymentMethodId == pmi);
//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 == null)
{
var payment = await _paymentService.AddPayment(paymentData, [txId]);
if (payment != null)
@ -359,9 +320,9 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
else
{
//else update it with the new data
alreadyExistingPaymentThatMatches.PaymentData = details;
alreadyExistingPaymentThatMatches.Payment.Details = JToken.FromObject(paymentData, handler.Serializer);
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches.Payment, invoice));
alreadyExistingPaymentThatMatches.Status = status;
alreadyExistingPaymentThatMatches.Details = JToken.FromObject(details, handler.Serializer);
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches, invoice));
}
}

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
@ -26,7 +27,7 @@ using static BTCPayServer.Client.Models.InvoicePaymentMethodDataModel;
namespace BTCPayServer.Services.Altcoins.Zcash.Services
{
public class ZcashListener : IHostedService
public class ZcashListener : EventHostedServiceBase
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
@ -37,9 +38,6 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
private readonly PaymentService _paymentService;
private readonly InvoiceActivator _invoiceActivator;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly CompositeDisposable leases = new CompositeDisposable();
private readonly Channel<Func<Task>> _requests = Channel.CreateUnbounded<Func<Task>>();
private CancellationTokenSource _Cts;
public ZcashListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
@ -49,7 +47,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
ILogger<ZcashListener> logger,
PaymentService paymentService,
InvoiceActivator invoiceActivator,
PaymentMethodHandlerDictionary handlers)
PaymentMethodHandlerDictionary handlers) : base(eventAggregator, logger)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
@ -62,63 +60,39 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
_handlers = handlers;
}
public Task StartAsync(CancellationToken cancellationToken)
protected override void SubscribeToEvents()
{
if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.Any())
{
return Task.CompletedTask;
base.SubscribeToEvents();
Subscribe<ZcashEvent>();
Subscribe<ZcashRPCProvider.ZcashDaemonStateChange>();
}
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_eventAggregator.Subscribe<ZcashEvent>(OnZcashEvent));
leases.Add(_eventAggregator.Subscribe<ZcashRPCProvider.ZcashDaemonStateChange>(e =>
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (_ZcashRpcProvider.IsAvailable(e.CryptoCode))
if (evt is ZcashRPCProvider.ZcashDaemonStateChange stateChanged)
{
_logger.LogInformation($"{e.CryptoCode} just became available");
_ = UpdateAnyPendingZcashLikePayment(e.CryptoCode);
if (_ZcashRpcProvider.IsAvailable(stateChanged.CryptoCode))
{
_logger.LogInformation($"{stateChanged.CryptoCode} just became available");
_ = UpdateAnyPendingZcashLikePayment(stateChanged.CryptoCode);
}
else
{
_logger.LogInformation($"{e.CryptoCode} just became unavailable");
_logger.LogInformation($"{stateChanged.CryptoCode} just became unavailable");
}
}));
_ = WorkThroughQueue(_Cts.Token);
return Task.CompletedTask;
}
private async Task WorkThroughQueue(CancellationToken token)
{
while (await _requests.Reader.WaitToReadAsync(token) && _requests.Reader.TryRead(out var action)) {
token.ThrowIfCancellationRequested();
try {
await action.Invoke();
}
catch (Exception e)
{
_logger.LogError($"error with action item {e}");
}
}
}
private void OnZcashEvent(ZcashEvent obj)
{
if (!_ZcashRpcProvider.IsAvailable(obj.CryptoCode))
else if (evt is ZcashEvent zcashEvent)
{
if (!_ZcashRpcProvider.IsAvailable(zcashEvent.CryptoCode))
return;
}
if (!string.IsNullOrEmpty(obj.BlockHash))
if (!string.IsNullOrEmpty(zcashEvent.BlockHash))
{
if (!_requests.Writer.TryWrite(() => OnNewBlock(obj.CryptoCode))) {
_logger.LogWarning($"Failed to write new block task to channel");
await OnNewBlock(zcashEvent.CryptoCode);
}
}
if (!string.IsNullOrEmpty(obj.TransactionHash))
if (!string.IsNullOrEmpty(zcashEvent.TransactionHash))
{
if (!_requests.Writer.TryWrite(() => OnTransactionUpdated(obj.CryptoCode, obj.TransactionHash))) {
_logger.LogWarning($"Failed to write new tx task to channel");
await OnTransactionUpdated(zcashEvent.CryptoCode, zcashEvent.TransactionHash);
}
}
}
@ -202,7 +176,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
var transferProcessingTasks = new List<Task>();
var updatedPaymentEntities = new BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)>();
var updatedPaymentEntities = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
foreach (var keyValuePair in tasks)
{
var transfers = keyValuePair.Value.Result.In;
@ -254,14 +228,6 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts?.Cancel();
return Task.CompletedTask;
}
private async Task OnNewBlock(string cryptoCode)
{
await UpdateAnyPendingZcashLikePayment(cryptoCode);
@ -276,7 +242,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
"get_transfer_by_txid",
new GetTransferByTransactionIdRequest() { TransactionId = transactionHash });
var paymentsToUpdate = new BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)>();
var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
//group all destinations of the tx together and loop through the sets
foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address))
@ -315,7 +281,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex,
long subaddressIndex,
string txId, long confirmations, long blockHeight, InvoiceEntity invoice,
BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
List<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
{
var network = _networkProvider.GetNetwork(cryptoCode);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
@ -329,9 +295,10 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
ConfirmationCount = confirmations,
BlockHeight = blockHeight
};
var status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing;
var paymentData = new Data.PaymentData()
{
Status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing,
Status = status,
Amount = ZcashMoney.Convert(totalAmount),
Created = DateTimeOffset.UtcNow,
Id = $"{txId}#{subaccountIndex}#{subaddressIndex}",
@ -340,11 +307,10 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
var alreadyExistingPaymentThatMatches = GetAllZcashLikePayments(invoice, cryptoCode)
.Select(entity => (Payment: entity, PaymentData: handler.ParsePaymentDetails(entity.Details)))
.SingleOrDefault(c => c.Payment.PaymentMethodId == pmi);
.SingleOrDefault(c => c.Id == paymentData.Id && c.PaymentMethodId == pmi);
//if it doesnt, add it and assign a new Zcashlike address to the system if a balance is still due
if (alreadyExistingPaymentThatMatches.Payment == null)
if (alreadyExistingPaymentThatMatches == null)
{
var payment = await _paymentService.AddPayment(paymentData, [txId]);
if (payment != null)
@ -353,9 +319,9 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
else
{
//else update it with the new data
alreadyExistingPaymentThatMatches.PaymentData = details;
alreadyExistingPaymentThatMatches.Payment.Details = JToken.FromObject(paymentData, handler.Serializer);
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches.Payment, invoice));
alreadyExistingPaymentThatMatches.Status = status;
alreadyExistingPaymentThatMatches.Details = JToken.FromObject(details, handler.Serializer);
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches, invoice));
}
}

View File

@ -93,7 +93,6 @@ namespace BTCPayServer.Services.Invoices
{
var dbPayment = dbPayments[payment.Id];
var invBlob = _invoiceRepository.ToEntity(dbPayment.InvoiceData);
var dbPaymentEntity = dbPayment.GetBlob();
var wasConfirmed = dbPayment.Status is PaymentStatus.Settled;
if (!wasConfirmed && payment.Status is PaymentStatus.Settled)
{