1
0
Fork 0
metamask-extension/ui/hooks/gasFeeInput/useGasFeeErrors.js

304 lines
7.7 KiB
JavaScript

import { useMemo } from 'react';
import { shallowEqual, 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 = (maxPriorityFeePerGas, supportsEIP1559) => {
if (!supportsEIP1559) {
return undefined;
}
if (bnLessThanEqualTo(maxPriorityFeePerGas, 0)) {
return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_BELOW_MINIMUM;
}
return undefined;
};
const validateMaxFee = (
maxFeePerGas,
maxPriorityFeeError,
maxPriorityFeePerGas,
supportsEIP1559,
) => {
if (maxPriorityFeeError || !supportsEIP1559) {
return undefined;
}
if (bnGreaterThan(maxPriorityFeePerGas, maxFeePerGas)) {
return GAS_FORM_ERRORS.MAX_FEE_IMBALANCE;
}
return undefined;
};
const validateGasPrice = (
isFeeMarketGasEstimate,
gasPrice,
supportsEIP1559,
transaction,
) => {
if (supportsEIP1559 && isFeeMarketGasEstimate) {
return undefined;
}
if (
(!supportsEIP1559 || transaction?.txParams?.gasPrice) &&
bnLessThanEqualTo(gasPrice, 0)
) {
return GAS_FORM_ERRORS.GAS_PRICE_TOO_LOW;
}
return undefined;
};
const getMaxPriorityFeeWarning = (
gasFeeEstimates,
isFeeMarketGasEstimate,
isGasEstimatesLoading,
maxPriorityFeePerGas,
supportsEIP1559,
) => {
if (!supportsEIP1559 || !isFeeMarketGasEstimate || isGasEstimatesLoading) {
return undefined;
}
if (
bnLessThan(
maxPriorityFeePerGas,
gasFeeEstimates?.low?.suggestedMaxPriorityFeePerGas,
)
) {
return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW;
}
if (
gasFeeEstimates?.high &&
bnGreaterThan(
maxPriorityFeePerGas,
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,
maxFeePerGas,
supportsEIP1559,
) => {
if (
maxPriorityFeeError ||
maxFeeError ||
!isFeeMarketGasEstimate ||
!supportsEIP1559 ||
isGasEstimatesLoading
) {
return undefined;
}
if (bnLessThan(maxFeePerGas, gasFeeEstimates?.low?.suggestedMaxFeePerGas)) {
return GAS_FORM_ERRORS.MAX_FEE_TOO_LOW;
}
if (
gasFeeEstimates?.high &&
bnGreaterThan(
maxFeePerGas,
gasFeeEstimates.high.suggestedMaxFeePerGas * HIGH_FEE_WARNING_MULTIPLIER,
)
) {
return GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING;
}
return undefined;
};
const hasBalanceError = (minimumCostInHexWei, transaction, ethBalance) => {
if (minimumCostInHexWei === undefined || ethBalance === undefined) {
return false;
}
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.gasEstimateType
* @param options.gasFeeEstimates
* @param options.isGasEstimatesLoading
* @param options.gasLimit
* @param options.gasPrice
* @param options.maxPriorityFeePerGas
* @param options.maxFeePerGas
* @param options.minimumCostInHexWei
* @param options.minimumGasLimit
* @param options.transaction
* @returns {GasFeeErrorsReturnType}
*/
export function useGasFeeErrors({
gasEstimateType,
gasFeeEstimates,
isGasEstimatesLoading,
gasLimit,
gasPrice,
maxPriorityFeePerGas,
maxFeePerGas,
minimumCostInHexWei,
minimumGasLimit,
transaction,
}) {
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(
maxPriorityFeePerGas,
supportsEIP1559,
);
const maxFeeError = validateMaxFee(
maxFeePerGas,
maxPriorityFeeError,
maxPriorityFeePerGas,
supportsEIP1559,
);
const gasPriceError = validateGasPrice(
isFeeMarketGasEstimate,
gasPrice,
supportsEIP1559,
transaction,
);
// Get all warnings
const maxPriorityFeeWarning = getMaxPriorityFeeWarning(
gasFeeEstimates,
isFeeMarketGasEstimate,
isGasEstimatesLoading,
maxPriorityFeePerGas,
supportsEIP1559,
);
const maxFeeWarning = getMaxFeeWarning(
gasFeeEstimates,
isGasEstimatesLoading,
isFeeMarketGasEstimate,
maxFeeError,
maxPriorityFeeError,
maxFeePerGas,
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, shallowEqual);
const balanceError = hasBalanceError(
minimumCostInHexWei,
transaction,
ethBalance,
);
return {
gasErrors: errorsAndWarnings,
hasGasErrors,
gasWarnings,
balanceError,
estimatesUnavailableWarning,
hasSimulationError: Boolean(transaction?.simulationFails),
};
}