1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Edit gas fee modal more changes (#12660)

This commit is contained in:
Jyoti Puri 2021-11-19 00:38:29 +05:30 committed by GitHub
parent d6d35d9acb
commit 2c6fb06114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 380 additions and 109 deletions

View File

@ -581,6 +581,9 @@
"dappSuggested": { "dappSuggested": {
"message": "Site suggested" "message": "Site suggested"
}, },
"dappSuggestedShortLabel": {
"message": "Site"
},
"dappSuggestedTooltip": { "dappSuggestedTooltip": {
"message": "$1 has recommended this price.", "message": "$1 has recommended this price.",
"description": "$1 represents the Dapp's origin" "description": "$1 represents the Dapp's origin"
@ -1035,9 +1038,6 @@
"gasPriceInfoTooltipContent": { "gasPriceInfoTooltipContent": {
"message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas." "message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas."
}, },
"gasPriceLabel": {
"message": "Gas price"
},
"gasTimingHoursShort": { "gasTimingHoursShort": {
"message": "$1 hrs", "message": "$1 hrs",
"description": "$1 represents a number of hours" "description": "$1 represents a number of hours"

View File

@ -30,6 +30,17 @@ export const GAS_RECOMMENDATIONS = {
HIGH: 'high', HIGH: 'high',
}; };
/**
* These represent types of gas estimation
*/
export const PRIORITY_LEVELS = {
LOW: 'low',
MEDIUM: 'medium',
HIGH: 'high',
CUSTOM: 'custom',
DAPP_SUGGESTED: 'dappSuggested',
};
/** /**
* Represents the user customizing their gas preference * Represents the user customizing their gas preference
*/ */

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PRIORITY_LEVELS } from '../../../../shared/constants/gas';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import Popover from '../../ui/popover'; import Popover from '../../ui/popover';
import I18nValue from '../../ui/i18n-value'; import I18nValue from '../../ui/i18n-value';
@ -32,9 +33,27 @@ const EditGasFeePopover = ({ onClose }) => {
<I18nValue messageKey="maxFee" /> <I18nValue messageKey="maxFee" />
</span> </span>
</div> </div>
<EditGasItem estimateType="low" onClose={onClose} /> <EditGasItem
<EditGasItem estimateType="medium" onClose={onClose} /> priorityLevel={PRIORITY_LEVELS.LOW}
<EditGasItem estimateType="high" onClose={onClose} /> onClose={onClose}
/>
<EditGasItem
priorityLevel={PRIORITY_LEVELS.MEDIUM}
onClose={onClose}
/>
<EditGasItem
priorityLevel={PRIORITY_LEVELS.HIGH}
onClose={onClose}
/>
<div className="edit-gas-fee-popover__content__separator" />
<EditGasItem
priorityLevel={PRIORITY_LEVELS.DAPP_SUGGESTED}
onClose={onClose}
/>
<EditGasItem
priorityLevel={PRIORITY_LEVELS.CUSTOM}
onClose={onClose}
/>
</div> </div>
</div> </div>
</> </>

View File

@ -71,16 +71,18 @@ describe('EditGasFeePopover', () => {
expect(screen.queryByText('🐢')).toBeInTheDocument(); expect(screen.queryByText('🐢')).toBeInTheDocument();
expect(screen.queryByText('🦊')).toBeInTheDocument(); expect(screen.queryByText('🦊')).toBeInTheDocument();
expect(screen.queryByText('🦍')).toBeInTheDocument(); expect(screen.queryByText('🦍')).toBeInTheDocument();
expect(screen.queryByText('🌐')).toBeInTheDocument();
expect(screen.queryByText('⚙')).toBeInTheDocument();
expect(screen.queryByText('Low')).toBeInTheDocument(); expect(screen.queryByText('Low')).toBeInTheDocument();
expect(screen.queryByText('Market')).toBeInTheDocument(); expect(screen.queryByText('Market')).toBeInTheDocument();
expect(screen.queryByText('Aggressive')).toBeInTheDocument(); expect(screen.queryByText('Aggressive')).toBeInTheDocument();
expect(screen.queryByText('Site')).toBeInTheDocument();
expect(screen.queryByText('Advanced')).toBeInTheDocument();
}); });
it('should show time estimates', () => { it('should show time estimates', () => {
renderComponent(); renderComponent();
console.log(document.body.innerHTML); expect(screen.queryAllByText('5 min')).toHaveLength(2);
expect(screen.queryByText('6 min')).toBeInTheDocument();
expect(screen.queryByText('30 sec')).toBeInTheDocument();
expect(screen.queryByText('15 sec')).toBeInTheDocument(); expect(screen.queryByText('15 sec')).toBeInTheDocument();
}); });

View File

@ -1,14 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils'; import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils';
import { PRIORITY_LEVELS } from '../../../../../shared/constants/gas';
import { PRIORITY_LEVEL_ICON_MAP } from '../../../../helpers/constants/gas'; import { PRIORITY_LEVEL_ICON_MAP } from '../../../../helpers/constants/gas';
import { PRIMARY } from '../../../../helpers/constants/common'; import { PRIMARY } from '../../../../helpers/constants/common';
import { import {
decGWEIToHexWEI, decGWEIToHexWEI,
decimalToHex, decimalToHex,
hexWEIToDecGWEI,
} from '../../../../helpers/utils/conversions.util'; } from '../../../../helpers/utils/conversions.util';
import { getAdvancedGasFeeValues } from '../../../../selectors';
import { toHumanReadableTime } from '../../../../helpers/utils/util'; import { toHumanReadableTime } from '../../../../helpers/utils/util';
import { useGasFeeContext } from '../../../../contexts/gasFee'; import { useGasFeeContext } from '../../../../contexts/gasFee';
import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useI18nContext } from '../../../../hooks/useI18nContext';
@ -16,59 +20,121 @@ import I18nValue from '../../../ui/i18n-value';
import InfoTooltip from '../../../ui/info-tooltip'; import InfoTooltip from '../../../ui/info-tooltip';
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'; import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display';
const EditGasItem = ({ estimateType, onClose }) => { import { useCustomTimeEstimate } from './useCustomTimeEstimate';
const EditGasItem = ({ priorityLevel, onClose }) => {
const { const {
estimateUsed, estimateUsed,
gasFeeEstimates, gasFeeEstimates,
gasLimit, gasLimit,
setEstimateToUse, maxFeePerGas: maxFeePerGasValue,
updateTransaction, maxPriorityFeePerGas: maxPriorityFeePerGasValue,
updateTransactionUsingGasFeeEstimates,
transaction: { dappSuggestedGasFees },
} = useGasFeeContext(); } = useGasFeeContext();
const t = useI18nContext(); const t = useI18nContext();
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues);
let maxFeePerGas;
let maxPriorityFeePerGas;
let minWaitTime;
const { minWaitTimeEstimate, suggestedMaxFeePerGas } = if (gasFeeEstimates[priorityLevel]) {
gasFeeEstimates[estimateType] || {}; maxFeePerGas = gasFeeEstimates[priorityLevel].suggestedMaxFeePerGas;
const hexMaximumTransactionFee = suggestedMaxFeePerGas } else if (
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
dappSuggestedGasFees
) {
maxFeePerGas = hexWEIToDecGWEI(dappSuggestedGasFees.maxFeePerGas);
maxPriorityFeePerGas = hexWEIToDecGWEI(
dappSuggestedGasFees.maxPriorityFeePerGas,
);
} else if (priorityLevel === PRIORITY_LEVELS.CUSTOM) {
if (estimateUsed === PRIORITY_LEVELS.CUSTOM) {
maxFeePerGas = maxFeePerGasValue;
maxPriorityFeePerGas = maxPriorityFeePerGasValue;
} else if (advancedGasFeeValues) {
maxFeePerGas =
gasFeeEstimates.estimatedBaseFee *
parseFloat(advancedGasFeeValues.maxBaseFee);
maxPriorityFeePerGas = advancedGasFeeValues.priorityFee;
}
}
const { waitTimeEstimate } = useCustomTimeEstimate({
gasFeeEstimates,
maxFeePerGas,
maxPriorityFeePerGas,
});
if (gasFeeEstimates[priorityLevel]) {
minWaitTime =
priorityLevel === PRIORITY_LEVELS.HIGH
? gasFeeEstimates?.high.minWaitTimeEstimate
: gasFeeEstimates?.low.maxWaitTimeEstimate;
} else {
minWaitTime = waitTimeEstimate;
}
const hexMaximumTransactionFee = maxFeePerGas
? getMaximumGasTotalInHexWei({ ? getMaximumGasTotalInHexWei({
gasLimit: decimalToHex(gasLimit), gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(suggestedMaxFeePerGas), maxFeePerGas: decGWEIToHexWEI(maxFeePerGas),
}) })
: null; : null;
const onOptionSelect = () => { const onOptionSelect = () => {
setEstimateToUse(estimateType); if (priorityLevel !== PRIORITY_LEVELS.CUSTOM) {
updateTransaction(estimateType); updateTransactionUsingGasFeeEstimates(priorityLevel);
}
// todo: open advance modal if priorityLevel is custom
onClose(); onClose();
}; };
return ( return (
<div <div
className={classNames('edit-gas-item', { className={classNames('edit-gas-item', {
'edit-gas-item--selected': estimateType === estimateUsed, 'edit-gas-item-selected': priorityLevel === estimateUsed,
'edit-gas-item-disabled':
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
!dappSuggestedGasFees,
})} })}
role="button" role="button"
onClick={onOptionSelect} onClick={onOptionSelect}
> >
<span className="edit-gas-item__name"> <span className="edit-gas-item__name">
<span className="edit-gas-item__icon"> <span
{PRIORITY_LEVEL_ICON_MAP[estimateType]} className={`edit-gas-item__icon edit-gas-item__icon-${priorityLevel}`}
>
{PRIORITY_LEVEL_ICON_MAP[priorityLevel]}
</span> </span>
<I18nValue messageKey={estimateType} /> <I18nValue
</span> messageKey={
<span priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED
className={`edit-gas-item__time-estimate edit-gas-item__time-estimate-${estimateType}`} ? 'dappSuggestedShortLabel'
> : priorityLevel
{minWaitTimeEstimate && toHumanReadableTime(t, minWaitTimeEstimate)} }
</span>
<span
className={`edit-gas-item__fee-estimate edit-gas-item__fee-estimate-${estimateType}`}
>
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
/> />
</span> </span>
<span
className={`edit-gas-item__time-estimate edit-gas-item__time-estimate-${priorityLevel}`}
>
{minWaitTime
? minWaitTime && toHumanReadableTime(t, minWaitTime)
: '--'}
</span>
<span
className={`edit-gas-item__fee-estimate edit-gas-item__fee-estimate-${priorityLevel}`}
>
{hexMaximumTransactionFee ? (
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
/>
) : (
'--'
)}
</span>
<span className="edit-gas-item__tooltip"> <span className="edit-gas-item__tooltip">
<InfoTooltip position="top" /> <InfoTooltip position="top" />
</span> </span>
@ -77,7 +143,7 @@ const EditGasItem = ({ estimateType, onClose }) => {
}; };
EditGasItem.propTypes = { EditGasItem.propTypes = {
estimateType: PropTypes.string, priorityLevel: PropTypes.string,
onClose: PropTypes.func, onClose: PropTypes.func,
}; };

View File

@ -14,6 +14,9 @@ jest.mock('../../../../store/actions', () => ({
.fn() .fn()
.mockImplementation(() => Promise.resolve()), .mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(), addPollingTokenToAppState: jest.fn(),
getGasFeeTimeEstimate: jest
.fn()
.mockImplementation(() => Promise.resolve('unknown')),
})); }));
const MOCK_FEE_ESTIMATE = { const MOCK_FEE_ESTIMATE = {
@ -38,7 +41,12 @@ const MOCK_FEE_ESTIMATE = {
estimatedBaseFee: '50', estimatedBaseFee: '50',
}; };
const renderComponent = (props) => { const DAPP_SUGGESTED_ESTIMATE = {
maxFeePerGas: '0x59682f10',
maxPriorityFeePerGas: '0x59682f00',
};
const renderComponent = (props, transactionProps, gasFeeContextProps) => {
const store = configureStore({ const store = configureStore({
metamask: { metamask: {
nativeCurrency: ETH, nativeCurrency: ETH,
@ -53,41 +61,78 @@ const renderComponent = (props) => {
selectedAddress: '0xAddress', selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true }, featureFlags: { advancedInlineGas: true },
gasFeeEstimates: MOCK_FEE_ESTIMATE, gasFeeEstimates: MOCK_FEE_ESTIMATE,
advancedGasFee: {
maxBaseFee: '1.5',
priorityFee: '2',
},
}, },
}); });
return renderWithProvider( return renderWithProvider(
<GasFeeContextProvider transaction={{ txParams: { gas: '0x5208' } }}> <GasFeeContextProvider
<EditGasItem estimateType="low" {...props} /> transaction={{ txParams: { gas: '0x5208' }, ...transactionProps }}
{...gasFeeContextProps}
>
<EditGasItem priorityLevel="low" {...props} />
</GasFeeContextProvider>, </GasFeeContextProvider>,
store, store,
); );
}; };
describe('EditGasItem', () => { describe('EditGasItem', () => {
it('should renders low gas estimate options for estimateType low', () => { it('should renders low gas estimate option for priorityLevel low', () => {
renderComponent({ estimateType: 'low' }); renderComponent({ priorityLevel: 'low' });
expect(screen.queryByText('🐢')).toBeInTheDocument(); expect(screen.queryByText('🐢')).toBeInTheDocument();
expect(screen.queryByText('Low')).toBeInTheDocument(); expect(screen.queryByText('Low')).toBeInTheDocument();
expect(screen.queryByText('6 min')).toBeInTheDocument(); expect(screen.queryByText('5 min')).toBeInTheDocument();
expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument(); expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument();
}); });
it('should renders market gas estimate options for estimateType medium', () => { it('should renders market gas estimate option for priorityLevel medium', () => {
renderComponent({ estimateType: 'medium' }); renderComponent({ priorityLevel: 'medium' });
expect(screen.queryByText('🦊')).toBeInTheDocument(); expect(screen.queryByText('🦊')).toBeInTheDocument();
expect(screen.queryByText('Market')).toBeInTheDocument(); expect(screen.queryByText('Market')).toBeInTheDocument();
expect(screen.queryByText('30 sec')).toBeInTheDocument(); expect(screen.queryByText('5 min')).toBeInTheDocument();
expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument(); expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument();
}); });
it('should renders aggressive gas estimate options for estimateType high', () => { it('should renders aggressive gas estimate option for priorityLevel high', () => {
renderComponent({ estimateType: 'high' }); renderComponent({ priorityLevel: 'high' });
expect(screen.queryByText('🦍')).toBeInTheDocument(); expect(screen.queryByText('🦍')).toBeInTheDocument();
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
expect(screen.queryByText('15 sec')).toBeInTheDocument(); expect(screen.queryByText('15 sec')).toBeInTheDocument();
expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument();
}); });
it('should highlight option is priorityLevel is currently selected', () => {
renderComponent({ priorityLevel: 'high' }, { userFeeLevel: 'high' });
expect(
document.getElementsByClassName('edit-gas-item-selected'),
).toHaveLength(1);
});
it('should renders site gas estimate option for priorityLevel dappSuggested', () => {
renderComponent(
{ priorityLevel: 'dappSuggested' },
{ dappSuggestedGasFees: DAPP_SUGGESTED_ESTIMATE },
);
expect(screen.queryByText('🌐')).toBeInTheDocument();
expect(screen.queryByText('Site')).toBeInTheDocument();
expect(screen.queryByTitle('0.0000315 ETH')).toBeInTheDocument();
});
it('should disable site gas estimate option for is transaction does not have dappSuggestedGasFees', async () => {
renderComponent({ priorityLevel: 'dappSuggested' });
expect(
document.getElementsByClassName('edit-gas-item-disabled'),
).toHaveLength(1);
});
it('should renders advance gas estimate option for priorityLevel custom', () => {
renderComponent({ priorityLevel: 'custom' });
expect(screen.queryByText('⚙')).toBeInTheDocument();
expect(screen.queryByText('Advanced')).toBeInTheDocument();
// below value of custom gas fee estimate is default obtained from state.metamask.advancedGasFee
expect(screen.queryByTitle('0.001575 ETH')).toBeInTheDocument();
});
}); });

View File

@ -3,6 +3,8 @@
color: $ui-4; color: $ui-4;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
display: flex;
align-items: center;
margin: 12px 0; margin: 12px 0;
padding: 4px 12px; padding: 4px 12px;
height: 32px; height: 32px;
@ -11,8 +13,13 @@
background-color: $ui-1; background-color: $ui-1;
} }
&-disabled {
cursor: default;
}
&__name { &__name {
display: inline-block; display: inline-flex;
align-items: center;
color: $ui-black; color: $ui-black;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
@ -21,6 +28,11 @@
&__icon { &__icon {
margin-right: 4px; margin-right: 4px;
&-custom {
font-size: 20px;
line-height: 0;
}
} }
&__time-estimate { &__time-estimate {

View File

@ -0,0 +1,83 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import BigNumber from 'bignumber.js';
import { GAS_ESTIMATE_TYPES } from '../../../../../shared/constants/gas';
import {
getGasEstimateType,
getIsGasEstimatesLoading,
} from '../../../../ducks/metamask/metamask';
import { getGasFeeTimeEstimate } from '../../../../store/actions';
export const useCustomTimeEstimate = ({
gasFeeEstimates,
maxFeePerGas,
maxPriorityFeePerGas,
}) => {
const gasEstimateType = useSelector(getGasEstimateType);
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
const returnNoEstimates =
isGasEstimatesLoading ||
gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET ||
!maxPriorityFeePerGas;
// 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);
useEffect(() => {
if (
isGasEstimatesLoading ||
gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET ||
!maxPriorityFeePerGas
)
return;
if (isUnknownLow) {
// getGasFeeTimeEstimate requires parameters in string format
getGasFeeTimeEstimate(
new BigNumber(maxPriorityFeePerGas, 10).toString(10),
new BigNumber(maxFeePerGas, 10).toString(10),
).then((result) => {
setCustomEstimatedTime(result);
});
}
}, [
gasEstimateType,
isUnknownLow,
isGasEstimatesLoading,
maxFeePerGas,
maxPriorityFeePerGas,
returnNoEstimates,
]);
if (returnNoEstimates) {
return {};
}
const { low = {}, medium = {}, high = {} } = gasFeeEstimates;
let waitTimeEstimate = '';
if (
isUnknownLow &&
customEstimatedTime &&
customEstimatedTime !== 'unknown' &&
customEstimatedTime?.upperTimeBound !== 'unknown'
) {
waitTimeEstimate = Number(customEstimatedTime?.upperTimeBound);
} else if (
Number(maxPriorityFeePerGas) >= Number(medium.suggestedMaxPriorityFeePerGas)
) {
waitTimeEstimate = high.minWaitTimeEstimate;
} else {
waitTimeEstimate = low.maxWaitTimeEstimate;
}
return { waitTimeEstimate };
};

View File

@ -31,5 +31,10 @@
width: 30%; width: 30%;
} }
} }
&__separator {
border-top: 1px solid $ui-grey;
margin: 8px 12px;
}
} }
} }

