Improve checkout page load time by fetching recommended fee in the background periodically (#5672)

This commit is contained in:
Nicolas Dorier
2024-01-18 17:16:57 +09:00
committed by GitHub
parent a753698ae7
commit d6806dc1f6
5 changed files with 64 additions and 16 deletions

View File

@@ -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<BTCPayNetwork>("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;

View File

@@ -91,7 +91,13 @@ namespace BTCPayServer.Tests
"test" + isTestnet,
prov.GetService<IHttpClientFactory>(),
isTestnet);
mempoolSpaceFeeProvider.CachedOnly = true;
await Assert.ThrowsAsync<InvalidOperationException>(() => 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]

View File

@@ -368,7 +368,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<WalletReceiveService>();
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
services.TryAddSingleton<IFeeProviderFactory, FeeProviderFactory>();
services.AddScheduledTask<FeeProviderFactory>(TimeSpan.FromMinutes(3.0));
services.AddSingleton<IFeeProviderFactory, FeeProviderFactory>(f => f.GetRequiredService<FeeProviderFactory>());
services.Configure<MvcOptions>((o) =>
{

View File

@@ -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<IFeeProvider> 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
};
}

View File

@@ -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<BlockFeeRate[]> 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<BlockFeeRate[]> 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<Dictionary<string, decimal>>();
var r = new List<BlockFeeRate>();
@@ -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;
}