1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

rely upon gas fee controller for gas price estimates (#11511)

This commit is contained in:
Brad Decker 2021-07-16 11:06:32 -05:00 committed by GitHub
parent 3fada25dfc
commit dc25a24de3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 525 additions and 726 deletions

View File

@ -162,6 +162,9 @@ export default class NetworkController extends EventEmitter {
*/
async getEIP1559Compatibility() {
const { EIPS } = this.networkDetails.getState();
if (process.env.SHOW_EIP_1559_UI === false) {
return false;
}
if (EIPS[1559] !== undefined) {
return EIPS[1559];
}

View File

@ -196,11 +196,17 @@ export default class MetamaskController extends EventEmitter {
getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind(
this,
),
getCurrentNetworkLegacyGasAPICompatibility: () =>
this.networkController.getCurrentChainId() === MAINNET_CHAIN_ID,
getChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
legacyAPIEndpoint: `https://gas-api.metaswap.codefi.network/networks/<chain_id>/gasPrices`,
EIP1559APIEndpoint: `https://gas-api.metaswap.codefi.network/networks/<chain_id>/suggestedGasFees`,
getCurrentNetworkLegacyGasAPICompatibility: () => {
const chainId = this.networkController.getCurrentChainId();
return process.env.IN_TEST || chainId === MAINNET_CHAIN_ID;
},
getChainId: () => {
return process.env.IN_TEST
? MAINNET_CHAIN_ID
: this.networkController.getCurrentChainId();
},
});
this.appStateController = new AppStateController({

View File

@ -1,22 +1,23 @@
import React from 'react';
import sinon from 'sinon';
import { shallowWithContext } from '../../../../../test/lib/render-helpers';
import { getGasFeeEstimatesAndStartPolling } from '../../../../store/actions';
import PageContainer from '../../../ui/page-container';
import { Tab } from '../../../ui/tabs';
import GasModalPageContainer from './gas-modal-page-container.component';
const mockBasicGasEstimates = {
average: '20',
};
jest.mock('../../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
}));
const propsMethodSpies = {
cancelAndClose: sinon.spy(),
onSubmit: sinon.spy(),
fetchBasicGasEstimates: sinon
.stub()
.returns(Promise.resolve(mockBasicGasEstimates)),
};
const mockGasPriceButtonGroupProps = {
@ -67,7 +68,6 @@ describe('GasModalPageContainer Component', () => {
<GasModalPageContainer
cancelAndClose={propsMethodSpies.cancelAndClose}
onSubmit={propsMethodSpies.onSubmit}
fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
updateCustomGasPrice={() => 'mockupdateCustomGasPrice'}
updateCustomGasLimit={() => 'mockupdateCustomGasLimit'}
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
@ -83,18 +83,15 @@ describe('GasModalPageContainer Component', () => {
afterEach(() => {
propsMethodSpies.cancelAndClose.resetHistory();
jest.clearAllMocks();
});
describe('componentDidMount', () => {
it('should call props.fetchBasicGasEstimates', () => {
propsMethodSpies.fetchBasicGasEstimates.resetHistory();
expect(propsMethodSpies.fetchBasicGasEstimates.callCount).toStrictEqual(
0,
);
it('should call getGasFeeEstimatesAndStartPolling', () => {
jest.clearAllMocks();
expect(getGasFeeEstimatesAndStartPolling).not.toHaveBeenCalled();
wrapper.instance().componentDidMount();
expect(propsMethodSpies.fetchBasicGasEstimates.callCount).toStrictEqual(
1,
);
expect(getGasFeeEstimatesAndStartPolling).toHaveBeenCalled();
});
});
@ -120,20 +117,18 @@ describe('GasModalPageContainer Component', () => {
});
it('should pass the correct renderTabs property to PageContainer', () => {
sinon.stub(GP, 'renderTabs').returns('mockTabs');
jest
.spyOn(GasModalPageContainer.prototype, 'renderTabs')
.mockImplementation(() => 'mockTabs');
const renderTabsWrapperTester = shallowWithContext(
<GasModalPageContainer
fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
customPriceIsExcessive={false}
/>,
<GasModalPageContainer customPriceIsExcessive={false} />,
{ context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } },
);
const { tabsComponent } = renderTabsWrapperTester
.find(PageContainer)
.props();
expect(tabsComponent).toStrictEqual('mockTabs');
GasModalPageContainer.prototype.renderTabs.restore();
GasModalPageContainer.prototype.renderTabs.mockClear();
});
});
@ -195,7 +190,6 @@ describe('GasModalPageContainer Component', () => {
<GasModalPageContainer
cancelAndClose={propsMethodSpies.cancelAndClose}
onSubmit={propsMethodSpies.onSubmit}
fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
updateCustomGasPrice={() => 'mockupdateCustomGasPrice'}
updateCustomGasLimit={() => 'mockupdateCustomGasLimit'}
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}

View File

@ -2,6 +2,10 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PageContainer from '../../../ui/page-container';
import { Tabs, Tab } from '../../../ui/tabs';
import {
disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling,
} from '../../../../store/actions';
import AdvancedTabContent from './advanced-tab-content';
import BasicTabContent from './basic-tab-content';
@ -17,7 +21,6 @@ export default class GasModalPageContainer extends Component {
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
insufficientBalance: PropTypes.bool,
fetchBasicGasEstimates: PropTypes.func,
gasPriceButtonGroupProps: PropTypes.object,
infoRowProps: PropTypes.shape({
originalTotalFiat: PropTypes.string,
@ -38,8 +41,29 @@ export default class GasModalPageContainer extends Component {
customPriceIsExcessive: PropTypes.bool.isRequired,
};
constructor(props) {
super(props);
this.state = {
pollingToken: undefined,
};
}
componentDidMount() {
this.props.fetchBasicGasEstimates();
this._isMounted = true;
getGasFeeEstimatesAndStartPolling().then((pollingToken) => {
if (this._isMounted) {
this.setState({ pollingToken });
} else {
disconnectGasFeeEstimatePoller(pollingToken);
}
});
}
componentWillUnmount() {
this._isMounted = false;
if (this.state.pollingToken) {
disconnectGasFeeEstimatePoller(this.state.pollingToken);
}
}
renderBasicTabContent(gasPriceButtonGroupProps) {

View File

@ -10,7 +10,6 @@ import {
setCustomGasPrice,
setCustomGasLimit,
resetCustomData,
fetchBasicGasEstimates,
} from '../../../../ducks/gas/gas.duck';
import {
getSendMaxModeState,
@ -225,7 +224,6 @@ const mapDispatchToProps = (dispatch) => {
return dispatch(createSpeedUpTransaction(txId, customGasSettings));
},
hideSidebar: () => dispatch(hideSidebar()),
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
};
};

View File

@ -4,11 +4,6 @@
// untangling is having the constants separate.
// Actions
export const BASIC_GAS_ESTIMATE_STATUS =
'metamask/gas/BASIC_GAS_ESTIMATE_STATUS';
export const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA';
export const SET_BASIC_GAS_ESTIMATE_DATA =
'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA';
export const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
export const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE';
export const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE';

View File

@ -1,36 +1,14 @@
import nock from 'nock';
import sinon from 'sinon';
import BN from 'bn.js';
import GasReducer, {
setBasicEstimateStatus,
setBasicGasEstimateData,
setCustomGasPrice,
setCustomGasLimit,
fetchBasicGasEstimates,
} from './gas.duck';
import GasReducer, { setCustomGasPrice, setCustomGasLimit } from './gas.duck';
import {
BASIC_GAS_ESTIMATE_STATUS,
SET_BASIC_GAS_ESTIMATE_DATA,
SET_CUSTOM_GAS_PRICE,
SET_CUSTOM_GAS_LIMIT,
SET_ESTIMATE_SOURCE,
} from './gas-action-constants';
jest.mock('../../helpers/utils/storage-helpers.js', () => ({
getStorageItem: jest.fn(),
setStorageItem: jest.fn(),
}));
describe('Gas Duck', () => {
let tempDateNow;
const mockGasPriceApiResponse = {
SafeGasPrice: 10,
ProposeGasPrice: 20,
FastGasPrice: 30,
};
beforeEach(() => {
tempDateNow = global.Date.now;
@ -51,22 +29,6 @@ describe('Gas Duck', () => {
price: null,
limit: null,
},
basicEstimates: {
average: null,
fast: null,
safeLow: null,
},
basicEstimateStatus: 'LOADING',
estimateSource: '',
};
const providerState = {
chainId: '0x1',
nickname: '',
rpcPrefs: {},
rpcUrl: '',
ticker: 'ETH',
type: 'mainnet',
};
describe('GasReducer()', () => {
@ -83,27 +45,6 @@ describe('Gas Duck', () => {
).toStrictEqual(mockState);
});
it('should set basicEstimateStatus to LOADING when receiving a BASIC_GAS_ESTIMATE_STATUS action with value LOADING', () => {
expect(
GasReducer(mockState, {
type: BASIC_GAS_ESTIMATE_STATUS,
value: 'LOADING',
}),
).toStrictEqual({ basicEstimateStatus: 'LOADING', ...mockState });
});
it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => {
expect(
GasReducer(mockState, {
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: { someProp: 'someData123' },
}),
).toStrictEqual({
basicEstimates: { someProp: 'someData123' },
...mockState,
});
});
it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => {
expect(
GasReducer(mockState, {
@ -123,100 +64,6 @@ describe('Gas Duck', () => {
});
});
it('should set estimateSource to Metaswaps when receiving a SET_ESTIMATE_SOURCE action with value Metaswaps', () => {
expect(
GasReducer(mockState, { type: SET_ESTIMATE_SOURCE, value: 'Metaswaps' }),
).toStrictEqual({ estimateSource: 'Metaswaps', ...mockState });
});
describe('basicEstimateStatus', () => {
it('should create the correct action', () => {
expect(setBasicEstimateStatus('LOADING')).toStrictEqual({
type: BASIC_GAS_ESTIMATE_STATUS,
value: 'LOADING',
});
});
});
describe('fetchBasicGasEstimates', () => {
it('should call fetch with the expected params', async () => {
const mockDistpatch = sinon.spy();
const windowFetchSpy = sinon.spy(window, 'fetch');
nock('https://api.metaswap.codefi.network')
.get('/gasPrices')
.reply(200, mockGasPriceApiResponse);
await fetchBasicGasEstimates()(mockDistpatch, () => ({
gas: { ...initState },
metamask: { provider: { ...providerState } },
}));
expect(mockDistpatch.getCall(0).args).toStrictEqual([
{ type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'LOADING' },
]);
expect(
windowFetchSpy
.getCall(0)
.args[0].startsWith('https://api.metaswap.codefi.network/gasPrices'),
).toStrictEqual(true);
expect(mockDistpatch.getCall(2).args).toStrictEqual([
{ type: 'metamask/gas/SET_ESTIMATE_SOURCE', value: 'MetaSwaps' },
]);
expect(mockDistpatch.getCall(4).args).toStrictEqual([
{ type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'READY' },
]);
});
it('should call fetch with the expected params for test network', async () => {
global.eth = { gasPrice: sinon.fake.returns(new BN(48199313, 10)) };
const mockDistpatch = sinon.spy();
const providerStateForTestNetwork = {
chainId: '0x5',
nickname: '',
rpcPrefs: {},
rpcUrl: '',
ticker: 'ETH',
type: 'goerli',
};
await fetchBasicGasEstimates()(mockDistpatch, () => ({
gas: { ...initState, basicPriceAEstimatesLastRetrieved: 1000000 },
metamask: { provider: { ...providerStateForTestNetwork } },
}));
expect(mockDistpatch.getCall(0).args).toStrictEqual([
{ type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'LOADING' },
]);
expect(mockDistpatch.getCall(1).args).toStrictEqual([
{ type: 'metamask/gas/SET_ESTIMATE_SOURCE', value: 'eth_gasprice' },
]);
expect(mockDistpatch.getCall(2).args).toStrictEqual([
{
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: {
average: 0.0482,
},
},
]);
expect(mockDistpatch.getCall(3).args).toStrictEqual([
{ type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'READY' },
]);
});
});
describe('setBasicGasEstimateData', () => {
it('should create the correct action', () => {
expect(setBasicGasEstimateData('mockBasicEstimatData')).toStrictEqual({
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: 'mockBasicEstimatData',
});
});
});
describe('setCustomGasPrice', () => {
it('should create the correct action', () => {
expect(setCustomGasPrice('mockCustomGasPrice')).toStrictEqual({

View File

@ -1,62 +1,20 @@
import { cloneDeep } from 'lodash';
import BigNumber from 'bignumber.js';
import {
getStorageItem,
setStorageItem,
} from '../../helpers/utils/storage-helpers';
import {
decGWEIToHexWEI,
getValueFromWeiHex,
} from '../../helpers/utils/conversions.util';
import { getIsMainnet, getCurrentChainId } from '../../selectors';
import fetchWithCache from '../../helpers/utils/fetch-with-cache';
import {
BASIC_GAS_ESTIMATE_STATUS,
RESET_CUSTOM_DATA,
SET_BASIC_GAS_ESTIMATE_DATA,
SET_CUSTOM_GAS_LIMIT,
SET_CUSTOM_GAS_PRICE,
SET_ESTIMATE_SOURCE,
} from './gas-action-constants';
export const BASIC_ESTIMATE_STATES = {
LOADING: 'LOADING',
FAILED: 'FAILED',
READY: 'READY',
};
export const GAS_SOURCE = {
METASWAPS: 'MetaSwaps',
ETHGASPRICE: 'eth_gasprice',
};
const initState = {
customData: {
price: null,
limit: null,
},
basicEstimates: {
safeLow: null,
average: null,
fast: null,
},
basicEstimateStatus: BASIC_ESTIMATE_STATES.LOADING,
estimateSource: '',
};
// Reducer
export default function reducer(state = initState, action) {
switch (action.type) {
case BASIC_GAS_ESTIMATE_STATUS:
return {
...state,
basicEstimateStatus: action.value,
};
case SET_BASIC_GAS_ESTIMATE_DATA:
return {
...state,
basicEstimates: action.value,
};
case SET_CUSTOM_GAS_PRICE:
return {
...state,
@ -78,138 +36,11 @@ export default function reducer(state = initState, action) {
...state,
customData: cloneDeep(initState.customData),
};
case SET_ESTIMATE_SOURCE:
return {
...state,
estimateSource: action.value,
};
default:
return state;
}
}
// Action Creators
export function setBasicEstimateStatus(status) {
return {
type: BASIC_GAS_ESTIMATE_STATUS,
value: status,
};
}
async function basicGasPriceQuery() {
const url = `https://api.metaswap.codefi.network/gasPrices`;
return await fetchWithCache(
url,
{
referrer: url,
referrerPolicy: 'no-referrer-when-downgrade',
method: 'GET',
mode: 'cors',
},
{ cacheRefreshTime: 75000 },
);
}
export function fetchBasicGasEstimates() {
return async (dispatch, getState) => {
const isMainnet = getIsMainnet(getState());
dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.LOADING));
let basicEstimates;
try {
dispatch(setEstimateSource(GAS_SOURCE.ETHGASPRICE));
if (isMainnet || process.env.IN_TEST) {
try {
basicEstimates = await fetchExternalBasicGasEstimates();
dispatch(setEstimateSource(GAS_SOURCE.METASWAPS));
} catch (error) {
basicEstimates = await fetchEthGasPriceEstimates(getState());
}
} else {
basicEstimates = await fetchEthGasPriceEstimates(getState());
}
dispatch(setBasicGasEstimateData(basicEstimates));
dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.READY));
} catch (error) {
dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.FAILED));
}
return basicEstimates;
};
}
async function fetchExternalBasicGasEstimates() {
const {
SafeGasPrice,
ProposeGasPrice,
FastGasPrice,
} = await basicGasPriceQuery();
const [safeLow, average, fast] = [
SafeGasPrice,
ProposeGasPrice,
FastGasPrice,
].map((price) => new BigNumber(price, 10).toNumber());
const basicEstimates = {
safeLow,
average,
fast,
};
return basicEstimates;
}
async function fetchEthGasPriceEstimates(state) {
const chainId = getCurrentChainId(state);
const [cachedTimeLastRetrieved, cachedBasicEstimates] = await Promise.all([
getStorageItem(`${chainId}_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED`),
getStorageItem(`${chainId}_BASIC_PRICE_ESTIMATES`),
]);
const timeLastRetrieved = cachedTimeLastRetrieved || 0;
if (cachedBasicEstimates && Date.now() - timeLastRetrieved < 75000) {
return cachedBasicEstimates;
}
const gasPrice = await global.eth.gasPrice();
const averageGasPriceInDecGWEI = getValueFromWeiHex({
value: gasPrice.toString(16),
numberOfDecimals: 4,
toDenomination: 'GWEI',
});
const basicEstimates = {
average: Number(averageGasPriceInDecGWEI),
};
const timeRetrieved = Date.now();
await Promise.all([
setStorageItem(`${chainId}_BASIC_PRICE_ESTIMATES`, basicEstimates),
setStorageItem(
`${chainId}_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED`,
timeRetrieved,
),
]);
return basicEstimates;
}
export function setCustomGasPriceForRetry(newPrice) {
return async (dispatch) => {
if (newPrice === '0x0') {
const { fast } = await fetchExternalBasicGasEstimates();
dispatch(setCustomGasPrice(decGWEIToHexWEI(fast)));
} else {
dispatch(setCustomGasPrice(newPrice));
}
};
}
export function setBasicGasEstimateData(basicGasEstimateData) {
return {
type: SET_BASIC_GAS_ESTIMATE_DATA,
value: basicGasEstimateData,
};
}
export function setCustomGasPrice(newPrice) {
return {
type: SET_CUSTOM_GAS_PRICE,
@ -227,10 +58,3 @@ export function setCustomGasLimit(newLimit) {
export function resetCustomData() {
return { type: RESET_CUSTOM_DATA };
}
export function setEstimateSource(estimateSource) {
return {
type: SET_ESTIMATE_SOURCE,
value: estimateSource,
};
}

View File

@ -50,15 +50,7 @@ import {
updateTokenType,
updateTransaction,
} from '../../store/actions';
import {
fetchBasicGasEstimates,
setCustomGasLimit,
BASIC_ESTIMATE_STATES,
} from '../gas/gas.duck';
import {
SET_BASIC_GAS_ESTIMATE_DATA,
BASIC_GAS_ESTIMATE_STATUS,
} from '../gas/gas-action-constants';
import { setCustomGasLimit } from '../gas/gas.duck';
import {
QR_CODE_DETECTED,
SELECTED_ACCOUNT_CHANGED,
@ -77,13 +69,18 @@ import {
isOriginContractAddress,
isValidDomainName,
} from '../../helpers/utils/util';
import { getTokens, getUnapprovedTxs } from '../metamask/metamask';
import {
getGasEstimateType,
getTokens,
getUnapprovedTxs,
} from '../metamask/metamask';
import { resetEnsResolution } from '../ens';
import {
isBurnAddress,
isValidHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import { ETH, GWEI } from '../../helpers/constants/common';
// typedefs
/**
@ -186,8 +183,19 @@ async function estimateGasLimitForSend({
let isSimpleSendOnNonStandardNetwork = false;
// blockGasLimit may be a falsy, but defined, value when we receive it from
// state, so we use logical or to fall back to MIN_GAS_LIMIT_HEX.
const blockGasLimit = options.blockGasLimit || MIN_GAS_LIMIT_HEX;
// state, so we use logical or to fall back to MIN_GAS_LIMIT_HEX. Some
// network implementations check the gas parameter supplied to
// eth_estimateGas for validity. For this reason, we set token sends
// blockGasLimit default to a higher number. Note that the current gasLimit
// on a BLOCK is 15,000,000 and will be 30,000,000 on mainnet after London.
// Meanwhile, MIN_GAS_LIMIT_HEX is 0x5208.
let blockGasLimit = MIN_GAS_LIMIT_HEX;
if (options.blockGasLimit) {
blockGasLimit = options.blockGasLimit;
} else if (sendToken) {
blockGasLimit = GAS_LIMITS.BASE_TOKEN_ESTIMATE;
}
// The parameters below will be sent to our background process to estimate
// how much gas will be used for a transaction. That background process is
// located in tx-gas-utils.js in the transaction controller folder.
@ -342,7 +350,7 @@ export const computeEstimatedGasLimit = createAsyncThunk(
if (send.stage !== SEND_STAGES.EDIT) {
const gasLimit = await estimateGasLimitForSend({
gasPrice: send.gas.gasPrice,
blockGasLimit: metamask.blockGasLimit,
blockGasLimit: metamask.currentBlockGasLimit,
selectedAddress: metamask.selectedAddress,
sendToken: send.asset.details,
to: send.recipient.address?.toLowerCase(),
@ -360,6 +368,29 @@ export const computeEstimatedGasLimit = createAsyncThunk(
},
);
/**
* This method is used to keep the original logic from the gas.duck.js file
* after receiving a gasPrice from eth_gasPrice. First, the returned gasPrice
* was converted to GWEI, then it was converted to a Number, then in the send
* duck (here) we would use getGasPriceInHexWei to get back to hexWei. Now that
* we receive a GWEI estimate from the controller, we still need to do this
* weird conversion to get the proper rounding.
* @param {T} gasPriceEstimate
* @returns
*/
function getRoundedGasPrice(gasPriceEstimate) {
const gasPriceInDecGwei = conversionUtil(gasPriceEstimate, {
numberOfDecimals: 4,
toDenomination: GWEI,
fromNumericBase: 'dec',
toNumericBase: 'dec',
fromCurrency: ETH,
fromDenomination: GWEI,
});
const gasPriceAsNumber = Number(gasPriceInDecGwei);
return getGasPriceInHexWei(gasPriceAsNumber);
}
/**
* Responsible for initializing required state for the send slice.
* This method is dispatched from the send page in the componentDidMount
@ -395,48 +426,31 @@ export const initializeSendState = createAsyncThunk(
// Default gasPrice to 1 gwei if all estimation fails
let gasPrice = '0x1';
let basicEstimateStatus = BASIC_ESTIMATE_STATES.LOADING;
let gasEstimatePollToken = null;
if (Boolean(process.env.SHOW_EIP_1559_UI) === false) {
// Initiate gas slices work to fetch gasPrice estimates. We need to get the
// new state after this is set to determine if initialization can proceed.
await thunkApi.dispatch(fetchBasicGasEstimates());
const {
gas: { basicEstimates, basicEstimateStatus: apiBasicEstimateStatus },
} = thunkApi.getState();
// Instruct the background process that polling for gas prices should begin
gasEstimatePollToken = await getGasFeeEstimatesAndStartPolling();
const {
metamask: { gasFeeEstimates, gasEstimateType },
} = thunkApi.getState();
basicEstimateStatus = apiBasicEstimateStatus;
if (basicEstimateStatus === BASIC_ESTIMATE_STATES.READY) {
gasPrice = getGasPriceInHexWei(basicEstimates.average);
}
} else {
// Instruct the background process that polling for gas prices should begin
gasEstimatePollToken = await getGasFeeEstimatesAndStartPolling();
const {
metamask: { gasFeeEstimates, gasEstimateType },
} = thunkApi.getState();
if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
gasPrice = getGasPriceInHexWei(gasFeeEstimates.medium);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
gasPrice = getGasPriceInHexWei(gasFeeEstimates.gasPrice);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
gasPrice = getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
);
}
basicEstimateStatus = BASIC_ESTIMATE_STATES.READY;
if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
gasPrice = getGasPriceInHexWei(gasFeeEstimates.medium);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
gasPrice = getRoundedGasPrice(gasFeeEstimates.gasPrice);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
gasPrice = getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
);
}
// Set a basic gasLimit in the event that other estimation fails
let gasLimit =
asset.type === ASSET_TYPES.TOKEN
? GAS_LIMITS.BASE_TOKEN_ESTIMATE
: GAS_LIMITS.SIMPLE;
if (
basicEstimateStatus === BASIC_ESTIMATE_STATES.READY &&
gasEstimateType !== GAS_ESTIMATE_TYPES.NONE &&
stage !== SEND_STAGES.EDIT &&
recipient.address
) {
@ -444,7 +458,7 @@ export const initializeSendState = createAsyncThunk(
// required gas. If this value isn't nullish, set it as the new gasLimit
const estimatedGasLimit = await estimateGasLimitForSend({
gasPrice,
blockGasLimit: metamask.blockGasLimit,
blockGasLimit: metamask.currentBlockGasLimit,
selectedAddress: fromAddress,
sendToken: asset.details,
to: recipient.address.toLowerCase(),
@ -508,9 +522,12 @@ export const initialState = {
isCustomGasSet: false,
// maximum gas needed for tx
gasLimit: '0x0',
// price in gwei to pay per gas
// price in wei to pay per gas
gasPrice: '0x0',
// maximum total price in gwei to pay
// expected price in wei necessary to pay per gas used for a transaction
// to be included in a reasonable timeframe. Comes from GasFeeController.
gasPriceEstimate: '0x0',
// maximum total price in wei to pay
gasTotal: '0x0',
// minimum supported gasLimit
minimumGasLimit: GAS_LIMITS.SIMPLE,
@ -761,7 +778,9 @@ const slice = createSlice({
// We keep a copy of txParams in state that could be submitted to the
// network if the form state is valid.
if (state.status === SEND_STATUSES.VALID) {
state.draftTransaction.txParams.from = state.account.address;
if (state.stage !== SEND_STAGES.EDIT) {
state.draftTransaction.txParams.from = state.account.address;
}
switch (state.asset.type) {
case ASSET_TYPES.TOKEN:
// When sending a token the to address is the contract address of
@ -926,7 +945,7 @@ const slice = createSlice({
break;
case state.asset.type === ASSET_TYPES.TOKEN &&
state.asset.details.isERC721 === true:
state.state = SEND_STATUSES.INVALID;
state.status = SEND_STATUSES.INVALID;
break;
default:
state.status = SEND_STATUSES.VALID;
@ -1063,58 +1082,36 @@ const slice = createSlice({
});
}
})
.addCase(SET_BASIC_GAS_ESTIMATE_DATA, (state, action) => {
// When we receive a new gasPrice via the gas duck we need to update
// the gasPrice in our slice. We call into the caseReducer
// updateGasPrice to also tap into the appropriate follow up checks
// and gasTotal calculation.
if (Boolean(process.env.SHOW_EIP_1559_UI) === false) {
slice.caseReducers.updateGasPrice(state, {
payload: getGasPriceInHexWei(action.value.average),
});
}
})
.addCase(BASIC_GAS_ESTIMATE_STATUS, (state, action) => {
// When we fetch gas prices we should temporarily set the form invalid
// Once the price updates we get that value in the
// SET_BASIC_GAS_ESTIMATE_DATA extraReducer above. Finally as long as
// the state is 'READY' we will revalidate the form.
switch (action.value) {
case BASIC_ESTIMATE_STATES.FAILED:
state.status = SEND_STATUSES.INVALID;
state.gas.isGasEstimateLoading = true;
break;
case BASIC_ESTIMATE_STATES.LOADING:
state.status = SEND_STATUSES.INVALID;
state.gas.isGasEstimateLoading = true;
break;
case BASIC_ESTIMATE_STATES.READY:
default:
state.gas.isGasEstimateLoading = false;
slice.caseReducers.validateSendState(state);
}
})
.addCase(GAS_FEE_ESTIMATES_UPDATED, (state, action) => {
// When the gasFeeController updates its gas fee estimates we need to
// update and validate state based on those new values
if (process.env.SHOW_EIP_1559_UI) {
const { gasFeeEstimates, gasEstimateType } = action.payload;
let payload = null;
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
payload = getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
payload = getGasPriceInHexWei(gasFeeEstimates.medium);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
payload = getGasPriceInHexWei(gasFeeEstimates.gasPrice);
}
if (payload) {
slice.caseReducers.updateGasPrice(state, {
payload,
});
}
const { gasFeeEstimates, gasEstimateType } = action.payload;
let payload = null;
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
payload = getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
payload = getGasPriceInHexWei(gasFeeEstimates.medium);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
payload = getRoundedGasPrice(gasFeeEstimates.gasPrice);
}
// If a new gasPrice can be derived, and either the gasPriceEstimate
// was '0x0' or the gasPrice selected matches the previous estimate,
// update the gasPrice. This will ensure that we only update the
// gasPrice if the user is using our previous estimated value.
if (
payload &&
(state.gas.gasPriceEstimate === '0x0' ||
state.gas.gasPrice === state.gas.gasPriceEstimate)
) {
slice.caseReducers.updateGasPrice(state, {
payload,
});
}
// Record the latest gasPriceEstimate for future comparisons
state.gas.gasPriceEstimate = payload ?? state.gas.gasPriceEstimate;
});
},
});
@ -1487,6 +1484,7 @@ export function getMinimumGasLimitForSend(state) {
export function getGasInputMode(state) {
const isMainnet = getIsMainnet(state);
const gasEstimateType = getGasEstimateType(state);
const showAdvancedGasFields = getAdvancedInlineGasShown(state);
if (state[name].gas.isCustomGasSet) {
return GAS_INPUT_MODES.CUSTOM;
@ -1494,6 +1492,16 @@ export function getGasInputMode(state) {
if ((!isMainnet && !process.env.IN_TEST) || showAdvancedGasFields) {
return GAS_INPUT_MODES.INLINE;
}
// We get eth_gasPrice estimation if the legacy API fails but we need to
// instruct the UI to render the INLINE inputs in this case, only on
// mainnet or IN_TEST.
if (
(isMainnet || process.env.IN_TEST) &&
gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE
) {
return GAS_INPUT_MODES.INLINE;
}
return GAS_INPUT_MODES.BASIC;
}

View File

@ -10,9 +10,8 @@ import {
KNOWN_RECIPIENT_ADDRESS_WARNING,
NEGATIVE_ETH_ERROR,
} from '../../pages/send/send.constants';
import { BASIC_ESTIMATE_STATES } from '../gas/gas.duck';
import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
import { GAS_LIMITS } from '../../../shared/constants/gas';
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas';
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction';
import sendReducer, {
initialState,
@ -953,6 +952,11 @@ describe('Send Slice', () => {
it('should dispatch async action thunk first with pending, then finally fulfilling from minimal state', async () => {
getState = jest.fn().mockReturnValue({
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
gasFeeEstimates: {},
networkDetails: {
EIPS: {},
},
accounts: {
'0xAddress': {
address: '0xAddress',
@ -970,6 +974,7 @@ describe('Send Slice', () => {
},
},
send: initialState,
gas: {
basicEstimateStatus: 'LOADING',
basicEstimatesStatus: {
@ -983,12 +988,12 @@ describe('Send Slice', () => {
const action = initializeSendState();
await action(dispatchSpy, getState, undefined);
expect(dispatchSpy).toHaveBeenCalledTimes(4);
expect(dispatchSpy).toHaveBeenCalledTimes(3);
expect(dispatchSpy.mock.calls[0][0].type).toStrictEqual(
'send/initializeSendState/pending',
);
expect(dispatchSpy.mock.calls[3][0].type).toStrictEqual(
expect(dispatchSpy.mock.calls[2][0].type).toStrictEqual(
'send/initializeSendState/fulfilled',
);
});
@ -1000,6 +1005,7 @@ describe('Send Slice', () => {
...initialState,
gas: {
gasPrice: '0x0',
gasPriceEstimate: '0x0',
gasLimit: '0x5208',
gasTotal: '0x0',
minimumGasLimit: '0x5208',
@ -1007,9 +1013,12 @@ describe('Send Slice', () => {
};
const action = {
type: 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA',
value: {
average: '1',
type: 'GAS_FEE_ESTIMATES_UPDATED',
payload: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
medium: '1',
},
},
};
@ -1020,40 +1029,6 @@ describe('Send Slice', () => {
expect(result.gas.gasTotal).toStrictEqual('0x1319718a5000');
});
});
describe('BASIC_GAS_ESTIMATE_STATUS', () => {
it('should invalidate the send status when status is LOADING', () => {
const validSendStatusState = {
...initialState,
status: SEND_STATUSES.VALID,
};
const action = {
type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS',
value: BASIC_ESTIMATE_STATES.LOADING,
};
const result = sendReducer(validSendStatusState, action);
expect(result.status).not.toStrictEqual(validSendStatusState.status);
});
it('should invalidate the send status when status is FAILED and use INLINE gas input mode', () => {
const validSendStatusState = {
...initialState,
status: SEND_STATUSES.VALID,
};
const action = {
type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS',
value: BASIC_ESTIMATE_STATES.FAILED,
};
const result = sendReducer(validSendStatusState, action);
expect(result.status).not.toStrictEqual(validSendStatusState.status);
});
});
});
describe('Action Creators', () => {

View File

@ -5,10 +5,7 @@ import { isBalanceSufficient } from '../pages/send/send.utils';
import { getSelectedAccount, getIsMainnet } from '../selectors';
import { getConversionRate } from '../ducks/metamask/metamask';
import {
setCustomGasLimit,
setCustomGasPriceForRetry,
} from '../ducks/gas/gas.duck';
import { setCustomGasLimit, setCustomGasPrice } from '../ducks/gas/gas.duck';
import { GAS_LIMITS } from '../../shared/constants/gas';
import { isLegacyTransaction } from '../../shared/modules/transaction.utils';
import { getMaximumGasTotalInHexWei } from '../../shared/modules/gas.utils';
@ -51,7 +48,7 @@ export function useCancelTransaction(transactionGroup) {
// To support the current process of cancelling or speeding up
// a transaction, we have to inform the custom gas state of the new
// gasPrice/gasLimit to start at.
dispatch(setCustomGasPriceForRetry(customGasSettings.gasPrice));
dispatch(setCustomGasPrice(customGasSettings.gasPrice));
dispatch(setCustomGasLimit(GAS_LIMITS.SIMPLE));
}
const tx = {

View File

@ -44,11 +44,17 @@ export function useGasFeeEstimates() {
const gasFeeEstimates = useSelector(getGasFeeEstimates);
const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds);
useEffect(() => {
let active = true;
let pollToken;
getGasFeeEstimatesAndStartPolling().then((newPollToken) => {
pollToken = newPollToken;
if (active) {
pollToken = newPollToken;
} else {
disconnectGasFeeEstimatePoller(newPollToken);
}
});
return () => {
active = false;
if (pollToken) {
disconnectGasFeeEstimatePoller(pollToken);
}

View File

@ -2,11 +2,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { useCallback, useState } from 'react';
import { showSidebar } from '../store/actions';
import {
fetchBasicGasEstimates,
setCustomGasPriceForRetry,
setCustomGasLimit,
} from '../ducks/gas/gas.duck';
import { setCustomGasLimit, setCustomGasPrice } from '../ducks/gas/gas.duck';
import { getIsMainnet } from '../selectors';
import { isLegacyTransaction } from '../../shared/modules/transaction.utils';
import { useMetricEvent } from './useMetricEvent';
@ -29,6 +25,7 @@ import { useIncrementedGasFees } from './useIncrementedGasFees';
export function useRetryTransaction(transactionGroup) {
const { primaryTransaction } = transactionGroup;
const isMainnet = useSelector(getIsMainnet);
const hideBasic = !(isMainnet || process.env.IN_TEST);
const customGasSettings = useIncrementedGasFees(transactionGroup);
const trackMetricsEvent = useMetricEvent({
@ -51,12 +48,11 @@ export function useRetryTransaction(transactionGroup) {
if (process.env.SHOW_EIP_1559_UI) {
setShowRetryEditGasPopover(true);
} else {
await dispatch(fetchBasicGasEstimates);
if (isLegacyTransaction(primaryTransaction)) {
// To support the current process of cancelling or speeding up
// a transaction, we have to inform the custom gas state of the new
// gasPrice to start at.
dispatch(setCustomGasPriceForRetry(customGasSettings.gasPrice));
dispatch(setCustomGasPrice(customGasSettings.gasPrice));
dispatch(setCustomGasLimit(primaryTransaction.txParams.gas));
}

View File

@ -8,6 +8,10 @@ import * as methodDataHook from './useMethodData';
import * as metricEventHook from './useMetricEvent';
import { useRetryTransaction } from './useRetryTransaction';
jest.mock('./useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
describe('useRetryTransaction', () => {
describe('when transaction meets retry enabled criteria', () => {
let useSelector;

View File

@ -16,8 +16,9 @@ export default class ConfirmSendEther extends Component {
handleEdit({ txData }) {
const { editTransaction, history } = this.props;
editTransaction(txData);
history.push(SEND_ROUTE);
editTransaction(txData).then(() => {
history.push(SEND_ROUTE);
});
}
shouldHideData() {

View File

@ -17,9 +17,9 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
editTransaction: (txData) => {
editTransaction: async (txData) => {
const { id } = txData;
dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString()));
await dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString()));
dispatch(clearConfirmTransaction());
},
};

View File

@ -25,6 +25,10 @@ import {
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
DEFAULT_ROUTE,
} from '../../helpers/constants/routes';
import {
disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling,
} from '../../store/actions';
import ConfTx from './conf-tx';
export default class ConfirmTransaction extends Component {
@ -38,7 +42,6 @@ export default class ConfirmTransaction extends Component {
sendTo: PropTypes.string,
setTransactionToConfirm: PropTypes.func,
clearConfirmTransaction: PropTypes.func,
fetchBasicGasEstimates: PropTypes.func,
mostRecentOverviewPage: PropTypes.string.isRequired,
transaction: PropTypes.object,
getContractMethodData: PropTypes.func,
@ -49,14 +52,19 @@ export default class ConfirmTransaction extends Component {
setDefaultHomeActiveTabName: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this._isMounted = true;
const {
totalUnapprovedCount = 0,
sendTo,
history,
mostRecentOverviewPage,
transaction: { txParams: { data, to } = {} } = {},
fetchBasicGasEstimates,
getContractMethodData,
transactionId,
paramsTransactionId,
@ -64,12 +72,19 @@ export default class ConfirmTransaction extends Component {
isTokenMethodAction,
} = this.props;
getGasFeeEstimatesAndStartPolling().then((pollingToken) => {
if (this._isMounted) {
this.setState({ pollingToken });
} else {
disconnectGasFeeEstimatePoller(pollingToken);
}
});
if (!totalUnapprovedCount && !sendTo) {
history.replace(mostRecentOverviewPage);
return;
}
fetchBasicGasEstimates();
getContractMethodData(data);
if (isTokenMethodAction) {
getTokenParams(to);
@ -80,6 +95,13 @@ export default class ConfirmTransaction extends Component {
}
}
componentWillUnmount() {
this._isMounted = false;
if (this.state.pollingToken) {
disconnectGasFeeEstimatePoller(this.state.pollingToken);
}
}
componentDidUpdate(prevProps) {
const {
setTransactionToConfirm,

View File

@ -6,7 +6,6 @@ import {
clearConfirmTransaction,
} from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { isTokenMethodAction } from '../../helpers/utils/transactions.util';
import { fetchBasicGasEstimates } from '../../ducks/gas/gas.duck';
import {
getContractMethodData,
@ -54,7 +53,6 @@ const mapDispatchToProps = (dispatch) => {
dispatch(setTransactionToConfirm(transactionId));
},
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
getContractMethodData: (data) => dispatch(getContractMethodData(data)),
getTokenParams: (tokenAddress) => dispatch(getTokenParams(tokenAddress)),
setDefaultHomeActiveTabName: (tabName) =>

View File

@ -5,6 +5,7 @@ import thunk from 'redux-thunk';
import { fireEvent } from '@testing-library/react';
import { initialState, SEND_STATUSES } from '../../../../../ducks/send';
import { renderWithProvider } from '../../../../../../test/jest';
import { GAS_ESTIMATE_TYPES } from '../../../../../../shared/constants/gas';
import AmountMaxButton from './amount-max-button';
const middleware = [thunk];
@ -15,8 +16,13 @@ describe('AmountMaxButton Component', () => {
const { getByText } = renderWithProvider(
<AmountMaxButton />,
configureMockStore(middleware)({
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
networkDetails: {
EIPS: {},
},
},
send: initialState,
gas: { basicEstimateStatus: 'LOADING' },
}),
);
expect(getByText('Max')).toBeTruthy();
@ -24,8 +30,13 @@ describe('AmountMaxButton Component', () => {
it('should dispatch action to set mode to MAX', () => {
const store = configureMockStore(middleware)({
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
networkDetails: {
EIPS: {},
},
},
send: { ...initialState, status: SEND_STATUSES.VALID },
gas: { basicEstimateStatus: 'READY' },
});
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
@ -40,12 +51,17 @@ describe('AmountMaxButton Component', () => {
it('should dispatch action to set amount mode to INPUT', () => {
const store = configureMockStore(middleware)({
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
networkDetails: {
EIPS: {},
},
},
send: {
...initialState,
status: SEND_STATUSES.VALID,
amount: { ...initialState.amount, mode: 'MAX' },
},
gas: { basicEstimateStatus: 'READY' },
});
const { getByText } = renderWithProvider(<AmountMaxButton />, store);

View File

@ -8,6 +8,7 @@ import { initialState, SEND_STAGES } from '../../ducks/send';
import { ensInitialState } from '../../ducks/ens';
import { renderWithProvider } from '../../../test/jest';
import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import Send from './send';
const middleware = [thunk];
@ -37,12 +38,19 @@ const baseStore = {
send: initialState,
ENS: ensInitialState,
gas: {
basicEstimateStatus: 'READY',
basicEstimates: { slow: '0x0', average: '0x1', fast: '0x2' },
customData: { limit: null, price: null },
},
history: { mostRecentOverviewPage: 'activity' },
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '0',
medium: '1',
fast: '2',
},
networkDetails: {
EIPS: {},
},
tokens: [],
preferences: {
useNativeCurrencyAsPrimaryCurrency: false,
@ -82,12 +90,6 @@ describe('Send Page', () => {
expect.objectContaining({
type: 'send/initializeSendState/pending',
}),
expect.objectContaining({
type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS',
}),
expect.objectContaining({
type: 'metamask/gas/SET_ESTIMATE_SOURCE',
}),
]),
);
});
@ -105,12 +107,6 @@ describe('Send Page', () => {
expect.objectContaining({
type: 'send/initializeSendState/pending',
}),
expect.objectContaining({
type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS',
}),
expect.objectContaining({
type: 'metamask/gas/SET_ESTIMATE_SOURCE',
}),
expect.objectContaining({
type: 'UI_MODAL_OPEN',
payload: { name: 'QR_SCANNER' },

View File

@ -8,10 +8,17 @@ import { decEthToConvertedCurrency as ethTotalToConvertedCurrency } from '../hel
import { formatETHFee } from '../helpers/utils/formatters';
import { calcGasTotal } from '../pages/send/send.utils';
import { GAS_ESTIMATE_TYPES } from '../helpers/constants/common';
import { getGasPrice } from '../ducks/send';
import { BASIC_ESTIMATE_STATES, GAS_SOURCE } from '../ducks/gas/gas.duck';
import { GAS_LIMITS } from '../../shared/constants/gas';
import {
GAS_ESTIMATE_TYPES as GAS_FEE_CONTROLLER_ESTIMATE_TYPES,
GAS_LIMITS,
} from '../../shared/constants/gas';
import {
getGasEstimateType,
getGasFeeEstimates,
isEIP1559Network,
} from '../ducks/metamask/metamask';
import { GAS_ESTIMATE_TYPES } from '../helpers/constants/common';
import { getCurrentCurrency, getIsMainnet, getShouldShowFiat } from '.';
const NUMBER_OF_DECIMALS_SM_BTNS = 5;
@ -25,13 +32,12 @@ export function getCustomGasPrice(state) {
}
export function getBasicGasEstimateLoadingStatus(state) {
return state.gas.basicEstimateStatus === 'LOADING';
return getIsGasEstimatesFetched(state) === false;
}
export function getAveragePriceEstimateInHexWEI(state) {
const averagePriceEstimate = state.gas.basicEstimates
? state.gas.basicEstimates.average
: '0x0';
const averagePriceEstimate = getAverageEstimate(state);
return getGasPriceInHexWei(averagePriceEstimate);
}
@ -51,23 +57,31 @@ export function getDefaultActiveButtonIndex(
}
export function getSafeLowEstimate(state) {
const {
gas: {
basicEstimates: { safeLow },
},
} = state;
const gasFeeEstimates = getGasFeeEstimates(state);
const gasEstimateType = getGasEstimateType(state);
return safeLow;
return gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.LEGACY
? gasFeeEstimates?.low
: null;
}
export function getAverageEstimate(state) {
const gasFeeEstimates = getGasFeeEstimates(state);
const gasEstimateType = getGasEstimateType(state);
return gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.LEGACY
? gasFeeEstimates?.medium
: null;
}
export function getFastPriceEstimate(state) {
const {
gas: {
basicEstimates: { fast },
},
} = state;
const gasFeeEstimates = getGasFeeEstimates(state);
return fast;
const gasEstimateType = getGasEstimateType(state);
return gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.LEGACY
? gasFeeEstimates?.high
: null;
}
export function isCustomPriceSafe(state) {
@ -97,7 +111,7 @@ export function isCustomPriceSafe(state) {
}
export function isCustomPriceSafeForCustomNetwork(state) {
const estimatedPrice = state.gas.basicEstimates.average;
const estimatedPrice = getAverageEstimate(state);
const customGasPrice = getCustomGasPrice(state);
@ -219,61 +233,56 @@ export function getRenderableGasButtonData(
currentCurrency,
nativeCurrency,
) {
const { safeLow, average, fast } = estimates;
const { low, medium, high } = estimates;
const slowEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInPrimaryCurrency: getRenderableEthFee(
safeLow,
gasLimit,
9,
nativeCurrency,
),
feeInPrimaryCurrency: getRenderableEthFee(low, gasLimit, 9, nativeCurrency),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
safeLow,
low,
gasLimit,
currentCurrency,
conversionRate,
)
: '',
priceInHexWei: getGasPriceInHexWei(safeLow),
priceInHexWei: getGasPriceInHexWei(low),
};
const averageEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInPrimaryCurrency: getRenderableEthFee(
average,
medium,
gasLimit,
9,
nativeCurrency,
),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
average,
medium,
gasLimit,
currentCurrency,
conversionRate,
)
: '',
priceInHexWei: getGasPriceInHexWei(average),
priceInHexWei: getGasPriceInHexWei(medium),
};
const fastEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInPrimaryCurrency: getRenderableEthFee(
fast,
high,
gasLimit,
9,
nativeCurrency,
),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
fast,
high,
gasLimit,
currentCurrency,
conversionRate,
)
: '',
priceInHexWei: getGasPriceInHexWei(fast),
priceInHexWei: getGasPriceInHexWei(high),
};
return {
@ -297,7 +306,7 @@ export function getRenderableBasicEstimateData(state, gasLimit) {
averageEstimateData,
fastEstimateData,
} = getRenderableGasButtonData(
state.gas.basicEstimates,
getGasFeeEstimates(state),
gasLimit,
showFiat,
conversionRate,
@ -308,7 +317,7 @@ export function getRenderableBasicEstimateData(state, gasLimit) {
}
export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
if (getBasicGasEstimateLoadingStatus(state)) {
if (getIsGasEstimatesFetched(state) === false) {
return [];
}
const showFiat = getShouldShowFiat(state);
@ -316,94 +325,88 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
state.send.gas.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE;
const { conversionRate } = state.metamask;
const currentCurrency = getCurrentCurrency(state);
const {
gas: {
basicEstimates: { safeLow, average, fast },
},
} = state;
const gasFeeEstimates = getGasFeeEstimates(state);
return [
{
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
safeLow,
gasFeeEstimates.low,
gasLimit,
currentCurrency,
conversionRate,
)
: '',
feeInPrimaryCurrency: getRenderableEthFee(
safeLow,
gasFeeEstimates.low,
gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS,
),
priceInHexWei: getGasPriceInHexWei(safeLow, true),
priceInHexWei: getGasPriceInHexWei(gasFeeEstimates.low, true),
},
{
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
average,
gasFeeEstimates.medium,
gasLimit,
currentCurrency,
conversionRate,
)
: '',
feeInPrimaryCurrency: getRenderableEthFee(
average,
gasFeeEstimates.medium,
gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS,
),
priceInHexWei: getGasPriceInHexWei(average, true),
priceInHexWei: getGasPriceInHexWei(gasFeeEstimates.medium, true),
},
{
gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(
fast,
gasFeeEstimates.high,
gasLimit,
currentCurrency,
conversionRate,
)
: '',
feeInPrimaryCurrency: getRenderableEthFee(
fast,
gasFeeEstimates.high,
gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS,
),
priceInHexWei: getGasPriceInHexWei(fast, true),
priceInHexWei: getGasPriceInHexWei(gasFeeEstimates.high, true),
},
];
}
export function getIsEthGasPriceFetched(state) {
const gasState = state.gas;
return Boolean(
gasState.estimateSource === GAS_SOURCE.ETHGASPRICE &&
gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY &&
getIsMainnet(state),
const gasEstimateType = getGasEstimateType(state);
return (
gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.ETH_GASPRICE &&
getIsMainnet(state)
);
}
export function getIsCustomNetworkGasPriceFetched(state) {
const gasState = state.gas;
return Boolean(
gasState.estimateSource === GAS_SOURCE.ETHGASPRICE &&
gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY &&
!getIsMainnet(state),
const gasEstimateType = getGasEstimateType(state);
return (
gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.ETH_GASPRICE &&
!getIsMainnet(state)
);
}
export function getNoGasPriceFetched(state) {
const gasState = state.gas;
return Boolean(gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.FAILED);
const gasEstimateType = getGasEstimateType(state);
return gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.NONE;
}
export function getIsGasEstimatesFetched(state) {
const gasState = state.gas;
return Boolean(
gasState.estimateSource === GAS_SOURCE.METASWAPS &&
gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY,
);
const gasEstimateType = getGasEstimateType(state);
if (isEIP1559Network(state)) {
return false;
}
return gasEstimateType !== GAS_FEE_CONTROLLER_ESTIMATE_TYPES.NONE;
}

View File

@ -1,4 +1,4 @@
import { GAS_LIMITS } from '../../shared/constants/gas';
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../shared/constants/gas';
import {
getCustomGasLimit,
getCustomGasPrice,
@ -18,36 +18,68 @@ describe('custom-gas selectors', () => {
describe('isCustomGasPriceSafe()', () => {
it('should return true for gas.customData.price 0x77359400', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '1',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: '0x77359400' },
basicEstimates: { safeLow: 1 },
},
};
expect(isCustomPriceSafe(mockState)).toStrictEqual(true);
});
it('should return true for gas.customData.price null', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '1',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: null },
basicEstimates: { safeLow: 1 },
},
};
expect(isCustomPriceSafe(mockState)).toStrictEqual(true);
});
it('should return true gas.customData.price undefined', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '1',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: undefined },
basicEstimates: { safeLow: 1 },
},
};
expect(isCustomPriceSafe(mockState)).toStrictEqual(true);
});
it('should return false gas.basicEstimates.safeLow undefined', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
gasFeeEstimates: {
low: undefined,
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: '0x77359400' },
basicEstimates: { safeLow: undefined },
},
};
expect(isCustomPriceSafe(mockState)).toStrictEqual(false);
@ -57,60 +89,117 @@ describe('custom-gas selectors', () => {
describe('isCustomPriceExcessive()', () => {
it('should return false for gas.customData.price null', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '150',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: null },
basicEstimates: { fast: 150 },
},
};
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
});
it('should return false gas.basicEstimates.fast undefined', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: undefined,
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: '0x77359400' },
basicEstimates: { fast: undefined },
},
};
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
});
it('should return false gas.basicEstimates.price 0x205d0bae00 (139)', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: '0x205d0bae00' },
basicEstimates: { fast: 139 },
},
};
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
});
it('should return false gas.basicEstimates.price 0x1bf08eb000 (120)', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: '0x1bf08eb000' },
basicEstimates: { fast: 139 },
},
};
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
});
it('should return false gas.basicEstimates.price 0x28bed01600 (175)', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: '0x28bed01600' },
basicEstimates: { fast: 139 },
},
};
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
});
it('should return true gas.basicEstimates.price 0x30e4f9b400 (210)', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: {
customData: { price: '0x30e4f9b400' },
basicEstimates: { fast: 139 },
},
};
expect(isCustomPriceExcessive(mockState)).toStrictEqual(true);
});
it('should return false gas.basicEstimates.price 0x28bed01600 (175) (checkSend=true)', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
send: {
gas: {
gasPrice: '0x28bed0160',
@ -118,13 +207,21 @@ describe('custom-gas selectors', () => {
},
gas: {
customData: { price: null },
basicEstimates: { fast: 139 },
},
};
expect(isCustomPriceExcessive(mockState, true)).toStrictEqual(false);
});
it('should return true gas.basicEstimates.price 0x30e4f9b400 (210) (checkSend=true)', () => {
const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
send: {
gas: {
gasPrice: '0x30e4f9b400',
@ -132,7 +229,6 @@ describe('custom-gas selectors', () => {
},
gas: {
customData: { price: null },
basicEstimates: { fast: 139 },
},
};
expect(isCustomPriceExcessive(mockState, true)).toStrictEqual(true);
@ -171,6 +267,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '2.5',
medium: '4',
high: '5',
},
networkDetails: {
EIPS: {},
},
conversionRate: 255.71,
currentCurrency: 'usd',
preferences: {
@ -181,19 +286,6 @@ describe('custom-gas selectors', () => {
chainId: '0x1',
},
},
gas: {
basicEstimates: {
blockTime: 14.16326530612245,
safeLow: 2.5,
safeLowWait: 6.6,
average: 4,
avgWait: 5.3,
fast: 5,
fastWait: 3.3,
fastest: 10,
fastestWait: 0.5,
},
},
},
},
{
@ -219,6 +311,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -234,19 +335,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
blockTime: 14.16326530612245,
safeLow: 5,
safeLowWait: 13.2,
average: 7,
avgWait: 10.1,
fast: 10,
fastWait: 6.6,
fastest: 20,
fastestWait: 1.0,
},
},
},
},
{
@ -272,6 +360,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -287,19 +384,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
blockTime: 14.16326530612245,
safeLow: 5,
safeLowWait: 13.2,
average: 7,
avgWait: 10.1,
fast: 10,
fastWait: 6.6,
fastest: 20,
fastestWait: 1.0,
},
},
},
},
{
@ -325,6 +409,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -340,13 +433,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
safeLow: 5,
average: 7,
fast: 10,
},
},
},
},
{
@ -372,6 +458,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -387,13 +482,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
safeLow: 5,
average: 7,
fast: 10,
},
},
},
},
];
@ -435,6 +523,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '25',
medium: '30',
high: '50',
},
networkDetails: {
EIPS: {},
},
conversionRate: 255.71,
currentCurrency: 'usd',
preferences: {
@ -450,13 +547,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
safeLow: 25,
average: 30,
fast: 50,
},
},
},
},
{
@ -482,6 +572,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -497,19 +596,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
blockTime: 14.16326530612245,
safeLow: 50,
safeLowWait: 13.2,
average: 75,
avgWait: 9.6,
fast: 100,
fastWait: 6.6,
fastest: 200,
fastestWait: 1.0,
},
},
},
},
{
@ -535,6 +621,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -550,19 +645,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
blockTime: 14.16326530612245,
safeLow: 50,
safeLowWait: 13.2,
average: 75,
avgWait: 9.6,
fast: 100,
fastWait: 6.6,
fastest: 200,
fastestWait: 1.0,
},
},
},
},
{
@ -588,6 +670,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -603,13 +694,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
safeLow: 50,
average: 75,
fast: 100,
},
},
},
},
{
@ -635,6 +719,15 @@ describe('custom-gas selectors', () => {
],
mockState: {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1,
currentCurrency: 'usd',
preferences: {
@ -650,13 +743,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
gas: {
basicEstimates: {
safeLow: 50,
average: 75,
fast: 100,
},
},
},
},
];