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.Data;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.Fees;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Storage.Models;
|
using BTCPayServer.Storage.Models;
|
||||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.FileSystemGlobbing;
|
using Microsoft.Extensions.FileSystemGlobbing;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
@ -73,6 +76,25 @@ namespace BTCPayServer.Tests
|
|||||||
await UnitTest1.CanUploadRemoveFiles(controller);
|
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]
|
[Fact]
|
||||||
public async Task CanQueryDirectProviders()
|
public async Task CanQueryDirectProviders()
|
||||||
{
|
{
|
||||||
|
@ -1266,7 +1266,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
metadataObj = JObject.Parse(model.Metadata);
|
metadataObj = JObject.Parse(model.Metadata);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(model.Metadata), "Metadata was not valid JSON");
|
ModelState.AddModelError(nameof(model.Metadata), "Metadata was not valid JSON");
|
||||||
}
|
}
|
||||||
|
@ -366,10 +366,7 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<WalletReceiveService>();
|
services.TryAddSingleton<WalletReceiveService>();
|
||||||
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
|
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
|
||||||
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
|
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
|
||||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
services.TryAddSingleton<IFeeProviderFactory,FeeProviderFactory>();
|
||||||
{
|
|
||||||
Fallback = new FeeRate(100L, 1)
|
|
||||||
});
|
|
||||||
|
|
||||||
services.Configure<MvcOptions>((o) =>
|
services.Configure<MvcOptions>((o) =>
|
||||||
{
|
{
|
||||||
|
@ -36,7 +36,6 @@ namespace BTCPayServer.Services
|
|||||||
|
|
||||||
async Task IHostedService.StartAsync(CancellationToken cancellationToken)
|
async Task IHostedService.StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_ = CashCow?.ScanRPCCapabilitiesAsync(cancellationToken);
|
|
||||||
#if ALTCOINS
|
#if ALTCOINS
|
||||||
var liquid = _prov.GetNetwork("LBTC");
|
var liquid = _prov.GetNetwork("LBTC");
|
||||||
if (liquid is not null)
|
if (liquid is not null)
|
||||||
@ -59,7 +58,9 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (CashCow is { } c)
|
||||||
|
await c.ScanRPCCapabilitiesAsync(cancellationToken);
|
||||||
#endif
|
#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 System.Threading.Tasks;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
@ -6,43 +6,11 @@ using NBXplorer.Models;
|
|||||||
|
|
||||||
namespace BTCPayServer.Services.Fees
|
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)
|
public async Task<FeeRate> GetFeeRateAsync(int blockTarget = 20)
|
||||||
{
|
{
|
||||||
try
|
return (await ExplorerClient.GetFeeRateAsync(blockTarget).ConfigureAwait(false)).FeeRate;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
|
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
|
||||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208,CA1303,CA2000,CA2016,CA1835,CA2249,CA9998,CA1704;CS8981</NoWarn>
|
<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>
|
<EnableNETAnalyzers>True</EnableNETAnalyzers>
|
||||||
<AnalysisLevel>6.0</AnalysisLevel>
|
<AnalysisLevel>6.0</AnalysisLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
Loading…
Reference in New Issue
Block a user