Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions src/gas/GasFeeController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
GasFeeController,
GetGasFeeState,
GasFeeStateChange,
LegacyGasPriceEstimate,
} from './GasFeeController';
import { EXTERNAL_GAS_PRICES_API_URL } from './gas-util';

const GAS_FEE_API = 'https://mock-gas-server.herokuapp.com/';

Expand All @@ -28,6 +30,8 @@ function getRestrictedMessenger() {

describe('GasFeeController', () => {
let gasFeeController: GasFeeController;
let getIsMainnet: jest.Mock<boolean>;
let getIsEIP1559Compatible: jest.Mock<Promise<boolean>>;

beforeAll(() => {
nock.disableNetConnect();
Expand All @@ -38,8 +42,12 @@ describe('GasFeeController', () => {
});

beforeEach(() => {
getIsMainnet = jest.fn().mockImplementation(() => false);
getIsEIP1559Compatible = jest
.fn()
.mockImplementation(() => Promise.resolve(true));
nock(GAS_FEE_API)
.get('/')
.get(/.+/u)
.reply(200, {
low: {
minWaitTimeEstimate: 60000,
Expand All @@ -60,14 +68,25 @@ describe('GasFeeController', () => {
suggestedMaxFeePerGas: '50',
},
estimatedBaseFee: '28',
});
})
.persist();

nock(EXTERNAL_GAS_PRICES_API_URL)
.get(/.+/u)
.reply(200, {
SafeGasPrice: '22',
ProposeGasPrice: '25',
FastGasPrice: '30',
})
.persist();

gasFeeController = new GasFeeController({
interval: 10000,
messenger: getRestrictedMessenger(),
getProvider: () => stub(),
onNetworkStateChange: () => stub(),
getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true), // change this for networkController.state.properties.isEIP1559Compatible ???
getIsMainnet,
getCurrentNetworkEIP1559Compatibility: getIsEIP1559Compatible, // change this for networkController.state.properties.isEIP1559Compatible ???
});
});

Expand All @@ -94,12 +113,28 @@ describe('GasFeeController', () => {
);
});

it('should _fetchGasFeeEstimateData', async () => {
expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({});
const estimates = await gasFeeController._fetchGasFeeEstimateData();
expect(estimates).toHaveProperty('gasFeeEstimates');
expect(gasFeeController.state.gasFeeEstimates).toHaveProperty(
'estimatedBaseFee',
);
describe('when on mainnet before london', () => {
it('should _fetchGasFeeEstimateData', async () => {
getIsMainnet.mockImplementation(() => true);
getIsEIP1559Compatible.mockImplementation(() => Promise.resolve(false));
expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({});
const estimates = await gasFeeController._fetchGasFeeEstimateData();
expect(estimates).toHaveProperty('gasFeeEstimates');
expect(
(gasFeeController.state.gasFeeEstimates as LegacyGasPriceEstimate).high,
).toBe('30');
});
});

describe('when on any network supporting EIP-1559', () => {
it('should _fetchGasFeeEstimateData', async () => {
getIsMainnet.mockImplementation(() => true);
expect(gasFeeController.state.gasFeeEstimates).toStrictEqual({});
const estimates = await gasFeeController._fetchGasFeeEstimateData();
expect(estimates).toHaveProperty('gasFeeEstimates');
expect(gasFeeController.state.gasFeeEstimates).toHaveProperty(
'estimatedBaseFee',
);
});
});
});
103 changes: 77 additions & 26 deletions src/gas/GasFeeController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,64 @@ import type {
} from '../network/NetworkController';
import {
fetchGasEstimates as defaultFetchGasEstimates,
fetchLegacyGasPriceEstimate as defaultFetchLegacyGasPriceEstimate,
fetchEthGasPriceEstimate as defaultFetchEthGasPriceEstimate,
fetchLegacyGasPriceEstimates as defaultFetchLegacyGasPriceEstimates,
calculateTimeEstimate,
} from './gas-util';

export type unknownString = 'unknown';

/**
* Indicates which type of gasEstimate the controller is currently returning.
* This is useful as a way of asserting that the shape of gasEstimates matches
* expectations. NONE is a special case indicating that no previous gasEstimate
* has been fetched.
*/
export const GAS_ESTIMATE_TYPES = {
FEE_MARKET: 'fee-market' as const,
LEGACY: 'legacy' as const,
ETH_GASPRICE: 'eth_gasPrice' as const,
NONE: 'none' as const,
};

export type GasEstimateType = typeof GAS_ESTIMATE_TYPES[keyof typeof GAS_ESTIMATE_TYPES];

export interface EstimatedGasFeeTimeBounds {
lowerTimeBound: number | null;
upperTimeBound: number | unknownString;
}

/**
* @type LegacyGasPriceEstimate
* @type EthGasPriceEstimate
*
* A single gas price estimate for networks and accounts that don't support EIP-1559
* This estimate comes from eth_gasPrice but is converted to dec gwei to match other
* return values
*
* @property gasPrice - A GWEI hex number, the result of a call to eth_gasPrice
* @property gasPrice - A GWEI dec string
*/

export interface LegacyGasPriceEstimate {
export interface EthGasPriceEstimate {
gasPrice: string;
}

/**
* @type LegacyGasPriceEstimate
*
* A set of gas price estimates for networks and accounts that don't support EIP-1559
* These estimates include low, medium and high all as strings representing gwei in
* decimal format.
*
* @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion
* @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion
* @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion
*/
export interface LegacyGasPriceEstimate {
high: string;
medium: string;
low: string;
}

/**
* @type Eip1559GasFee
*
Expand All @@ -48,8 +83,8 @@ export interface LegacyGasPriceEstimate {
export interface Eip1559GasFee {
minWaitTimeEstimate: number; // a time duration in milliseconds
maxWaitTimeEstimate: number; // a time duration in milliseconds
suggestedMaxPriorityFeePerGas: string; // a GWEI hex number
suggestedMaxFeePerGas: string; // a GWEI hex number
suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number
suggestedMaxFeePerGas: string; // a GWEI decimal number
}

function isEIP1559GasFee(object: any): object is Eip1559GasFee {
Expand All @@ -70,7 +105,7 @@ function isEIP1559GasFee(object: any): object is Eip1559GasFee {
* @property low - A GasFee for a minimum necessary combination of tip and maxFee
* @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 estimatedNextBlockBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI hex number
* @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number
*/

export interface GasFeeEstimates {
Expand All @@ -95,6 +130,7 @@ function isEIP1559Estimate(object: any): object is GasFeeEstimates {
const metadata = {
gasFeeEstimates: { persist: true, anonymous: false },
estimatedGasFeeTimeBounds: { persist: true, anonymous: false },
gasEstimateType: { persist: true, anonymous: false },
};

/**
Expand All @@ -108,9 +144,11 @@ const metadata = {
export type GasFeeState = {
gasFeeEstimates:
| GasFeeEstimates
| EthGasPriceEstimate
| LegacyGasPriceEstimate
| Record<string, never>;
estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record<string, never>;
gasEstimateType: GasEstimateType;
};

const name = 'GasFeeController';
Expand All @@ -128,6 +166,7 @@ export type GetGasFeeState = {
const defaultState = {
gasFeeEstimates: {},
estimatedGasFeeTimeBounds: {},
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
};

/**
Expand All @@ -142,12 +181,16 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {

private fetchGasEstimates;

private fetchLegacyGasPriceEstimate;
private fetchEthGasPriceEstimate;

private fetchLegacyGasPriceEstimates;

private getCurrentNetworkEIP1559Compatibility;

private getCurrentAccountEIP1559Compatibility;

private getIsMainnet;

private ethQuery: any;

/**
Expand All @@ -159,9 +202,11 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {
messenger,
state,
fetchGasEstimates = defaultFetchGasEstimates,
fetchLegacyGasPriceEstimate = defaultFetchLegacyGasPriceEstimate,
fetchEthGasPriceEstimate = defaultFetchEthGasPriceEstimate,
fetchLegacyGasPriceEstimates = defaultFetchLegacyGasPriceEstimates,
getCurrentNetworkEIP1559Compatibility,
getCurrentAccountEIP1559Compatibility,
getIsMainnet,
getProvider,
onNetworkStateChange,
}: {
Expand All @@ -175,9 +220,11 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {
>;
state?: Partial<GasFeeState>;
fetchGasEstimates?: typeof defaultFetchGasEstimates;
fetchLegacyGasPriceEstimate?: typeof defaultFetchLegacyGasPriceEstimate;
fetchEthGasPriceEstimate?: typeof defaultFetchEthGasPriceEstimate;
fetchLegacyGasPriceEstimates?: typeof defaultFetchLegacyGasPriceEstimates;
getCurrentNetworkEIP1559Compatibility: () => Promise<boolean>;
getCurrentAccountEIP1559Compatibility?: () => boolean;
getIsMainnet: () => boolean;
getProvider: () => NetworkController['provider'];
onNetworkStateChange: (listener: (state: NetworkState) => void) => void;
}) {
Expand All @@ -189,10 +236,12 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {
});
this.intervalDelay = interval;
this.fetchGasEstimates = fetchGasEstimates;
this.fetchLegacyGasPriceEstimate = fetchLegacyGasPriceEstimate;
this.fetchEthGasPriceEstimate = fetchEthGasPriceEstimate;
this.fetchLegacyGasPriceEstimates = fetchLegacyGasPriceEstimates;
this.pollTokens = new Set();
this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility;
this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility;
this.getIsMainnet = getIsMainnet;

const provider = getProvider();
this.ethQuery = new EthQuery(provider);
Expand Down Expand Up @@ -226,18 +275,20 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {
* @returns GasFeeEstimates
*/
async _fetchGasFeeEstimateData(): Promise<GasFeeState | undefined> {
let estimates;
let estimates: GasFeeState['gasFeeEstimates'];
let estimatedGasFeeTimeBounds = {};
let isEIP1559Compatible;
let gasEstimateType: GasEstimateType = GAS_ESTIMATE_TYPES.NONE;
const isMainnet = this.getIsMainnet();
try {
isEIP1559Compatible = await this.getEIP1559Compatibility();
} catch (e) {
console.error(e);
isEIP1559Compatible = false;
}

if (isEIP1559Compatible) {
try {
try {
if (isEIP1559Compatible) {
estimates = await this.fetchGasEstimates();
const {
suggestedMaxPriorityFeePerGas,
Expand All @@ -247,28 +298,28 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {
suggestedMaxPriorityFeePerGas,
suggestedMaxFeePerGas,
);
} catch (error) {
try {
estimates = await this.fetchLegacyGasPriceEstimate(this.ethQuery);
} catch (error2) {
throw new Error(
`Gas fee/price estimation failed. Message: ${error2.message}`,
);
}
gasEstimateType = GAS_ESTIMATE_TYPES.FEE_MARKET;
} else if (isMainnet) {
estimates = await this.fetchLegacyGasPriceEstimates();
gasEstimateType = GAS_ESTIMATE_TYPES.LEGACY;
} else {
throw new Error('Main gas fee/price estimation failed. Use fallback');
}
} else {
} catch {
try {
estimates = await this.fetchLegacyGasPriceEstimate(this.ethQuery);
} catch (error2) {
estimates = await this.fetchEthGasPriceEstimate(this.ethQuery);
gasEstimateType = GAS_ESTIMATE_TYPES.ETH_GASPRICE;
} catch (error) {
throw new Error(
`Gas fee/price estimation failed. Message: ${error2.message}`,
`Gas fee/price estimation failed. Message: ${error.message}`,
);
}
}

const newState: GasFeeState = {
gasFeeEstimates: estimates,
estimatedGasFeeTimeBounds,
gasEstimateType,
};

this.update(() => {
Expand Down
28 changes: 28 additions & 0 deletions src/gas/gas-util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import nock from 'nock';
import {
EXTERNAL_GAS_PRICES_API_URL,
fetchLegacyGasPriceEstimates,
} from './gas-util';

describe('gas utils', () => {
describe('fetchLegacyGasPriceEstimates', () => {
it('should fetch external gasPrices and return high/medium/low', async () => {
const scope = nock(EXTERNAL_GAS_PRICES_API_URL)
.get(/.+/u)
.reply(200, {
SafeGasPrice: '22',
ProposeGasPrice: '25',
FastGasPrice: '30',
})
.persist();
const result = await fetchLegacyGasPriceEstimates();
expect(result).toMatchObject({
high: '30',
medium: '25',
low: '22',
});
scope.done();
nock.cleanAll();
});
});
});
Loading