Skip to content

Commit

Permalink
Refactor fee provider
Browse files Browse the repository at this point in the history
The fee provider ended up glued with a hardcoded factory. This PR:
* removes this glue and uses the DI to register fee provider for a network. (allows plugins to add their own fee providers, for any network
* Add a 10 second timeout to mempoolspace fee fetching as they are slow at times
  • Loading branch information
Kukks committed Jan 3, 2024
1 parent 1ac1443 commit b705ba3
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 50 deletions.
6 changes: 3 additions & 3 deletions BTCPayServer/Hosting/BTCPayServerServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ public static IServiceCollection RegisterJsonConverter(this IServiceCollection s
services.AddSingleton<IJsonConverterRegistration, JsonConverterRegistration>((s) => new JsonConverterRegistration(create));
return services;
}
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs)
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration,
Logs logs, ServiceProvider bootstrapServiceProvider)
{
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
services.AddSingleton<JsonSerializerSettings>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value.SerializerSettings);
Expand Down Expand Up @@ -363,8 +364,7 @@ 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.AddFeeProviders(bootstrapServiceProvider);
services.Configure<MvcOptions>((o) =>
{
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(WalletId)));
Expand Down
2 changes: 1 addition & 1 deletion BTCPayServer/Hosting/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public void ConfigureServices(IServiceCollection services)
opts.ValidationInterval = TimeSpan.FromMinutes(5.0);
});

services.AddBTCPayServer(Configuration, Logs);
services.AddBTCPayServer(Configuration, Logs, bootstrapServiceProvider);
services.AddProviderStorage();
services.AddSession();
services.AddSignalR();
Expand Down
72 changes: 50 additions & 22 deletions BTCPayServer/Services/Fees/FeeProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -1,45 +1,73 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBXplorer;

namespace BTCPayServer.Services.Fees;

