Skip to content

Commit

Permalink
Pull in networkCongestion along with gas estimates (#632)
Browse files Browse the repository at this point in the history
The Metaswap API now includes a `networkCongestion` property when
requesting EIP-1559-compatible gas fee estimates. This value, which is a
number from 0 to 1, where 0 represents "not congested" and 1 represents
"extremely congested", can be used to communicate the status of the
network to users when they are sending transactions.

This commit ensures that GasFeeController includes `networkCongestion`
as part of the state it persists, no matter if using the Metaswap API to
obtain this information or falling back to `eth_feeHistory`.

This is a part of the EIP-1559 v2 work.
  • Loading branch information
mcmire authored Dec 7, 2021
1 parent e1d1419 commit 1ad1546
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 58 deletions.
1 change: 1 addition & 0 deletions src/gas/GasFeeController.test.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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',
},
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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

0 comments on commit 1ad1546

Please sign in to comment.