From b7affb1d34707872a43414029e38b67a33fe3a5e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 16 Oct 2024 23:15:27 +0900 Subject: [PATCH] Fix monero payments --- .../TestData/InvoiceMigrationTestVectors.json | 34 ++++++ ...GreenfieldStoreOnChainWalletsController.cs | 2 +- .../Monero/Services/MoneroListener.cs | 109 ++++++------------ .../Altcoins/Zcash/Services/ZcashListener.cs | 106 ++++++----------- .../Services/Invoices/PaymentService.cs | 1 - 5 files changed, 106 insertions(+), 146 deletions(-) diff --git a/BTCPayServer.Tests/TestData/InvoiceMigrationTestVectors.json b/BTCPayServer.Tests/TestData/InvoiceMigrationTestVectors.json index a3dda6d26..ac9490fab 100644 --- a/BTCPayServer.Tests/TestData/InvoiceMigrationTestVectors.json +++ b/BTCPayServer.Tests/TestData/InvoiceMigrationTestVectors.json @@ -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 + } } ] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs index 620413d4b..658985edb 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs @@ -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); diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index 901a77953..e707a977a 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -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> taskQueue = new Queue>(); - private CancellationTokenSource _Cts; public MoneroListener(InvoiceRepository invoiceRepository, EventAggregator eventAggregator, @@ -47,7 +46,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services ILogger logger, PaymentMethodHandlerDictionary handlers, InvoiceActivator invoiceActivator, - PaymentService paymentService) + PaymentService paymentService) : base(eventAggregator, logger) { _invoiceRepository = invoiceRepository; _eventAggregator = eventAggregator; @@ -60,73 +59,43 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services _paymentService = paymentService; } - public Task StartAsync(CancellationToken cancellationToken) + protected override void SubscribeToEvents() { - if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any()) - { - return Task.CompletedTask; - } - _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + base.SubscribeToEvents(); + Subscribe(); + Subscribe(); + } - leases.Add(_eventAggregator.Subscribe(OnMoneroEvent)); - leases.Add(_eventAggregator.Subscribe(e => + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + if (evt is MoneroRPCProvider.MoneroDaemonStateChange stateChange) { - if (_moneroRpcProvider.IsAvailable(e.CryptoCode)) + if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode)) { - _logger.LogInformation($"{e.CryptoCode} just became available"); - _ = UpdateAnyPendingMoneroLikePayment(e.CryptoCode); + _logger.LogInformation($"{stateChange.CryptoCode} just became available"); + _ = UpdateAnyPendingMoneroLikePayment(stateChange.CryptoCode); } else { - _logger.LogInformation($"{e.CryptoCode} just became unavailable"); + _logger.LogInformation($"{stateChange.CryptoCode} just became unavailable"); } - })); - _ = WorkThroughQueue(_Cts.Token); - return Task.CompletedTask; - } - - private async Task WorkThroughQueue(CancellationToken token) - { - while (!token.IsCancellationRequested) + } + else if (evt is MoneroEvent moneroEvent) { - if (taskQueue.TryDequeue(out var t)) + if (!_moneroRpcProvider.IsAvailable(moneroEvent.CryptoCode)) + return; + + if (!string.IsNullOrEmpty(moneroEvent.BlockHash)) { - try - { - - await t.Invoke(token); - } - catch (Exception e) - { - - _logger.LogError($"error with queue item", e); - } + await OnNewBlock(moneroEvent.CryptoCode); } - else + if (!string.IsNullOrEmpty(moneroEvent.TransactionHash)) { - await Task.Delay(TimeSpan.FromSeconds(1), token); + await OnTransactionUpdated(moneroEvent.CryptoCode, moneroEvent.TransactionHash); } } } - private void OnMoneroEvent(MoneroEvent obj) - { - if (!_moneroRpcProvider.IsAvailable(obj.CryptoCode)) - { - return; - } - - if (!string.IsNullOrEmpty(obj.BlockHash)) - { - taskQueue.Enqueue(token => OnNewBlock(obj.CryptoCode)); - } - - if (!string.IsNullOrEmpty(obj.TransactionHash)) - { - taskQueue.Enqueue(token => OnTransactionUpdated(obj.CryptoCode, obj.TransactionHash)); - } - } - private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) { _logger.LogInformation( @@ -204,7 +173,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services var transferProcessingTasks = new List(); - 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)); } } diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs index b0202ff49..c903fc625 100644 --- a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs +++ b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs @@ -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,19 +38,16 @@ 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> _requests = Channel.CreateUnbounded>(); - private CancellationTokenSource _Cts; public ZcashListener(InvoiceRepository invoiceRepository, EventAggregator eventAggregator, ZcashRPCProvider ZcashRpcProvider, ZcashLikeConfiguration ZcashLikeConfiguration, BTCPayNetworkProvider networkProvider, - ILogger logger, + ILogger 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; - } - _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + base.SubscribeToEvents(); + Subscribe(); + Subscribe(); + } - leases.Add(_eventAggregator.Subscribe(OnZcashEvent)); - leases.Add(_eventAggregator.Subscribe(e => + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + if (evt is ZcashRPCProvider.ZcashDaemonStateChange stateChanged) { - if (_ZcashRpcProvider.IsAvailable(e.CryptoCode)) + if (_ZcashRpcProvider.IsAvailable(stateChanged.CryptoCode)) { - _logger.LogInformation($"{e.CryptoCode} just became available"); - _ = UpdateAnyPendingZcashLikePayment(e.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; - } + } + else if (evt is ZcashEvent zcashEvent) + { + if (!_ZcashRpcProvider.IsAvailable(zcashEvent.CryptoCode)) + return; - 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) + if (!string.IsNullOrEmpty(zcashEvent.BlockHash)) { - _logger.LogError($"error with action item {e}"); + await OnNewBlock(zcashEvent.CryptoCode); } - } - } - - private void OnZcashEvent(ZcashEvent obj) - { - if (!_ZcashRpcProvider.IsAvailable(obj.CryptoCode)) - { - return; - } - - if (!string.IsNullOrEmpty(obj.BlockHash)) - { - if (!_requests.Writer.TryWrite(() => OnNewBlock(obj.CryptoCode))) { - _logger.LogWarning($"Failed to write new block task to channel"); - } - } - - if (!string.IsNullOrEmpty(obj.TransactionHash)) - { - if (!_requests.Writer.TryWrite(() => OnTransactionUpdated(obj.CryptoCode, obj.TransactionHash))) { - _logger.LogWarning($"Failed to write new tx task to channel"); + if (!string.IsNullOrEmpty(zcashEvent.TransactionHash)) + { + await OnTransactionUpdated(zcashEvent.CryptoCode, zcashEvent.TransactionHash); } } } @@ -202,7 +176,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services var transferProcessingTasks = new List(); - 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)); } } diff --git a/BTCPayServer/Services/Invoices/PaymentService.cs b/BTCPayServer/Services/Invoices/PaymentService.cs index 3d591cd1d..627db1973 100644 --- a/BTCPayServer/Services/Invoices/PaymentService.cs +++ b/BTCPayServer/Services/Invoices/PaymentService.cs @@ -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) {