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

Handling gas price fetch failure (#10767)

This commit is contained in:
Niranjana Binoy 2021-04-28 14:02:01 -04:00 committed by GitHub
parent b73f543b23
commit f1fc51667a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 274 additions and 94 deletions

View File

@ -684,6 +684,9 @@
"estimatedProcessingTimes": { "estimatedProcessingTimes": {
"message": "Estimated Processing Times" "message": "Estimated Processing Times"
}, },
"ethGasPriceFetchWarning": {
"message": "Backup gas price is provided as the main gas estimation service is unavailable right now."
},
"eth_accounts": { "eth_accounts": {
"message": "View the addresses of your permitted accounts (required)", "message": "View the addresses of your permitted accounts (required)",
"description": "The description for the `eth_accounts` permission" "description": "The description for the `eth_accounts` permission"
@ -780,6 +783,9 @@
"gasPriceExtremelyLow": { "gasPriceExtremelyLow": {
"message": "Gas Price Extremely Low" "message": "Gas Price Extremely Low"
}, },
"gasPriceFetchFailed": {
"message": "Gas price estimation failed due to network error."
},
"gasPriceInfoTooltipContent": { "gasPriceInfoTooltipContent": {
"message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas." "message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas."
}, },

View File

@ -22,6 +22,7 @@ export default class ConfirmPageContainerContent extends Component {
titleComponent: PropTypes.node, titleComponent: PropTypes.node,
warning: PropTypes.string, warning: PropTypes.string,
origin: PropTypes.string.isRequired, origin: PropTypes.string.isRequired,
ethGasPriceWarning: PropTypes.string,
// Footer // Footer
onCancelAll: PropTypes.func, onCancelAll: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
@ -81,11 +82,15 @@ export default class ConfirmPageContainerContent extends Component {
unapprovedTxCount, unapprovedTxCount,
rejectNText, rejectNText,
origin, origin,
ethGasPriceWarning,
} = this.props; } = this.props;
return ( return (
<div className="confirm-page-container-content"> <div className="confirm-page-container-content">
{warning && <ConfirmPageContainerWarning warning={warning} />} {warning && <ConfirmPageContainerWarning warning={warning} />}
{ethGasPriceWarning && (
<ConfirmPageContainerWarning warning={ethGasPriceWarning} />
)}
<ConfirmPageContainerSummary <ConfirmPageContainerSummary
className={classnames({ className={classnames({
'confirm-page-container-summary--border': 'confirm-page-container-summary--border':

View File

@ -1,7 +1,6 @@
.confirm-page-container-warning { .confirm-page-container-warning {
background-color: #fffcdb; background-color: #fffcdb;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
border-bottom: 1px solid $geyser; border-bottom: 1px solid $geyser;
padding: 12px 24px; padding: 12px 24px;

View File

@ -43,6 +43,7 @@ export default class ConfirmPageContainer extends Component {
warning: PropTypes.string, warning: PropTypes.string,
unapprovedTxCount: PropTypes.number, unapprovedTxCount: PropTypes.number,
origin: PropTypes.string.isRequired, origin: PropTypes.string.isRequired,
ethGasPriceWarning: PropTypes.string,
// Navigation // Navigation
totalTx: PropTypes.number, totalTx: PropTypes.number,
positionOfCurrentTx: PropTypes.number, positionOfCurrentTx: PropTypes.number,
@ -103,6 +104,7 @@ export default class ConfirmPageContainer extends Component {
hideSenderToRecipient, hideSenderToRecipient,
showAccountInHeader, showAccountInHeader,
origin, origin,
ethGasPriceWarning,
} = this.props; } = this.props;
const renderAssetImage = contentComponent || !identiconAddress; const renderAssetImage = contentComponent || !identiconAddress;
@ -162,6 +164,7 @@ export default class ConfirmPageContainer extends Component {
unapprovedTxCount={unapprovedTxCount} unapprovedTxCount={unapprovedTxCount}
rejectNText={this.context.t('rejectTxsN', [unapprovedTxCount])} rejectNText={this.context.t('rejectTxsN', [unapprovedTxCount])}
origin={origin} origin={origin}
ethGasPriceWarning={ethGasPriceWarning}
/> />
)} )}
{contentComponent && ( {contentComponent && (

View File

@ -123,7 +123,16 @@ export default class GasModalPageContainer extends Component {
infoRowProps: { newTotalFiat, newTotalEth, sendAmount, transactionFee }, infoRowProps: { newTotalFiat, newTotalEth, sendAmount, transactionFee },
} = this.props; } = this.props;
let tabsToRender = [ let tabsToRender;
if (hideBasic) {
tabsToRender = [
{
name: this.context.t('advanced'),
content: this.renderAdvancedTabContent(),
},
];
} else {
tabsToRender = [
{ {
name: this.context.t('basic'), name: this.context.t('basic'),
content: this.renderBasicTabContent(gasPriceButtonGroupProps), content: this.renderBasicTabContent(gasPriceButtonGroupProps),
@ -133,9 +142,6 @@ export default class GasModalPageContainer extends Component {
content: this.renderAdvancedTabContent(), content: this.renderAdvancedTabContent(),
}, },
]; ];
if (hideBasic) {
tabsToRender = tabsToRender.slice(1);
} }
return ( return (

View File

@ -3,8 +3,7 @@ import sinon from 'sinon';
import BN from 'bn.js'; import BN from 'bn.js';
import GasReducer, { import GasReducer, {
basicGasEstimatesLoadingStarted, setBasicEstimateStatus,
basicGasEstimatesLoadingFinished,
setBasicGasEstimateData, setBasicGasEstimateData,
setCustomGasPrice, setCustomGasPrice,
setCustomGasLimit, setCustomGasLimit,
@ -49,7 +48,8 @@ describe('Gas Duck', () => {
fast: null, fast: null,
safeLow: null, safeLow: null,
}, },
basicEstimateIsLoading: true, basicEstimateStatus: 'LOADING',
estimateSource: '',
}; };
const providerState = { const providerState = {
@ -61,14 +61,12 @@ describe('Gas Duck', () => {
type: 'mainnet', type: 'mainnet',
}; };
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = const BASIC_GAS_ESTIMATE_STATUS = 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS';
'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED';
const BASIC_GAS_ESTIMATE_LOADING_STARTED =
'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED';
const SET_BASIC_GAS_ESTIMATE_DATA = const SET_BASIC_GAS_ESTIMATE_DATA =
'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'; 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA';
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'; const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'; const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE';
const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE';
describe('GasReducer()', () => { describe('GasReducer()', () => {
it('should initialize state', () => { it('should initialize state', () => {
@ -84,16 +82,13 @@ describe('Gas Duck', () => {
).toStrictEqual(mockState); ).toStrictEqual(mockState);
}); });
it('should set basicEstimateIsLoading to true when receiving a BASIC_GAS_ESTIMATE_LOADING_STARTED action', () => { it('should set basicEstimateStatus to LOADING when receiving a BASIC_GAS_ESTIMATE_STATUS action with value LOADING', () => {
expect( expect(
GasReducer(mockState, { type: BASIC_GAS_ESTIMATE_LOADING_STARTED }), GasReducer(mockState, {
).toStrictEqual({ basicEstimateIsLoading: true, ...mockState }); type: BASIC_GAS_ESTIMATE_STATUS,
}); value: 'LOADING',
}),
it('should set basicEstimateIsLoading to false when receiving a BASIC_GAS_ESTIMATE_LOADING_FINISHED action', () => { ).toStrictEqual({ basicEstimateStatus: 'LOADING', ...mockState });
expect(
GasReducer(mockState, { type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }),
).toStrictEqual({ basicEstimateIsLoading: false, ...mockState });
}); });
it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => { it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => {
@ -127,18 +122,17 @@ describe('Gas Duck', () => {
}); });
}); });
describe('basicGasEstimatesLoadingStarted', () => { it('should set estimateSource to Metaswaps when receiving a SET_ESTIMATE_SOURCE action with value Metaswaps', () => {
it('should create the correct action', () => { expect(
expect(basicGasEstimatesLoadingStarted()).toStrictEqual({ GasReducer(mockState, { type: SET_ESTIMATE_SOURCE, value: 'Metaswaps' }),
type: BASIC_GAS_ESTIMATE_LOADING_STARTED, ).toStrictEqual({ estimateSource: 'Metaswaps', ...mockState });
});
});
}); });
describe('basicGasEstimatesLoadingFinished', () => { describe('basicEstimateStatus', () => {
it('should create the correct action', () => { it('should create the correct action', () => {
expect(basicGasEstimatesLoadingFinished()).toStrictEqual({ expect(setBasicEstimateStatus('LOADING')).toStrictEqual({
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED, type: BASIC_GAS_ESTIMATE_STATUS,
value: 'LOADING',
}); });
}); });
}); });
@ -158,7 +152,7 @@ describe('Gas Duck', () => {
})); }));
expect(mockDistpatch.getCall(0).args).toStrictEqual([ expect(mockDistpatch.getCall(0).args).toStrictEqual([
{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED }, { type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'LOADING' },
]); ]);
expect( expect(
@ -168,7 +162,11 @@ describe('Gas Duck', () => {
).toStrictEqual(true); ).toStrictEqual(true);
expect(mockDistpatch.getCall(2).args).toStrictEqual([ expect(mockDistpatch.getCall(2).args).toStrictEqual([
{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }, { type: 'metamask/gas/SET_ESTIMATE_SOURCE', value: 'MetaSwaps' },
]);
expect(mockDistpatch.getCall(4).args).toStrictEqual([
{ type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'READY' },
]); ]);
}); });
@ -190,9 +188,12 @@ describe('Gas Duck', () => {
metamask: { provider: { ...providerStateForTestNetwork } }, metamask: { provider: { ...providerStateForTestNetwork } },
})); }));
expect(mockDistpatch.getCall(0).args).toStrictEqual([ expect(mockDistpatch.getCall(0).args).toStrictEqual([
{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED }, { type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'LOADING' },
]); ]);
expect(mockDistpatch.getCall(1).args).toStrictEqual([ 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, type: SET_BASIC_GAS_ESTIMATE_DATA,
value: { value: {
@ -200,8 +201,8 @@ describe('Gas Duck', () => {
}, },
}, },
]); ]);
expect(mockDistpatch.getCall(2).args).toStrictEqual([ expect(mockDistpatch.getCall(3).args).toStrictEqual([
{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }, { type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'READY' },
]); ]);
}); });
}); });

