@rainbow-me/fee-suggestions
Version:
JavaScript library that suggest fees on Ethereum after EIP-1559 using historical data using ethers.js
222 lines (197 loc) • 5.57 kB
text/typescript
import BigNumber from 'bignumber.js';
import { TREND_THRESHOLDS } from './constants';
import { Reward } from './entities';
type BigNumberish = number | string | BigNumber;
const ethUnits = {
gwei: 1000000000,
};
const { FALLING, MEDIAN_SLOPE, RAISING, SURGING } = TREND_THRESHOLDS;
export const multiply = (
numberOne: BigNumberish,
numberTwo: BigNumberish
): BigNumber => new BigNumber(numberOne).times(numberTwo);
export const divide = (
numberOne: BigNumberish,
numberTwo: BigNumberish
): BigNumber => {
if (!(numberOne || numberTwo)) return new BigNumber(0);
return new BigNumber(numberOne).dividedBy(numberTwo);
};
export const gweiToWei = (gweiAmount: BigNumberish) => {
const weiAmount = multiply(gweiAmount, ethUnits.gwei).toFixed(0);
return weiAmount;
};
export const weiToGwei = (weiAmount: BigNumberish) => {
const gweiAmount = divide(weiAmount, ethUnits.gwei).toFixed();
return gweiAmount;
};
export const weiToGweiNumber = (weiAmount: BigNumberish) => {
const gweiAmount = divide(weiAmount, ethUnits.gwei).toNumber();
return gweiAmount;
};
export const weiToString = (weiAmount: BigNumberish) => {
return new BigNumber(weiAmount).toString();
};
export const samplingCurve = (
sumWeight: number,
sampleMin: number,
sampleMax: number
) => {
if (sumWeight <= sampleMin) {
return 0;
}
if (sumWeight >= sampleMax) {
return 1;
}
return (
(1 -
Math.cos(
((sumWeight - sampleMin) * 2 * Math.PI) / (sampleMax - sampleMin)
)) /
2
);
};
export const linearRegression = (y: number[]) => {
const x = Array.from(Array(y.length + 1).keys());
const n = y.length;
let sumX = 0;
let sumY = 0;
let sumXY = 0;
let sumXX = 0;
for (let i = 0; i < y.length; i++) {
const cY = Number(y[i]);
const cX = Number(x[i]);
sumX += cX;
sumY += cY;
sumXY += cX * cY;
sumXX += cX * cX;
}
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
return slope;
};
export const suggestBaseFee = (
baseFee: number[],
order: number[],
timeFactor: number,
sampleMin: number,
sampleMax: number
) => {
if (timeFactor < 1e-6) {
return baseFee[baseFee.length - 1];
}
const pendingWeight =
(1 - Math.exp(-1 / timeFactor)) /
(1 - Math.exp(-baseFee.length / timeFactor));
let sumWeight = 0;
let result = 0;
let samplingCurveLast = 0;
for (let or of order) {
sumWeight +=
pendingWeight * Math.exp((or - baseFee.length + 1) / timeFactor);
const samplingCurveValue = samplingCurve(sumWeight, sampleMin, sampleMax);
result += (samplingCurveValue - samplingCurveLast) * baseFee[or];
if (samplingCurveValue >= 1) {
return result;
}
samplingCurveLast = samplingCurveValue;
}
return result;
};
export const getOutlierBlocksToRemove = (
blocksRewards: Reward[],
index: number
) => {
const blocks: number[] = [];
blocksRewards
.map((reward) => weiToGweiNumber(reward[index]))
.forEach((gweiReward, i) => {
if (gweiReward > 5) {
blocks.push(i);
}
});
return blocks;
};
export const rewardsFilterOutliers = (
blocksRewards: Reward[],
outlierBlocks: number[],
rewardIndex: number
) =>
blocksRewards
.filter((_, index) => !outlierBlocks.includes(index))
.map((reward) => weiToGweiNumber(reward[rewardIndex]));
const calculateGroupInfo = (baseFees: number[]) => {
const sortedBaseFees = baseFees.sort((a, b) => a - b);
const min = sortedBaseFees[0];
const max = sortedBaseFees[sortedBaseFees.length - 1];
const median = sortedBaseFees[Math.floor(sortedBaseFees.length / 2)];
return { max, median, min };
};
const createSubsets = (numbers: number[], n: number) => {
const subsets: number[][] = [];
for (let i = 0; i < numbers.length; i = i + n) {
subsets.push(numbers.slice(i, i + n));
}
return subsets;
};
export const getData = (numbers: number[], n: number) => {
const subsets = createSubsets(numbers, n);
const subsetsInfo = subsets.map((subset) => calculateGroupInfo(subset));
const {
max: lastMax,
min: lastMin,
median: lastMedian,
} = subsetsInfo[subsetsInfo.length - 1];
const medianData = subsetsInfo.map((data) => data.median);
const medianSlope = linearRegression(medianData);
return {
max: lastMax,
median: lastMedian,
medianSlope,
min: lastMin,
};
};
export const calculateBaseFeeTrend = (
baseFees: number[],
currentBaseFee: string
) => {
let trend = 0;
try {
// taking 50 blocks
const baseFees50Blocks = baseFees.slice(51);
// divide it in groups of 5
const n50 = {
g5: getData(baseFees50Blocks, 5),
};
// taking 100 blocks
const baseFees100Blocks = baseFees.slice(1);
// divide it in groups of 25
const n100 = {
g25: getData(baseFees100Blocks, 25),
};
const maxByMedian = n100.g25.max / n100.g25.median;
const minByMedian = n100.g25.min / n100.g25.median;
if (maxByMedian > SURGING) {
trend = 2;
} else if (maxByMedian > RAISING && minByMedian > FALLING) {
trend = 1;
} else if (maxByMedian < RAISING && minByMedian > FALLING) {
if (n50.g5.medianSlope < MEDIAN_SLOPE) {
trend = -1;
} else {
trend = 0;
}
} else if (maxByMedian < RAISING && minByMedian < FALLING) {
trend = -1;
} else {
// if none is on the threshold
if (weiToGweiNumber(currentBaseFee) > n100.g25.median) {
trend = 1;
} else {
trend = -1;
}
}
} catch (e) {
//
}
return trend;
};