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

Use eth_feeHistory as a fallback for gas estimates #614

Merged
merged 1 commit into from
Dec 2, 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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
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",
Copy link
Contributor Author

@mcmire mcmire Nov 1, 2021

Choose a reason for hiding this comment

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

jest-when lets you make assertions on different invocations of mock functions (e.g. let the return value be Y only when X is given). See the README for more: https://www.npmjs.com/package/jest-when

"@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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
calculateTimeEstimate,
} from './gas-util';
import determineGasFeeCalculations from './determineGasFeeCalculations';
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
114 changes: 114 additions & 0 deletions src/gas/determineGasFeeCalculations.test.ts
Original file line number Diff line number Diff line change
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('determineGasFeeCalculations', () => {
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,113 @@ describe('determineGasFeeCalculations', () => {
});
});

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 gasFeeCalculations = await determineGasFeeCalculations(options);

expect(gasFeeCalculations).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 gasFeeCalculations = await determineGasFeeCalculations(
options,
);

expect(gasFeeCalculations).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 = determineGasFeeCalculations(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 gasFeeCalculations = await determineGasFeeCalculations(
options,
);

expect(gasFeeCalculations).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 = determineGasFeeCalculations(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/determineGasFeeCalculations.ts
Original file line number Diff line number Diff line change
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 determineGasFeeCalculations({
isLegacyGasAPICompatible,
fetchGasEstimates,
fetchGasEstimatesUrl,
fetchGasEstimatesViaEthFeeHistory,
fetchLegacyGasPriceEstimates,
fetchLegacyGasPriceEstimatesUrl,
fetchEthGasPriceEstimate,
Expand All @@ -50,6 +53,9 @@ export default async function determineGasFeeCalculations({
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 determineGasFeeCalculations({
}): Promise<GasFeeCalculations> {
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