Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull in networkCongestion along with gas estimates #632

Merged
merged 1 commit into from Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/gas/GasFeeController.test.ts
Expand Up @@ -76,6 +76,7 @@ function buildMockGasFeeStateFeeMarket({
suggestedMaxFeePerGas: (30 * modifier).toString(),
},
estimatedBaseFee: (100 * modifier).toString(),
networkCongestion: 0.1 * modifier,
},
estimatedGasFeeTimeBounds: {
lowerTimeBound: 1000 * modifier,
Expand Down
3 changes: 3 additions & 0 deletions src/gas/GasFeeController.ts
Expand Up @@ -118,13 +118,16 @@ export type Eip1559GasFee = {
* @property medium - A GasFee for a recommended combination of tip and maxFee
* @property high - A GasFee for a high combination of tip and maxFee
* @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number
* @property networkCongestion - A normalized number that can be used to gauge the congestion
* level of the network, with 0 meaning not congested and 1 meaning extremely congested
*/

export type GasFeeEstimates = {
low: Eip1559GasFee;
medium: Eip1559GasFee;
high: Eip1559GasFee;
estimatedBaseFee: string;
networkCongestion: number;
};

const metadata = {
Expand Down
1 change: 1 addition & 0 deletions src/gas/determineGasFeeCalculations.test.ts
Expand Up @@ -58,6 +58,7 @@ function buildMockDataForFetchGasEstimates(): GasFeeEstimates {
suggestedMaxFeePerGas: '30',
},
estimatedBaseFee: '100',
networkCongestion: 0.5,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/gas/fetchBlockFeeHistory.ts
Expand Up @@ -48,7 +48,7 @@ export type EthFeeHistoryResponse = {
* used for the block, indexed by those percentiles. (See docs for {@link fetchBlockFeeHistory} for more
* on how this works.)
*/
type Block<Percentile extends number> = {
export type Block<Percentile extends number> = {
number: BN;
baseFeePerGas: BN;
gasUsedRatio: number;
Expand Down
103 changes: 68 additions & 35 deletions src/gas/fetchGasEstimatesViaEthFeeHistory.test.ts
@@ -1,47 +1,79 @@
import { BN } from 'ethereumjs-util';
import { mocked } from 'ts-jest/utils';
import { when } from 'jest-when';
import fetchBlockFeeHistory from './fetchBlockFeeHistory';
import fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHistory';

jest.mock('./fetchBlockFeeHistory');

const mockedFetchFeeHistory = mocked(fetchBlockFeeHistory, true);
const mockedFetchBlockFeeHistory = mocked(fetchBlockFeeHistory, true);

describe('fetchGasEstimatesViaEthFeeHistory', () => {
it('calculates target fees for low, medium, and high transaction priority levels', async () => {
it('calculates target fees for low, medium, and high transaction priority levels, as well as the network congestion level', async () => {
const ethQuery = {};
mockedFetchFeeHistory.mockResolvedValue([
{
number: new BN(1),
baseFeePerGas: new BN(0),
gasUsedRatio: 1,
priorityFeesByPercentile: {
10: new BN(0),
20: new BN(1_000_000_000),
30: new BN(0),
when(mockedFetchBlockFeeHistory)
.calledWith({
ethQuery,
numberOfBlocks: 5,
percentiles: [10, 20, 30],
})
.mockResolvedValue([
{
number: new BN(1),
baseFeePerGas: new BN(300_000_000_000),
gasUsedRatio: 1,
priorityFeesByPercentile: {
10: new BN(0),
20: new BN(1_000_000_000),
30: new BN(0),
},
},
},
{
number: new BN(2),
baseFeePerGas: new BN(0),
gasUsedRatio: 1,
priorityFeesByPercentile: {
10: new BN(500_000_000),
20: new BN(1_600_000_000),
30: new BN(3_000_000_000),
{
number: new BN(2),
baseFeePerGas: new BN(100_000_000_000),
gasUsedRatio: 1,
priorityFeesByPercentile: {
10: new BN(500_000_000),
20: new BN(1_600_000_000),
30: new BN(3_000_000_000),
},
},
},
{
number: new BN(3),
baseFeePerGas: new BN(100_000_000_000),
gasUsedRatio: 1,
priorityFeesByPercentile: {
10: new BN(500_000_000),
20: new BN(2_000_000_000),
30: new BN(3_000_000_000),
{
number: new BN(3),
baseFeePerGas: new BN(200_000_000_000),
gasUsedRatio: 1,
priorityFeesByPercentile: {
10: new BN(500_000_000),
20: new BN(2_000_000_000),
30: new BN(3_000_000_000),
},
},
},
]);
])
.calledWith({
ethQuery,
numberOfBlocks: 20_000,
endBlock: new BN(3),
})
.mockResolvedValue([
{
number: new BN(1),
baseFeePerGas: new BN(300_000_000_000),
gasUsedRatio: 1,
priorityFeesByPercentile: {},
},
{
number: new BN(2),
baseFeePerGas: new BN(100_000_000_000),
gasUsedRatio: 1,
priorityFeesByPercentile: {},
},
{
number: new BN(3),
baseFeePerGas: new BN(200_000_000_000),
gasUsedRatio: 1,
priorityFeesByPercentile: {},
},
]);

const gasFeeEstimates = await fetchGasEstimatesViaEthFeeHistory(ethQuery);

Expand All @@ -50,21 +82,22 @@ describe('fetchGasEstimatesViaEthFeeHistory', () => {
minWaitTimeEstimate: 15_000,
maxWaitTimeEstimate: 30_000,
suggestedMaxPriorityFeePerGas: '1',
suggestedMaxFeePerGas: '111',
suggestedMaxFeePerGas: '221',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the base fee per gas values above so that's why these changed.

},
medium: {
minWaitTimeEstimate: 15_000,
maxWaitTimeEstimate: 45_000,
suggestedMaxPriorityFeePerGas: '1.552',
suggestedMaxFeePerGas: '121.552',
suggestedMaxFeePerGas: '241.552',
},
high: {
minWaitTimeEstimate: 15_000,
maxWaitTimeEstimate: 60_000,
suggestedMaxPriorityFeePerGas: '2.94',
suggestedMaxFeePerGas: '127.94',
suggestedMaxFeePerGas: '252.94',
},
estimatedBaseFee: '100',
estimatedBaseFee: '200',
networkCongestion: 0.5,
});
});
});
67 changes: 49 additions & 18 deletions src/gas/fetchGasEstimatesViaEthFeeHistory.ts
@@ -1,15 +1,16 @@
// This code is translated from the MetaSwap API:
// <https://gitlab.com/ConsenSys/codefi/products/metaswap/gas-api>

import { BN } from 'ethereumjs-util';
import { fromWei } from 'ethjs-unit';
import fetchBlockFeeHistory from './fetchBlockFeeHistory';
import fetchBlockFeeHistory, { Block } from './fetchBlockFeeHistory';
import { Eip1559GasFee, GasFeeEstimates } from './GasFeeController';

type EthQuery = any;
type PriorityLevel = typeof PRIORITY_LEVELS[number];
type Percentile = typeof PRIORITY_LEVEL_PERCENTILES[number];

// This code is translated from the MetaSwap API:
// <https://gitlab.com/ConsenSys/codefi/products/metaswap/gas-api/-/blob/017436f628b2d5967f6e8734780a9114f9e58af9/src/eip1559/feeHistory.ts>

const NUMBER_OF_HISTORICAL_BLOCKS_TO_FETCH = 20_000;
const NUMBER_OF_RECENT_BLOCKS_TO_FETCH = 5;
const PRIORITY_LEVELS = ['low', 'medium', 'high'] as const;
const PRIORITY_LEVEL_PERCENTILES = [10, 20, 30] as const;
Expand Down Expand Up @@ -66,15 +67,14 @@ function medianOf(numbers: BN[]): BN {
*
* @param priorityLevel - The level of fees that dictates how soon a transaction may go through
* ("low", "medium", or "high").
* @param latestBaseFeePerGas - The base fee per gas recorded for the latest block in WEI, as a BN.
* @param blocks - More information about blocks we can use to calculate estimates.
* @param blocks - A set of blocks as obtained from {@link fetchBlockFeeHistory}.
* @returns The estimates.
*/
function calculateGasEstimatesForPriorityLevel(
priorityLevel: PriorityLevel,
latestBaseFeePerGas: BN,
blocks: { priorityFeesByPercentile: Record<Percentile, BN> }[],
blocks: Block<Percentile>[],
): Eip1559GasFee {
const latestBaseFeePerGas = blocks[blocks.length - 1].baseFeePerGas;
const settings = SETTINGS_BY_PRIORITY_LEVEL[priorityLevel];

const adjustedBaseFee = latestBaseFeePerGas
Expand Down Expand Up @@ -109,24 +109,47 @@ function calculateGasEstimatesForPriorityLevel(
* Calculates a set of estimates suitable for different priority levels based on the data returned
* by `eth_feeHistory`.
*
* @param latestBaseFeePerGas - The base fee per gas recorded for the latest block in WEI, as a BN.
* @param blocks - More information about blocks we can use to calculate estimates.
* @param blocks - A set of blocks as obtained from {@link fetchBlockFeeHistory}.
* @returns The estimates.
*/
function calculateGasEstimatesForAllPriorityLevels(
latestBaseFeePerGas: BN,
blocks: { priorityFeesByPercentile: Record<Percentile, BN> }[],
) {
blocks: Block<Percentile>[],
): Pick<GasFeeEstimates, PriorityLevel> {
return PRIORITY_LEVELS.reduce((obj, priorityLevel) => {
const gasEstimatesForPriorityLevel = calculateGasEstimatesForPriorityLevel(
priorityLevel,
latestBaseFeePerGas,
blocks,
);
return { ...obj, [priorityLevel]: gasEstimatesForPriorityLevel };
}, {} as Pick<GasFeeEstimates, PriorityLevel>);
}

/**
* Calculates the approximate normalized ranking of the latest base fee in the given blocks among
* the entirety of the blocks. That is, sorts all of the base fees, then finds the rank of the first
* base fee that meets or exceeds the latest base fee among the base fees. The result is the rank
* normalized as a number between 0 and 1, where 0 means that the latest base fee is the least of
* all and 1 means that the latest base fee is the greatest of all. This can ultimately be used to
* render a visualization of the status of the network for users.
*
* @param blocks - A set of blocks as obtained from {@link fetchBlockFeeHistory}.
* @returns A promise of a number between 0 and 1.
*/
async function calculateNetworkCongestionLevelFrom(
blocks: Block<Percentile>[],
): Promise<number> {
const latestBaseFeePerGas = blocks[blocks.length - 1].baseFeePerGas;
const sortedBaseFeesPerGas = blocks
.map((block) => block.baseFeePerGas)
.sort((a, b) => a.cmp(b));
const indexOfBaseFeeNearestToLatest = sortedBaseFeesPerGas.findIndex(
(baseFeePerGas) => baseFeePerGas.gte(latestBaseFeePerGas),
);
return indexOfBaseFeeNearestToLatest !== -1
? indexOfBaseFeeNearestToLatest / (blocks.length - 1)
: 0;
}

/**
* Generates gas fee estimates based on gas fees that have been used in the recent past so that
* those estimates can be displayed to users.
Expand All @@ -145,20 +168,28 @@ function calculateGasEstimatesForAllPriorityLevels(
export default async function fetchGasEstimatesViaEthFeeHistory(
ethQuery: EthQuery,
): Promise<GasFeeEstimates> {
const blocks = await fetchBlockFeeHistory<Percentile>({
const recentBlocks = await fetchBlockFeeHistory<Percentile>({
ethQuery,
numberOfBlocks: NUMBER_OF_RECENT_BLOCKS_TO_FETCH,
percentiles: PRIORITY_LEVEL_PERCENTILES,
});
const latestBlock = blocks[blocks.length - 1];
const latestBlock = recentBlocks[recentBlocks.length - 1];
const historicalBlocks = await fetchBlockFeeHistory<Percentile>({
ethQuery,
numberOfBlocks: NUMBER_OF_HISTORICAL_BLOCKS_TO_FETCH,
endBlock: latestBlock.number,
});
const levelSpecificGasEstimates = calculateGasEstimatesForAllPriorityLevels(
latestBlock.baseFeePerGas,
blocks,
recentBlocks,
);
const estimatedBaseFee = fromWei(latestBlock.baseFeePerGas, 'gwei');
const networkCongestion = await calculateNetworkCongestionLevelFrom(
historicalBlocks,
);

return {
...levelSpecificGasEstimates,
estimatedBaseFee,
networkCongestion,
};
}
8 changes: 4 additions & 4 deletions src/gas/gas-util.ts
Expand Up @@ -33,12 +33,11 @@ export async function fetchGasEstimates(
url: string,
clientId?: string,
): Promise<GasFeeEstimates> {
const estimates: GasFeeEstimates = await handleFetch(
const estimates = await handleFetch(
url,
clientId ? { headers: makeClientIdHeader(clientId) } : undefined,
);
const normalizedEstimates: GasFeeEstimates = {
estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee),
return {
low: {
...estimates.low,
suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(
Expand Down Expand Up @@ -66,8 +65,9 @@ export async function fetchGasEstimates(
estimates.high.suggestedMaxFeePerGas,
),
},
estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee),
networkCongestion: estimates.networkCongestion,
};
return normalizedEstimates;
}

/**
Expand Down