diff --git a/ui/hooks/useGasFeeErrors.js b/ui/hooks/useGasFeeErrors.js new file mode 100644 index 000000000..cffb8a729 --- /dev/null +++ b/ui/hooks/useGasFeeErrors.js @@ -0,0 +1,267 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../shared/constants/gas'; +import { + conversionLessThan, + conversionGreaterThan, +} from '../../shared/modules/conversion.utils'; +import { + checkNetworkAndAccountSupports1559, + getSelectedAccount, +} from '../selectors'; +import { addHexes } from '../helpers/utils/conversions.util'; +import { isLegacyTransaction } from '../helpers/utils/transactions.util'; +import { + bnGreaterThan, + bnLessThan, + bnLessThanEqualTo, +} from '../helpers/utils/util'; +import { GAS_FORM_ERRORS } from '../helpers/constants/gas'; + +const HIGH_FEE_WARNING_MULTIPLIER = 1.5; + +const validateGasLimit = (gasLimit, minimumGasLimit) => { + const gasLimitTooLow = conversionLessThan( + { value: gasLimit, fromNumericBase: 'dec' }, + { value: minimumGasLimit || GAS_LIMITS.SIMPLE, fromNumericBase: 'hex' }, + ); + + if (gasLimitTooLow) return GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS; + return undefined; +}; + +const validateMaxPriorityFee = (maxPriorityFeePerGasToUse, supportsEIP1559) => { + if (supportsEIP1559 && bnLessThanEqualTo(maxPriorityFeePerGasToUse, 0)) { + return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_BELOW_MINIMUM; + } + return undefined; +}; + +const validateMaxFee = ( + maxFeePerGasToUse, + maxPriorityFeeError, + maxPriorityFeePerGasToUse, + supportsEIP1559, +) => { + if (maxPriorityFeeError) return undefined; + if ( + supportsEIP1559 && + bnGreaterThan(maxPriorityFeePerGasToUse, maxFeePerGasToUse) + ) { + return GAS_FORM_ERRORS.MAX_FEE_IMBALANCE; + } + return undefined; +}; + +const validateGasPrice = ( + isFeeMarketGasEstimate, + gasPriceToUse, + supportsEIP1559, + transaction, +) => { + if ( + (!supportsEIP1559 || transaction?.txParams?.gasPrice) && + !isFeeMarketGasEstimate && + bnLessThanEqualTo(gasPriceToUse, 0) + ) { + return GAS_FORM_ERRORS.GAS_PRICE_TOO_LOW; + } + return undefined; +}; + +const getMaxPriorityFeeWarning = ( + gasFeeEstimates, + isFeeMarketGasEstimate, + isGasEstimatesLoading, + maxPriorityFeePerGasToUse, + supportsEIP1559, +) => { + if (!supportsEIP1559 || !isFeeMarketGasEstimate || isGasEstimatesLoading) + return undefined; + if ( + bnLessThan( + maxPriorityFeePerGasToUse, + gasFeeEstimates?.low?.suggestedMaxPriorityFeePerGas, + ) + ) { + return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW; + } + if ( + gasFeeEstimates?.high && + bnGreaterThan( + maxPriorityFeePerGasToUse, + gasFeeEstimates.high.suggestedMaxPriorityFeePerGas * + HIGH_FEE_WARNING_MULTIPLIER, + ) + ) { + return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_HIGH_WARNING; + } + return undefined; +}; + +const getMaxFeeWarning = ( + gasFeeEstimates, + isGasEstimatesLoading, + isFeeMarketGasEstimate, + maxFeeError, + maxPriorityFeeError, + maxFeePerGasToUse, + supportsEIP1559, +) => { + if ( + maxPriorityFeeError || + maxFeeError || + !isFeeMarketGasEstimate || + !supportsEIP1559 + ) { + return undefined; + } + if ( + !isGasEstimatesLoading && + bnLessThan(maxFeePerGasToUse, gasFeeEstimates?.low?.suggestedMaxFeePerGas) + ) { + return GAS_FORM_ERRORS.MAX_FEE_TOO_LOW; + } + if ( + gasFeeEstimates?.high && + bnGreaterThan( + maxFeePerGasToUse, + gasFeeEstimates.high.suggestedMaxFeePerGas * HIGH_FEE_WARNING_MULTIPLIER, + ) + ) { + return GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING; + } + return undefined; +}; + +const getBalanceError = (minimumCostInHexWei, transaction, ethBalance) => { + const minimumTxCostInHexWei = addHexes( + minimumCostInHexWei, + transaction?.txParams?.value || '0x0', + ); + + return conversionGreaterThan( + { value: minimumTxCostInHexWei, fromNumericBase: 'hex' }, + { value: ethBalance, fromNumericBase: 'hex' }, + ); +}; + +/** + * @typedef {Object} GasFeeErrorsReturnType + * @property {Object} [gasErrors] - combined map of errors and warnings. + * @property {boolean} [hasGasErrors] - true if there are errors that can block submission. + * @property {Object} gasWarnings - map of gas warnings for EIP-1559 fields. + * @property {boolean} [balanceError] - true if user balance is less than transaction value. + * @property {boolean} [estimatesUnavailableWarning] - true if supportsEIP1559 is true and + * estimate is not of type fee-market. + */ +export function useGasFeeErrors({ + transaction, + gasEstimateType, + gasFeeEstimates, + gasLimit, + gasPriceToUse, + isGasEstimatesLoading, + maxPriorityFeePerGasToUse, + maxFeePerGasToUse, + minimumCostInHexWei, + minimumGasLimit, +}) { + const supportsEIP1559 = + useSelector(checkNetworkAndAccountSupports1559) && + !isLegacyTransaction(transaction?.txParams); + + const isFeeMarketGasEstimate = + gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET; + + // Get all errors + const gasLimitError = validateGasLimit(gasLimit, minimumGasLimit); + + const maxPriorityFeeError = validateMaxPriorityFee( + maxPriorityFeePerGasToUse, + supportsEIP1559, + ); + + const maxFeeError = validateMaxFee( + maxFeePerGasToUse, + maxPriorityFeeError, + maxPriorityFeePerGasToUse, + supportsEIP1559, + ); + + const gasPriceError = validateGasPrice( + isFeeMarketGasEstimate, + gasPriceToUse, + supportsEIP1559, + transaction, + ); + + // Get all warnings + const maxPriorityFeeWarning = getMaxPriorityFeeWarning( + gasFeeEstimates, + isFeeMarketGasEstimate, + isGasEstimatesLoading, + maxPriorityFeePerGasToUse, + supportsEIP1559, + ); + + const maxFeeWarning = getMaxFeeWarning( + gasFeeEstimates, + isGasEstimatesLoading, + isFeeMarketGasEstimate, + maxFeeError, + maxPriorityFeeError, + maxFeePerGasToUse, + supportsEIP1559, + ); + + // Separating errors from warnings so we can know which value problems + // are blocking or simply useful information for the users + + const gasErrors = useMemo(() => { + const errors = {}; + if (gasLimitError) errors.gasLimit = gasLimitError; + if (maxPriorityFeeError) errors.maxPriorityFee = maxPriorityFeeError; + if (maxFeeError) errors.maxFee = maxFeeError; + if (gasPriceError) errors.gasPrice = gasPriceError; + return errors; + }, [gasLimitError, maxPriorityFeeError, maxFeeError, gasPriceError]); + + const gasWarnings = useMemo(() => { + const warnings = {}; + if (maxPriorityFeeWarning) warnings.maxPriorityFee = maxPriorityFeeWarning; + if (maxFeeWarning) warnings.maxFee = maxFeeWarning; + return warnings; + }, [maxPriorityFeeWarning, maxFeeWarning]); + + const estimatesUnavailableWarning = + supportsEIP1559 && !isFeeMarketGasEstimate; + + // Determine if we have any errors which should block submission + const hasGasErrors = Boolean(Object.keys(gasErrors).length); + + // Combine the warnings and errors into one object for easier use within the UI. + // This object should have no effect on whether or not the user can submit the form + const errorsAndWarnings = useMemo( + () => ({ + ...gasWarnings, + ...gasErrors, + }), + [gasErrors, gasWarnings], + ); + + const { balance: ethBalance } = useSelector(getSelectedAccount); + const balanceError = getBalanceError( + minimumCostInHexWei, + transaction, + ethBalance, + ); + + return { + gasErrors: errorsAndWarnings, + hasGasErrors, + gasWarnings, + balanceError, + estimatesUnavailableWarning, + }; +} diff --git a/ui/hooks/useGasFeeErrors.test.js b/ui/hooks/useGasFeeErrors.test.js new file mode 100644 index 000000000..7e3c0e832 --- /dev/null +++ b/ui/hooks/useGasFeeErrors.test.js @@ -0,0 +1,364 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useSelector } from 'react-redux'; + +import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas'; +import { GAS_FORM_ERRORS } from '../helpers/constants/gas'; +import { + checkNetworkAndAccountSupports1559, + getSelectedAccount, +} from '../selectors'; + +import { useGasFeeErrors } from './useGasFeeErrors'; + +jest.mock('./useGasFeeEstimates', () => ({ + useGasFeeEstimates: jest.fn(), +})); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + + return { + ...actual, + useSelector: jest.fn(), + }; +}); + +const LEGACY_GAS_ESTIMATE_RETURN_VALUE = { + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + gasFeeEstimates: { + low: '10', + medium: '20', + high: '30', + }, + estimatedGasFeeTimeBounds: {}, +}; + +const FEE_MARKET_ESTIMATE_RETURN_VALUE = { + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates: { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', + }, + estimatedGasFeeTimeBounds: {}, +}; + +const generateUseSelectorRouter = ({ + checkNetworkAndAccountSupports1559Response, +} = {}) => (selector) => { + if (selector === getSelectedAccount) { + return { + balance: '0x440aa47cc2556', + }; + } + if (selector === checkNetworkAndAccountSupports1559) { + return checkNetworkAndAccountSupports1559Response; + } + return undefined; +}; + +const configureEIP1559 = () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + checkNetworkAndAccountSupports1559Response: true, + }), + ); +}; + +const configureLegacy = () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + checkNetworkAndAccountSupports1559Response: false, + }), + ); +}; + +const renderUseGasFeeErrorsHook = (props) => { + return renderHook(() => + useGasFeeErrors({ + transaction: { txParams: { type: '0x2', value: '100' } }, + gasLimit: '21000', + gasPriceToUse: '10', + maxPriorityFeePerGasToUse: '10', + maxFeePerGasToUse: '100', + minimumCostInHexWei: '0x5208', + minimumGasLimit: '0x5208', + ...FEE_MARKET_ESTIMATE_RETURN_VALUE, + ...props, + }), + ); +}; + +describe('useGasFeeErrors', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('gasLimit validation', () => { + beforeEach(() => { + configureLegacy(); + }); + it('does not returns gasLimitError if gasLimit is not below minimum', () => { + const { result } = renderUseGasFeeErrorsHook( + LEGACY_GAS_ESTIMATE_RETURN_VALUE, + ); + expect(result.current.gasErrors.gasLimit).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + it('returns gasLimitError if gasLimit is below minimum', () => { + const { result } = renderUseGasFeeErrorsHook({ + gasLimit: '100', + ...LEGACY_GAS_ESTIMATE_RETURN_VALUE, + }); + expect(result.current.gasErrors.gasLimit).toBe( + GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS, + ); + expect(result.current.hasGasErrors).toBe(true); + }); + }); + + describe('maxPriorityFee validation', () => { + describe('EIP1559 compliant estimates', () => { + beforeEach(() => { + configureEIP1559(); + }); + it('does not return maxPriorityFeeError if maxPriorityFee is not 0', () => { + const { result } = renderUseGasFeeErrorsHook(); + expect(result.current.gasErrors.maxPriorityFee).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + it('return maxPriorityFeeError if maxPriorityFee is 0', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxPriorityFeePerGasToUse: '0', + }); + expect(result.current.gasErrors.maxPriorityFee).toBe( + GAS_FORM_ERRORS.MAX_PRIORITY_FEE_BELOW_MINIMUM, + ); + expect(result.current.hasGasErrors).toBe(true); + }); + }); + describe('Legacy estimates', () => { + beforeEach(() => { + configureLegacy(); + }); + it('does not return maxPriorityFeeError if maxPriorityFee is 0', () => { + const { result } = renderUseGasFeeErrorsHook( + LEGACY_GAS_ESTIMATE_RETURN_VALUE, + ); + expect(result.current.gasErrors.maxPriorityFee).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + }); + }); + + describe('maxFee validation', () => { + describe('EIP1559 compliant estimates', () => { + beforeEach(() => { + configureEIP1559(); + }); + it('does not return maxFeeError if maxFee is greater than maxPriorityFee', () => { + const { result } = renderUseGasFeeErrorsHook(); + expect(result.current.gasErrors.maxFee).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + it('return maxFeeError if maxFee is less than maxPriorityFee', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxFeePerGasToUse: '1', + maxPriorityFeePerGasToUse: '10', + }); + expect(result.current.gasErrors.maxFee).toBe( + GAS_FORM_ERRORS.MAX_FEE_IMBALANCE, + ); + expect(result.current.hasGasErrors).toBe(true); + }); + it('does not return MAX_FEE_IMBALANCE error if maxPriorityFeePerGasToUse is 0', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxFeePerGasToUse: '1', + maxPriorityFeePerGasToUse: '0', + }); + expect(result.current.gasErrors.maxFee).toBeUndefined(); + }); + }); + describe('Legacy estimates', () => { + beforeEach(() => { + configureLegacy(); + }); + it('does not return maxFeeError if maxFee is less than maxPriorityFee', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxFeePerGasToUse: '1', + maxPriorityFeePerGasToUse: '10', + ...LEGACY_GAS_ESTIMATE_RETURN_VALUE, + }); + expect(result.current.gasErrors.maxFee).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + }); + }); + + describe('gasPrice validation', () => { + describe('EIP1559 compliant estimates', () => { + beforeEach(() => { + configureEIP1559(); + }); + it('does not return gasPriceError if gasPrice is 0', () => { + const { result } = renderUseGasFeeErrorsHook({ gasPriceToUse: '0' }); + expect(result.current.gasErrors.gasPrice).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + }); + describe('Legacy estimates', () => { + beforeEach(() => { + configureLegacy(); + }); + it('returns gasPriceError if gasPrice is 0', () => { + const { result } = renderUseGasFeeErrorsHook({ + gasPriceToUse: '0', + ...LEGACY_GAS_ESTIMATE_RETURN_VALUE, + }); + expect(result.current.gasErrors.gasPrice).toBe( + GAS_FORM_ERRORS.GAS_PRICE_TOO_LOW, + ); + expect(result.current.hasGasErrors).toBe(true); + }); + it('does not return gasPriceError if gasPrice is > 0', () => { + const { result } = renderUseGasFeeErrorsHook( + LEGACY_GAS_ESTIMATE_RETURN_VALUE, + ); + expect(result.current.gasErrors.gasPrice).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + }); + }); + + describe('maxPriorityFee warning', () => { + describe('EIP1559 compliant estimates', () => { + beforeEach(() => { + configureEIP1559(); + }); + it('does not return maxPriorityFeeWarning if maxPriorityFee is > suggestedMaxPriorityFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook(); + expect(result.current.gasWarnings.maxPriorityFee).toBeUndefined(); + }); + it('return maxPriorityFeeWarning if maxPriorityFee is < suggestedMaxPriorityFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxPriorityFeePerGasToUse: '1', + }); + expect(result.current.gasWarnings.maxPriorityFee).toBe( + GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW, + ); + }); + it('return maxPriorityFeeWarning if maxPriorityFee is > gasFeeEstimates.high.suggestedMaxPriorityFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxPriorityFeePerGasToUse: '100', + }); + expect(result.current.gasWarnings.maxPriorityFee).toBe( + GAS_FORM_ERRORS.MAX_PRIORITY_FEE_HIGH_WARNING, + ); + }); + }); + describe('Legacy estimates', () => { + beforeEach(() => { + configureLegacy(); + }); + it('does not return maxPriorityFeeWarning if maxPriorityFee is < gasFeeEstimates.low.suggestedMaxPriorityFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxPriorityFeePerGasToUse: '1', + ...LEGACY_GAS_ESTIMATE_RETURN_VALUE, + }); + expect(result.current.gasWarnings.maxPriorityFee).toBeUndefined(); + expect(result.current.hasGasErrors).toBe(false); + }); + }); + }); + + describe('maxFee warning', () => { + describe('EIP1559 compliant estimates', () => { + beforeEach(() => { + configureEIP1559(); + }); + it('does not return maxFeeWarning if maxFee is > suggestedMaxFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook(); + expect(result.current.gasWarnings.maxFee).toBeUndefined(); + }); + it('return maxFeeWarning if maxFee is < suggestedMaxFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxFeePerGasToUse: '20', + }); + expect(result.current.gasWarnings.maxFee).toBe( + GAS_FORM_ERRORS.MAX_FEE_TOO_LOW, + ); + }); + it('return maxFeeWarning if gasFeeEstimates are high and maxFee is > suggestedMaxFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxFeePerGasToUse: '1000', + }); + expect(result.current.gasWarnings.maxFee).toBe( + GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING, + ); + }); + }); + describe('Legacy estimates', () => { + beforeEach(() => { + configureLegacy(); + }); + it('does not return maxFeeWarning if maxFee is < suggestedMaxFeePerGas', () => { + const { result } = renderUseGasFeeErrorsHook({ + maxFeePerGasToUse: '1', + ...LEGACY_GAS_ESTIMATE_RETURN_VALUE, + }); + expect(result.current.gasWarnings.maxFee).toBeUndefined(); + }); + }); + }); + + describe('Balance Error', () => { + it('is false if balance is greater than transaction value', () => { + configureEIP1559(); + const { result } = renderUseGasFeeErrorsHook(); + expect(result.current.balanceError).toBe(false); + }); + it('is true if balance is less than transaction value', () => { + configureLegacy(); + const { result } = renderUseGasFeeErrorsHook({ + transaction: { txParams: { type: '0x2', value: '0x440aa47cc2556' } }, + ...LEGACY_GAS_ESTIMATE_RETURN_VALUE, + }); + expect(result.current.balanceError).toBe(true); + }); + }); + + describe('estimatesUnavailableWarning', () => { + it('is false if supportsEIP1559 and gasEstimateType is fee-market', () => { + configureEIP1559(); + const { result } = renderUseGasFeeErrorsHook(); + expect(result.current.estimatesUnavailableWarning).toBe(false); + }); + it('is true if supportsEIP1559 and gasEstimateType is not fee-market', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + checkNetworkAndAccountSupports1559Response: true, + }), + ); + const { result } = renderUseGasFeeErrorsHook( + LEGACY_GAS_ESTIMATE_RETURN_VALUE, + ); + expect(result.current.estimatesUnavailableWarning).toBe(true); + }); + }); +}); diff --git a/ui/hooks/useGasFeeInputs.js b/ui/hooks/useGasFeeInputs.js index f58106123..6f52425bf 100644 --- a/ui/hooks/useGasFeeInputs.js +++ b/ui/hooks/useGasFeeInputs.js @@ -2,16 +2,8 @@ import { addHexPrefix } from 'ethereumjs-util'; import { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { isEqual } from 'lodash'; -import { - GAS_ESTIMATE_TYPES, - EDIT_GAS_MODES, - GAS_LIMITS, -} from '../../shared/constants/gas'; -import { - multiplyCurrencies, - conversionLessThan, - conversionGreaterThan, -} from '../../shared/modules/conversion.utils'; +import { GAS_ESTIMATE_TYPES, EDIT_GAS_MODES } from '../../shared/constants/gas'; +import { multiplyCurrencies } from '../../shared/modules/conversion.utils'; import { getMaximumGasTotalInHexWei, getMinimumGasTotalInHexWei, @@ -20,7 +12,6 @@ import { PRIMARY, SECONDARY } from '../helpers/constants/common'; import { checkNetworkAndAccountSupports1559, getShouldShowFiat, - getSelectedAccount, getAdvancedInlineGasShown, } from '../selectors'; @@ -29,21 +20,14 @@ import { decGWEIToHexWEI, decimalToHex, hexToDecimal, - addHexes, } from '../helpers/utils/conversions.util'; -import { - bnGreaterThan, - bnLessThan, - bnLessThanEqualTo, -} from '../helpers/utils/util'; import { GAS_FORM_ERRORS } from '../helpers/constants/gas'; import { isLegacyTransaction } from '../helpers/utils/transactions.util'; import { useCurrencyDisplay } from './useCurrencyDisplay'; import { useGasFeeEstimates } from './useGasFeeEstimates'; import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; - -const HIGH_FEE_WARNING_MULTIPLIER = 1.5; +import { useGasFeeErrors } from './useGasFeeErrors'; /** * Opaque string type representing a decimal (base 10) number in GWEI @@ -155,7 +139,6 @@ export function useGasFeeInputs( minimumGasLimit = '0x5208', editGasMode, ) { - const { balance: ethBalance } = useSelector(getSelectedAccount); const supportsEIP1559 = useSelector(checkNetworkAndAccountSupports1559) && !isLegacyTransaction(transaction?.txParams); @@ -380,102 +363,24 @@ export function useGasFeeInputs( }, ); - let estimatesUnavailableWarning = null; - - // Separating errors from warnings so we can know which value problems - // are blocking or simply useful information for the users - const gasErrors = {}; - const gasWarnings = {}; - - const gasLimitTooLow = conversionLessThan( - { value: gasLimit, fromNumericBase: 'dec' }, - { value: minimumGasLimit || GAS_LIMITS.SIMPLE, fromNumericBase: 'hex' }, - ); - - if (gasLimitTooLow) { - gasErrors.gasLimit = GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS; - } - - // This ensures these are applied when the api fails to return a fee market type - // It is okay if these errors get overwritten below, as those overwrites can only - // happen when the estimate api is live. - if (supportsEIP1559) { - if (bnLessThanEqualTo(maxPriorityFeePerGasToUse, 0)) { - gasErrors.maxPriorityFee = GAS_FORM_ERRORS.MAX_PRIORITY_FEE_BELOW_MINIMUM; - } else if (bnGreaterThan(maxPriorityFeePerGasToUse, maxFeePerGasToUse)) { - gasErrors.maxFee = GAS_FORM_ERRORS.MAX_FEE_IMBALANCE; - } - } - - if (supportsEIP1559 && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { - if (bnLessThanEqualTo(maxPriorityFeePerGasToUse, 0)) { - gasErrors.maxPriorityFee = GAS_FORM_ERRORS.MAX_PRIORITY_FEE_BELOW_MINIMUM; - } else if ( - !isGasEstimatesLoading && - bnLessThan( - maxPriorityFeePerGasToUse, - gasFeeEstimates?.low?.suggestedMaxPriorityFeePerGas, - ) - ) { - gasWarnings.maxPriorityFee = GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW; - } else if (bnGreaterThan(maxPriorityFeePerGasToUse, maxFeePerGasToUse)) { - gasErrors.maxFee = GAS_FORM_ERRORS.MAX_FEE_IMBALANCE; - } else if ( - gasFeeEstimates?.high && - bnGreaterThan( - maxPriorityFeePerGasToUse, - gasFeeEstimates.high.suggestedMaxPriorityFeePerGas * - HIGH_FEE_WARNING_MULTIPLIER, - ) - ) { - gasWarnings.maxPriorityFee = - GAS_FORM_ERRORS.MAX_PRIORITY_FEE_HIGH_WARNING; - } - - if ( - !isGasEstimatesLoading && - bnLessThan(maxFeePerGasToUse, gasFeeEstimates?.low?.suggestedMaxFeePerGas) - ) { - gasWarnings.maxFee = GAS_FORM_ERRORS.MAX_FEE_TOO_LOW; - } else if ( - gasFeeEstimates?.high && - bnGreaterThan( - maxFeePerGasToUse, - gasFeeEstimates.high.suggestedMaxFeePerGas * - HIGH_FEE_WARNING_MULTIPLIER, - ) - ) { - gasWarnings.maxFee = GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING; - } - } else if (supportsEIP1559) { - estimatesUnavailableWarning = true; - } else if ( - (!supportsEIP1559 || transaction?.txParams?.gasPrice) && - bnLessThanEqualTo(gasPriceToUse, 0) - ) { - gasErrors.gasPrice = GAS_FORM_ERRORS.GAS_PRICE_TOO_LOW; - } - - // Determine if we have any errors which should block submission - const hasBlockingGasErrors = Boolean(Object.keys(gasErrors).length); - - // Now that we've determined errors that block submission, we can pool the warnings - // and errors into one object for easier use within the UI. This object should have - // no effect on whether or not the user can submit the form - const errorsAndWarnings = { - ...gasWarnings, - ...gasErrors, - }; - - const minimumTxCostInHexWei = addHexes( + const { + gasErrors, + hasGasErrors, + gasWarnings, + balanceError, + estimatesUnavailableWarning, + } = useGasFeeErrors({ + transaction, + gasEstimateType, + gasFeeEstimates, + gasLimit, + gasPriceToUse, + isGasEstimatesLoading, + maxPriorityFeePerGasToUse, + maxFeePerGasToUse, minimumCostInHexWei, - transaction?.txParams?.value || '0x0', - ); - - const balanceError = conversionGreaterThan( - { value: minimumTxCostInHexWei, fromNumericBase: 'hex' }, - { value: ethBalance, fromNumericBase: 'hex' }, - ); + minimumGasLimit, + }); const handleGasLimitOutOfBoundError = useCallback(() => { if (gasErrors.gasLimit === GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS) { @@ -521,9 +426,6 @@ export function useGasFeeInputs( gasFeeEstimates, gasEstimateType, estimatedGasFeeTimeBounds, - gasErrors: errorsAndWarnings, - hasGasErrors: hasBlockingGasErrors, - gasWarnings, onManualChange: () => { setInternalEstimateToUse('custom'); handleGasLimitOutOfBoundError(); @@ -534,8 +436,11 @@ export function useGasFeeInputs( setMaxPriorityFeePerGas(maxPriorityFeePerGasToUse); setGasPriceHasBeenManuallySet(true); }, + estimatedBaseFee: gasSettings.baseFeePerGas, + gasErrors, + hasGasErrors, + gasWarnings, balanceError, estimatesUnavailableWarning, - estimatedBaseFee: gasSettings.baseFeePerGas, }; }