Skip to content

Commit

Permalink
Use eth_feeHistory as a fallback for gas estimates
Browse files Browse the repository at this point in the history
If we are on an EIP-1559-supported network and the Metaswap API fails
for some reason, fall back to using `eth_feeHistory` to calculate gas
estimates (which the API uses anyway). This code is more or less taken
from the code for the API ([1]).

[1]: https://gitlab.com/ConsenSys/codefi/products/metaswap/gas-api/-/blob/eae6927b1a0c445e02cb3cba9e9e6b0f35857a12/src/eip1559/feeHistory.ts
  • Loading branch information
mcmire committed Nov 1, 2021
1 parent b16cb47 commit b4f23df
Show file tree
Hide file tree
Showing 11 changed files with 1,124 additions and 59 deletions.
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -76,6 +76,7 @@
"@metamask/eslint-config-nodejs": "^9.0.0",
"@metamask/eslint-config-typescript": "^9.0.1",
"@types/jest": "^26.0.22",
"@types/jest-when": "^2.7.3",
"@types/node": "^14.14.31",
"@types/punycode": "^2.1.0",
"@types/sinon": "^9.0.10",
Expand All @@ -93,6 +94,7 @@
"ethjs-provider-http": "^0.1.6",
"jest": "^26.4.2",
"jest-environment-jsdom": "^25.0.0",
"jest-when": "^3.4.2",
"nock": "^13.0.7",
"prettier": "^2.2.1",
"prettier-plugin-packagejson": "^2.2.11",
Expand Down
2 changes: 2 additions & 0 deletions src/gas/GasFeeController.ts
Expand Up @@ -17,6 +17,7 @@ import {
calculateTimeEstimate,
} from './gas-util';
import determineGasFeeSuggestions from './determineGasFeeSuggestions';
import fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHistory';

