using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Nethereum.Hex.HexTypes;
using Nethereum.JsonRpc.Client;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.RPC.Eth.Transactions;
using Nethereum.Util;
namespace Nethereum.RPC.Fee1559Suggestions
{
///
/// SuggestFees returns a series of maxFeePerGas / maxPriorityFeePerGas values suggested for different time preferences.The first element corresponds to the highest time preference(most urgent transaction).
/// The basic idea behind the algorithm is similar to the old "gas price oracle" used in Geth; it takes the prices of recent blocks and makes a suggestion based on a low percentile of those prices.With EIP-1559 though the base fee of each block provides a less noisy and more reliable price signal.
/// This allows for more sophisticated suggestions with a variable width(exponentially weighted) base fee time window.
/// The window width corresponds to the time preference of the user.
/// The underlying assumption is that price fluctuations over a given past time period indicate the probability of similar price levels being re-tested by the market over a similar length future time period.
/// This is a port of Felfodi Zsolt example https://github.com/zsfelfoldi/feehistory
/// Original: https://github.com/zsfelfoldi/ethereum-docs/blob/master/eip1559/feeHistory_example.js
///
public class TimePreferenceFeeSuggestionStrategy:IFee1559SuggestionStrategy
{
public double SampleMin { get; set; } = 0.1;
public double SampleMax { get; set; } = 0.3;
public int MaxTimeFactor { get; set; } = 15;
public double ExtraTipRatio { get; set; } = 0.25;
public BigInteger FallbackTip { get; set; } = 2000000000;
public TimePreferenceFeeSuggestionStrategy()
{
}
#if !DOTNET35
private EthFeeHistory ethFeeHistory;
public IClient Client { get; set; }
public TimePreferenceFeeSuggestionStrategy(IClient client)
{
ethFeeHistory = new EthFeeHistory(client);
}
///
/// Suggest fee returns the first element of the time preferences, this is highest time preference(most urgent transaction).
/// The maxPriorityFeePerGas if supplied will override the calculated value, and if is bigger than the MaxFeePerGas calculated, it will be also overriden with the same value
///
public async Task SuggestFeeAsync(BigInteger? maxPriorityFeePerGas = null)
{
var fees = await SuggestFeesAsync().ConfigureAwait(false);
var returnFee = fees.First(); // using the first fee as it is the fastest
if (maxPriorityFeePerGas != null)
{
returnFee.MaxPriorityFeePerGas = maxPriorityFeePerGas;
if (returnFee.MaxFeePerGas < maxPriorityFeePerGas)
{
returnFee.MaxFeePerGas = maxPriorityFeePerGas;
}
}
return returnFee;
}
private async Task SuggestTipAsync(BigInteger firstBlock, decimal[] gasUsedRatio)
{
var ptr = gasUsedRatio.Length - 1;
var needBlocks = 5;
var rewards = new List();
while (needBlocks > 0 && ptr >= 0)
{
var blockCount = MaxBlockCount(gasUsedRatio, ptr, needBlocks);
if (blockCount > 0)
{
// feeHistory API call with reward percentile specified is expensive and therefore is only requested for a few non-full recent blocks.
var feeHistory = await ethFeeHistory.SendRequestAsync(blockCount.ToHexBigInteger(), new BlockParameter(new HexBigInteger(firstBlock + ptr)), new double[] { 0 }).ConfigureAwait(false);
for (var i = 0; i < feeHistory.Reward.Length; i++)
{
rewards.Add(feeHistory.Reward[i][0]);
}
if (feeHistory.Reward.Length < blockCount)
{
break;
}
needBlocks -= blockCount;
}
ptr -= blockCount + 1;
}
if (rewards.Count == 0)
{
return FallbackTip;
}
rewards.Sort();
return rewards[(int)Math.Truncate((double)(rewards.Count / 2))];
}
///
/// SuggestFees returns a series of maxFeePerGas / maxPriorityFeePerGas values suggested for different time preferences.The first element corresponds to the highest time preference(most urgent transaction).
/// The basic idea behind the algorithm is similar to the old "gas price oracle" used in Geth; it takes the prices of recent blocks and makes a suggestion based on a low percentile of those prices.With EIP-1559 though the base fee of each block provides a less noisy and more reliable price signal.
/// This allows for more sophisticated suggestions with a variable width(exponentially weighted) base fee time window.
/// The window width corresponds to the time preference of the user.
/// The underlying assumption is that price fluctuations over a given past time period indicate the probability of similar price levels being re-tested by the market over a similar length future time period.
///
public async Task SuggestFeesAsync()
{
// feeHistory API call without a reward percentile specified is cheap even with a light client backend because it only needs block headers.
// Therefore we can afford to fetch a hundred blocks of base fee history in order to make meaningful estimates on variable time scales.
var feeHistory = await ethFeeHistory.SendRequestAsync(100.ToHexBigInteger(), BlockParameter.CreateLatest()).ConfigureAwait(false);
var gasUsedRatio = feeHistory.GasUsedRatio;
var tip = await SuggestTipAsync(feeHistory.OldestBlock, gasUsedRatio).ConfigureAwait(false);
return SuggestFees(feeHistory, tip);
}
#endif
#if DOTNET35
public static class Comparer
{
public static Comparer Create(Comparison comparison)
{
if (comparison == null) throw new ArgumentNullException("comparison");
return new ComparisonComparer(comparison);
}
private sealed class ComparisonComparer : Comparer
{
private readonly Comparison comparison;
public ComparisonComparer(Comparison comparison)
{
this.comparison = comparison;
}
public override int Compare(T x, T y)
{
return comparison(x, y);
}
}
}
#endif
public Fee1559[] SuggestFees(FeeHistoryResult feeHistory, BigInteger tip)
{
var baseFee = feeHistory.BaseFeePerGas.Select(x => x == null? 0 : x.Value).ToArray();
var gasUsedRatio = feeHistory.GasUsedRatio;
// If a block is full then the baseFee of the next block is copied. The reason is that in full blocks the minimal tip might not be enough to get included.
// The last (pending) block is also assumed to end up being full in order to give some upwards bias for urgent suggestions.
baseFee[baseFee.Length - 1] *= 9 / 8;
for (var i = gasUsedRatio.Length - 1; i >= 0; i--)
{
if (gasUsedRatio[i] > (decimal)0.9)
{
baseFee[i] = baseFee[i + 1];
}
}
var order = new int[baseFee.Length];
for (var i = 0; i < baseFee.Length; i++)
{
order[i] = i;
}
#if DOTNET35
var comparer = Comparer.Create
#else
var comparer = Comparer.Create
#endif
((int x, int y) =>
{
var aa = baseFee[x];
var bb = baseFee[y];
if (aa < bb)
{
return -1;
}
if (aa > bb)
{
return 1;
}
return 0;
});
Array.Sort(order, comparer);
var result = new List();
BigDecimal maxBaseFee = 0;
for (var timeFactor = MaxTimeFactor; timeFactor >= 0; timeFactor--)
{
var bf = SuggestBaseFee(baseFee, order, timeFactor);
var t = new BigDecimal(tip, 0);
if (bf > maxBaseFee)
{
maxBaseFee = bf;
}
else
{
// If a narrower time window yields a lower base fee suggestion than a wider window then we are probably in a price dip.
// In this case getting included with a low tip is not guaranteed; instead we use the higher base fee suggestion
// and also offer extra tip to increase the chance of getting included in the base fee dip.
t += (maxBaseFee - bf) * ExtraTipRatio;
bf = maxBaseFee;
}
result.Add(new Fee1559()
{
BaseFee = bf.Floor().Mantissa,
MaxFeePerGas = (bf + t).Floor().Mantissa,
MaxPriorityFeePerGas = t.Floor().Mantissa
});
}
result.Reverse();
return result.ToArray();
}
///
/// // suggestBaseFee calculates an average of base fees in the sampleMin to sampleMax percentile range of recent base fee history, each block weighted with an exponential time function based on timeFactor.
///
protected BigDecimal SuggestBaseFee(BigInteger[] baseFee, int[] order, int timeFactor)
{
if (timeFactor < 1e-6)
{
return new BigDecimal(baseFee[baseFee.Length - 1], 0);
}
var pendingWeight = (1 - Math.Exp(-1 / timeFactor)) / (1 - Math.Exp(-baseFee.Length / timeFactor));
double sumWeight = 0;
BigDecimal result = 0;
double samplingCurveLast = 0;
for (var i = 0; i < order.Length; i++)
{
sumWeight += pendingWeight * Math.Exp((order[i] - baseFee.Length + 1) / timeFactor);
var samplingCurveValue = SamplingCurve(sumWeight);
result += (samplingCurveValue - samplingCurveLast) * new BigDecimal(baseFee[order[i]], 0);
if (samplingCurveValue >= 1)
{
return result;
}
samplingCurveLast = samplingCurveValue;
}
return result;
}
// samplingCurve is a helper function for the base fee percentile range calculation.
protected double SamplingCurve(double sumWeight)
{
if (sumWeight <= SampleMin)
{
return 0;
}
if (sumWeight >= SampleMax)
{
return 1;
}
return (1 - Math.Cos((sumWeight - SampleMin) * 2 * Math.PI / (SampleMax - SampleMin))) / 2;
}
// maxBlockCount returns the number of consecutive blocks suitable for tip suggestion (gasUsedRatio non-zero and not higher than 0.9).
public int MaxBlockCount(decimal[] gasUsedRatio, int ptr, int needBlocks)
{
int blockCount = 0;
while (needBlocks > 0 && ptr >= 0)
{
if (gasUsedRatio[ptr] == 0 || gasUsedRatio[ptr] > (decimal) 0.9)
{
break;
}
ptr--;
needBlocks--;
blockCount++;
}
return blockCount;
}
}
}