View File

@ -8,15 +8,24 @@ import {
import { getIsMainnet, getCurrentChainId } from '../../selectors'; import { getIsMainnet, getCurrentChainId } from '../../selectors';
import fetchWithCache from '../../helpers/utils/fetch-with-cache'; import fetchWithCache from '../../helpers/utils/fetch-with-cache';
const BASIC_ESTIMATE_STATES = {
LOADING: 'LOADING',
FAILED: 'FAILED',
READY: 'READY',
};
const GAS_SOURCE = {
METASWAPS: 'MetaSwaps',
ETHGASPRICE: 'eth_gasprice',
};
// Actions // Actions
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = const BASIC_GAS_ESTIMATE_STATUS = 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS';
'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED';
const BASIC_GAS_ESTIMATE_LOADING_STARTED =
'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED';
const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA'; const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA';
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'; const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA';
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'; const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT';
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'; const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE';
const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE';
const initState = { const initState = {
customData: { customData: {
@ -28,21 +37,17 @@ const initState = {
average: null, average: null,
fast: null, fast: null,
}, },
basicEstimateIsLoading: true, 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_LOADING_STARTED: case BASIC_GAS_ESTIMATE_STATUS:
return { return {
...state, ...state,
basicEstimateIsLoading: true, basicEstimateStatus: action.value,
};
case BASIC_GAS_ESTIMATE_LOADING_FINISHED:
return {
...state,
basicEstimateIsLoading: false,
}; };
case SET_BASIC_GAS_ESTIMATE_DATA: case SET_BASIC_GAS_ESTIMATE_DATA:
return { return {
@ -70,21 +75,21 @@ 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 // Action Creators
export function basicGasEstimatesLoadingStarted() { export function setBasicEstimateStatus(status) {
return { return {
type: BASIC_GAS_ESTIMATE_LOADING_STARTED, type: BASIC_GAS_ESTIMATE_STATUS,
}; value: status,
}
export function basicGasEstimatesLoadingFinished() {
return {
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED,
}; };
} }
@ -106,17 +111,25 @@ export function fetchBasicGasEstimates() {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const isMainnet = getIsMainnet(getState()); const isMainnet = getIsMainnet(getState());
dispatch(basicGasEstimatesLoadingStarted()); dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.LOADING));
let basicEstimates; let basicEstimates;
try {
dispatch(setEstimateSource(GAS_SOURCE.ETHGASPRICE));
if (isMainnet || process.env.IN_TEST) { if (isMainnet || process.env.IN_TEST) {
try {
basicEstimates = await fetchExternalBasicGasEstimates(); basicEstimates = await fetchExternalBasicGasEstimates();
dispatch(setEstimateSource(GAS_SOURCE.METASWAPS));
} catch (error) {
basicEstimates = await fetchEthGasPriceEstimates(getState());
}
} else { } else {
basicEstimates = await fetchEthGasPriceEstimates(getState()); basicEstimates = await fetchEthGasPriceEstimates(getState());
} }
dispatch(setBasicGasEstimateData(basicEstimates)); dispatch(setBasicGasEstimateData(basicEstimates));
dispatch(basicGasEstimatesLoadingFinished()); dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.READY));
} catch (error) {
dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.FAILED));
}
return basicEstimates; return basicEstimates;
}; };
@ -211,3 +224,10 @@ 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