public class FeeProviderFactory : IFeeProviderFactory
public static class FeeProviderExtensions
{
public FeeProviderFactory(
BTCPayServerEnvironment Environment,
ExplorerClientProvider ExplorerClients,
IHttpClientFactory HttpClientFactory,
IMemoryCache MemoryCache)
public static IServiceCollection AddFeeProviders(this IServiceCollection services, IServiceProvider bootstrapServiceProvider)
{
_FeeProviders = new ();
var networkProvider = bootstrapServiceProvider.GetRequiredService<NBXplorerNetworkProvider>();

// TODO: Pluginify this
foreach ((var network, var client) in ExplorerClients.GetAll())
foreach (var network in networkProvider.GetAll())
{
List<IFeeProvider> providers = new List<IFeeProvider>();
if (network.IsBTC && Environment.NetworkType != ChainName.Regtest)

if (network.CryptoCode == "BTC" && network.NBitcoinNetwork.ChainName != ChainName.Regtest)
{
providers.Add(new MempoolSpaceFeeProvider(
MemoryCache,

services.AddKeyedSingleton<IFeeProvider>(network.CryptoCode, (provider, o) => new MempoolSpaceFeeProvider(
provider.GetRequiredService<IMemoryCache>(),
$"MempoolSpaceFeeProvider-{network.CryptoCode}",
HttpClientFactory,
network is BTCPayNetwork n &&
provider.GetRequiredService<IHttpClientFactory>(),
network is { } n &&
n.NBitcoinNetwork.ChainName == ChainName.Testnet));
}
providers.Add(new NBXplorerFeeProvider(client));
providers.Add(new StaticFeeProvider(new FeeRate(100L, 1)));
var fallback = new FallbackFeeProvider(providers.ToArray());
_FeeProviders.Add(network, fallback);

services.AddKeyedSingleton<IFeeProvider>(network.CryptoCode, (provider, o) => new NBXplorerFeeProvider(
provider.GetRequiredService<ExplorerClientProvider>().GetExplorerClient(network.CryptoCode)));

services.AddKeyedSingleton<IFeeProvider>(network.CryptoCode,
(provider, o) => new StaticFeeProvider(new FeeRate(100L, 1)));
}
services.AddSingleton<IFeeProviderFactory, FeeProviderFactory>();
return services;
}

}

public class FeeProviderFactory : IFeeProviderFactory
{
private readonly IEnumerable<IFeeProvider> _feeProviders;
private readonly IServiceProvider _serviceProvider;

public FeeProviderFactory(
ExplorerClientProvider explorerClients,
IHttpClientFactory httpClientFactory,
IMemoryCache memoryCache,
IEnumerable<IFeeProvider> feeProviders,
IServiceProvider serviceProvider)
{
_feeProviders = feeProviders;
_serviceProvider = serviceProvider;
}
private readonly Dictionary<BTCPayNetworkBase, IFeeProvider> _FeeProviders;

private readonly ConcurrentDictionary<BTCPayNetworkBase, FallbackFeeProvider> _cached = new();
public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network)
{
return _FeeProviders.TryGetValue(network, out var prov) ? prov : throw new NotSupportedException($"No fee provider for this network ({network.CryptoCode})");
return _cached.GetOrAdd(network, n =>
{
var feeProviders = _serviceProvider.GetKeyedServices<IFeeProvider>(network.CryptoCode).Concat(_feeProviders).ToArray();
if (!feeProviders.Any())
throw new NotSupportedException($"No fee provider for this network ({network.CryptoCode})");
return new FallbackFeeProvider(feeProviders.ToArray());
});
}
}
61 changes: 37 additions & 24 deletions BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class MempoolSpaceFeeProvider(
IHttpClientFactory HttpClientFactory,
bool Testnet) : IFeeProvider
{
private readonly string ExplorerLink = Testnet switch
private string ExplorerLink = Testnet switch
{
true => "https://mempool.space/testnet/api/v1/fees/recommended",
false => "https://mempool.space/api/v1/fees/recommended"
Expand All @@ -34,32 +34,45 @@ public async Task<FeeRate> GetFeeRateAsync(int blockTarget = 20)
result[result.Keys.MinBy(key => Math.Abs(key - blockTarget))];
}

public Task<Dictionary<int, FeeRate>> GetFeeRatesAsync()
public async Task<Dictionary<int, FeeRate>> GetFeeRatesAsync()
{
return MemoryCache.GetOrCreateAsync(CacheKey, async entry =>
try
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var client = HttpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider));
using var result = await client.GetAsync(ExplorerLink);
result.EnsureSuccessStatusCode();
var recommendedFees = await result.Content.ReadAsAsync<Dictionary<string, decimal>>();
var feesByBlockTarget = new Dictionary<int, FeeRate>();
foreach ((var feeId, decimal value) in recommendedFees)
return (await MemoryCache.GetOrCreateAsync(CacheKey, async entry =>
{
var target = feeId switch
{
"fastestFee" => 1,
"halfHourFee" => 3,
"hourFee" => 6,
"economyFee" when recommendedFees.TryGetValue("minimumFee", out var minFee) && minFee == value => 144,
"economyFee" => 72,
"minimumFee" => 144,
_ => -1
};
feesByBlockTarget.TryAdd(target, new FeeRate(RandomizeByPercentage(value, 10)));
}
return feesByBlockTarget;
})!;
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await GetFeeRatesCore();
}))!;
}
catch (Exception e)
{
MemoryCache.Remove(CacheKey);
throw;
}
}

protected virtual async Task<Dictionary<int, FeeRate>> GetFeeRatesCore()
{
var client = HttpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider));
using var result = await client.GetAsync(ExplorerLink, new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
result.EnsureSuccessStatusCode();
var recommendedFees = await result.Content.ReadAsAsync<Dictionary<string, decimal>>();
var feesByBlockTarget = new Dictionary<int, FeeRate>();
foreach ((var feeId, decimal value) in recommendedFees)
{
var target = feeId switch
{
"fastestFee" => 1,
"halfHourFee" => 3,
"hourFee" => 6,
"economyFee" when recommendedFees.TryGetValue("minimumFee", out var minFee) && minFee == value => 144,
"economyFee" => 72,
"minimumFee" => 144,
_ => -1
};
feesByBlockTarget.TryAdd(target, new FeeRate(RandomizeByPercentage(value, 10)));
}
return feesByBlockTarget;
}

static decimal RandomizeByPercentage(decimal value, int percentage)
Expand Down

0 comments on commit b705ba3

Please sign in to comment.