Use Mempoolspace fees (#5490)

* Use Mempoolspace fees

Since bitcoind's fee estiomates are horrible, I would use an altenrative, but that adds a third party to the mix. We can either:
* Accept the risk (it is only for fee estimation anyway)
* Offer a toggle in the server settings
* Move this code to a plugin

* refactor

* Refactor

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri 2023-11-28 10:26:35 +01:00 committed by GitHub
parent 3ffae30b95
commit 75bf8a5086
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 188 additions and 44 deletions

View File

@ -11,11 +11,14 @@ using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Rates;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing;
using NBitcoin;
using NBitpayClient;
@ -73,6 +76,25 @@ namespace BTCPayServer.Tests
await UnitTest1.CanUploadRemoveFiles(controller);
}
[Fact]
public async Task CanQueryMempoolFeeProvider()
{
IServiceCollection collection = new ServiceCollection();
collection.AddMemoryCache();
collection.AddHttpClient();
var prov = collection.BuildServiceProvider();
foreach (var isTestnet in new[] { true, false })
{
var mempoolSpaceFeeProvider = new MempoolSpaceFeeProvider(
prov.GetService<IMemoryCache>(),
"test" + isTestnet,
prov.GetService<IHttpClientFactory>(),
isTestnet);
var rates = await mempoolSpaceFeeProvider.GetFeeRatesAsync();
Assert.NotEmpty(rates);
await mempoolSpaceFeeProvider.GetFeeRateAsync(20);
}
}
[Fact]
public async Task CanQueryDirectProviders()
{

View File

@ -1266,7 +1266,7 @@ namespace BTCPayServer.Controllers
{
metadataObj = JObject.Parse(model.Metadata);
}
catch (Exception e)
catch (Exception)
{
ModelState.AddModelError(nameof(model.Metadata), "Metadata was not valid JSON");
}

View File

@ -366,10 +366,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<WalletReceiveService>();
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
Fallback = new FeeRate(100L, 1)
});
services.TryAddSingleton<IFeeProviderFactory,FeeProviderFactory>();
services.Configure<MvcOptions>((o) =>
{

View File

@ -36,7 +36,6 @@ namespace BTCPayServer.Services
async Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
_ = CashCow?.ScanRPCCapabilitiesAsync(cancellationToken);
#if ALTCOINS
var liquid = _prov.GetNetwork("LBTC");
if (liquid is not null)
@ -59,7 +58,9 @@ namespace BTCPayServer.Services
}
}
}
#else
if (CashCow is { } c)
await c.ScanRPCCapabilitiesAsync(cancellationToken);
#endif
}

View File

@ -0,0 +1,28 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NBitcoin;
namespace BTCPayServer.Services.Fees
{
public class FallbackFeeProvider(IFeeProvider[] Providers) : IFeeProvider
{
public async Task<FeeRate> GetFeeRateAsync(int blockTarget = 20)
{
for (int i = 0; i < Providers.Length; i++)
{
try
{
return await Providers[i].GetFeeRateAsync(blockTarget);
}
catch when (i < Providers.Length - 1)
{
}
}
throw new NotSupportedException("No provider available");
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
namespace BTCPayServer.Services.Fees;
public class FeeProviderFactory : IFeeProviderFactory
{
public FeeProviderFactory(
BTCPayServerEnvironment Environment,
ExplorerClientProvider ExplorerClients,
IHttpClientFactory HttpClientFactory,
IMemoryCache MemoryCache)
{
_FeeProviders = new ();
// TODO: Pluginify this
foreach ((var network, var client) in ExplorerClients.GetAll())
{
List<IFeeProvider> providers = new List<IFeeProvider>();
if (network.IsBTC && Environment.NetworkType != ChainName.Regtest)
{
providers.Add(new MempoolSpaceFeeProvider(
MemoryCache,
$"MempoolSpaceFeeProvider-{network.CryptoCode}",
HttpClientFactory,
network is BTCPayNetwork 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);
}
}
private readonly Dictionary<BTCPayNetworkBase, IFeeProvider> _FeeProviders;
public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network)
{
return _FeeProviders.TryGetValue(network, out var prov) ? prov : throw new NotSupportedException($"No fee provider for this network ({network.CryptoCode})");
}
}

View File

@ -0,0 +1,64 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Dom;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
namespace BTCPayServer.Services.Fees;
public class MempoolSpaceFeeProvider(
IMemoryCache MemoryCache,
string CacheKey,
IHttpClientFactory HttpClientFactory,
bool Testnet) : IFeeProvider
{
private readonly string ExplorerLink = Testnet switch
{
true => "https://mempool.space/testnet/api/v1/fees/recommended",
false => "https://mempool.space/api/v1/fees/recommended"
};
public async Task<FeeRate> GetFeeRateAsync(int blockTarget = 20)
{
var result = await GetFeeRatesAsync();
return result.TryGetValue(blockTarget, out var feeRate)
? feeRate
:
//try get the closest one
result[result.Keys.MinBy(key => Math.Abs(key - blockTarget))];
}
public Task<Dictionary<int, FeeRate>> GetFeeRatesAsync()
{
return MemoryCache.GetOrCreateAsync(CacheKey, async entry =>
{
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)
{
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(value));
}
return feesByBlockTarget;
})!;
}
}

View File

@ -1,4 +1,4 @@
using System;
#nullable enable
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer;
@ -6,43 +6,11 @@ using NBXplorer.Models;
namespace BTCPayServer.Services.Fees
{
public class NBXplorerFeeProviderFactory : IFeeProviderFactory
public class NBXplorerFeeProvider(ExplorerClient ExplorerClient) : IFeeProvider
{
public NBXplorerFeeProviderFactory(ExplorerClientProvider explorerClients)
{
ArgumentNullException.ThrowIfNull(explorerClients);
_ExplorerClients = explorerClients;
}
private readonly ExplorerClientProvider _ExplorerClients;
public FeeRate Fallback { get; set; }
public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network)
{
return new NBXplorerFeeProvider(this, _ExplorerClients.GetExplorerClient(network));
}
}
public class NBXplorerFeeProvider : IFeeProvider
{
public NBXplorerFeeProvider(NBXplorerFeeProviderFactory parent, ExplorerClient explorerClient)
{
ArgumentNullException.ThrowIfNull(explorerClient);
_Factory = parent;
_ExplorerClient = explorerClient;
}
readonly NBXplorerFeeProviderFactory _Factory;
readonly ExplorerClient _ExplorerClient;
public async Task<FeeRate> GetFeeRateAsync(int blockTarget = 20)
{
try
{
return (await _ExplorerClient.GetFeeRateAsync(blockTarget).ConfigureAwait(false)).FeeRate;
}
catch (NBXplorerException ex) when (ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable")
{
return _Factory.Fallback;
}
return (await ExplorerClient.GetFeeRateAsync(blockTarget).ConfigureAwait(false)).FeeRate;
}
}
}

View File

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Services.Fees;
public class StaticFeeProvider : IFeeProvider
{
private readonly FeeRate _feeRate;
public StaticFeeProvider(FeeRate feeRate)
{
_feeRate = feeRate;
}
public Task<FeeRate> GetFeeRateAsync(int blockTarget = 20)
{
return Task.FromResult(_feeRate);
}
}

View File

@ -3,7 +3,7 @@
<TargetFramework>net8.0</TargetFramework>
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208,CA1303,CA2000,CA2016,CA1835,CA2249,CA9998,CA1704;CS8981</NoWarn>
<LangVersion>10.0</LangVersion>
<LangVersion>12.0</LangVersion>
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<AnalysisLevel>6.0</AnalysisLevel>
</PropertyGroup>