@ -2,3 +2,6 @@ export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds';
export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow'; export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow';
export const TRANSACTION_ERROR_KEY = 'transactionError'; export const TRANSACTION_ERROR_KEY = 'transactionError';
export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract'; export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract';
export const ETH_GAS_PRICE_FETCH_WARNING_KEY = 'ethGasPriceFetchWarning';
export const GAS_PRICE_FETCH_FAILURE_ERROR_KEY = 'gasPriceFetchFailed';
export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive';

View File

@ -24,6 +24,8 @@ import {
getUseNonceField, getUseNonceField,
getCustomNonceValue, getCustomNonceValue,
getNextSuggestedNonce, getNextSuggestedNonce,
getNoGasPriceFetched,
getIsEthGasPriceFetched,
} from '../../selectors'; } from '../../selectors';
import { currentNetworkTxListSelector } from '../../selectors/transactions'; import { currentNetworkTxListSelector } from '../../selectors/transactions';
import Loading from '../../components/ui/loading-screen'; import Loading from '../../components/ui/loading-screen';
@ -116,6 +118,8 @@ export default function ConfirmApprove() {
const customData = customPermissionAmount const customData = customPermissionAmount
? getCustomTxParamsData(data, { customPermissionAmount, decimals }) ? getCustomTxParamsData(data, { customPermissionAmount, decimals })
: null; : null;
const isEthGasPrice = useSelector(getIsEthGasPriceFetched);
const noGasPrice = useSelector(getNoGasPriceFetched);
return tokenSymbol === undefined ? ( return tokenSymbol === undefined ? (
<Loading /> <Loading />
@ -136,7 +140,13 @@ export default function ConfirmApprove() {
tokenSymbol={tokenSymbol} tokenSymbol={tokenSymbol}
tokenBalance={tokenBalance} tokenBalance={tokenBalance}
showCustomizeGasModal={() => showCustomizeGasModal={() =>
dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })) dispatch(
showModal({
name: 'CUSTOMIZE_GAS',
txData,
hideBasic: isEthGasPrice || noGasPrice,
}),
)
} }
showEditApprovalPermissionModal={({ showEditApprovalPermissionModal={({
/* eslint-disable no-shadow */ /* eslint-disable no-shadow */

View File

@ -12,6 +12,8 @@ import {
INSUFFICIENT_FUNDS_ERROR_KEY, INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY, TRANSACTION_ERROR_KEY,
GAS_LIMIT_TOO_LOW_ERROR_KEY, GAS_LIMIT_TOO_LOW_ERROR_KEY,
ETH_GAS_PRICE_FETCH_WARNING_KEY,
GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
} from '../../helpers/constants/error-keys'; } from '../../helpers/constants/error-keys';
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'; import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display';
import { PRIMARY, SECONDARY } from '../../helpers/constants/common'; import { PRIMARY, SECONDARY } from '../../helpers/constants/common';
@ -23,6 +25,7 @@ import {
TRANSACTION_STATUSES, TRANSACTION_STATUSES,
} from '../../../../shared/constants/transaction'; } from '../../../../shared/constants/transaction';
import { getTransactionTypeTitle } from '../../helpers/utils/transactions.util'; import { getTransactionTypeTitle } from '../../helpers/utils/transactions.util';
import ErrorMessage from '../../components/ui/error-message';
export default class ConfirmTransactionBase extends Component { export default class ConfirmTransactionBase extends Component {
static contextTypes = { static contextTypes = {
@ -95,12 +98,15 @@ export default class ConfirmTransactionBase extends Component {
showAccountInHeader: PropTypes.bool, showAccountInHeader: PropTypes.bool,
mostRecentOverviewPage: PropTypes.string.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired,
isMainnet: PropTypes.bool, isMainnet: PropTypes.bool,
isEthGasPrice: PropTypes.bool,
noGasPrice: PropTypes.bool,
}; };
state = { state = {
submitting: false, submitting: false,
submitError: null, submitError: null,
submitWarning: '', submitWarning: '',
ethGasPriceWarning: '',
}; };
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -114,12 +120,14 @@ export default class ConfirmTransactionBase extends Component {
customNonceValue, customNonceValue,
toAddress, toAddress,
tryReverseResolveAddress, tryReverseResolveAddress,
isEthGasPrice,
} = this.props; } = this.props;
const { const {
customNonceValue: prevCustomNonceValue, customNonceValue: prevCustomNonceValue,
nextNonce: prevNextNonce, nextNonce: prevNextNonce,
toAddress: prevToAddress, toAddress: prevToAddress,
transactionStatus: prevTxStatus, transactionStatus: prevTxStatus,
isEthGasPrice: prevIsEthGasPrice,
} = prevProps; } = prevProps;
const statusUpdated = transactionStatus !== prevTxStatus; const statusUpdated = transactionStatus !== prevTxStatus;
const txDroppedOrConfirmed = const txDroppedOrConfirmed =
@ -151,6 +159,18 @@ export default class ConfirmTransactionBase extends Component {
if (toAddress && toAddress !== prevToAddress) { if (toAddress && toAddress !== prevToAddress) {
tryReverseResolveAddress(toAddress); tryReverseResolveAddress(toAddress);
} }
if (isEthGasPrice !== prevIsEthGasPrice) {
if (isEthGasPrice) {
this.setState({
ethGasPriceWarning: this.context.t(ETH_GAS_PRICE_FETCH_WARNING_KEY),
});
} else {
this.setState({
ethGasPriceWarning: '',
});
}
}
} }
getErrorKey() { getErrorKey() {
@ -160,6 +180,7 @@ export default class ConfirmTransactionBase extends Component {
hexTransactionFee, hexTransactionFee,
txData: { simulationFails, txParams: { value: amount } = {} } = {}, txData: { simulationFails, txParams: { value: amount } = {} } = {},
customGas, customGas,
noGasPrice,
} = this.props; } = this.props;
const insufficientBalance = const insufficientBalance =
@ -194,6 +215,13 @@ export default class ConfirmTransactionBase extends Component {
}; };
} }
if (noGasPrice) {
return {
valid: false,
errorKey: GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
};
}
return { return {
valid: true, valid: true,
}; };
@ -243,9 +271,12 @@ export default class ConfirmTransactionBase extends Component {
nextNonce, nextNonce,
getNextNonce, getNextNonce,
isMainnet, isMainnet,
isEthGasPrice,
noGasPrice,
} = this.props; } = this.props;
const notMainnetOrTest = !(isMainnet || process.env.IN_TEST); const notMainnetOrTest = !(isMainnet || process.env.IN_TEST);
const gasPriceFetchFailure = isEthGasPrice || noGasPrice;
return ( return (
<div className="confirm-page-container-content__details"> <div className="confirm-page-container-content__details">
@ -253,18 +284,26 @@ export default class ConfirmTransactionBase extends Component {
<ConfirmDetailRow <ConfirmDetailRow
label="Gas Fee" label="Gas Fee"
value={hexTransactionFee} value={hexTransactionFee}
headerText={notMainnetOrTest ? '' : 'Edit'} headerText={notMainnetOrTest || gasPriceFetchFailure ? '' : 'Edit'}
headerTextClassName={ headerTextClassName={
notMainnetOrTest ? '' : 'confirm-detail-row__header-text--edit' notMainnetOrTest || gasPriceFetchFailure
? ''
: 'confirm-detail-row__header-text--edit'
}
onHeaderClick={
notMainnetOrTest || gasPriceFetchFailure
? null
: () => this.handleEditGas()
} }
onHeaderClick={notMainnetOrTest ? null : () => this.handleEditGas()}
secondaryText={ secondaryText={
hideFiatConversion hideFiatConversion
? this.context.t('noConversionRateAvailable') ? this.context.t('noConversionRateAvailable')
: '' : ''
} }
/> />
{advancedInlineGasShown || notMainnetOrTest ? ( {advancedInlineGasShown ||
notMainnetOrTest ||
gasPriceFetchFailure ? (
<AdvancedGasInputs <AdvancedGasInputs
updateCustomGasPrice={(newGasPrice) => updateCustomGasPrice={(newGasPrice) =>
updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice }) updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice })
@ -279,6 +318,11 @@ export default class ConfirmTransactionBase extends Component {
isSpeedUp={false} isSpeedUp={false}
/> />
) : null} ) : null}
{noGasPrice ? (
<div className="confirm-page-container-content__error-container">
<ErrorMessage errorKey={GAS_PRICE_FETCH_FAILURE_ERROR_KEY} />
</div>
) : null}
</div> </div>
<div <div
className={ className={
@ -672,7 +716,12 @@ export default class ConfirmTransactionBase extends Component {
showAccountInHeader, showAccountInHeader,
txData, txData,
} = this.props; } = this.props;
const { submitting, submitError, submitWarning } = this.state; const {
submitting,
submitError,
submitWarning,
ethGasPriceWarning,
} = this.state;
const { name } = methodData; const { name } = methodData;
const { valid, errorKey } = this.getErrorKey(); const { valid, errorKey } = this.getErrorKey();
@ -696,7 +745,6 @@ export default class ConfirmTransactionBase extends Component {
functionType = t('contractInteraction'); functionType = t('contractInteraction');
} }
} }
return ( return (
<ConfirmPageContainer <ConfirmPageContainer
fromName={fromName} fromName={fromName}
@ -739,6 +787,7 @@ export default class ConfirmTransactionBase extends Component {
onSubmit={() => this.handleSubmit()} onSubmit={() => this.handleSubmit()}
hideSenderToRecipient={hideSenderToRecipient} hideSenderToRecipient={hideSenderToRecipient}
origin={txData.origin} origin={txData.origin}
ethGasPriceWarning={ethGasPriceWarning}
/> />
); );
} }

View File

@ -37,6 +37,8 @@ import {
getUseNonceField, getUseNonceField,
getPreferences, getPreferences,
transactionFeeSelector, transactionFeeSelector,
getNoGasPriceFetched,
getIsEthGasPriceFetched,
} from '../../selectors'; } from '../../selectors';
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils'; import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils';
@ -150,6 +152,8 @@ const mapStateToProps = (state, ownProps) => {
}; };
} }
customNonceValue = getCustomNonceValue(state); customNonceValue = getCustomNonceValue(state);
const isEthGasPrice = getIsEthGasPriceFetched(state);
const noGasPrice = getNoGasPriceFetched(state);
return { return {
balance, balance,
@ -189,6 +193,8 @@ const mapStateToProps = (state, ownProps) => {
nextNonce, nextNonce,
mostRecentOverviewPage: getMostRecentOverviewPage(state), mostRecentOverviewPage: getMostRecentOverviewPage(state),
isMainnet, isMainnet,
isEthGasPrice,
noGasPrice,
}; };
}; };
@ -207,7 +213,12 @@ export const mapDispatchToProps = (dispatch) => {
}, },
showCustomizeGasModal: ({ txData, onSubmit, validate }) => { showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
return dispatch( return dispatch(
showModal({ name: 'CUSTOMIZE_GAS', txData, onSubmit, validate }), showModal({
name: 'CUSTOMIZE_GAS',
txData,
onSubmit,
validate,
}),
); );
}, },
updateGasAndCalculate: (updatedTx) => { updateGasAndCalculate: (updatedTx) => {
@ -278,6 +289,7 @@ const getValidateEditGas = ({ balance, conversionRate, txData }) => {
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { balance, conversionRate, txData, unapprovedTxs } = stateProps; const { balance, conversionRate, txData, unapprovedTxs } = stateProps;
const { const {
cancelAllTransactions: dispatchCancelAllTransactions, cancelAllTransactions: dispatchCancelAllTransactions,
showCustomizeGasModal: dispatchShowCustomizeGasModal, showCustomizeGasModal: dispatchShowCustomizeGasModal,

View File

@ -2,6 +2,11 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import PageContainerContent from '../../../components/ui/page-container/page-container-content.component'; import PageContainerContent from '../../../components/ui/page-container/page-container-content.component';
import Dialog from '../../../components/ui/dialog'; import Dialog from '../../../components/ui/dialog';
import {
ETH_GAS_PRICE_FETCH_WARNING_KEY,
GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
GAS_PRICE_EXCESSIVE_ERROR_KEY,
} from '../../../helpers/constants/error-keys';
import SendAmountRow from './send-amount-row'; import SendAmountRow from './send-amount-row';
import SendGasRow from './send-gas-row'; import SendGasRow from './send-gas-row';
import SendHexDataRow from './send-hex-data-row'; import SendHexDataRow from './send-hex-data-row';
@ -21,16 +26,30 @@ export default class SendContent extends Component {
warning: PropTypes.string, warning: PropTypes.string,
error: PropTypes.string, error: PropTypes.string,
gasIsExcessive: PropTypes.bool.isRequired, gasIsExcessive: PropTypes.bool.isRequired,
isEthGasPrice: PropTypes.bool,
noGasPrice: PropTypes.bool,
}; };
updateGas = (updateData) => this.props.updateGas(updateData); updateGas = (updateData) => this.props.updateGas(updateData);
render() { render() {
const { warning, error, gasIsExcessive } = this.props; const {
warning,
error,
gasIsExcessive,
isEthGasPrice,
noGasPrice,
} = this.props;
let gasError;
if (gasIsExcessive) gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY;
else if (noGasPrice) gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY;
return ( return (
<PageContainerContent> <PageContainerContent>
<div className="send-v2__form"> <div className="send-v2__form">
{gasIsExcessive && this.renderError(true)} {gasError && this.renderError(gasError)}
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)}
{error && this.renderError()} {error && this.renderError()}
{warning && this.renderWarning()} {warning && this.renderWarning()}
{this.maybeRenderAddContact()} {this.maybeRenderAddContact()}
@ -68,24 +87,22 @@ export default class SendContent extends Component {
); );
} }
renderWarning() { renderWarning(gasWarning = '') {
const { t } = this.context; const { t } = this.context;
const { warning } = this.props; const { warning } = this.props;
return ( return (
<Dialog type="warning" className="send__error-dialog"> <Dialog type="warning" className="send__error-dialog">
{t(warning)} {gasWarning === '' ? t(warning) : t(gasWarning)}
</Dialog> </Dialog>
); );
} }
renderError(gasError = false) { renderError(gasError = '') {
const { t } = this.context; const { t } = this.context;
const { error } = this.props; const { error } = this.props;
return ( return (
<Dialog type="error" className="send__error-dialog"> <Dialog type="error" className="send__error-dialog">
{gasError ? t('gasPriceExcessive') : t(error)} {gasError === '' ? t(error) : t(gasError)}
</Dialog> </Dialog>
); );
} }

View File

@ -3,6 +3,8 @@ import {
getSendTo, getSendTo,
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
getAddressBookEntry, getAddressBookEntry,
getIsEthGasPriceFetched,
getNoGasPriceFetched,
} from '../../../selectors'; } from '../../../selectors';
import * as actions from '../../../store/actions'; import * as actions from '../../../store/actions';
@ -19,6 +21,8 @@ function mapStateToProps(state) {
), ),
contact: getAddressBookEntry(state, to), contact: getAddressBookEntry(state, to),
to, to,
isEthGasPrice: getIsEthGasPriceFetched(state),
noGasPrice: getNoGasPriceFetched(state),
}; };
} }