const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/';
export const LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`;
Expand Down Expand Up @@ -376,6 +377,7 @@ export class GasFeeController extends BaseController<
'<chain_id>',
`${chainId}`,
),
fetchGasEstimatesViaEthFeeHistory,
fetchLegacyGasPriceEstimates,
fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(
'<chain_id>',
Expand Down
110 changes: 110 additions & 0 deletions src/gas/determineGasFeeSuggestions.test.ts
Expand Up @@ -13,8 +13,10 @@ import {
fetchEthGasPriceEstimate,
calculateTimeEstimate,
} from './gas-util';
import fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHistory';

jest.mock('./gas-util');
jest.mock('./fetchGasEstimatesViaEthFeeHistory');

const mockedFetchGasEstimates = mocked(fetchGasEstimates, true);
const mockedFetchLegacyGasPriceEstimates = mocked(
Expand All @@ -23,6 +25,10 @@ const mockedFetchLegacyGasPriceEstimates = mocked(
);
const mockedFetchEthGasPriceEstimate = mocked(fetchEthGasPriceEstimate, true);
const mockedCalculateTimeEstimate = mocked(calculateTimeEstimate, true);
const mockedFetchGasEstimatesViaEthFeeHistory = mocked(
fetchGasEstimatesViaEthFeeHistory,
true,
);

/**
* Builds mock data for the `fetchGasEstimates` function. All of the data here is filled in to make
Expand Down Expand Up @@ -102,6 +108,7 @@ describe('determineGasFeeSuggestions', () => {
isEIP1559Compatible: false,
isLegacyGasAPICompatible: false,
fetchGasEstimates: mockedFetchGasEstimates,
fetchGasEstimatesViaEthFeeHistory: mockedFetchGasEstimatesViaEthFeeHistory,
fetchGasEstimatesUrl: 'http://doesnt-matter',
fetchLegacyGasPriceEstimates: mockedFetchLegacyGasPriceEstimates,
fetchLegacyGasPriceEstimatesUrl: 'http://doesnt-matter',
Expand Down Expand Up @@ -144,6 +151,109 @@ describe('determineGasFeeSuggestions', () => {
});
});

describe('assuming neither fetchGasEstimatesViaEthFeeHistory nor calculateTimeEstimate throws errors', () => {
it('returns a combination of the fetched fee and time estimates', async () => {
const gasFeeEstimates = buildMockDataForFetchGasEstimates();
mockedFetchGasEstimatesViaEthFeeHistory.mockResolvedValue(
gasFeeEstimates,
);
const estimatedGasFeeTimeBounds = buildMockDataForCalculateTimeEstimate();
mockedCalculateTimeEstimate.mockReturnValue(
estimatedGasFeeTimeBounds,
);

const gasFeeSuggestions = await determineGasFeeSuggestions(options);

expect(gasFeeSuggestions).toStrictEqual({
gasFeeEstimates,
estimatedGasFeeTimeBounds,
gasEstimateType: 'fee-market',
});
});
});

describe('when fetchGasEstimatesViaEthFeeHistory throws an error', () => {
beforeEach(() => {
mockedFetchGasEstimatesViaEthFeeHistory.mockImplementation(() => {
throw new Error('Some API failure');
});
});

describe('assuming fetchEthGasPriceEstimate does not throw an error', () => {
it('returns the fetched fee estimates and an empty set of time estimates', async () => {
const gasFeeEstimates = buildMockDataForFetchEthGasPriceEstimate();
mockedFetchEthGasPriceEstimate.mockResolvedValue(gasFeeEstimates);

const gasFeeSuggestions = await determineGasFeeSuggestions(options);

expect(gasFeeSuggestions).toStrictEqual({
gasFeeEstimates,
estimatedGasFeeTimeBounds: {},
gasEstimateType: 'eth_gasPrice',
});
});
});

describe('when fetchEthGasPriceEstimate throws an error', () => {
it('throws an error that wraps that error', async () => {
mockedFetchEthGasPriceEstimate.mockImplementation(() => {
throw new Error('fetchEthGasPriceEstimate failed');
});

const promise = determineGasFeeSuggestions(options);

await expect(promise).rejects.toThrow(
'Gas fee/price estimation failed. Message: fetchEthGasPriceEstimate failed',
);
});
});
});

describe('when fetchGasEstimatesViaEthFeeHistory does not throw an error, but calculateTimeEstimate throws an error', () => {
beforeEach(() => {
mockedCalculateTimeEstimate.mockImplementation(() => {
throw new Error('Some API failure');
});
});

describe('assuming fetchEthGasPriceEstimate does not throw an error', () => {
it('returns the fetched fee estimates and an empty set of time estimates', async () => {
const gasFeeEstimates = buildMockDataForFetchEthGasPriceEstimate();
mockedFetchEthGasPriceEstimate.mockResolvedValue(gasFeeEstimates);

const gasFeeSuggestions = await determineGasFeeSuggestions(options);

expect(gasFeeSuggestions).toStrictEqual({
gasFeeEstimates,
estimatedGasFeeTimeBounds: {},
gasEstimateType: 'eth_gasPrice',
});
});
});

describe('when fetchEthGasPriceEstimate throws an error', () => {
it('throws an error that wraps that error', async () => {
mockedFetchEthGasPriceEstimate.mockImplementation(() => {
throw new Error('fetchEthGasPriceEstimate failed');
});

const promise = determineGasFeeSuggestions(options);

await expect(promise).rejects.toThrow(
'Gas fee/price estimation failed. Message: fetchEthGasPriceEstimate failed',
);
});
});
});
});

describe('when fetchGasEstimates does not throw an error, but calculateTimeEstimate throws an error', () => {
beforeEach(() => {
mockedCalculateTimeEstimate.mockImplementation(() => {
throw new Error('Some API failure');
});
});

describe('assuming fetchEthGasPriceEstimate does not throw an error', () => {
it('returns the fetched fee estimates and an empty set of time estimates', async () => {
const gasFeeEstimates = buildMockDataForFetchEthGasPriceEstimate();
Expand Down
13 changes: 12 additions & 1 deletion src/gas/determineGasFeeSuggestions.ts
Expand Up @@ -20,6 +20,8 @@ import {
* API.
* @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific
* estimates.
* @param args.fetchGasEstimatesViaEthFeeHistory - A function that fetches gas estimates using
* `eth_feeHistory` (an EIP-1559 feature).
* @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an
* non-EIP-1559-specific API.
* @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain
Expand All @@ -36,6 +38,7 @@ export default async function determineGasFeeSuggestions({
isLegacyGasAPICompatible,
fetchGasEstimates,
fetchGasEstimatesUrl,
fetchGasEstimatesViaEthFeeHistory,
fetchLegacyGasPriceEstimates,
fetchLegacyGasPriceEstimatesUrl,
fetchEthGasPriceEstimate,
Expand All @@ -50,6 +53,9 @@ export default async function determineGasFeeSuggestions({
clientId?: string,
) => Promise<GasFeeEstimates>;
fetchGasEstimatesUrl: string;
fetchGasEstimatesViaEthFeeHistory: (
ethQuery: any,
) => Promise<GasFeeEstimates>;
fetchLegacyGasPriceEstimates: (
url: string,
clientId?: string,
Expand All @@ -66,7 +72,12 @@ export default async function determineGasFeeSuggestions({
}): Promise<GasFeeSuggestions> {
try {
if (isEIP1559Compatible) {
const estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);
let estimates: GasFeeEstimates;
try {
estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);
} catch {
estimates = await fetchGasEstimatesViaEthFeeHistory(ethQuery);
}
const {
suggestedMaxPriorityFeePerGas,
suggestedMaxFeePerGas,
Expand Down

0 comments on commit b4f23df

Please sign in to comment.