mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
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:
parent
3ffae30b95
commit
75bf8a5086
@ -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()
|
||||
{
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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) =>
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
28
BTCPayServer/Services/Fees/FallbackFeeProvider.cs
Normal file
28
BTCPayServer/Services/Fees/FallbackFeeProvider.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
45
BTCPayServer/Services/Fees/FeeProviderFactory.cs
Normal file
45
BTCPayServer/Services/Fees/FeeProviderFactory.cs
Normal 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})");
|
||||
}
|
||||
}
|
64
BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs
Normal file
64
BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs
Normal 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;
|
||||
})!;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
BTCPayServer/Services/Fees/StaticFeeProvider.cs
Normal file
19
BTCPayServer/Services/Fees/StaticFeeProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
<PropertyGroup>
|
||||
<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>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208,CA1303,CA2000,CA2016,CA1835,CA2249,CA9998,CA1704;CS8981</NoWarn>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<EnableNETAnalyzers>True</EnableNETAnalyzers>
|
||||
<AnalysisLevel>6.0</AnalysisLevel>
|
||||
</PropertyGroup>
|
||||
|
Loading…
Reference in New Issue
Block a user