using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Nethereum.Hex.HexTypes;
using Nethereum.JsonRpc.Client;
using Nethereum.RPC.Eth.Blocks;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.RPC.Eth.Transactions;
using Nethereum.Util;
namespace Nethereum.RPC.Fee1559Suggestions
{
///
/// Suggest a priority fee based on the Fee history of previous blocks and the median of all its values
/// Base fee is suggested based on the latest block number price and depending on its value increase it by a percentage.
/// This is based on MyCrypto example here https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts
///
public class MedianPriorityFeeHistorySuggestionStrategy:IFee1559SuggestionStrategy
{
///
///How many blocks to consider for priority fee estimation
///
public static int FeeHistoryNumberOfBlocks { get; set; } = 10;
///
// Which percentile of effective priority fees to include
///
public static double FEE_HISTORY_PERCENTILE = 5;
// Which base fee to trigger priority fee estimation at
public static long PRIORITY_FEE_ESTIMATION_TRIGGER = 100_000_000_000; // GWEI
// Returned if above trigger is not met
public static long DefaultPriorityFee { get; set; } = 3_000_000_000;
public static Fee1559 FallbackFeeSuggestion { get; set; } = new Fee1559()
{
MaxFeePerGas = 20_000_000_000,
MaxPriorityFeePerGas = DefaultPriorityFee
};
public MedianPriorityFeeHistorySuggestionStrategy() { }
#if !DOTNET35
public IClient Client { get; set; }
private IEthGetBlockWithTransactionsHashesByNumber _ethGetBlockWithTransactionsHashes;
private IEthFeeHistory _ethFeeHistory;
public MedianPriorityFeeHistorySuggestionStrategy(IClient client)
{
Client = client;
_ethGetBlockWithTransactionsHashes = new EthGetBlockWithTransactionsHashesByNumber(client);
_ethFeeHistory = new EthFeeHistory(client);
}
public async Task EstimatePriorityFeeAsync(BigInteger baseFee, HexBigInteger blockNumber)
{
if (baseFee < PRIORITY_FEE_ESTIMATION_TRIGGER)
{
return DefaultPriorityFee;
}
var feeHistory = await _ethFeeHistory.SendRequestAsync(new HexBigInteger(FeeHistoryNumberOfBlocks), new BlockParameter(blockNumber), new double[] {
FEE_HISTORY_PERCENTILE }
).ConfigureAwait(false);
return EstimatePriorityFee(feeHistory);
}
public async Task SuggestFeeAsync(BigInteger? maxPriorityFeePerGas = null)
{
var lastBlock = await _ethGetBlockWithTransactionsHashes.SendRequestAsync(BlockParameter.CreateLatest()).ConfigureAwait(false);
if (lastBlock.BaseFeePerGas == null)
{
return FallbackFeeSuggestion;
}
var baseFee = lastBlock.BaseFeePerGas;
if (maxPriorityFeePerGas == null)
{
var estimatedPriorityFee = await EstimatePriorityFeeAsync(
baseFee,
lastBlock.Number
);
if (estimatedPriorityFee == null)
{
return FallbackFeeSuggestion;
}
maxPriorityFeePerGas = BigInteger.Max(estimatedPriorityFee.Value, DefaultPriorityFee);
}
return SuggestMaxFeeUsingMultiplier(maxPriorityFeePerGas, baseFee);
}
#endif
public const int PRIORITY_FEE_INCREASE_BOUNDARY = 200;
public virtual double GetBaseFeeMultiplier(BigInteger baseFee)
{
if (baseFee < 40_000_000_000)
{
return 2.0;
}
else if (baseFee < 100_000_000_000)
{
return 1.6;
}
else if (baseFee < 200_000_000_000)
{
return 1.4;
}
else
{
return 1.2;
}
}
public BigInteger? EstimatePriorityFee(FeeHistoryResult feeHistory)
{
var rewards = feeHistory.Reward
?.Select((r) => r[0].Value)
.Where((r) => r != 0)
.ToList();
rewards.Sort();
if (rewards == null || rewards.Count == 0)
{
return null;
}
var percentageIncreases = new List();
for (var i = 0; i < rewards.Count - 1; i++)
{
var next = rewards[i + 1];
var p = ((next - rewards[i]) / rewards[i]) * 100;
percentageIncreases.Add(p);
}
var highestIncrease = percentageIncreases.Max();
var highestIncreaseIndex = percentageIncreases.IndexOf(highestIncrease);
//// If we have big increase in value, we could be considering "outliers" in our estimate
//// Skip the low elements and take a new median
var values =
(highestIncrease > PRIORITY_FEE_INCREASE_BOUNDARY &&
highestIncreaseIndex >= Math.Floor((double)(rewards.Count / 2))
? rewards.Skip(highestIncreaseIndex)
: rewards).ToArray();
var valuesIndex = (int)Math.Floor((double)(values.Length / 2));
return values[valuesIndex];
}
public Fee1559 SuggestMaxFeeUsingMultiplier(BigInteger? maxPriorityFeePerGas, HexBigInteger baseFee)
{
var multiplier = GetBaseFeeMultiplier(baseFee);
var potentialMaxFee = (BigDecimal)baseFee.Value * multiplier;
var maxFeePerGas = maxPriorityFeePerGas > potentialMaxFee
? potentialMaxFee + maxPriorityFeePerGas
: potentialMaxFee;
return new Fee1559()
{
MaxFeePerGas = maxFeePerGas.Value.Floor().Mantissa,
MaxPriorityFeePerGas = maxPriorityFeePerGas,
BaseFee = baseFee
};
}
}
}