diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index cf3ac597c..ba3299f3d 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -39,7 +39,7 @@ namespace BTCPayServer.Tests if (!Directory.Exists(_Directory)) Directory.CreateDirectory(_Directory); - NetworkProvider = networkProvider; + _NetworkProvider = networkProvider; ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork); ExplorerNode.ScanRPCCapabilities(); @@ -214,8 +214,14 @@ namespace BTCPayServer.Tests { return new TestAccount(this); } - - public BTCPayNetworkProvider NetworkProvider { get; private set; } + BTCPayNetworkProvider _NetworkProvider; + public BTCPayNetworkProvider NetworkProvider + { + get + { + return PayTester?.Networks ?? _NetworkProvider; + } + } public RPCClient ExplorerNode { get; set; diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index 6d3e5cb3e..19ffd99b4 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -91,7 +91,13 @@ namespace BTCPayServer.Tests "test" + isTestnet, prov.GetService(), isTestnet); + mempoolSpaceFeeProvider.CachedOnly = true; + await Assert.ThrowsAsync(() => mempoolSpaceFeeProvider.GetFeeRateAsync()); + mempoolSpaceFeeProvider.CachedOnly = false; var rates = await mempoolSpaceFeeProvider.GetFeeRatesAsync(); + mempoolSpaceFeeProvider.CachedOnly = true; + await mempoolSpaceFeeProvider.GetFeeRateAsync(); + mempoolSpaceFeeProvider.CachedOnly = false; Assert.NotEmpty(rates); @@ -121,10 +127,7 @@ namespace BTCPayServer.Tests //ENSURE THESE ARE LOGICAL Assert.True(recommendedFees[0].FeeRate >= recommendedFees[1].FeeRate, $"{recommendedFees[0].Target}:{recommendedFees[0].FeeRate} >= {recommendedFees[1].Target}:{recommendedFees[1].FeeRate}"); Assert.True(recommendedFees[1].FeeRate >= recommendedFees[2].FeeRate, $"{recommendedFees[1].Target}:{recommendedFees[1].FeeRate} >= {recommendedFees[2].Target}:{recommendedFees[2].FeeRate}"); - Assert.True(recommendedFees[2].FeeRate >= recommendedFees[3].FeeRate, $"{recommendedFees[2].Target}:{recommendedFees[2].FeeRate} >= {recommendedFees[3].Target}:{recommendedFees[3].FeeRate}"); - - - + Assert.True(recommendedFees[2].FeeRate >= recommendedFees[3].FeeRate, $"{recommendedFees[2].Target}:{recommendedFees[2].FeeRate} >= {recommendedFees[3].Target}:{recommendedFees[3].FeeRate}"); } } [Fact] diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index f68f6d030..a14e13ebf 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -368,7 +368,8 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.AddSingleton(provider => provider.GetService()); services.TryAddSingleton(CurrencyNameTable.Instance); - services.TryAddSingleton(); + services.AddScheduledTask(TimeSpan.FromMinutes(3.0)); + services.AddSingleton(f => f.GetRequiredService()); services.Configure((o) => { diff --git a/BTCPayServer/Services/Fees/FeeProviderFactory.cs b/BTCPayServer/Services/Fees/FeeProviderFactory.cs index 887d0ccdc..a6374db29 100644 --- a/BTCPayServer/Services/Fees/FeeProviderFactory.cs +++ b/BTCPayServer/Services/Fees/FeeProviderFactory.cs @@ -1,14 +1,18 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.HostedServices; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Caching.Memory; using NBitcoin; namespace BTCPayServer.Services.Fees; -public class FeeProviderFactory : IFeeProviderFactory +public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask { public FeeProviderFactory( BTCPayServerEnvironment Environment, @@ -16,7 +20,7 @@ public class FeeProviderFactory : IFeeProviderFactory IHttpClientFactory HttpClientFactory, IMemoryCache MemoryCache) { - _FeeProviders = new (); + _FeeProviders = new(); // TODO: Pluginify this foreach ((var network, var client) in ExplorerClients.GetAll()) @@ -29,7 +33,10 @@ public class FeeProviderFactory : IFeeProviderFactory $"MempoolSpaceFeeProvider-{network.CryptoCode}", HttpClientFactory, network is BTCPayNetwork n && - n.NBitcoinNetwork.ChainName == ChainName.Testnet)); + n.NBitcoinNetwork.ChainName == ChainName.Testnet) + { + CachedOnly = true + }); } providers.Add(new NBXplorerFeeProvider(client)); providers.Add(new StaticFeeProvider(new FeeRate(100L, 1))); @@ -42,4 +49,27 @@ public class FeeProviderFactory : IFeeProviderFactory { return _FeeProviders.TryGetValue(network, out var prov) ? prov : throw new NotSupportedException($"No fee provider for this network ({network.CryptoCode})"); } + + public async Task Do(CancellationToken cancellationToken) + { + try + { + await RefreshCache(_FeeProviders.Values); + } + // Do not spam logs if mempoolspace is down + catch (TaskCanceledException) + { + } + catch (HttpRequestException) + { + } + } + private Task RefreshCache(IEnumerable feeProviders) => Task.WhenAll(feeProviders.Select(fp => RefreshCache(fp))); + private Task RefreshCache(IFeeProvider fp) => + fp switch + { + FallbackFeeProvider ffp => Task.WhenAll(ffp.Providers.Select(p => RefreshCache(p))), + MempoolSpaceFeeProvider mempool => mempool.RefreshCache(), + _ => Task.CompletedTask + }; } diff --git a/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs b/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs index 90e53db9f..777002bde 100644 --- a/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs +++ b/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs @@ -52,14 +52,23 @@ public class MempoolSpaceFeeProvider( var a = (decimal)(target - lb.Blocks) / (decimal)(hb.Blocks - lb.Blocks); return new FeeRate((1 - a) * lb.FeeRate.SatoshiPerByte + a * hb.FeeRate.SatoshiPerByte); } + readonly TimeSpan Expiration = TimeSpan.FromMinutes(25); + public async Task RefreshCache() + { + var rate = await GetFeeRatesCore(); + memoryCache.Set(cacheKey, rate, Expiration); + } + public bool CachedOnly { get; set; } internal async Task GetFeeRatesAsync() { + if (CachedOnly) + return memoryCache.Get(cacheKey) as BlockFeeRate[] ?? throw new InvalidOperationException("Fee rates unavailable"); try { return (await memoryCache.GetOrCreateAsync(cacheKey, async entry => { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); + entry.AbsoluteExpirationRelativeToNow = Expiration; return await GetFeeRatesCore(); }))!; } @@ -73,8 +82,7 @@ public class MempoolSpaceFeeProvider( async Task GetFeeRatesCore() { var client = httpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider)); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - using var result = await client.GetAsync(ExplorerLink, cts.Token); + using var result = await client.GetAsync(ExplorerLink); result.EnsureSuccessStatusCode(); var recommendedFees = await result.Content.ReadAsAsync>(); var r = new List(); @@ -97,8 +105,8 @@ public class MempoolSpaceFeeProvider( { // Randomize a bit ordered[i] = ordered[i] with { FeeRate = new FeeRate(RandomizeByPercentage(ordered[i].FeeRate.SatoshiPerByte, 10m)) }; - if (i > 0) // Make sure feerate always increase - ordered[i] = ordered[i] with { FeeRate = FeeRate.Max(ordered[i - 1].FeeRate, ordered[i].FeeRate) }; + if (i > 0) // Make sure feerate always decrease + ordered[i] = ordered[i] with { FeeRate = FeeRate.Min(ordered[i - 1].FeeRate, ordered[i].FeeRate) }; } return ordered; }