View File

@ -17,12 +17,10 @@ export default function TransactionDetail({ rows = [], onEdit }) {
const t = useI18nContext(); const t = useI18nContext();
const { const {
gasLimit, gasLimit,
gasPrice,
estimateUsed, estimateUsed,
maxFeePerGas, maxFeePerGas,
maxPriorityFeePerGas, maxPriorityFeePerGas,
transaction, transaction,
supportsEIP1559,
} = useGasFeeContext(); } = useGasFeeContext();
if (EIP_1559_V2 && estimateUsed) { if (EIP_1559_V2 && estimateUsed) {
@ -48,23 +46,14 @@ export default function TransactionDetail({ rows = [], onEdit }) {
<Typography fontSize="12px" color={COLORS.GREY}> <Typography fontSize="12px" color={COLORS.GREY}>
{t('dappSuggestedTooltip', [transaction.origin])} {t('dappSuggestedTooltip', [transaction.origin])}
</Typography> </Typography>
{supportsEIP1559 ? ( <Typography fontSize="12px">
<> <b>{t('maxBaseFee')}</b>
<Typography fontSize="12px"> {maxFeePerGas}
<b>{t('maxBaseFee')}</b> </Typography>
{maxFeePerGas} <Typography fontSize="12px">
</Typography> <b>{t('maxPriorityFee')}</b>
<Typography fontSize="12px"> {maxPriorityFeePerGas}
<b>{t('maxPriorityFee')}</b> </Typography>
{maxPriorityFeePerGas}
</Typography>
</>
) : (
<Typography fontSize="12px">
<b>{t('gasPriceLabel')}</b>
{gasPrice}
</Typography>
)}
<Typography fontSize="12px"> <Typography fontSize="12px">
<b>{t('gasLimit')}</b> <b>{t('gasLimit')}</b>
{gasLimit} {gasLimit}

View File

@ -31,7 +31,7 @@ export function useGasFeeContext() {
GasFeeContextProvider.propTypes = { GasFeeContextProvider.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
defaultEstimateToUse: PropTypes.string, defaultEstimateToUse: PropTypes.string,
transaction: PropTypes.object.isRequired, transaction: PropTypes.object,
minimumGasLimit: PropTypes.string, minimumGasLimit: PropTypes.string,
editGasMode: PropTypes.string, editGasMode: PropTypes.string,
}; };

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
@ -108,6 +108,19 @@ export function useGasFeeInputs(
return estimateToUse; return estimateToUse;
}); });
/**
* In EIP-1559 V2 designs change to gas estimate is always updated to transaction
* Thus callback setEstimateToUse can be deprecate in favour of this useEffect
* so that transaction is source of truth whenever possible.
*/
useEffect(() => {
if (areDappSuggestedAndTxParamGasFeesTheSame(transaction)) {
setEstimateUsed('dappSuggested');
} else if (transaction?.userFeeLevel) {
setEstimateUsed(transaction?.userFeeLevel);
}
}, [setEstimateUsed, transaction]);
const [gasLimit, setGasLimit] = useState(() => const [gasLimit, setGasLimit] = useState(() =>
Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')), Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')),
); );
@ -198,7 +211,7 @@ export function useGasFeeInputs(
} }
}, [minimumGasLimit, gasErrors.gasLimit, transaction]); }, [minimumGasLimit, gasErrors.gasLimit, transaction]);
const { updateTransaction } = useTransactionFunctions({ const { updateTransactionUsingGasFeeEstimates } = useTransactionFunctions({
defaultEstimateToUse, defaultEstimateToUse,
gasLimit, gasLimit,
gasPrice, gasPrice,
@ -289,6 +302,6 @@ export function useGasFeeInputs(
gasWarnings, gasWarnings,
hasGasErrors, hasGasErrors,
supportsEIP1559, supportsEIP1559,
updateTransaction, updateTransactionUsingGasFeeEstimates,
}; };
} }

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { PRIORITY_LEVELS } from '../../../shared/constants/gas';
import { import {
decGWEIToHexWEI, decGWEIToHexWEI,
decimalToHex, decimalToHex,
@ -16,24 +17,19 @@ export const useTransactionFunctions = ({
const dispatch = useDispatch(); const dispatch = useDispatch();
const updateTransaction = useCallback( const updateTransaction = useCallback(
(estimateType) => { (estimateUsed, maxFeePerGas, maxPriorityFeePerGas) => {
const newGasSettings = { const newGasSettings = {
gas: decimalToHex(gasLimit), gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit), gasLimit: decimalToHex(gasLimit),
estimateSuggested: defaultEstimateToUse, estimateSuggested: defaultEstimateToUse,
estimateUsed: estimateType, estimateUsed,
maxFeePerGas,
maxPriorityFeePerGas,
}; };
newGasSettings.maxFeePerGas = decGWEIToHexWEI(
gasFeeEstimates[estimateType].suggestedMaxFeePerGas,
);
newGasSettings.maxPriorityFeePerGas = decGWEIToHexWEI(
gasFeeEstimates[estimateType].suggestedMaxPriorityFeePerGas,
);
const updatedTxMeta = { const updatedTxMeta = {
...transaction, ...transaction,
userFeeLevel: estimateType || 'custom', userFeeLevel: estimateUsed || 'custom',
txParams: { txParams: {
...transaction.txParams, ...transaction.txParams,
...newGasSettings, ...newGasSettings,
@ -42,8 +38,35 @@ export const useTransactionFunctions = ({
dispatch(updateTransactionFn(updatedTxMeta)); dispatch(updateTransactionFn(updatedTxMeta));
}, },
[defaultEstimateToUse, dispatch, gasLimit, gasFeeEstimates, transaction], [defaultEstimateToUse, dispatch, gasLimit, transaction],
); );
return { updateTransaction }; const updateTransactionUsingGasFeeEstimates = useCallback(
(gasFeeEstimateToUse) => {
if (gasFeeEstimateToUse === PRIORITY_LEVELS.DAPP_SUGGESTED) {
const {
maxFeePerGas,
maxPriorityFeePerGas,
} = transaction?.dappSuggestedGasFees;
updateTransaction(
PRIORITY_LEVELS.CUSTOM,
maxFeePerGas,
maxPriorityFeePerGas,
);
} else {
const {
suggestedMaxFeePerGas,
suggestedMaxPriorityFeePerGas,
} = gasFeeEstimates[gasFeeEstimateToUse];
updateTransaction(
gasFeeEstimateToUse,
decGWEIToHexWEI(suggestedMaxFeePerGas),
decGWEIToHexWEI(suggestedMaxPriorityFeePerGas),
);
}
},
[gasFeeEstimates, transaction?.dappSuggestedGasFees, updateTransaction],
);
return { updateTransactionUsingGasFeeEstimates };
}; };

View File

@ -26,7 +26,6 @@ const GasDetailsItem = ({
isMainnet, isMainnet,
maxFeePerGas, maxFeePerGas,
maxPriorityFeePerGas, maxPriorityFeePerGas,
supportsEIP1559,
txData, txData,
useNativeCurrencyAsPrimaryCurrency, useNativeCurrencyAsPrimaryCurrency,
}) => { }) => {
@ -120,16 +119,14 @@ const GasDetailsItem = ({
</Box>, </Box>,
])} ])}
subTitle={ subTitle={
supportsEIP1559 && ( <GasTiming
<GasTiming maxPriorityFeePerGas={hexWEIToDecGWEI(
maxPriorityFeePerGas={hexWEIToDecGWEI( maxPriorityFeePerGas || txData.txParams.maxPriorityFeePerGas,
maxPriorityFeePerGas || txData.txParams.maxPriorityFeePerGas, )}
)} maxFeePerGas={hexWEIToDecGWEI(
maxFeePerGas={hexWEIToDecGWEI( maxFeePerGas || txData.txParams.maxFeePerGas,
maxFeePerGas || txData.txParams.maxFeePerGas, )}
)} />
/>
)
} }
/> />
); );
@ -141,7 +138,6 @@ GasDetailsItem.propTypes = {
isMainnet: PropTypes.bool, isMainnet: PropTypes.bool,
maxFeePerGas: PropTypes.string, maxFeePerGas: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string, maxPriorityFeePerGas: PropTypes.string,
supportsEIP1559: PropTypes.bool,
txData: PropTypes.object, txData: PropTypes.object,
useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { screen } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { ETH } from '../../../helpers/constants/common'; import { ETH } from '../../../helpers/constants/common';
import { GasFeeContextProvider } from '../../../contexts/gasFee'; import { GasFeeContextProvider } from '../../../contexts/gasFee';
@ -14,6 +14,7 @@ jest.mock('../../../store/actions', () => ({
.fn() .fn()
.mockImplementation(() => Promise.resolve()), .mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(), addPollingTokenToAppState: jest.fn(),
getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()),
})); }));
const render = (props) => { const render = (props) => {
@ -37,28 +38,34 @@ const render = (props) => {
return renderWithProvider( return renderWithProvider(
<GasFeeContextProvider {...props}> <GasFeeContextProvider {...props}>
<GasDetailsItem txData={{}} {...props} /> <GasDetailsItem txData={{ txParams: {} }} {...props} />
</GasFeeContextProvider>, </GasFeeContextProvider>,
store, store,
); );
}; };
describe('GasDetailsItem', () => { describe('GasDetailsItem', () => {
it('should render label', () => { it('should render label', async () => {
render(); render();
expect(screen.queryByText('Gas')).toBeInTheDocument(); await waitFor(() => {
expect(screen.queryByText('(estimated)')).toBeInTheDocument(); expect(screen.queryByText('Gas')).toBeInTheDocument();
expect(screen.queryByText('Max fee:')).toBeInTheDocument(); expect(screen.queryByText('(estimated)')).toBeInTheDocument();
expect(screen.queryByText('ETH')).toBeInTheDocument(); expect(screen.queryByText('Max fee:')).toBeInTheDocument();
expect(screen.queryByText('ETH')).toBeInTheDocument();
});
}); });
it('should show warning icon if estimates are high', () => { it('should show warning icon if estimates are high', async () => {
render({ defaultEstimateToUse: 'high' }); render({ defaultEstimateToUse: 'high' });
expect(screen.queryByText('⚠ Max fee:')).toBeInTheDocument(); await waitFor(() => {
expect(screen.queryByText('⚠ Max fee:')).toBeInTheDocument();
});
}); });
it('should not show warning icon if estimates are not high', () => { it('should not show warning icon if estimates are not high', async () => {
render({ defaultEstimateToUse: 'low' }); render({ defaultEstimateToUse: 'low' });
expect(screen.queryByText('Max fee:')).toBeInTheDocument(); await waitFor(() => {
expect(screen.queryByText('Max fee:')).toBeInTheDocument();
});
}); });
}); });