diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 4d992ed9b..868a077a7 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -763,6 +763,12 @@ "gasPrice": { "message": "Gas Price (GWEI)" }, + "gasPriceExcessive": { + "message": "Your gas fee is set unnecessarily high. Consider lowering the amount." + }, + "gasPriceExcessiveInput": { + "message": "Gas Price Is Excessive" + }, "gasPriceExtremelyLow": { "message": "Gas Price Extremely Low" }, diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js index e394b453a..a775cec65 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -20,10 +20,12 @@ export default class AdvancedGasInputs extends Component { isSpeedUp: PropTypes.bool, customGasLimitMessage: PropTypes.string, minimumGasLimit: PropTypes.number, + customPriceIsExcessive: PropTypes.bool, }; static defaultProps = { minimumGasLimit: Number(MIN_GAS_LIMIT_DEC), + customPriceIsExcessive: false, }; constructor(props) { @@ -75,6 +77,7 @@ export default class AdvancedGasInputs extends Component { customPriceIsSafe, isSpeedUp, gasPrice, + customPriceIsExcessive, }) { const { t } = this.context; @@ -93,6 +96,11 @@ export default class AdvancedGasInputs extends Component { errorText: t('gasPriceExtremelyLow'), errorType: 'warning', }; + } else if (customPriceIsExcessive) { + return { + errorText: t('gasPriceExcessiveInput'), + errorType: 'error', + }; } return {}; @@ -185,6 +193,7 @@ export default class AdvancedGasInputs extends Component { isSpeedUp, customGasLimitMessage, minimumGasLimit, + customPriceIsExcessive, } = this.props; const { gasPrice, gasLimit } = this.state; @@ -196,6 +205,7 @@ export default class AdvancedGasInputs extends Component { customPriceIsSafe, isSpeedUp, gasPrice, + customPriceIsExcessive, }); const gasPriceErrorComponent = gasPriceErrorType ? (
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 4c27f9900..f57cce92d 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -35,6 +35,7 @@ export default class GasModalPageContainer extends Component { isSpeedUp: PropTypes.bool, isRetry: PropTypes.bool, disableSave: PropTypes.bool, + customPriceIsExcessive: PropTypes.bool.isRequired, }; componentDidMount() { @@ -57,6 +58,7 @@ export default class GasModalPageContainer extends Component { customPriceIsSafe, isSpeedUp, isRetry, + customPriceIsExcessive, infoRowProps: { transactionFee }, } = this.props; @@ -71,6 +73,7 @@ export default class GasModalPageContainer extends Component { customPriceIsSafe={customPriceIsSafe} isSpeedUp={isSpeedUp} isRetry={isRetry} + customPriceIsExcessive={customPriceIsExcessive} /> ); } diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index fc1b111b9..136fdccc8 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -37,6 +37,7 @@ import { getTokenBalance, getSendMaxModeState, getAveragePriceEstimateInHexWEI, + isCustomPriceExcessive, } from '../../../../selectors'; import { @@ -141,6 +142,7 @@ const mapStateToProps = (state, ownProps) => { customGasTotal, newTotalFiat, customPriceIsSafe: isCustomPriceSafe(state), + customPriceIsExcessive: isCustomPriceExcessive(state), maxModeOn, gasPriceButtonGroupProps: { buttonDataLoading, diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 100e30780..0ba5aabbc 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -126,6 +126,7 @@ describe('gas-modal-page-container container', function () { conversionRate: 50, customModalGasLimitInHex: 'aaaaaaaa', customModalGasPriceInHex: 'ffffffff', + customPriceIsExcessive: false, customGasTotal: 'aaaaaaa955555556', customPriceIsSafe: true, gasPriceButtonGroupProps: { diff --git a/ui/app/pages/send/send-content/send-content.component.js b/ui/app/pages/send/send-content/send-content.component.js index 7b38d14ae..6f4fd7924 100644 --- a/ui/app/pages/send/send-content/send-content.component.js +++ b/ui/app/pages/send/send-content/send-content.component.js @@ -20,15 +20,17 @@ export default class SendContent extends Component { isOwnedAccount: PropTypes.bool, warning: PropTypes.string, error: PropTypes.string, + gasIsExcessive: PropTypes.bool.isRequired, }; updateGas = (updateData) => this.props.updateGas(updateData); render() { - const { warning, error } = this.props; + const { warning, error, gasIsExcessive } = this.props; return (
+ {gasIsExcessive && this.renderError(true)} {error && this.renderError()} {warning && this.renderWarning()} {this.maybeRenderAddContact()} @@ -77,13 +79,13 @@ export default class SendContent extends Component { ); } - renderError() { + renderError(gasError = false) { const { t } = this.context; const { error } = this.props; return ( - {t(error)} + {gasError ? t('gasPriceExcessive') : t(error)} ); } diff --git a/ui/app/pages/send/send.component.js b/ui/app/pages/send/send.component.js index 9a366d3da..23696807a 100644 --- a/ui/app/pages/send/send.component.js +++ b/ui/app/pages/send/send.component.js @@ -58,6 +58,7 @@ export default class SendTransactionScreen extends Component { qrCodeDetected: PropTypes.func.isRequired, qrCodeData: PropTypes.object, sendTokenAddress: PropTypes.string, + gasIsExcessive: PropTypes.bool.isRequired, }; static contextTypes = { @@ -382,7 +383,7 @@ export default class SendTransactionScreen extends Component { } renderSendContent() { - const { history, showHexData } = this.props; + const { history, showHexData, gasIsExcessive } = this.props; const { toWarning, toError } = this.state; return [ @@ -394,6 +395,7 @@ export default class SendTransactionScreen extends Component { showHexData={showHexData} warning={toWarning} error={toError} + gasIsExcessive={gasIsExcessive} />, , ]; diff --git a/ui/app/pages/send/send.container.js b/ui/app/pages/send/send.container.js index 3f0ddb0a0..898546a81 100644 --- a/ui/app/pages/send/send.container.js +++ b/ui/app/pages/send/send.container.js @@ -23,6 +23,7 @@ import { getSelectedAddress, getAddressBook, getSendTokenAddress, + isCustomPriceExcessive, } from '../../selectors'; import { @@ -67,6 +68,7 @@ function mapStateToProps(state) { tokenBalance: getTokenBalance(state), tokenContract: getSendTokenContract(state), sendTokenAddress: getSendTokenAddress(state), + gasIsExcessive: isCustomPriceExcessive(state, true), }; } diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index 2cb40f5a6..2ac6b4767 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -9,7 +9,12 @@ import { formatETHFee } from '../helpers/utils/formatters'; import { calcGasTotal } from '../pages/send/send.utils'; import { GAS_ESTIMATE_TYPES } from '../helpers/constants/common'; -import { getCurrentCurrency, getIsMainnet, getPreferences } from '.'; +import { + getCurrentCurrency, + getIsMainnet, + getPreferences, + getGasPrice, +} from '.'; const NUMBER_OF_DECIMALS_SM_BTNS = 5; @@ -31,7 +36,7 @@ export function getAveragePriceEstimateInHexWEI(state) { } export function getFastPriceEstimateInHexWEI(state) { - const fastPriceEstimate = state.gas.basicEstimates.fast; + const fastPriceEstimate = getFastPriceEstimate(state); return getGasPriceInHexWei(fastPriceEstimate || '0x0'); } @@ -55,6 +60,16 @@ export function getSafeLowEstimate(state) { return safeLow; } +export function getFastPriceEstimate(state) { + const { + gas: { + basicEstimates: { fast }, + }, + } = state; + + return fast; +} + export function isCustomPriceSafe(state) { const safeLow = getSafeLowEstimate(state); @@ -81,6 +96,31 @@ export function isCustomPriceSafe(state) { return customPriceSafe; } +export function isCustomPriceExcessive(state, checkSend = false) { + const customPrice = checkSend ? getGasPrice(state) : getCustomGasPrice(state); + const fastPrice = getFastPriceEstimate(state); + + if (!customPrice || !fastPrice) { + return false; + } + + // Custom gas should be considered excessive when it is 1.5 times greater than the fastest estimate. + const customPriceExcessive = conversionGreaterThan( + { + value: customPrice, + fromNumericBase: 'hex', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }, + { + fromNumericBase: 'dec', + value: Math.floor(fastPrice * 1.5), + }, + ); + + return customPriceExcessive; +} + export function basicPriceEstimateToETHTotal( estimate, gasLimit, diff --git a/ui/app/selectors/tests/custom-gas.test.js b/ui/app/selectors/tests/custom-gas.test.js index 3afb1afdd..ddfd6b136 100644 --- a/ui/app/selectors/tests/custom-gas.test.js +++ b/ui/app/selectors/tests/custom-gas.test.js @@ -7,6 +7,7 @@ const { getRenderableBasicEstimateData, getRenderableEstimateDataForSmallButtonsFromGWEI, isCustomPriceSafe, + isCustomPriceExcessive, } = proxyquire('../custom-gas', {}); describe('custom-gas selectors', function () { @@ -55,6 +56,91 @@ describe('custom-gas selectors', function () { }); }); + describe('isCustomPriceExcessive()', function () { + it('should return false for gas.customData.price null', function () { + const mockState = { + gas: { + customData: { price: null }, + basicEstimates: { fast: 150 }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState), false); + }); + it('should return false gas.basicEstimates.fast undefined', function () { + const mockState = { + gas: { + customData: { price: '0x77359400' }, + basicEstimates: { fast: undefined }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState), false); + }); + it('should return false gas.basicEstimates.price 0x205d0bae00 (139)', function () { + const mockState = { + gas: { + customData: { price: '0x205d0bae00' }, + basicEstimates: { fast: 139 }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState), false); + }); + it('should return false gas.basicEstimates.price 0x1bf08eb000 (120)', function () { + const mockState = { + gas: { + customData: { price: '0x1bf08eb000' }, + basicEstimates: { fast: 139 }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState), false); + }); + it('should return false gas.basicEstimates.price 0x28bed01600 (175)', function () { + const mockState = { + gas: { + customData: { price: '0x28bed01600' }, + basicEstimates: { fast: 139 }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState), false); + }); + it('should return true gas.basicEstimates.price 0x30e4f9b400 (210)', function () { + const mockState = { + gas: { + customData: { price: '0x30e4f9b400' }, + basicEstimates: { fast: 139 }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState), true); + }); + it('should return false gas.basicEstimates.price 0x28bed01600 (175) (checkSend=true)', function () { + const mockState = { + metamask: { + send: { + gasPrice: '0x28bed0160', + }, + }, + gas: { + customData: { price: null }, + basicEstimates: { fast: 139 }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState, true), false); + }); + it('should return true gas.basicEstimates.price 0x30e4f9b400 (210) (checkSend=true)', function () { + const mockState = { + metamask: { + send: { + gasPrice: '0x30e4f9b400', + }, + }, + gas: { + customData: { price: null }, + basicEstimates: { fast: 139 }, + }, + }; + assert.strictEqual(isCustomPriceExcessive(mockState, true), true); + }); + }); + describe('getCustomGasLimit()', function () { it('should return gas.customData.limit', function () { const mockState = { gas: { customData: { limit: 'mockLimit' } } };