diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 033943367..c0e1f3703 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -39,7 +39,7 @@ services: - postgres bitcoin-nbxplorer: - image: nicolasdorier/nbxplorer:1.0.0.45 + image: nicolasdorier/nbxplorer:1.0.0.47 ports: - "32838:32838" expose: @@ -57,7 +57,7 @@ services: - bitcoind litecoin-nbxplorer: - image: nicolasdorier/nbxplorer:1.0.0.45 + image: nicolasdorier/nbxplorer:1.0.0.47 ports: - "32839:32839" expose: diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 134efd480..a741f33c4 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -24,7 +24,7 @@ - + diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 4095a9ded..f857233fa 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -17,6 +17,7 @@ using BTCPayServer.Controllers; using BTCPayServer.Events; using Microsoft.AspNetCore.Hosting; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services; namespace BTCPayServer.HostedServices { @@ -150,7 +151,7 @@ namespace BTCPayServer.HostedServices } var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray(); - var payments = await GetPaymentsWithTransaction(null, derivationStrategies, invoice); + var payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); foreach (Task coinsAsync in GetCoinsPerNetwork(context, invoice, derivationStrategies)) { var coins = await coinsAsync; @@ -173,7 +174,7 @@ namespace BTCPayServer.HostedServices } if (dirtyAddress) { - payments = await GetPaymentsWithTransaction(payments, derivationStrategies, invoice); + payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); } var network = coins.Wallet.Network; var cryptoData = invoice.GetCryptoData(network, _NetworkProvider); @@ -293,58 +294,23 @@ namespace BTCPayServer.HostedServices } - class AccountedPaymentEntities : List - { - public AccountedPaymentEntities(AccountedPaymentEntities existing) - { - if (existing != null) - _Transactions = existing._Transactions; - } - - Dictionary _Transactions = new Dictionary(); - - public void AddToCache(IEnumerable transactions) - { - foreach (var tx in transactions) - _Transactions.TryAdd(tx.Transaction.GetHash(), tx); - } - public TransactionResult GetTransaction(uint256 txId) - { - _Transactions.TryGetValue(txId, out TransactionResult result); - return result; - } - - internal IEnumerable GetTransactions() - { - return _Transactions.Values; - } - } - private async Task GetPaymentsWithTransaction(AccountedPaymentEntities previous, DerivationStrategy[] derivations, InvoiceEntity invoice) + private async Task> GetPaymentsWithTransaction(DerivationStrategy[] derivations, InvoiceEntity invoice) { List updatedPaymentEntities = new List(); - AccountedPaymentEntities accountedPayments = new AccountedPaymentEntities(previous); + List accountedPayments = new List(); foreach (var network in derivations.Select(d => d.Network)) { var wallet = _WalletProvider.GetWallet(network); if (wallet == null) continue; - var hashesToFetch = new HashSet(invoice - .GetPayments(network) + var transactions = await wallet.GetTransactions(invoice.GetPayments(wallet.Network) .Select(t => t.Outpoint.Hash) - .Where(h => accountedPayments?.GetTransaction(h) == null) - .ToList()); - - - if (hashesToFetch.Count > 0) - { - accountedPayments.AddToCache((await wallet.GetTransactions(hashesToFetch.ToArray())).Select(t => t.Value)); - } - var conflicts = GetConflicts(accountedPayments.GetTransactions()); + .ToArray()); + var conflicts = GetConflicts(transactions.Select(t => t.Value)); foreach (var payment in invoice.GetPayments(network)) { - TransactionResult tx = accountedPayments.GetTransaction(payment.Outpoint.Hash); - if (tx == null) + if (!transactions.TryGetValue(payment.Outpoint.Hash, out TransactionResult tx)) continue; AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity() @@ -370,7 +336,6 @@ namespace BTCPayServer.HostedServices return accountedPayments; } - class TransactionConflict { public Dictionary Transactions { get; set; } = new Dictionary(); diff --git a/BTCPayServer/HostedServices/NBXplorerListener.cs b/BTCPayServer/HostedServices/NBXplorerListener.cs index 329f89198..52687b399 100644 --- a/BTCPayServer/HostedServices/NBXplorerListener.cs +++ b/BTCPayServer/HostedServices/NBXplorerListener.cs @@ -12,6 +12,7 @@ using NBXplorer; using System.Collections.Concurrent; using NBXplorer.DerivationStrategy; using BTCPayServer.Events; +using BTCPayServer.Services; namespace BTCPayServer.HostedServices { @@ -24,9 +25,11 @@ namespace BTCPayServer.HostedServices private TaskCompletionSource _RunningTask; private CancellationTokenSource _Cts; NBXplorerDashboard _Dashboards; + TransactionCacheProvider _TxCache; public NBXplorerListener(ExplorerClientProvider explorerClients, NBXplorerDashboard dashboard, + TransactionCacheProvider cacheProvider, InvoiceRepository invoiceRepository, EventAggregator aggregator, IApplicationLifetime lifetime) { @@ -36,6 +39,7 @@ namespace BTCPayServer.HostedServices _ExplorerClients = explorerClients; _Aggregator = aggregator; _Lifetime = lifetime; + _TxCache = cacheProvider; } CompositeDisposable leases = new CompositeDisposable(); @@ -130,11 +134,13 @@ namespace BTCPayServer.HostedServices switch (newEvent) { case NBXplorer.Models.NewBlockEvent evt: + _TxCache.GetTransactionCache(network).NewBlock(evt.Hash, evt.PreviousBlockHash); _Aggregator.Publish(new Events.NewBlockEvent()); break; case NBXplorer.Models.NewTransactionEvent evt: - foreach (var txout in evt.Match.Outputs) + foreach (var txout in evt.Outputs) { + _TxCache.GetTransactionCache(network).AddToCache(evt.TransactionData); _Aggregator.Publish(new Events.TxOutReceivedEvent() { Network = network, diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index c47024ff0..698ca93f9 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -142,6 +142,8 @@ namespace BTCPayServer.Hosting BlockTarget = 20 }); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/BTCPayServer/Services/TransactionCache.cs b/BTCPayServer/Services/TransactionCache.cs new file mode 100644 index 000000000..c91f33eb7 --- /dev/null +++ b/BTCPayServer/Services/TransactionCache.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NBitcoin; +using NBXplorer.Models; + +namespace BTCPayServer.Services +{ + public class TransactionCacheProvider + { + IOptions _Options; + public TransactionCacheProvider(IOptions options) + { + _Options = options; + } + + ConcurrentDictionary _TransactionCaches = new ConcurrentDictionary(); + public TransactionCache GetTransactionCache(BTCPayNetwork network) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + return _TransactionCaches.GetOrAdd(network.CryptoCode, c => new TransactionCache(_Options, network)); + } + } + public class TransactionCache : IDisposable + { + IOptions _Options; + public TransactionCache(IOptions options, BTCPayNetwork network) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + _Options = options; + _MemoryCache = new MemoryCache(_Options); + Network = network; + } + + uint256 _LastHash; + int _ConfOffset; + IMemoryCache _MemoryCache; + + public void NewBlock(uint256 newHash, uint256 previousHash) + { + if (_LastHash != previousHash) + { + var old = _MemoryCache; + _ConfOffset = 0; + _MemoryCache = new MemoryCache(_Options); + Thread.MemoryBarrier(); + old.Dispose(); + } + else + _ConfOffset++; + _LastHash = newHash; + } + + public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(60); + + public BTCPayNetwork Network { get; private set; } + + public void AddToCache(TransactionResult tx) + { + _MemoryCache.Set(tx.Transaction.GetHash(), tx, DateTimeOffset.UtcNow + CacheSpan); + } + + + public TransactionResult GetTransaction(uint256 txId) + { + _MemoryCache.TryGetValue(txId.ToString(), out object tx); + + var result = tx as TransactionResult; + var confOffset = _ConfOffset; + if (result != null && result.Confirmations > 0 && confOffset > 0) + { + var serializer = new NBXplorer.Serializer(Network.NBitcoinNetwork); + result = serializer.ToObject(serializer.ToString(result)); + result.Confirmations += confOffset; + result.Height += confOffset; + } + return result; + } + + public void Dispose() + { + _MemoryCache.Dispose(); + } + } +} diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index f8808587c..24c7e13f4 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -33,13 +33,14 @@ namespace BTCPayServer.Services.Wallets public class BTCPayWallet { private ExplorerClient _Client; - - public BTCPayWallet(ExplorerClient client, BTCPayNetwork network) + private TransactionCache _Cache; + public BTCPayWallet(ExplorerClient client, TransactionCache cache, BTCPayNetwork network) { if (client == null) throw new ArgumentNullException(nameof(client)); _Client = client; _Network = network; + _Cache = cache; } @@ -65,11 +66,16 @@ namespace BTCPayServer.Services.Wallets await _Client.TrackAsync(derivationStrategy); } - public Task GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken)) + public async Task GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken)) { if (txId == null) throw new ArgumentNullException(nameof(txId)); - return _Client.GetTransactionAsync(txId, cancellation); + var tx = _Cache.GetTransaction(txId); + if (tx != null) + return tx; + tx = await _Client.GetTransactionAsync(txId, cancellation); + _Cache.AddToCache(tx); + return tx; } public async Task GetCoins(DerivationStrategyBase strategy, KnownState state, CancellationToken cancellation = default(CancellationToken)) diff --git a/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs b/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs index 7b8e46756..0c1971a4f 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs @@ -10,11 +10,15 @@ namespace BTCPayServer.Services.Wallets { private ExplorerClientProvider _Client; BTCPayNetworkProvider _NetworkProvider; - public BTCPayWalletProvider(ExplorerClientProvider client, BTCPayNetworkProvider networkProvider) + TransactionCacheProvider _TransactionCacheProvider; + public BTCPayWalletProvider(ExplorerClientProvider client, + TransactionCacheProvider transactionCacheProvider, + BTCPayNetworkProvider networkProvider) { if (client == null) throw new ArgumentNullException(nameof(client)); _Client = client; + _TransactionCacheProvider = transactionCacheProvider; _NetworkProvider = networkProvider; } @@ -32,7 +36,7 @@ namespace BTCPayServer.Services.Wallets var client = _Client.GetExplorerClient(cryptoCode); if (network == null || client == null) return null; - return new BTCPayWallet(client, network); + return new BTCPayWallet(client, _TransactionCacheProvider.GetTransactionCache(network), network); } } }