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:
parent
3fada25dfc
commit
dc25a24de3
@ -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];
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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}
|
||||
|
@ -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) {
|
||||
|
@ -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()),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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 = {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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());
|
||||
},
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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) =>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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' },
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user