mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +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