diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3267e12e3..1d6c044f6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1122,6 +1122,11 @@ export default class MetamaskController extends EventEmitter { this.gasFeeController.disconnectPoller, this.gasFeeController, ), + + getGasFeeTimeEstimate: nodeify( + this.gasFeeController.getTimeEstimate, + this.gasFeeController, + ), }; } diff --git a/ui/components/app/edit-gas-display/edit-gas-display.component.js b/ui/components/app/edit-gas-display/edit-gas-display.component.js index ee80bddee..f44e5aa26 100644 --- a/ui/components/app/edit-gas-display/edit-gas-display.component.js +++ b/ui/components/app/edit-gas-display/edit-gas-display.component.js @@ -15,7 +15,6 @@ import { COLORS, TYPOGRAPHY, FONT_WEIGHT, - TEXT_ALIGN, } from '../../../helpers/constants/design-system'; import { areDappSuggestedAndTxParamGasFeesTheSame } from '../../../helpers/utils/confirm-tx.util'; @@ -52,7 +51,6 @@ export default function EditGasDisplay({ setEstimateToUse, estimatedMinimumFiat, estimatedMaximumFiat, - hasGasErrors, dappSuggestedGasFeeAcknowledged, setDappSuggestedGasFeeAcknowledged, showAdvancedForm, @@ -134,7 +132,12 @@ export default function EditGasDisplay({ , ]) } - timing={} + timing={ + + } /> {requireDappAcknowledgement && ( )} - {hasGasErrors && ( - - - {t('editGasTooLow')}{' '} - - - - )} {networkSupports1559 && !requireDappAcknowledgement && ![EDIT_GAS_MODES.SPEED_UP, EDIT_GAS_MODES.CANCEL].includes(mode) && ( @@ -259,7 +246,6 @@ EditGasDisplay.propTypes = { setEstimateToUse: PropTypes.func, estimatedMinimumFiat: PropTypes.string, estimatedMaximumFiat: PropTypes.string, - hasGasErrors: PropTypes.boolean, dappSuggestedGasFeeAcknowledged: PropTypes.boolean, setDappSuggestedGasFeeAcknowledged: PropTypes.func, showAdvancedForm: PropTypes.bool, diff --git a/ui/components/app/edit-gas-display/index.scss b/ui/components/app/edit-gas-display/index.scss index e3ccea500..ae5f96fa0 100644 --- a/ui/components/app/edit-gas-display/index.scss +++ b/ui/components/app/edit-gas-display/index.scss @@ -21,14 +21,6 @@ } } - &__error .info-tooltip { - display: inline-block; - - path { - fill: $error-1; - } - } - &__dapp-acknowledgement-warning { margin-bottom: 20px; } diff --git a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js index 8796b894a..8eb5e6840 100644 --- a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js +++ b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js @@ -235,7 +235,6 @@ export default function EditGasPopover({ onEducationClick={() => setShowEducationContent(true)} mode={mode} transaction={transaction} - hasGasErrors={hasGasErrors} gasErrors={gasErrors} onManualChange={onManualChange} minimumGasLimit={minimumGasLimitDec} diff --git a/ui/components/app/gas-timing/gas-timing.component.js b/ui/components/app/gas-timing/gas-timing.component.js index b3680da1b..8c05c34d3 100644 --- a/ui/components/app/gas-timing/gas-timing.component.js +++ b/ui/components/app/gas-timing/gas-timing.component.js @@ -1,35 +1,86 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates'; +import { usePrevious } from '../../../hooks/usePrevious'; import { I18nContext } from '../../../contexts/i18n'; import Typography from '../../ui/typography/typography'; -import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; +import { + TYPOGRAPHY, + FONT_WEIGHT, +} from '../../../helpers/constants/design-system'; +import InfoTooltip from '../../ui/info-tooltip/info-tooltip'; + +import { getGasTimeEstimate } from '../../../store/actions'; // Once we reach this second threshold, we switch to minutes as a unit const SECOND_CUTOFF = 90; -export default function GasTiming({ maxPriorityFeePerGas }) { +// Shows "seconds" as unit of time if under SECOND_CUTOFF, otherwise "minutes" +const toHumanReadableTime = (milliseconds = 1, t) => { + const seconds = Math.ceil(milliseconds / 1000); + if (seconds <= SECOND_CUTOFF) { + return t('gasTimingSeconds', [seconds]); + } + return t('gasTimingMinutes', [Math.ceil(seconds / 60)]); +}; +export default function GasTiming({ + maxFeePerGas = 0, + maxPriorityFeePerGas = 0, +}) { const { gasFeeEstimates, isGasEstimatesLoading, gasEstimateType, } = useGasFeeEstimates(); - const t = useContext(I18nContext); + const [customEstimatedTime, setCustomEstimatedTime] = useState(null); - // Shows "seconds" as unit of time if under SECOND_CUTOFF, otherwise "minutes" - const toHumanReadableTime = (milliseconds = 1) => { - const seconds = Math.ceil(milliseconds / 1000); - if (seconds <= SECOND_CUTOFF) { - return t('gasTimingSeconds', [seconds]); + // If the user has chosen a value lower than the low gas fee estimate, + // We'll need to use the useEffect hook below to make a call to calculate + // the time to show + const isUnknownLow = + gasFeeEstimates?.low && + Number(maxPriorityFeePerGas) < + Number(gasFeeEstimates.low.suggestedMaxPriorityFeePerGas); + + const previousMaxFeePerGas = usePrevious(maxFeePerGas); + const previousMaxPriorityFeePerGas = usePrevious(maxPriorityFeePerGas); + const previousIsUnknownLow = usePrevious(isUnknownLow); + + useEffect(() => { + const priority = maxPriorityFeePerGas; + const fee = maxFeePerGas; + + if ( + isUnknownLow || + priority !== previousMaxPriorityFeePerGas || + fee !== previousMaxFeePerGas + ) { + getGasTimeEstimate(priority, fee).then((result) => { + if (maxFeePerGas === fee && maxPriorityFeePerGas === priority) { + setCustomEstimatedTime(result); + } + }); } - return t('gasTimingMinutes', [Math.ceil(seconds / 60)]); - }; + + if (isUnknownLow !== false && previousIsUnknownLow === true) { + setCustomEstimatedTime(null); + } + }, [ + maxPriorityFeePerGas, + maxFeePerGas, + isUnknownLow, + previousMaxFeePerGas, + previousMaxPriorityFeePerGas, + previousIsUnknownLow, + ]); + + const t = useContext(I18nContext); // Don't show anything if we don't have enough information if ( @@ -39,39 +90,69 @@ export default function GasTiming({ maxPriorityFeePerGas }) { return null; } - const { low, medium, high } = gasFeeEstimates; + const { low = {}, medium = {}, high = {} } = gasFeeEstimates; let text = ''; - let attitude = ''; + let attitude = 'positive'; + let fontWeight = FONT_WEIGHT.NORMAL; // Anything medium or faster is positive if ( Number(maxPriorityFeePerGas) >= Number(medium.suggestedMaxPriorityFeePerGas) ) { - attitude = 'positive'; - // High+ is very likely, medium is likely if ( Number(maxPriorityFeePerGas) < Number(high.suggestedMaxPriorityFeePerGas) ) { + // Medium text = t('gasTimingPositive', [ - toHumanReadableTime(medium.maxWaitTimeEstimate), + toHumanReadableTime(low.maxWaitTimeEstimate, t), ]); } else { + // High text = t('gasTimingVeryPositive', [ - toHumanReadableTime(high.maxWaitTimeEstimate), + toHumanReadableTime(high.minWaitTimeEstimate, t), ]); } } else { attitude = 'negative'; - text = t('gasTimingNegative', [ - toHumanReadableTime(low.maxWaitTimeEstimate), - ]); + + // If the user has chosen a value less than our low estimate, + // calculate a potential wait time + if (isUnknownLow) { + // If we didn't get any useful information, show the + // "unknown processing time" message + if ( + !customEstimatedTime || + customEstimatedTime === 'unknown' || + customEstimatedTime?.upperTimeBound === 'unknown' + ) { + fontWeight = FONT_WEIGHT.BOLD; + text = ( + <> + {t('editGasTooLow')}{' '} + + > + ); + } else { + text = t('gasTimingNegative', [ + toHumanReadableTime(Number(customEstimatedTime?.upperTimeBound), t), + ]); + } + } else { + text = t('gasTimingNegative', [ + toHumanReadableTime(low.maxWaitTimeEstimate, t), + ]); + } } return ( } />, diff --git a/ui/store/actions.js b/ui/store/actions.js index 25d923c8f..2a7b33bfd 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -2791,6 +2791,13 @@ export function disconnectGasFeeEstimatePoller(pollToken) { return promisifiedBackground.disconnectGasFeeEstimatePoller(pollToken); } +export function getGasTimeEstimate(maxPriorityFeePerGas, maxFeePerGas) { + return promisifiedBackground.getGasTimeEstimate( + maxPriorityFeePerGas, + maxFeePerGas, + ); +} + // MetaMetrics /** * @typedef {import('../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload