Skip to content

Commit

Permalink
Add calculateFees function (#6)
Browse files Browse the repository at this point in the history
* Add calculateFees function

* Test both functions

* Test edge case

* Update README

* Bump version

* Test another edge case

* Fix a few PR comments
  • Loading branch information
FrederikBolding authored Sep 20, 2021
1 parent 959d62f commit df22c24
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 114 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ console.log(maxFeePerGas, maxPriorityFeePerGas);

## API

The library exposes a single function to estimate gas fees based on the latest 10 blocks.
The library exposes a function to estimate gas fees based on the latest 10 blocks, and the underlying function used to calculate the estimate.

### `estimateFees(provider)`

- `provider` - A Web3 instance, Ethers.js provider, JSON-RPC endpoint, or EIP-1193 compatible provider.
- Returns: \<Promise\<EstimationResult\>\> - An object containing the estimated `maxFeePerGas`, `maxPriorityFeePerGas`, and `baseFee`, as `bigint` (all values in Wei).

### `calculateFees(baseFee, feeHistory)`

- `baseFee` - The current base fee as a `bigint` (in Wei).
- `feeHistory` - The fee history object returned by a node when calling `eth_feeHistory`.
- Returns: \<EstimationResult\> - An object containing the estimated `maxFeePerGas`, `maxPriorityFeePerGas`, and `baseFee`, as `bigint` (all values in Wei).

### Providers

Currently, gas-estimation has support for four different providers:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mycrypto/gas-estimation",
"version": "1.0.0",
"version": "1.1.0",
"description": "The MyCrypto EIP 1559 gas estimation strategy, now provided as a library.",
"repository": "MyCryptoHQ/gas-estimation",
"author": "MyCrypto",
Expand Down
160 changes: 79 additions & 81 deletions src/eip1559.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { estimateFees, FALLBACK_ESTIMATE } from './eip1559';
import { estimateFees, calculateFees, FALLBACK_ESTIMATE } from './eip1559';

const block = {
hash: '0x38b34c2313e148a0916406a204536c03e5bf77312c558d25d3b63d8a4e30af47',
Expand Down Expand Up @@ -30,16 +30,7 @@ describe('estimateFees', () => {
const mockProvider = {
send: jest.fn()
};
it('estimates without using priority fees', () => {
mockProvider.send.mockResolvedValueOnce(block);
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({
baseFee: 10000000000n,
maxFeePerGas: 20000000000n,
maxPriorityFeePerGas: 3000000000n
});
});

it('estimates priority fees', async () => {
it('estimates using calculateFees', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return feeHistory;
Expand All @@ -52,112 +43,119 @@ describe('estimateFees', () => {
maxPriorityFeePerGas: 5000000000n
});
});
it('falls back if no baseFeePerGas on block', async () => {
mockProvider.send.mockResolvedValueOnce({ ...block, baseFeePerGas: undefined });
return expect(estimateFees(mockProvider)).resolves.toStrictEqual(FALLBACK_ESTIMATE);
});
it('skips fee history if trigger not met', async () => {
mockProvider.send.mockResolvedValueOnce({ ...block, baseFeePerGas: '0x7' });
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({
baseFee: 7n,
maxFeePerGas: 3000000000n,
maxPriorityFeePerGas: 3000000000n
});
});
});

it('estimates priority fees removing low outliers', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return {
...feeHistory,
reward: [
['0x1'],
['0x1'],
['0x1'],
['0x1'],
['0x1'],
['0x1a13b8600'],
['0x12a05f1f9'],
['0x3b9aca00'],
['0x1a13b8600']
]
};
}
return { ...block, baseFeePerGas: '0x174876e800' };
describe('calculateFees', () => {
it('estimates without using priority fees', () => {
const baseFee = BigInt(block.baseFeePerGas);
return expect(calculateFees(baseFee, feeHistory)).toStrictEqual({
baseFee: 10000000000n,
maxFeePerGas: 20000000000n,
maxPriorityFeePerGas: 5000000000n
});
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({
});

it('estimates priority fees', () => {
const baseFee = BigInt('0x174876e800');
return expect(calculateFees(baseFee, feeHistory)).toStrictEqual({
baseFee: 100000000000n,
maxFeePerGas: 160000000000n,
maxPriorityFeePerGas: 5000000000n
});
});

it('uses 1.6 multiplier for base if above 40 gwei', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return feeHistory;
}
return { ...block, baseFeePerGas: '0x11766ffa76' };
it('estimates priority fees removing low outliers', () => {
const baseFee = BigInt('0x174876e800');
return expect(
calculateFees(baseFee, {
...feeHistory,
reward: [
['0x1'],
['0x1'],
['0x1'],
['0x1'],
['0x1'],
['0x1a13b8600'],
['0x12a05f1f9'],
['0x3b9aca00'],
['0x1a13b8600']
]
})
).toStrictEqual({
baseFee: 100000000000n,
maxFeePerGas: 160000000000n,
maxPriorityFeePerGas: 5000000000n
});
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({
});

it('uses 1.6 multiplier for base if above 40 gwei', () => {
const baseFee = BigInt('0x11766ffa76');
return expect(calculateFees(baseFee, feeHistory)).toStrictEqual({
baseFee: 75001494134n,
maxFeePerGas: 120000000000n,
maxPriorityFeePerGas: 3000000000n
maxPriorityFeePerGas: 5000000000n
});
});

it('uses 1.4 multiplier for base if above 100 gwei', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return feeHistory;
}
return { ...block, baseFeePerGas: '0x2e90edd000' };
});
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({
it('uses 1.4 multiplier for base if above 100 gwei', () => {
const baseFee = BigInt('0x2e90edd000');
return expect(calculateFees(baseFee, feeHistory)).toStrictEqual({
baseFee: 200000000000n,
maxFeePerGas: 280000000000n,
maxPriorityFeePerGas: 5000000000n
});
});

it('uses 1.2 multiplier for base if above 200 gwei', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return feeHistory;
}
return { ...block, baseFeePerGas: '0x45d964b800' };
});
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({
it('uses 1.2 multiplier for base if above 200 gwei', () => {
const baseFee = BigInt('0x45d964b800');
return expect(calculateFees(baseFee, feeHistory)).toStrictEqual({
baseFee: 300000000000n,
maxFeePerGas: 360000000000n,
maxPriorityFeePerGas: 5000000000n
});
});

it('handles baseFee being smaller than priorityFee', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return feeHistory;
}
return { ...block, baseFeePerGas: '0x7' };
});
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({
it('handles baseFee being smaller than priorityFee', () => {
const baseFee = BigInt('0x7');
return expect(calculateFees(baseFee, undefined)).toStrictEqual({
baseFee: 7n,
maxFeePerGas: 3000000000n,
maxPriorityFeePerGas: 3000000000n
});
});

it('falls back if no baseFeePerGas on block', async () => {
mockProvider.send.mockResolvedValueOnce({ ...block, baseFeePerGas: undefined });
return expect(estimateFees(mockProvider)).resolves.toStrictEqual(FALLBACK_ESTIMATE);
it('defaults if no fee history available', () => {
const baseFee = BigInt('0x45d964b800');
return expect(calculateFees(baseFee, undefined)).toStrictEqual({
baseFee: 300000000000n,
maxFeePerGas: 360000000000n,
maxPriorityFeePerGas: FALLBACK_ESTIMATE.maxPriorityFeePerGas
});
});

it('falls back if priority fetching fails', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return { ...feeHistory, reward: undefined };
}
return { ...block, baseFeePerGas: '0x45d964b800' };
it('handles empty rewards for fee history', () => {
const baseFee = BigInt('0x45d964b800');
return expect(calculateFees(baseFee, { ...feeHistory, reward: undefined })).toStrictEqual({
baseFee: 300000000000n,
maxFeePerGas: 360000000000n,
maxPriorityFeePerGas: FALLBACK_ESTIMATE.maxPriorityFeePerGas
});
return expect(estimateFees(mockProvider)).resolves.toStrictEqual(FALLBACK_ESTIMATE);
});

it('falls back if gas is VERY high', async () => {
mockProvider.send.mockImplementation((method) => {
if (method === 'eth_feeHistory') {
return feeHistory;
}
return { ...block, baseFeePerGas: '0x91812d7d600' };
});
return expect(estimateFees(mockProvider)).resolves.toStrictEqual(FALLBACK_ESTIMATE);
it('falls back if gas is VERY high', () => {
const baseFee = BigInt('0x91812d7d600');
return expect(calculateFees(baseFee, feeHistory)).toStrictEqual(FALLBACK_ESTIMATE);
});
});
65 changes: 34 additions & 31 deletions src/eip1559.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ProviderLike } from '@mycrypto/eth-scan';

import { getFeeHistory, getLatestBlock } from './provider';
import type { EstimationResult } from './types';
import type { EstimationResult, FeeHistory } from './types';
import { gwei, hexlify, max, roundToWholeGwei } from './utils';

const MAX_GAS_FAST = gwei(1500n);
Expand Down Expand Up @@ -35,20 +35,10 @@ const getBaseFeeMultiplier = (baseFee: bigint) => {
}
};

const estimatePriorityFee = async (
provider: ProviderLike,
baseFee: bigint,
blockNumber: bigint
) => {
if (baseFee < PRIORITY_FEE_ESTIMATION_TRIGGER) {
return DEFAULT_PRIORITY_FEE;
const calculatePriorityFeeEstimate = (feeHistory?: FeeHistory) => {
if (!feeHistory) {
return null;
}
const feeHistory = await getFeeHistory(
provider,
hexlify(FEE_HISTORY_BLOCKS),
hexlify(blockNumber),
[FEE_HISTORY_PERCENTILE]
);

const rewards = feeHistory.reward
?.map((r) => BigInt(r[0]))
Expand Down Expand Up @@ -82,25 +72,11 @@ const estimatePriorityFee = async (
return values[Math.floor(values.length / 2)];
};

export const estimateFees = async (provider: ProviderLike): Promise<EstimationResult> => {
export const calculateFees = (baseFee: bigint, feeHistory?: FeeHistory): EstimationResult => {
try {
const latestBlock = await getLatestBlock(provider);
const estimatedPriorityFee = calculatePriorityFeeEstimate(feeHistory);

if (!latestBlock.baseFeePerGas) {
throw new Error('An error occurred while fetching current base fee, falling back');
}

const baseFee = BigInt(latestBlock.baseFeePerGas);

const blockNumber = BigInt(latestBlock.number);

const estimatedPriorityFee = await estimatePriorityFee(provider, baseFee, blockNumber);

if (estimatedPriorityFee === null) {
throw new Error('An error occurred while estimating priority fee, falling back');
}

const maxPriorityFeePerGas = max([estimatedPriorityFee, DEFAULT_PRIORITY_FEE]);
const maxPriorityFeePerGas = max([estimatedPriorityFee ?? 0n, DEFAULT_PRIORITY_FEE]);

const multiplier = getBaseFeeMultiplier(baseFee);

Expand All @@ -125,3 +101,30 @@ export const estimateFees = async (provider: ProviderLike): Promise<EstimationRe
return FALLBACK_ESTIMATE;
}
};

export const estimateFees = async (provider: ProviderLike): Promise<EstimationResult> => {
try {
const latestBlock = await getLatestBlock(provider);

if (!latestBlock.baseFeePerGas) {
throw new Error('An error occurred while fetching current base fee, falling back');
}

const baseFee = BigInt(latestBlock.baseFeePerGas);

const blockNumber = BigInt(latestBlock.number);

const feeHistory =
baseFee >= PRIORITY_FEE_ESTIMATION_TRIGGER
? await getFeeHistory(provider, hexlify(FEE_HISTORY_BLOCKS), hexlify(blockNumber), [
FEE_HISTORY_PERCENTILE
])
: undefined;

return calculateFees(baseFee, feeHistory);
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
return FALLBACK_ESTIMATE;
}
};

0 comments on commit df22c24

Please sign in to comment.