View File

@ -26,6 +26,8 @@ export default class SendGasRow extends Component {
gasLimit: PropTypes.string, gasLimit: PropTypes.string,
insufficientBalance: PropTypes.bool, insufficientBalance: PropTypes.bool,
isMainnet: PropTypes.bool, isMainnet: PropTypes.bool,
isEthGasPrice: PropTypes.bool,
noGasPrice: PropTypes.bool,
}; };
static contextTypes = { static contextTypes = {
@ -35,11 +37,19 @@ export default class SendGasRow extends Component {
renderAdvancedOptionsButton() { renderAdvancedOptionsButton() {
const { metricsEvent } = this.context; const { metricsEvent } = this.context;
const { showCustomizeGasModal, isMainnet } = this.props; const {
showCustomizeGasModal,
isMainnet,
isEthGasPrice,
noGasPrice,
} = this.props;
// Tests should behave in same way as mainnet, but are using Localhost // Tests should behave in same way as mainnet, but are using Localhost
if (!isMainnet && !process.env.IN_TEST) { if (!isMainnet && !process.env.IN_TEST) {
return null; return null;
} }
if (isEthGasPrice || noGasPrice) {
return null;
}
return ( return (
<div <div
className="advanced-gas-options-btn" className="advanced-gas-options-btn"
@ -92,8 +102,11 @@ export default class SendGasRow extends Component {
gasLimit, gasLimit,
insufficientBalance, insufficientBalance,
isMainnet, isMainnet,
isEthGasPrice,
noGasPrice,
} = this.props; } = this.props;
const { metricsEvent } = this.context; const { metricsEvent } = this.context;
const gasPriceFetchFailure = isEthGasPrice || noGasPrice;
const gasPriceButtonGroup = ( const gasPriceButtonGroup = (
<div> <div>
@ -148,7 +161,11 @@ export default class SendGasRow extends Component {
</div> </div>
); );
// Tests should behave in same way as mainnet, but are using Localhost // Tests should behave in same way as mainnet, but are using Localhost
if (advancedInlineGasShown || (!isMainnet && !process.env.IN_TEST)) { if (
advancedInlineGasShown ||
(!isMainnet && !process.env.IN_TEST) ||
gasPriceFetchFailure
) {
return advancedGasInputs; return advancedGasInputs;
} else if (gasButtonGroupShown) { } else if (gasButtonGroupShown) {
return gasPriceButtonGroup; return gasPriceButtonGroup;

View File

@ -18,6 +18,8 @@ import {
getRenderableEstimateDataForSmallButtonsFromGWEI, getRenderableEstimateDataForSmallButtonsFromGWEI,
getDefaultActiveButtonIndex, getDefaultActiveButtonIndex,
getIsMainnet, getIsMainnet,
getIsEthGasPriceFetched,
getNoGasPriceFetched,
} from '../../../../selectors'; } from '../../../../selectors';
import { isBalanceSufficient, calcGasTotal } from '../../send.utils'; import { isBalanceSufficient, calcGasTotal } from '../../send.utils';
import { calcMaxAmount } from '../send-amount-row/amount-max-button/amount-max-button.utils'; import { calcMaxAmount } from '../send-amount-row/amount-max-button/amount-max-button.utils';
@ -64,6 +66,8 @@ function mapStateToProps(state) {
balance, balance,
conversionRate, conversionRate,
}); });
const isEthGasPrice = getIsEthGasPriceFetched(state);
const noGasPrice = getNoGasPriceFetched(state);
return { return {
balance: getSendFromBalance(state), balance: getSendFromBalance(state),
@ -85,6 +89,8 @@ function mapStateToProps(state) {
sendToken: getSendToken(state), sendToken: getSendToken(state),
tokenBalance: getTokenBalance(state), tokenBalance: getTokenBalance(state),
isMainnet: getIsMainnet(state), isMainnet: getIsMainnet(state),
isEthGasPrice,
noGasPrice,
}; };
} }

View File

@ -27,6 +27,7 @@ export default class SendFooter extends Component {
gasEstimateType: PropTypes.string, gasEstimateType: PropTypes.string,
gasIsLoading: PropTypes.bool, gasIsLoading: PropTypes.bool,
mostRecentOverviewPage: PropTypes.string.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired,
noGasPrice: PropTypes.bool,
}; };
static contextTypes = { static contextTypes = {
@ -109,6 +110,7 @@ export default class SendFooter extends Component {
to, to,
gasLimit, gasLimit,
gasIsLoading, gasIsLoading,
noGasPrice,
} = this.props; } = this.props;
const missingTokenBalance = sendToken && !tokenBalance; const missingTokenBalance = sendToken && !tokenBalance;
const gasLimitTooLow = gasLimit < 5208; // 5208 is hex value of 21000, minimum gas limit const gasLimitTooLow = gasLimit < 5208; // 5208 is hex value of 21000, minimum gas limit
@ -118,7 +120,8 @@ export default class SendFooter extends Component {
missingTokenBalance || missingTokenBalance ||
!(data || to) || !(data || to) ||
gasLimitTooLow || gasLimitTooLow ||
gasIsLoading; gasIsLoading ||
noGasPrice;
return shouldBeDisabled; return shouldBeDisabled;
} }

View File

@ -49,6 +49,7 @@ describe('SendFooter Component', () => {
update={propsMethodSpies.update} update={propsMethodSpies.update}
sendErrors={{}} sendErrors={{}}
mostRecentOverviewPage="mostRecentOverviewPage" mostRecentOverviewPage="mostRecentOverviewPage"
noGasPrice={false}
/>, />,
{ context: { t: (str) => str, metricsEvent: () => ({}) } }, { context: { t: (str) => str, metricsEvent: () => ({}) } },
); );

View File

@ -24,6 +24,7 @@ import {
getGasIsLoading, getGasIsLoading,
getRenderableEstimateDataForSmallButtonsFromGWEI, getRenderableEstimateDataForSmallButtonsFromGWEI,
getDefaultActiveButtonIndex, getDefaultActiveButtonIndex,
getNoGasPriceFetched,
} from '../../../selectors'; } from '../../../selectors';
import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { addHexPrefix } from '../../../../../app/scripts/lib/util'; import { addHexPrefix } from '../../../../../app/scripts/lib/util';
@ -67,6 +68,7 @@ function mapStateToProps(state) {
gasEstimateType, gasEstimateType,
gasIsLoading: getGasIsLoading(state), gasIsLoading: getGasIsLoading(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state), mostRecentOverviewPage: getMostRecentOverviewPage(state),
noGasPrice: getNoGasPriceFetched(state),
}; };
} }

