mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
add useGasEstimates hook (#11479)
This commit is contained in:
parent
2cb807dc20
commit
f51a8451b8
73
ui/hooks/useGasFeeEstimates.js
Normal file
73
ui/hooks/useGasFeeEstimates.js
Normal file
@ -0,0 +1,73 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
|
||||
import {
|
||||
getEstimatedGasFeeTimeBounds,
|
||||
getGasEstimateType,
|
||||
getGasFeeEstimates,
|
||||
isEIP1559Network,
|
||||
} from '../ducks/metamask/metamask';
|
||||
import {
|
||||
disconnectGasFeeEstimatePoller,
|
||||
getGasFeeEstimatesAndStartPolling,
|
||||
} from '../store/actions';
|
||||
|
||||
/**
|
||||
* @typedef {keyof typeof GAS_ESTIMATE_TYPES} GasEstimateTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} GasEstimates
|
||||
* @property {GasEstimateTypes} gasEstimateType - The type of estimate provided
|
||||
* @property {import(
|
||||
* '@metamask/controllers'
|
||||
* ).GasFeeState['gasFeeEstimates']} gasFeeEstimates - The estimate object
|
||||
* @property {import(
|
||||
* '@metamask/controllers'
|
||||
* ).GasFeeState['estimatedGasFeeTimeBounds']} [estimatedGasFeeTimeBounds] -
|
||||
* estimated time boundaries for fee-market type estimates
|
||||
* @property {boolean} isGasEstimateLoading - indicates whether the gas
|
||||
* estimates are currently loading.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the current gasFeeEstimates from state and begins polling for new
|
||||
* estimates. When this hook is removed from the tree it will signal to the
|
||||
* GasFeeController that it is done requiring new gas estimates. Also checks
|
||||
* the returned gas estimate for validity on the current network.
|
||||
*
|
||||
* @returns {GasFeeEstimates} - GasFeeEstimates object
|
||||
*/
|
||||
export function useGasFeeEstimates() {
|
||||
const supportsEIP1559 = useSelector(isEIP1559Network);
|
||||
const gasEstimateType = useSelector(getGasEstimateType);
|
||||
const gasFeeEstimates = useSelector(getGasFeeEstimates);
|
||||
const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds);
|
||||
useEffect(() => {
|
||||
let pollToken;
|
||||
getGasFeeEstimatesAndStartPolling().then((newPollToken) => {
|
||||
pollToken = newPollToken;
|
||||
});
|
||||
return () => {
|
||||
if (pollToken) {
|
||||
disconnectGasFeeEstimatePoller(pollToken);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// We consider the gas estimate to be loading if the gasEstimateType is
|
||||
// 'NONE' or if the current gasEstimateType does not match the type we expect
|
||||
// for the current network. e.g, a ETH_GASPRICE estimate when on a network
|
||||
// supporting EIP-1559.
|
||||
const isGasEstimatesLoading =
|
||||
gasEstimateType === GAS_ESTIMATE_TYPES.NONE ||
|
||||
(supportsEIP1559 && gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) ||
|
||||
(!supportsEIP1559 && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET);
|
||||
|
||||
return {
|
||||
gasFeeEstimates,
|
||||
gasEstimateType,
|
||||
estimatedGasFeeTimeBounds,
|
||||
isGasEstimatesLoading,
|
||||
};
|
||||
}
|
238
ui/hooks/useGasFeeEstimates.test.js
Normal file
238
ui/hooks/useGasFeeEstimates.test.js
Normal file
@ -0,0 +1,238 @@
|
||||
import { cleanup, renderHook } from '@testing-library/react-hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
|
||||
import createRandomId from '../../shared/modules/random-id';
|
||||
import {
|
||||
getGasEstimateType,
|
||||
getGasFeeEstimates,
|
||||
isEIP1559Network,
|
||||
} from '../ducks/metamask/metamask';
|
||||
import {
|
||||
disconnectGasFeeEstimatePoller,
|
||||
getGasFeeEstimatesAndStartPolling,
|
||||
} from '../store/actions';
|
||||
import { useGasFeeEstimates } from './useGasFeeEstimates';
|
||||
|
||||
jest.mock('../store/actions', () => ({
|
||||
disconnectGasFeeEstimatePoller: jest.fn(),
|
||||
getGasFeeEstimatesAndStartPolling: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const actual = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const DEFAULT_OPTS = {
|
||||
isEIP1559Network: false,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
|
||||
gasFeeEstimates: {
|
||||
low: '10',
|
||||
medium: '20',
|
||||
high: '30',
|
||||
},
|
||||
};
|
||||
|
||||
const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => {
|
||||
if (selector === isEIP1559Network) {
|
||||
return opts.isEIP1559Network ?? DEFAULT_OPTS.isEIP1559Network;
|
||||
}
|
||||
if (selector === getGasEstimateType) {
|
||||
return opts.gasEstimateType ?? DEFAULT_OPTS.gasEstimateType;
|
||||
}
|
||||
if (selector === getGasFeeEstimates) {
|
||||
return opts.gasFeeEstimates ?? DEFAULT_OPTS.gasFeeEstimates;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
describe('useGasFeeEstimates', () => {
|
||||
let tokens = [];
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
tokens = [];
|
||||
getGasFeeEstimatesAndStartPolling.mockImplementation(() => {
|
||||
const token = createRandomId();
|
||||
tokens.push(token);
|
||||
return Promise.resolve(token);
|
||||
});
|
||||
disconnectGasFeeEstimatePoller.mockImplementation((token) => {
|
||||
tokens = tokens.filter((tkn) => tkn !== token);
|
||||
});
|
||||
useSelector.mockImplementation(generateUseSelectorRouter());
|
||||
});
|
||||
|
||||
it('registers with the controller', () => {
|
||||
renderHook(() => useGasFeeEstimates());
|
||||
expect(tokens).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('clears token with the controller on unmount', async () => {
|
||||
renderHook(() => useGasFeeEstimates());
|
||||
expect(tokens).toHaveLength(1);
|
||||
const expectedToken = tokens[0];
|
||||
await cleanup();
|
||||
expect(getGasFeeEstimatesAndStartPolling).toHaveBeenCalledTimes(1);
|
||||
expect(disconnectGasFeeEstimatePoller).toHaveBeenCalledWith(expectedToken);
|
||||
expect(tokens).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('works with LEGACY gas prices', () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useGasFeeEstimates());
|
||||
expect(current).toMatchObject({
|
||||
gasFeeEstimates: DEFAULT_OPTS.gasFeeEstimates,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
|
||||
estimatedGasFeeTimeBounds: undefined,
|
||||
isGasEstimatesLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with ETH_GASPRICE gas prices', () => {
|
||||
const gasFeeEstimates = { gasPrice: '10' };
|
||||
useSelector.mockImplementation(
|
||||
generateUseSelectorRouter({
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
|
||||
gasFeeEstimates,
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useGasFeeEstimates());
|
||||
expect(current).toMatchObject({
|
||||
gasFeeEstimates,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
|
||||
estimatedGasFeeTimeBounds: undefined,
|
||||
isGasEstimatesLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with FEE_MARKET gas prices', () => {
|
||||
const gasFeeEstimates = {
|
||||
low: {
|
||||
minWaitTimeEstimate: 180000,
|
||||
maxWaitTimeEstimate: 300000,
|
||||
suggestedMaxPriorityFeePerGas: '3',
|
||||
suggestedMaxFeePerGas: '53',
|
||||
},
|
||||
medium: {
|
||||
minWaitTimeEstimate: 15000,
|
||||
maxWaitTimeEstimate: 60000,
|
||||
suggestedMaxPriorityFeePerGas: '7',
|
||||
suggestedMaxFeePerGas: '70',
|
||||
},
|
||||
high: {
|
||||
minWaitTimeEstimate: 0,
|
||||
maxWaitTimeEstimate: 15000,
|
||||
suggestedMaxPriorityFeePerGas: '10',
|
||||
suggestedMaxFeePerGas: '100',
|
||||
},
|
||||
estimatedBaseFee: '50',
|
||||
};
|
||||
useSelector.mockImplementation(
|
||||
generateUseSelectorRouter({
|
||||
isEIP1559Network: true,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
|
||||
gasFeeEstimates,
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useGasFeeEstimates());
|
||||
expect(current).toMatchObject({
|
||||
gasFeeEstimates,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
|
||||
estimatedGasFeeTimeBounds: undefined,
|
||||
isGasEstimatesLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('indicates that gas estimates are loading when gasEstimateType is NONE', () => {
|
||||
useSelector.mockImplementation(
|
||||
generateUseSelectorRouter({
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
|
||||
gasFeeEstimates: {},
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useGasFeeEstimates());
|
||||
expect(current).toMatchObject({
|
||||
gasFeeEstimates: {},
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
|
||||
estimatedGasFeeTimeBounds: undefined,
|
||||
isGasEstimatesLoading: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('indicates that gas estimates are loading when gasEstimateType is not FEE_MARKET but network supports EIP-1559', () => {
|
||||
useSelector.mockImplementation(
|
||||
generateUseSelectorRouter({
|
||||
isEIP1559Network: true,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
|
||||
gasFeeEstimates: {
|
||||
gasPrice: '10',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useGasFeeEstimates());
|
||||
expect(current).toMatchObject({
|
||||
gasFeeEstimates: { gasPrice: '10' },
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
|
||||
estimatedGasFeeTimeBounds: undefined,
|
||||
isGasEstimatesLoading: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('indicates that gas estimates are loading when gasEstimateType is FEE_MARKET but network does not support EIP-1559', () => {
|
||||
const gasFeeEstimates = {
|
||||
low: {
|
||||
minWaitTimeEstimate: 180000,
|
||||
maxWaitTimeEstimate: 300000,
|
||||
suggestedMaxPriorityFeePerGas: '3',
|
||||
suggestedMaxFeePerGas: '53',
|
||||
},
|
||||
medium: {
|
||||
minWaitTimeEstimate: 15000,
|
||||
maxWaitTimeEstimate: 60000,
|
||||
suggestedMaxPriorityFeePerGas: '7',
|
||||
suggestedMaxFeePerGas: '70',
|
||||
},
|
||||
high: {
|
||||
minWaitTimeEstimate: 0,
|
||||
maxWaitTimeEstimate: 15000,
|
||||
suggestedMaxPriorityFeePerGas: '10',
|
||||
suggestedMaxFeePerGas: '100',
|
||||
},
|
||||
estimatedBaseFee: '50',
|
||||
};
|
||||
useSelector.mockImplementation(
|
||||
generateUseSelectorRouter({
|
||||
isEIP1559Network: false,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
|
||||
gasFeeEstimates,
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useGasFeeEstimates());
|
||||
expect(current).toMatchObject({
|
||||
gasFeeEstimates,
|
||||
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
|
||||
estimatedGasFeeTimeBounds: undefined,
|
||||
isGasEstimatesLoading: true,
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user