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() { async getEIP1559Compatibility() {
const { EIPS } = this.networkDetails.getState(); const { EIPS } = this.networkDetails.getState();
if (process.env.SHOW_EIP_1559_UI === false) {
return false;
}
if (EIPS[1559] !== undefined) { if (EIPS[1559] !== undefined) {
return EIPS[1559]; return EIPS[1559];
} }

View File

@ -196,11 +196,17 @@ export default class MetamaskController extends EventEmitter {
getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind( getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind(
this, this,
), ),
getCurrentNetworkLegacyGasAPICompatibility: () => legacyAPIEndpoint: `https://gas-api.metaswap.codefi.network/networks/<chain_id>/gasPrices`,
this.networkController.getCurrentChainId() === MAINNET_CHAIN_ID, EIP1559APIEndpoint: `https://gas-api.metaswap.codefi.network/networks/<chain_id>/suggestedGasFees`,
getChainId: this.networkController.getCurrentChainId.bind( getCurrentNetworkLegacyGasAPICompatibility: () => {
this.networkController, 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({ this.appStateController = new AppStateController({

View File

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

View File

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

View File

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

View File

@ -4,11 +4,6 @@
// untangling is having the constants separate. // untangling is having the constants separate.
// Actions // 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 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_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
export const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'; 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 sinon from 'sinon';
import BN from 'bn.js';
import GasReducer, { import GasReducer, { setCustomGasPrice, setCustomGasLimit } from './gas.duck';
setBasicEstimateStatus,
setBasicGasEstimateData,
setCustomGasPrice,
setCustomGasLimit,
fetchBasicGasEstimates,
} from './gas.duck';
import { import {
BASIC_GAS_ESTIMATE_STATUS,
SET_BASIC_GAS_ESTIMATE_DATA,
SET_CUSTOM_GAS_PRICE, SET_CUSTOM_GAS_PRICE,
SET_CUSTOM_GAS_LIMIT, SET_CUSTOM_GAS_LIMIT,
SET_ESTIMATE_SOURCE,
} from './gas-action-constants'; } from './gas-action-constants';
jest.mock('../../helpers/utils/storage-helpers.js', () => ({
getStorageItem: jest.fn(),
setStorageItem: jest.fn(),
}));
describe('Gas Duck', () => { describe('Gas Duck', () => {
let tempDateNow; let tempDateNow;
const mockGasPriceApiResponse = {
SafeGasPrice: 10,
ProposeGasPrice: 20,
FastGasPrice: 30,
};
beforeEach(() => { beforeEach(() => {
tempDateNow = global.Date.now; tempDateNow = global.Date.now;
@ -51,22 +29,6 @@ describe('Gas Duck', () => {
price: null, price: null,
limit: 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()', () => { describe('GasReducer()', () => {
@ -83,27 +45,6 @@ describe('Gas Duck', () => {
).toStrictEqual(mockState); ).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', () => { it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => {
expect( expect(
GasReducer(mockState, { 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', () => { describe('setCustomGasPrice', () => {
it('should create the correct action', () => { it('should create the correct action', () => {
expect(setCustomGasPrice('mockCustomGasPrice')).toStrictEqual({ expect(setCustomGasPrice('mockCustomGasPrice')).toStrictEqual({

View File

@ -1,62 +1,20 @@
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import BigNumber from 'bignumber.js';
import { 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, RESET_CUSTOM_DATA,
SET_BASIC_GAS_ESTIMATE_DATA,
SET_CUSTOM_GAS_LIMIT, SET_CUSTOM_GAS_LIMIT,
SET_CUSTOM_GAS_PRICE, SET_CUSTOM_GAS_PRICE,
SET_ESTIMATE_SOURCE,
} from './gas-action-constants'; } 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 = { const initState = {
customData: { customData: {
price: null, price: null,
limit: null, limit: null,
}, },
basicEstimates: {
safeLow: null,
average: null,
fast: null,
},
basicEstimateStatus: BASIC_ESTIMATE_STATES.LOADING,
estimateSource: '',
}; };
// Reducer // Reducer
export default function reducer(state = initState, action) { export default function reducer(state = initState, action) {
switch (action.type) { 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: case SET_CUSTOM_GAS_PRICE:
return { return {
...state, ...state,
@ -78,138 +36,11 @@ export default function reducer(state = initState, action) {
...state, ...state,
customData: cloneDeep(initState.customData), customData: cloneDeep(initState.customData),
}; };
case SET_ESTIMATE_SOURCE:
return {
...state,
estimateSource: action.value,
};
default: default:
return state; 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) { export function setCustomGasPrice(newPrice) {
return { return {
type: SET_CUSTOM_GAS_PRICE, type: SET_CUSTOM_GAS_PRICE,
@ -227,10 +58,3 @@ export function setCustomGasLimit(newLimit) {
export function resetCustomData() { export function resetCustomData() {
return { type: RESET_CUSTOM_DATA }; return { type: RESET_CUSTOM_DATA };
} }
export function setEstimateSource(estimateSource) {
return {
type: SET_ESTIMATE_SOURCE,
value: estimateSource,
};
}

View File

@ -50,15 +50,7 @@ import {
updateTokenType, updateTokenType,
updateTransaction, updateTransaction,
} from '../../store/actions'; } from '../../store/actions';
import { import { setCustomGasLimit } from '../gas/gas.duck';
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 { import {
QR_CODE_DETECTED, QR_CODE_DETECTED,
SELECTED_ACCOUNT_CHANGED, SELECTED_ACCOUNT_CHANGED,
@ -77,13 +69,18 @@ import {
isOriginContractAddress, isOriginContractAddress,
isValidDomainName, isValidDomainName,
} from '../../helpers/utils/util'; } from '../../helpers/utils/util';
import { getTokens, getUnapprovedTxs } from '../metamask/metamask'; import {
getGasEstimateType,
getTokens,
getUnapprovedTxs,
} from '../metamask/metamask';
import { resetEnsResolution } from '../ens'; import { resetEnsResolution } from '../ens';
import { import {
isBurnAddress, isBurnAddress,
isValidHexAddress, isValidHexAddress,
} from '../../../shared/modules/hexstring-utils'; } from '../../../shared/modules/hexstring-utils';
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import { ETH, GWEI } from '../../helpers/constants/common';
// typedefs // typedefs
/** /**
@ -186,8 +183,19 @@ async function estimateGasLimitForSend({
let isSimpleSendOnNonStandardNetwork = false; let isSimpleSendOnNonStandardNetwork = false;
// blockGasLimit may be a falsy, but defined, value when we receive it from // 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. // state, so we use logical or to fall back to MIN_GAS_LIMIT_HEX. Some
const blockGasLimit = options.blockGasLimit || MIN_GAS_LIMIT_HEX; // 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 // 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 // how much gas will be used for a transaction. That background process is
// located in tx-gas-utils.js in the transaction controller folder. // 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) { if (send.stage !== SEND_STAGES.EDIT) {
const gasLimit = await estimateGasLimitForSend({ const gasLimit = await estimateGasLimitForSend({
gasPrice: send.gas.gasPrice, gasPrice: send.gas.gasPrice,
blockGasLimit: metamask.blockGasLimit, blockGasLimit: metamask.currentBlockGasLimit,
selectedAddress: metamask.selectedAddress, selectedAddress: metamask.selectedAddress,
sendToken: send.asset.details, sendToken: send.asset.details,
to: send.recipient.address?.toLowerCase(), 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. * Responsible for initializing required state for the send slice.
* This method is dispatched from the send page in the componentDidMount * 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 // Default gasPrice to 1 gwei if all estimation fails
let gasPrice = '0x1'; let gasPrice = '0x1';
let basicEstimateStatus = BASIC_ESTIMATE_STATES.LOADING;
let gasEstimatePollToken = null; let gasEstimatePollToken = null;
if (Boolean(process.env.SHOW_EIP_1559_UI) === false) { // Instruct the background process that polling for gas prices should begin
// Initiate gas slices work to fetch gasPrice estimates. We need to get the gasEstimatePollToken = await getGasFeeEstimatesAndStartPolling();
// new state after this is set to determine if initialization can proceed. const {
await thunkApi.dispatch(fetchBasicGasEstimates()); metamask: { gasFeeEstimates, gasEstimateType },
const { } = thunkApi.getState();
gas: { basicEstimates, basicEstimateStatus: apiBasicEstimateStatus },
} = thunkApi.getState();
basicEstimateStatus = apiBasicEstimateStatus; if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
gasPrice = getGasPriceInHexWei(gasFeeEstimates.medium);
if (basicEstimateStatus === BASIC_ESTIMATE_STATES.READY) { } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
gasPrice = getGasPriceInHexWei(basicEstimates.average); gasPrice = getRoundedGasPrice(gasFeeEstimates.gasPrice);
} } else if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
} else { gasPrice = getGasPriceInHexWei(
// Instruct the background process that polling for gas prices should begin gasFeeEstimates.medium.suggestedMaxFeePerGas,
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;
} }
// Set a basic gasLimit in the event that other estimation fails // Set a basic gasLimit in the event that other estimation fails
let gasLimit = let gasLimit =
asset.type === ASSET_TYPES.TOKEN asset.type === ASSET_TYPES.TOKEN
? GAS_LIMITS.BASE_TOKEN_ESTIMATE ? GAS_LIMITS.BASE_TOKEN_ESTIMATE
: GAS_LIMITS.SIMPLE; : GAS_LIMITS.SIMPLE;
if ( if (
basicEstimateStatus === BASIC_ESTIMATE_STATES.READY && gasEstimateType !== GAS_ESTIMATE_TYPES.NONE &&
stage !== SEND_STAGES.EDIT && stage !== SEND_STAGES.EDIT &&
recipient.address recipient.address
) { ) {
@ -444,7 +458,7 @@ export const initializeSendState = createAsyncThunk(
// required gas. If this value isn't nullish, set it as the new gasLimit // required gas. If this value isn't nullish, set it as the new gasLimit
const estimatedGasLimit = await estimateGasLimitForSend({ const estimatedGasLimit = await estimateGasLimitForSend({
gasPrice, gasPrice,
blockGasLimit: metamask.blockGasLimit, blockGasLimit: metamask.currentBlockGasLimit,
selectedAddress: fromAddress, selectedAddress: fromAddress,
sendToken: asset.details, sendToken: asset.details,
to: recipient.address.toLowerCase(), to: recipient.address.toLowerCase(),
@ -508,9 +522,12 @@ export const initialState = {
isCustomGasSet: false, isCustomGasSet: false,
// maximum gas needed for tx // maximum gas needed for tx
gasLimit: '0x0', gasLimit: '0x0',
// price in gwei to pay per gas // price in wei to pay per gas
gasPrice: '0x0', 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', gasTotal: '0x0',
// minimum supported gasLimit // minimum supported gasLimit
minimumGasLimit: GAS_LIMITS.SIMPLE, 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 // We keep a copy of txParams in state that could be submitted to the
// network if the form state is valid. // network if the form state is valid.
if (state.status === SEND_STATUSES.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) { switch (state.asset.type) {
case ASSET_TYPES.TOKEN: case ASSET_TYPES.TOKEN:
// When sending a token the to address is the contract address of // When sending a token the to address is the contract address of
@ -926,7 +945,7 @@ const slice = createSlice({
break; break;
case state.asset.type === ASSET_TYPES.TOKEN && case state.asset.type === ASSET_TYPES.TOKEN &&
state.asset.details.isERC721 === true: state.asset.details.isERC721 === true:
state.state = SEND_STATUSES.INVALID; state.status = SEND_STATUSES.INVALID;
break; break;
default: default:
state.status = SEND_STATUSES.VALID; 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) => { .addCase(GAS_FEE_ESTIMATES_UPDATED, (state, action) => {
// When the gasFeeController updates its gas fee estimates we need to // When the gasFeeController updates its gas fee estimates we need to
// update and validate state based on those new values // update and validate state based on those new values
if (process.env.SHOW_EIP_1559_UI) { const { gasFeeEstimates, gasEstimateType } = action.payload;
const { gasFeeEstimates, gasEstimateType } = action.payload; let payload = null;
let payload = null; if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { payload = getGasPriceInHexWei(
payload = getGasPriceInHexWei( gasFeeEstimates.medium.suggestedMaxFeePerGas,
gasFeeEstimates.medium.suggestedMaxFeePerGas, );
); } else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) { payload = getGasPriceInHexWei(gasFeeEstimates.medium);
payload = getGasPriceInHexWei(gasFeeEstimates.medium); } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) { payload = getRoundedGasPrice(gasFeeEstimates.gasPrice);
payload = getGasPriceInHexWei(gasFeeEstimates.gasPrice);
}
if (payload) {
slice.caseReducers.updateGasPrice(state, {
payload,
});
}
} }
// 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) { export function getGasInputMode(state) {
const isMainnet = getIsMainnet(state); const isMainnet = getIsMainnet(state);
const gasEstimateType = getGasEstimateType(state);
const showAdvancedGasFields = getAdvancedInlineGasShown(state); const showAdvancedGasFields = getAdvancedInlineGasShown(state);
if (state[name].gas.isCustomGasSet) { if (state[name].gas.isCustomGasSet) {
return GAS_INPUT_MODES.CUSTOM; return GAS_INPUT_MODES.CUSTOM;
@ -1494,6 +1492,16 @@ export function getGasInputMode(state) {
if ((!isMainnet && !process.env.IN_TEST) || showAdvancedGasFields) { if ((!isMainnet && !process.env.IN_TEST) || showAdvancedGasFields) {
return GAS_INPUT_MODES.INLINE; 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; return GAS_INPUT_MODES.BASIC;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,17 @@ import { decEthToConvertedCurrency as ethTotalToConvertedCurrency } from '../hel
import { formatETHFee } from '../helpers/utils/formatters'; import { formatETHFee } from '../helpers/utils/formatters';
import { calcGasTotal } from '../pages/send/send.utils'; import { calcGasTotal } from '../pages/send/send.utils';
import { GAS_ESTIMATE_TYPES } from '../helpers/constants/common';
import { getGasPrice } from '../ducks/send'; import { getGasPrice } from '../ducks/send';
import { BASIC_ESTIMATE_STATES, GAS_SOURCE } from '../ducks/gas/gas.duck'; import {
import { GAS_LIMITS } from '../../shared/constants/gas'; 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 '.'; import { getCurrentCurrency, getIsMainnet, getShouldShowFiat } from '.';
const NUMBER_OF_DECIMALS_SM_BTNS = 5; const NUMBER_OF_DECIMALS_SM_BTNS = 5;
@ -25,13 +32,12 @@ export function getCustomGasPrice(state) {
} }
export function getBasicGasEstimateLoadingStatus(state) { export function getBasicGasEstimateLoadingStatus(state) {
return state.gas.basicEstimateStatus === 'LOADING'; return getIsGasEstimatesFetched(state) === false;
} }
export function getAveragePriceEstimateInHexWEI(state) { export function getAveragePriceEstimateInHexWEI(state) {
const averagePriceEstimate = state.gas.basicEstimates const averagePriceEstimate = getAverageEstimate(state);
? state.gas.basicEstimates.average
: '0x0';
return getGasPriceInHexWei(averagePriceEstimate); return getGasPriceInHexWei(averagePriceEstimate);
} }
@ -51,23 +57,31 @@ export function getDefaultActiveButtonIndex(
} }
export function getSafeLowEstimate(state) { export function getSafeLowEstimate(state) {
const { const gasFeeEstimates = getGasFeeEstimates(state);
gas: { const gasEstimateType = getGasEstimateType(state);
basicEstimates: { safeLow },
},
} = 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) { export function getFastPriceEstimate(state) {
const { const gasFeeEstimates = getGasFeeEstimates(state);
gas: {
basicEstimates: { fast },
},
} = state;
return fast; const gasEstimateType = getGasEstimateType(state);
return gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.LEGACY
? gasFeeEstimates?.high
: null;
} }
export function isCustomPriceSafe(state) { export function isCustomPriceSafe(state) {
@ -97,7 +111,7 @@ export function isCustomPriceSafe(state) {
} }
export function isCustomPriceSafeForCustomNetwork(state) { export function isCustomPriceSafeForCustomNetwork(state) {
const estimatedPrice = state.gas.basicEstimates.average; const estimatedPrice = getAverageEstimate(state);
const customGasPrice = getCustomGasPrice(state); const customGasPrice = getCustomGasPrice(state);
@ -219,61 +233,56 @@ export function getRenderableGasButtonData(
currentCurrency, currentCurrency,
nativeCurrency, nativeCurrency,
) { ) {
const { safeLow, average, fast } = estimates; const { low, medium, high } = estimates;
const slowEstimateData = { const slowEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW, gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInPrimaryCurrency: getRenderableEthFee( feeInPrimaryCurrency: getRenderableEthFee(low, gasLimit, 9, nativeCurrency),
safeLow,
gasLimit,
9,
nativeCurrency,
),
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee( ? getRenderableConvertedCurrencyFee(
safeLow, low,
gasLimit, gasLimit,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
) )
: '', : '',
priceInHexWei: getGasPriceInHexWei(safeLow), priceInHexWei: getGasPriceInHexWei(low),
}; };
const averageEstimateData = { const averageEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE, gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInPrimaryCurrency: getRenderableEthFee( feeInPrimaryCurrency: getRenderableEthFee(
average, medium,
gasLimit, gasLimit,
9, 9,
nativeCurrency, nativeCurrency,
), ),
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee( ? getRenderableConvertedCurrencyFee(
average, medium,
gasLimit, gasLimit,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
) )
: '', : '',
priceInHexWei: getGasPriceInHexWei(average), priceInHexWei: getGasPriceInHexWei(medium),
}; };
const fastEstimateData = { const fastEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.FAST, gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInPrimaryCurrency: getRenderableEthFee( feeInPrimaryCurrency: getRenderableEthFee(
fast, high,
gasLimit, gasLimit,
9, 9,
nativeCurrency, nativeCurrency,
), ),
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee( ? getRenderableConvertedCurrencyFee(
fast, high,
gasLimit, gasLimit,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
) )
: '', : '',
priceInHexWei: getGasPriceInHexWei(fast), priceInHexWei: getGasPriceInHexWei(high),
}; };
return { return {
@ -297,7 +306,7 @@ export function getRenderableBasicEstimateData(state, gasLimit) {
averageEstimateData, averageEstimateData,
fastEstimateData, fastEstimateData,
} = getRenderableGasButtonData( } = getRenderableGasButtonData(
state.gas.basicEstimates, getGasFeeEstimates(state),
gasLimit, gasLimit,
showFiat, showFiat,
conversionRate, conversionRate,
@ -308,7 +317,7 @@ export function getRenderableBasicEstimateData(state, gasLimit) {
} }
export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) { export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
if (getBasicGasEstimateLoadingStatus(state)) { if (getIsGasEstimatesFetched(state) === false) {
return []; return [];
} }
const showFiat = getShouldShowFiat(state); const showFiat = getShouldShowFiat(state);
@ -316,94 +325,88 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
state.send.gas.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE; state.send.gas.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE;
const { conversionRate } = state.metamask; const { conversionRate } = state.metamask;
const currentCurrency = getCurrentCurrency(state); const currentCurrency = getCurrentCurrency(state);
const { const gasFeeEstimates = getGasFeeEstimates(state);
gas: {
basicEstimates: { safeLow, average, fast },
},
} = state;
return [ return [
{ {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW, gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee( ? getRenderableConvertedCurrencyFee(
safeLow, gasFeeEstimates.low,
gasLimit, gasLimit,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
) )
: '', : '',
feeInPrimaryCurrency: getRenderableEthFee( feeInPrimaryCurrency: getRenderableEthFee(
safeLow, gasFeeEstimates.low,
gasLimit, gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS, NUMBER_OF_DECIMALS_SM_BTNS,
), ),
priceInHexWei: getGasPriceInHexWei(safeLow, true), priceInHexWei: getGasPriceInHexWei(gasFeeEstimates.low, true),
}, },
{ {
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE, gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee( ? getRenderableConvertedCurrencyFee(
average, gasFeeEstimates.medium,
gasLimit, gasLimit,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
) )
: '', : '',
feeInPrimaryCurrency: getRenderableEthFee( feeInPrimaryCurrency: getRenderableEthFee(
average, gasFeeEstimates.medium,
gasLimit, gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS, NUMBER_OF_DECIMALS_SM_BTNS,
), ),
priceInHexWei: getGasPriceInHexWei(average, true), priceInHexWei: getGasPriceInHexWei(gasFeeEstimates.medium, true),
}, },
{ {
gasEstimateType: GAS_ESTIMATE_TYPES.FAST, gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInSecondaryCurrency: showFiat feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee( ? getRenderableConvertedCurrencyFee(
fast, gasFeeEstimates.high,
gasLimit, gasLimit,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
) )
: '', : '',
feeInPrimaryCurrency: getRenderableEthFee( feeInPrimaryCurrency: getRenderableEthFee(
fast, gasFeeEstimates.high,
gasLimit, gasLimit,
NUMBER_OF_DECIMALS_SM_BTNS, NUMBER_OF_DECIMALS_SM_BTNS,
), ),
priceInHexWei: getGasPriceInHexWei(fast, true), priceInHexWei: getGasPriceInHexWei(gasFeeEstimates.high, true),
}, },
]; ];
} }
export function getIsEthGasPriceFetched(state) { export function getIsEthGasPriceFetched(state) {
const gasState = state.gas; const gasEstimateType = getGasEstimateType(state);
return Boolean( return (
gasState.estimateSource === GAS_SOURCE.ETHGASPRICE && gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.ETH_GASPRICE &&
gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY && getIsMainnet(state)
getIsMainnet(state),
); );
} }
export function getIsCustomNetworkGasPriceFetched(state) { export function getIsCustomNetworkGasPriceFetched(state) {
const gasState = state.gas; const gasEstimateType = getGasEstimateType(state);
return Boolean( return (
gasState.estimateSource === GAS_SOURCE.ETHGASPRICE && gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.ETH_GASPRICE &&
gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY && !getIsMainnet(state)
!getIsMainnet(state),
); );
} }
export function getNoGasPriceFetched(state) { export function getNoGasPriceFetched(state) {
const gasState = state.gas; const gasEstimateType = getGasEstimateType(state);
return Boolean(gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.FAILED); return gasEstimateType === GAS_FEE_CONTROLLER_ESTIMATE_TYPES.NONE;
} }
export function getIsGasEstimatesFetched(state) { export function getIsGasEstimatesFetched(state) {
const gasState = state.gas; const gasEstimateType = getGasEstimateType(state);
return Boolean( if (isEIP1559Network(state)) {
gasState.estimateSource === GAS_SOURCE.METASWAPS && return false;
gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY, }
); 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 { import {
getCustomGasLimit, getCustomGasLimit,
getCustomGasPrice, getCustomGasPrice,
@ -18,36 +18,68 @@ describe('custom-gas selectors', () => {
describe('isCustomGasPriceSafe()', () => { describe('isCustomGasPriceSafe()', () => {
it('should return true for gas.customData.price 0x77359400', () => { it('should return true for gas.customData.price 0x77359400', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '1',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: '0x77359400' }, customData: { price: '0x77359400' },
basicEstimates: { safeLow: 1 },
}, },
}; };
expect(isCustomPriceSafe(mockState)).toStrictEqual(true); expect(isCustomPriceSafe(mockState)).toStrictEqual(true);
}); });
it('should return true for gas.customData.price null', () => { it('should return true for gas.customData.price null', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '1',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: null }, customData: { price: null },
basicEstimates: { safeLow: 1 },
}, },
}; };
expect(isCustomPriceSafe(mockState)).toStrictEqual(true); expect(isCustomPriceSafe(mockState)).toStrictEqual(true);
}); });
it('should return true gas.customData.price undefined', () => { it('should return true gas.customData.price undefined', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '1',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: undefined }, customData: { price: undefined },
basicEstimates: { safeLow: 1 },
}, },
}; };
expect(isCustomPriceSafe(mockState)).toStrictEqual(true); expect(isCustomPriceSafe(mockState)).toStrictEqual(true);
}); });
it('should return false gas.basicEstimates.safeLow undefined', () => { it('should return false gas.basicEstimates.safeLow undefined', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
gasFeeEstimates: {
low: undefined,
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: '0x77359400' }, customData: { price: '0x77359400' },
basicEstimates: { safeLow: undefined },
}, },
}; };
expect(isCustomPriceSafe(mockState)).toStrictEqual(false); expect(isCustomPriceSafe(mockState)).toStrictEqual(false);
@ -57,60 +89,117 @@ describe('custom-gas selectors', () => {
describe('isCustomPriceExcessive()', () => { describe('isCustomPriceExcessive()', () => {
it('should return false for gas.customData.price null', () => { it('should return false for gas.customData.price null', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '150',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: null }, customData: { price: null },
basicEstimates: { fast: 150 },
}, },
}; };
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false); expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
}); });
it('should return false gas.basicEstimates.fast undefined', () => { it('should return false gas.basicEstimates.fast undefined', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: undefined,
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: '0x77359400' }, customData: { price: '0x77359400' },
basicEstimates: { fast: undefined },
}, },
}; };
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false); expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
}); });
it('should return false gas.basicEstimates.price 0x205d0bae00 (139)', () => { it('should return false gas.basicEstimates.price 0x205d0bae00 (139)', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: '0x205d0bae00' }, customData: { price: '0x205d0bae00' },
basicEstimates: { fast: 139 },
}, },
}; };
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false); expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
}); });
it('should return false gas.basicEstimates.price 0x1bf08eb000 (120)', () => { it('should return false gas.basicEstimates.price 0x1bf08eb000 (120)', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: '0x1bf08eb000' }, customData: { price: '0x1bf08eb000' },
basicEstimates: { fast: 139 },
}, },
}; };
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false); expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
}); });
it('should return false gas.basicEstimates.price 0x28bed01600 (175)', () => { it('should return false gas.basicEstimates.price 0x28bed01600 (175)', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: '0x28bed01600' }, customData: { price: '0x28bed01600' },
basicEstimates: { fast: 139 },
}, },
}; };
expect(isCustomPriceExcessive(mockState)).toStrictEqual(false); expect(isCustomPriceExcessive(mockState)).toStrictEqual(false);
}); });
it('should return true gas.basicEstimates.price 0x30e4f9b400 (210)', () => { it('should return true gas.basicEstimates.price 0x30e4f9b400 (210)', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
gas: { gas: {
customData: { price: '0x30e4f9b400' }, customData: { price: '0x30e4f9b400' },
basicEstimates: { fast: 139 },
}, },
}; };
expect(isCustomPriceExcessive(mockState)).toStrictEqual(true); expect(isCustomPriceExcessive(mockState)).toStrictEqual(true);
}); });
it('should return false gas.basicEstimates.price 0x28bed01600 (175) (checkSend=true)', () => { it('should return false gas.basicEstimates.price 0x28bed01600 (175) (checkSend=true)', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
send: { send: {
gas: { gas: {
gasPrice: '0x28bed0160', gasPrice: '0x28bed0160',
@ -118,13 +207,21 @@ describe('custom-gas selectors', () => {
}, },
gas: { gas: {
customData: { price: null }, customData: { price: null },
basicEstimates: { fast: 139 },
}, },
}; };
expect(isCustomPriceExcessive(mockState, true)).toStrictEqual(false); expect(isCustomPriceExcessive(mockState, true)).toStrictEqual(false);
}); });
it('should return true gas.basicEstimates.price 0x30e4f9b400 (210) (checkSend=true)', () => { it('should return true gas.basicEstimates.price 0x30e4f9b400 (210) (checkSend=true)', () => {
const mockState = { const mockState = {
metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
high: '139',
},
networkDetails: {
EIPS: {},
},
},
send: { send: {
gas: { gas: {
gasPrice: '0x30e4f9b400', gasPrice: '0x30e4f9b400',
@ -132,7 +229,6 @@ describe('custom-gas selectors', () => {
}, },
gas: { gas: {
customData: { price: null }, customData: { price: null },
basicEstimates: { fast: 139 },
}, },
}; };
expect(isCustomPriceExcessive(mockState, true)).toStrictEqual(true); expect(isCustomPriceExcessive(mockState, true)).toStrictEqual(true);
@ -171,6 +267,15 @@ describe('custom-gas selectors', () => {
], ],
mockState: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '2.5',
medium: '4',
high: '5',
},
networkDetails: {
EIPS: {},
},
conversionRate: 255.71, conversionRate: 255.71,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -181,19 +286,6 @@ describe('custom-gas selectors', () => {
chainId: '0x1', 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: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -234,19 +335,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, 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: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -287,19 +384,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, 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: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -340,13 +433,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, gasLimit: GAS_LIMITS.SIMPLE,
}, },
}, },
gas: {
basicEstimates: {
safeLow: 5,
average: 7,
fast: 10,
},
},
}, },
}, },
{ {
@ -372,6 +458,15 @@ describe('custom-gas selectors', () => {
], ],
mockState: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '5',
medium: '7',
high: '10',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -387,13 +482,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, gasLimit: GAS_LIMITS.SIMPLE,
}, },
}, },
gas: {
basicEstimates: {
safeLow: 5,
average: 7,
fast: 10,
},
},
}, },
}, },
]; ];
@ -435,6 +523,15 @@ describe('custom-gas selectors', () => {
], ],
mockState: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '25',
medium: '30',
high: '50',
},
networkDetails: {
EIPS: {},
},
conversionRate: 255.71, conversionRate: 255.71,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -450,13 +547,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, gasLimit: GAS_LIMITS.SIMPLE,
}, },
}, },
gas: {
basicEstimates: {
safeLow: 25,
average: 30,
fast: 50,
},
},
}, },
}, },
{ {
@ -482,6 +572,15 @@ describe('custom-gas selectors', () => {
], ],
mockState: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -497,19 +596,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, 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: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -550,19 +645,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, 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: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -603,13 +694,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, gasLimit: GAS_LIMITS.SIMPLE,
}, },
}, },
gas: {
basicEstimates: {
safeLow: 50,
average: 75,
fast: 100,
},
},
}, },
}, },
{ {
@ -635,6 +719,15 @@ describe('custom-gas selectors', () => {
], ],
mockState: { mockState: {
metamask: { metamask: {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '50',
medium: '75',
high: '100',
},
networkDetails: {
EIPS: {},
},
conversionRate: 2557.1, conversionRate: 2557.1,
currentCurrency: 'usd', currentCurrency: 'usd',
preferences: { preferences: {
@ -650,13 +743,6 @@ describe('custom-gas selectors', () => {
gasLimit: GAS_LIMITS.SIMPLE, gasLimit: GAS_LIMITS.SIMPLE,
}, },
}, },
gas: {
basicEstimates: {
safeLow: 50,
average: 75,
fast: 100,
},
},
}, },
}, },
]; ];