mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Make sure that mempool space fee rate doesn't drop below node's minRelayTxFee (#6853)
This commit is contained in:
@@ -9,6 +9,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Hosting;
|
using BTCPayServer.Hosting;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Models.WalletViewModels;
|
using BTCPayServer.Models.WalletViewModels;
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ namespace BTCPayServer.Services.Fees;
|
|||||||
public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask
|
public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask
|
||||||
{
|
{
|
||||||
public FeeProviderFactory(
|
public FeeProviderFactory(
|
||||||
BTCPayServerEnvironment Environment,
|
BTCPayServerEnvironment Environment,
|
||||||
ExplorerClientProvider ExplorerClients,
|
ExplorerClientProvider ExplorerClients,
|
||||||
IHttpClientFactory HttpClientFactory,
|
IHttpClientFactory HttpClientFactory,
|
||||||
IMemoryCache MemoryCache)
|
NBXplorerDashboard Dashboard,
|
||||||
|
IMemoryCache MemoryCache)
|
||||||
{
|
{
|
||||||
_FeeProviders = new();
|
_FeeProviders = new();
|
||||||
|
|
||||||
@@ -41,9 +42,26 @@ public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask
|
|||||||
providers.Add(new NBXplorerFeeProvider(client));
|
providers.Add(new NBXplorerFeeProvider(client));
|
||||||
providers.Add(new StaticFeeProvider(new FeeRate(100L, 1)));
|
providers.Add(new StaticFeeProvider(new FeeRate(100L, 1)));
|
||||||
var fallback = new FallbackFeeProvider(providers.ToArray());
|
var fallback = new FallbackFeeProvider(providers.ToArray());
|
||||||
_FeeProviders.Add(network, fallback);
|
|
||||||
|
_FeeProviders.Add(network, new ClampFeeProvider(fallback, Dashboard, network.CryptoCode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ClampFeeProvider(IFeeProvider inner, NBXplorerDashboard dashboard, string cryptoCode) : IFeeProvider
|
||||||
|
{
|
||||||
|
public async Task<FeeRate> GetFeeRateAsync(int blockTarget = 20)
|
||||||
|
{
|
||||||
|
var rate = await inner.GetFeeRateAsync(blockTarget);
|
||||||
|
var d = dashboard.Get(cryptoCode);
|
||||||
|
var min = d?.MempoolInfo?.MempoolMinfeeRate;
|
||||||
|
min ??= d?.Status?.BitcoinStatus?.MinRelayTxFee;
|
||||||
|
if (min is not null && rate < min)
|
||||||
|
rate = min;
|
||||||
|
rate = FeeRate.Min(rate, new FeeRate(2000m));
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly Dictionary<BTCPayNetworkBase, IFeeProvider> _FeeProviders;
|
private readonly Dictionary<BTCPayNetworkBase, IFeeProvider> _FeeProviders;
|
||||||
public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network)
|
public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using Org.BouncyCastle.Asn1.X509;
|
|
||||||
using YamlDotNet.Core.Tokens;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Fees;
|
namespace BTCPayServer.Services.Fees;
|
||||||
|
|
||||||
@@ -20,7 +15,7 @@ public class MempoolSpaceFeeProvider(
|
|||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
bool testnet) : IFeeProvider
|
bool testnet) : IFeeProvider
|
||||||
{
|
{
|
||||||
private string ExplorerLink = testnet switch
|
private readonly string _explorerLink = testnet switch
|
||||||
{
|
{
|
||||||
true => "https://mempool.space/testnet/api/v1/fees/recommended",
|
true => "https://mempool.space/testnet/api/v1/fees/recommended",
|
||||||
false => "https://mempool.space/api/v1/fees/recommended"
|
false => "https://mempool.space/api/v1/fees/recommended"
|
||||||
@@ -31,30 +26,31 @@ public class MempoolSpaceFeeProvider(
|
|||||||
var result = await GetFeeRatesAsync();
|
var result = await GetFeeRatesAsync();
|
||||||
|
|
||||||
return InterpolateOrBound(result, blockTarget);
|
return InterpolateOrBound(result, blockTarget);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static FeeRate InterpolateOrBound(BlockFeeRate[] ordered, int target)
|
internal static FeeRate InterpolateOrBound(BlockFeeRate[] ordered, int target)
|
||||||
{
|
{
|
||||||
(BlockFeeRate lb, BlockFeeRate hb) = (ordered[0], ordered[^1]);
|
var (lb, hb) = (ordered[0], ordered[^1]);
|
||||||
target = Math.Clamp(target, lb.Blocks, hb.Blocks);
|
target = Math.Clamp(target, lb.Blocks, hb.Blocks);
|
||||||
for (int i = 0; i < ordered.Length; i++)
|
foreach (var t in ordered)
|
||||||
{
|
{
|
||||||
if (ordered[i].Blocks > lb.Blocks && ordered[i].Blocks <= target)
|
if (t.Blocks > lb.Blocks && t.Blocks <= target)
|
||||||
lb = ordered[i];
|
lb = t;
|
||||||
if (ordered[i].Blocks < hb.Blocks && ordered[i].Blocks >= target)
|
if (t.Blocks < hb.Blocks && t.Blocks >= target)
|
||||||
hb = ordered[i];
|
hb = t;
|
||||||
}
|
}
|
||||||
if (hb.Blocks == lb.Blocks)
|
if (hb.Blocks == lb.Blocks)
|
||||||
return hb.FeeRate;
|
return hb.FeeRate;
|
||||||
var a = (decimal)(target - lb.Blocks) / (decimal)(hb.Blocks - lb.Blocks);
|
var a = (decimal)(target - lb.Blocks) / (hb.Blocks - lb.Blocks);
|
||||||
return new FeeRate((1 - a) * lb.FeeRate.SatoshiPerByte + a * hb.FeeRate.SatoshiPerByte);
|
return new FeeRate((1 - a) * lb.FeeRate.SatoshiPerByte + a * hb.FeeRate.SatoshiPerByte);
|
||||||
}
|
}
|
||||||
readonly TimeSpan Expiration = TimeSpan.FromMinutes(25);
|
|
||||||
|
private readonly TimeSpan _expiration = TimeSpan.FromMinutes(25);
|
||||||
public async Task RefreshCache()
|
public async Task RefreshCache()
|
||||||
{
|
{
|
||||||
var rate = await GetFeeRatesCore();
|
var rate = await GetFeeRatesCore();
|
||||||
memoryCache.Set(cacheKey, rate, Expiration);
|
memoryCache.Set(cacheKey, rate, _expiration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CachedOnly { get; set; }
|
public bool CachedOnly { get; set; }
|
||||||
@@ -66,7 +62,7 @@ public class MempoolSpaceFeeProvider(
|
|||||||
{
|
{
|
||||||
return (await memoryCache.GetOrCreateAsync(cacheKey, async entry =>
|
return (await memoryCache.GetOrCreateAsync(cacheKey, async entry =>
|
||||||
{
|
{
|
||||||
entry.AbsoluteExpirationRelativeToNow = Expiration;
|
entry.AbsoluteExpirationRelativeToNow = _expiration;
|
||||||
return await GetFeeRatesCore();
|
return await GetFeeRatesCore();
|
||||||
}))!;
|
}))!;
|
||||||
}
|
}
|
||||||
@@ -80,11 +76,12 @@ public class MempoolSpaceFeeProvider(
|
|||||||
async Task<BlockFeeRate[]> GetFeeRatesCore()
|
async Task<BlockFeeRate[]> GetFeeRatesCore()
|
||||||
{
|
{
|
||||||
var client = httpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider));
|
var client = httpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider));
|
||||||
using var result = await client.GetAsync(ExplorerLink);
|
using var result = await client.GetAsync(_explorerLink);
|
||||||
result.EnsureSuccessStatusCode();
|
result.EnsureSuccessStatusCode();
|
||||||
var recommendedFees = await result.Content.ReadAsAsync<Dictionary<string, decimal>>();
|
var recommendedFees = await result.Content.ReadAsAsync<Dictionary<string, decimal>>();
|
||||||
var r = new List<BlockFeeRate>();
|
var r = new List<BlockFeeRate>();
|
||||||
foreach ((var feeId, decimal value) in recommendedFees)
|
|
||||||
|
foreach (var (feeId, value) in recommendedFees)
|
||||||
{
|
{
|
||||||
var target = feeId switch
|
var target = feeId switch
|
||||||
{
|
{
|
||||||
@@ -98,6 +95,7 @@ public class MempoolSpaceFeeProvider(
|
|||||||
};
|
};
|
||||||
r.Add(new(target, new FeeRate(value)));
|
r.Add(new(target, new FeeRate(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
var ordered = r.OrderBy(k => k.Blocks).ToArray();
|
var ordered = r.OrderBy(k => k.Blocks).ToArray();
|
||||||
for (var i = 0; i < ordered.Length; i++)
|
for (var i = 0; i < ordered.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -108,18 +106,12 @@ public class MempoolSpaceFeeProvider(
|
|||||||
}
|
}
|
||||||
return ordered;
|
return ordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static decimal RandomizeByPercentage(decimal value, decimal percentage)
|
internal static decimal RandomizeByPercentage(decimal value, decimal percentage)
|
||||||
{
|
{
|
||||||
if (value is 1)
|
if (value == 1.0m)
|
||||||
return 1;
|
return 1.0m;
|
||||||
decimal range = (value * percentage) / 100m;
|
var range = (value * percentage) / 100m;
|
||||||
var res = value + (range * 2.0m) * ((decimal)(Random.Shared.NextDouble() - 0.5));
|
return value + (range * 2.0m) * ((decimal)(Random.Shared.NextDouble() - 0.5));
|
||||||
return res switch
|
|
||||||
{
|
|
||||||
< 1m => 1m,
|
|
||||||
> 2000m => 2000m,
|
|
||||||
_ => res
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user