Skip to content

Commit

Permalink
Improve checkout page load time by fetching recommended fee in the ba…
Browse files Browse the repository at this point in the history
…ckground periodically
  • Loading branch information
NicolasDorier committed Jan 18, 2024
1 parent 3eec9cb commit 23d6e0f
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 7 deletions.
6 changes: 6 additions & 0 deletions BTCPayServer.Tests/ThirdPartyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ public async Task CanQueryMempoolFeeProvider()
"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);


Expand Down
3 changes: 2 additions & 1 deletion BTCPayServer/Hosting/BTCPayServerServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ public static IServiceCollection AddBTCPayServer(this IServiceCollection service
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) =>
{
Expand Down
36 changes: 33 additions & 3 deletions BTCPayServer/Services/Fees/FeeProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
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,
ExplorerClientProvider ExplorerClients,
IHttpClientFactory HttpClientFactory,
IMemoryCache MemoryCache)
{
_FeeProviders = new ();
_FeeProviders = new();

// TODO: Pluginify this
foreach ((var network, var client) in ExplorerClients.GetAll())
Expand All @@ -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)));
Expand All @@ -42,4 +49,27 @@ public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network)
{
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
};
}
14 changes: 11 additions & 3 deletions BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,23 @@ internal static FeeRate InterpolateOrBound(BlockFeeRate[] ordered, int target)
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();
}))!;
}
Expand All @@ -73,8 +82,7 @@ internal record BlockFeeRate(int Blocks, FeeRate FeeRate);
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>();
Expand Down

0 comments on commit 23d6e0f

Please sign in to comment.