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. */ /** * @param options * @param options.transaction * @param options.gasEstimateType * @param options.gasFeeEstimates * @param options.gasLimit * @param options.gasPriceToUse * @param options.isGasEstimatesLoading * @param options.maxPriorityFeePerGasToUse * @param options.maxFeePerGasToUse * @param options.minimumCostInHexWei * @param options.minimumGasLimit * @returns {GasFeeErrorsReturnType} */ 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, }; }