View File

@ -27,12 +27,14 @@ export function getCustomGasPrice(state) {
} }
export function getBasicGasEstimateLoadingStatus(state) { export function getBasicGasEstimateLoadingStatus(state) {
return state.gas.basicEstimateIsLoading; return state.gas.basicEstimateStatus === 'LOADING';
} }
export function getAveragePriceEstimateInHexWEI(state) { export function getAveragePriceEstimateInHexWEI(state) {
const averagePriceEstimate = state.gas.basicEstimates.average; const averagePriceEstimate = state.gas.basicEstimates
return getGasPriceInHexWei(averagePriceEstimate || '0x0'); ? state.gas.basicEstimates.average
: '0x0';
return getGasPriceInHexWei(averagePriceEstimate);
} }
export function getFastPriceEstimateInHexWEI(state) { export function getFastPriceEstimateInHexWEI(state) {
@ -355,3 +357,17 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
}, },
]; ];
} }
export function getIsEthGasPriceFetched(state) {
const gasState = state.gas;
return Boolean(
gasState.estimateSource === 'eth_gasprice' &&
gasState.basicEstimateStatus === 'READY' &&
getIsMainnet(state),
);
}
export function getNoGasPriceFetched(state) {
const gasState = state.gas;
return Boolean(gasState.basicEstimateStatus === 'FAILED');
}