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

using 1559 V2 for cancel speed up flows (#13019)

This commit is contained in:
Jyoti Puri 2022-01-06 08:17:26 +05:30 committed by GitHub
parent dbfdf3b0eb
commit f5dcd12293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1245 additions and 258 deletions

View File

@ -422,6 +422,14 @@
"cancelSpeedUp": {
"message": "cancel or speed up a tranaction."
},
"cancelSpeedUpLabel": {
"message": "This gas fee will $1 the original.",
"description": "$1 is text 'replace' in bold"
},
"cancelSpeedUpTransactionTooltip": {
"message": "To $1 a transaction the gas fee must be increased by at least 10% for it to be recognized by the network.",
"description": "$1 is string 'cancel' or 'speed up'"
},
"cancellationGasFee": {
"message": "Cancellation Gas Fee"
},
@ -739,6 +747,10 @@
"directDepositEtherExplainer": {
"message": "If you already have some Ether, the quickest way to get Ether in your new wallet by direct deposit."
},
"disabledGasOptionToolTipMessage": {
"message": "“$1” is disabled because it does not meet the minimum of a 10% increase from the original gas fee.",
"description": "$1 is gas estimate type which can be market or aggressive"
},
"disconnect": {
"message": "Disconnect"
},
@ -793,6 +805,9 @@
"editAddressNickname": {
"message": "Edit address nickname"
},
"editCancellationGasFeeModalTitle": {
"message": "Edit cancellation gas fee"
},
"editContact": {
"message": "Edit Contact"
},
@ -921,6 +936,9 @@
"editPermission": {
"message": "Edit Permission"
},
"editSpeedUpEditGasFeeModalTitle": {
"message": "Edit speed up gas fee"
},
"enableAutoDetect": {
"message": " Enable Autodetect"
},
@ -1157,6 +1175,9 @@
"functionType": {
"message": "Function Type"
},
"gas": {
"message": "Gas"
},
"gasDisplayAcknowledgeDappButtonText": {
"message": "Edit suggested gas fee"
},
@ -1709,6 +1730,15 @@
"metametricsTitle": {
"message": "Join 6M+ users to improve MetaMask"
},
"minimum": {
"message": "minimum"
},
"minimumCancelSpeedupGasFee": {
"message": "+10%"
},
"minimumEstimate": {
"message": "10% Minimum"
},
"mismatchedChain": {
"message": "The network details for this chain ID do not match our records. We recommend that you $1 before proceeding.",
"description": "$1 is a clickable link with text defined by the 'mismatchedChainLinkText' key"
@ -2289,6 +2319,9 @@
"removeNFT": {
"message": "Remove NFT"
},
"replace": {
"message": "replace"
},
"requestsAwaitingAcknowledgement": {
"message": "requests waiting to be acknowledged"
},
@ -3169,9 +3202,6 @@
"transactionDetailGasHeading": {
"message": "Estimated gas fee"
},
"transactionDetailGasHeadingV2": {
"message": "Gas"
},
"transactionDetailGasInfoV2": {
"message": "estimated"
},

View File

@ -34,6 +34,7 @@ export const GAS_RECOMMENDATIONS = {
* These represent types of gas estimation
*/
export const PRIORITY_LEVELS = {
MINIMUM: 'minimum',
LOW: 'low',
MEDIUM: 'medium',
HIGH: 'high',

View File

@ -120,7 +120,7 @@ const converter = ({
convertedValue = toSpecifiedDenomination[toDenomination](convertedValue);
}
if (numberOfDecimals) {
if (numberOfDecimals !== undefined && numberOfDecimals !== null) {
convertedValue = convertedValue.round(
numberOfDecimals,
BigNumber.ROUND_HALF_DOWN,

View File

@ -1,5 +1,6 @@
{
"appState": {
"isLoading": false,
"gasIsLoading": false,
"currentView": {
"name": "accountDetail",

View File

@ -14,8 +14,8 @@ import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults';
const AdvancedGasFeePopover = () => {
const t = useI18nContext();
const {
closeModal,
closeAllModals,
closeModal,
currentModal,
} = useTransactionModalContext();

View File

@ -1,11 +1,13 @@
/** Please import your files in alphabetical order **/
@import 'account-list-item/index';
@import 'account-menu/index';
@import 'app-loading-spinner/index';
@import 'import-token-link/index';
@import 'advanced-gas-controls/index';
@import 'alerts/alerts';
@import 'app-header/index';
@import 'asset-list-item/asset-list-item';
@import 'cancel-speedup-popover/index';
@import 'confirm-page-container/index';
@import 'collectibles-items/index';
@import 'collectibles-tab/index';
@ -28,6 +30,8 @@
@import 'gas-customization/gas-modal-page-container/index';
@import 'gas-customization/gas-price-button-group/index';
@import 'gas-customization/index';
@import 'gas-details-item/index';
@import 'gas-details-item/gas-details-item-title/index';
@import 'gas-timing/index';
@import 'home-notification/index';
@import 'info-box/index';

View File

@ -0,0 +1,30 @@
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import { getAppIsLoading } from '../../../selectors';
import Spinner from '../../ui/spinner';
const AppLoadingSpinner = ({ className }) => {
const appIsLoading = useSelector(getAppIsLoading);
if (!appIsLoading) {
return null;
}
return (
<div
className={`${className} app-loading-spinner`}
role="alert"
aria-busy="true"
>
<Spinner color="#F7C06C" className="app-loading-spinner__inner" />
</div>
);
};
AppLoadingSpinner.propTypes = {
className: PropTypes.string,
};
export default AppLoadingSpinner;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import configureStore from '../../../store/store';
import AppLoadingSpinner from './app-loading-spinner';
const render = (params) => {
const store = configureStore({
...params,
});
return renderWithProvider(<AppLoadingSpinner />, store);
};
describe('AppLoadingSpinner', () => {
it('should return null if app state is not loading', () => {
render();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
it('should show spinner if app state is loading', () => {
render({ appState: { isLoading: true } });
expect(screen.queryByRole('alert')).toBeInTheDocument();
});
});

View File

@ -0,0 +1 @@
export { default } from './app-loading-spinner';

View File

@ -0,0 +1,13 @@
.app-loading-spinner {
background-color: rgba(255, 255, 255, 0.75);
display: flex;
align-items: center;
justify-content: center;
position: absolute;
height: 100%;
width: 100%;
&__inner {
width: 50px;
}
}

View File

@ -0,0 +1,159 @@
import { useSelector } from 'react-redux';
import React, { useEffect } from 'react';
import {
EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../../shared/constants/gas';
import {
ALIGN_ITEMS,
DISPLAY,
FLEX_DIRECTION,
TYPOGRAPHY,
} from '../../../helpers/constants/design-system';
import { getAppIsLoading } from '../../../selectors';
import { gasEstimateGreaterThanGasUsedPlusTenPercent } from '../../../helpers/utils/gas';
import { useGasFeeContext } from '../../../contexts/gasFee';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useTransactionModalContext } from '../../../contexts/transaction-modal';
import EditGasFeeButton from '../edit-gas-fee-button';
import GasDetailsItem from '../gas-details-item';
import Box from '../../ui/box';
import Button from '../../ui/button';
import I18nValue from '../../ui/i18n-value';
import InfoTooltip from '../../ui/info-tooltip';
import Popover from '../../ui/popover';
import Typography from '../../ui/typography';
import AppLoadingSpinner from '../app-loading-spinner';
const CancelSpeedupPopover = () => {
const {
cancelTransaction,
editGasMode,
gasFeeEstimates,
speedUpTransaction,
transaction,
updateTransaction,
updateTransactionToMinimumGasFee,
updateTransactionUsingEstimate,
} = useGasFeeContext();
const t = useI18nContext();
const { closeModal, currentModal } = useTransactionModalContext();
const appIsLoading = useSelector(getAppIsLoading);
useEffect(() => {
if (
transaction.previousGas ||
appIsLoading ||
currentModal !== 'cancelSpeedUpTransaction'
) {
return;
}
// If gas used previously + 10% is less than medium estimated gas
// estimate is set to medium, else estimate is set to minimum
const gasUsedLessThanMedium =
gasFeeEstimates &&
gasEstimateGreaterThanGasUsedPlusTenPercent(
transaction,
gasFeeEstimates,
PRIORITY_LEVELS.MEDIUM,
);
if (gasUsedLessThanMedium) {
updateTransactionUsingEstimate(PRIORITY_LEVELS.MEDIUM);
return;
}
updateTransactionToMinimumGasFee();
}, [
appIsLoading,
currentModal,
editGasMode,
gasFeeEstimates,
transaction,
updateTransaction,
updateTransactionToMinimumGasFee,
updateTransactionUsingEstimate,
]);
if (currentModal !== 'cancelSpeedUpTransaction') {
return null;
}
const submitTransactionChange = () => {
if (editGasMode === EDIT_GAS_MODES.CANCEL) {
cancelTransaction();
} else {
speedUpTransaction();
}
closeModal('cancelSpeedUpTransaction');
};
return (
<Popover
title={
<>
{editGasMode === EDIT_GAS_MODES.CANCEL
? `${t('cancel')}`
: `🚀${t('speedUp')}`}
</>
}
onClose={() => closeModal('cancelSpeedUpTransaction')}
className="cancel-speedup-popover"
>
<AppLoadingSpinner className="cancel-speedup-popover__spinner" />
<div className="cancel-speedup-popover__wrapper">
<Typography
boxProps={{ alignItems: ALIGN_ITEMS.CENTER, display: DISPLAY.FLEX }}
variant={TYPOGRAPHY.H6}
margin={[0, 0, 2, 0]}
>
<I18nValue
messageKey="cancelSpeedUpLabel"
options={[
<strong key="cancelSpeedupReplace">
<I18nValue messageKey="replace" />
</strong>,
]}
/>
<InfoTooltip
position="top"
contentText={
<Box>
{t('cancelSpeedUpTransactionTooltip', [
EDIT_GAS_MODES.CANCEL ? t('cancel') : t('speedUp'),
])}
<div>
<a
href="https://community.metamask.io/t/how-to-speed-up-or-cancel-transactions-on-metamask/3296"
target="_blank"
rel="noopener noreferrer"
>
{t('learnMoreUpperCase')}
</a>
</div>
</Box>
}
/>
</Typography>
<div className="cancel-speedup-popover__separator" />
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
flexDirection={FLEX_DIRECTION.COLUMN}
marginTop={4}
>
<Box className="cancel-speedup-popover__edit-gas-button">
<EditGasFeeButton />
</Box>
<Box className="cancel-speedup-popover__gas-details">
<GasDetailsItem />
</Box>
</Box>
<Button type="primary" onClick={submitTransactionChange}>
<I18nValue messageKey="submit" />
</Button>
</div>
</Popover>
);
};
export default CancelSpeedupPopover;

View File

@ -0,0 +1,88 @@
import React from 'react';
import { act, screen } from '@testing-library/react';
import {
EDIT_GAS_MODES,
GAS_ESTIMATE_TYPES,
} from '../../../../shared/constants/gas';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import mockEstimates from '../../../../test/data/mock-estimates.json';
import mockState from '../../../../test/data/mock-state.json';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import configureStore from '../../../store/store';
import CancelSpeedupPopover from './cancel-speedup-popover';
jest.mock('../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(),
removePollingTokenFromAppState: jest.fn(),
updateTransaction: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }),
}));
jest.mock('../../../contexts/transaction-modal', () => ({
useTransactionModalContext: () => ({
closeModal: () => undefined,
currentModal: 'cancelSpeedUpTransaction',
}),
}));
const render = (props) => {
const store = configureStore({
metamask: {
...mockState.metamask,
accounts: {
[mockState.metamask.selectedAddress]: {
address: mockState.metamask.selectedAddress,
balance: '0x1F4',
},
},
featureFlags: { advancedInlineGas: true },
gasFeeEstimates:
mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET].gasFeeEstimates,
},
});
return renderWithProvider(
<GasFeeContextProvider
transaction={{
userFeeLevel: 'minimum',
txParams: {
gas: '0x5208',
maxFeePerGas: '0x59682f10',
maxPriorityFeePerGas: '0x59682f00',
},
}}
editGasMode={EDIT_GAS_MODES.CANCEL}
{...props}
>
<CancelSpeedupPopover />
</GasFeeContextProvider>,
store,
);
};
describe('CancelSpeedupPopover', () => {
it('should have ❌Cancel in header if editGasMode is cancel', async () => {
await act(async () => render());
expect(screen.queryByText('❌Cancel')).toBeInTheDocument();
});
it('should have 🚀Speed Up in header if editGasMode is speedup', async () => {
await act(async () => render({ editGasMode: EDIT_GAS_MODES.SPEED_UP }));
expect(screen.queryByText('🚀Speed Up')).toBeInTheDocument();
});
it('should show correct gas values', async () => {
await act(async () =>
render({
editGasMode: EDIT_GAS_MODES.SPEED_UP,
}),
);
expect(screen.queryAllByTitle('0.0000315 ETH').length).toBeGreaterThan(0);
});
});

View File

@ -0,0 +1 @@
export { default } from './cancel-speedup-popover';

View File

@ -0,0 +1,27 @@
.cancel-speedup-popover {
&__wrapper {
padding: 0 16px 16px;
.info-tooltip {
margin-left: 4px;
}
}
&__edit-gas-button {
align-self: flex-end;
}
&__gas-details {
padding-top: 10px;
}
&__spinner {
margin-top: -30px;
height: calc(100% + 30px);
}
&__separator {
border-bottom: 1px solid $ui-grey;
width: 100%;
}
}

View File

@ -41,14 +41,19 @@ export default function EditGasFeeButton({ userAcknowledgedGasMissing }) {
) {
icon = 'swapSuggested';
title = 'swapSuggested';
} else if (estimateUsed === PRIORITY_LEVELS.MINIMUM) {
icon = undefined;
title = 'minimumEstimate';
}
return (
<div className="edit-gas-fee-button">
<button onClick={() => openModal('editGasFee')}>
{icon && (
<span className="edit-gas-fee-button__icon">
{`${PRIORITY_LEVEL_ICON_MAP[icon]} `}
</span>
)}
<span className="edit-gas-fee-button__label">{t(title)}</span>
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
</button>

View File

@ -73,6 +73,11 @@ describe('EditGasFeeButton', () => {
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
});
it('should render edit link with text 10% Minimum if minimum gas estimates are selected', () => {
render({ contextProps: { transaction: { userFeeLevel: 'minimum' } } });
expect(screen.queryByText('10% Minimum')).toBeInTheDocument();
});
it('should render edit link with text Site suggested if site suggested estimated are used', () => {
render({
contextProps: {
@ -103,6 +108,7 @@ describe('EditGasFeeButton', () => {
render({
contextProps: {
defaultEstimateToUse: 'custom',
transaction: {},
},
});
expect(screen.queryByText('⚙')).toBeInTheDocument();

View File

@ -14,23 +14,39 @@ import Typography from '../../ui/typography/typography';
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../helpers/constants/error-keys';
import { useGasFeeContext } from '../../../contexts/gasFee';
import AppLoadingSpinner from '../app-loading-spinner';
import EditGasItem from './edit-gas-item';
import NetworkStatistics from './network-statistics';
const EditGasFeePopover = () => {
const { balanceError, editGasMode } = useGasFeeContext();
const t = useI18nContext();
const { closeModal, currentModal } = useTransactionModalContext();
const {
closeAllModals,
closeModal,
currentModal,
openModalCount,
} = useTransactionModalContext();
if (currentModal !== 'editGasFee') return null;
let popupTitle = 'editGasFeeModalTitle';
if (editGasMode === EDIT_GAS_MODES.CANCEL) {
popupTitle = 'editCancellationGasFeeModalTitle';
} else if (editGasMode === EDIT_GAS_MODES.SPEED_UP) {
popupTitle = 'editSpeedUpEditGasFeeModalTitle';
}
return (
<Popover
title={t('editGasFeeModalTitle')}
onClose={() => closeModal('editGasFee')}
title={t(popupTitle)}
// below logic ensures that back button is visible only if there are other modals open before this.
onBack={openModalCount === 1 ? undefined : () => closeModal('editGasFee')}
onClose={closeAllModals}
className="edit-gas-fee-popover"
>
<>
<AppLoadingSpinner />
<div className="edit-gas-fee-popover__wrapper">
<div className="edit-gas-fee-popover__content">
{balanceError && (
@ -49,13 +65,17 @@ const EditGasFeePopover = () => {
<I18nValue messageKey="maxFee" />
</span>
</div>
{editGasMode !== EDIT_GAS_MODES.SWAPS && (
{(editGasMode === EDIT_GAS_MODES.CANCEL ||
editGasMode === EDIT_GAS_MODES.SPEED_UP) && (
<EditGasItem priorityLevel={PRIORITY_LEVELS.MINIMUM} />
)}
{editGasMode === EDIT_GAS_MODES.MODIFY_IN_PLACE && (
<EditGasItem priorityLevel={PRIORITY_LEVELS.LOW} />
)}
<EditGasItem priorityLevel={PRIORITY_LEVELS.MEDIUM} />
<EditGasItem priorityLevel={PRIORITY_LEVELS.HIGH} />
<div className="edit-gas-fee-popover__content__separator" />
{editGasMode !== EDIT_GAS_MODES.SWAPS && (
{editGasMode === EDIT_GAS_MODES.MODIFY_IN_PLACE && (
<EditGasItem priorityLevel={PRIORITY_LEVELS.DAPP_SUGGESTED} />
)}
<EditGasItem priorityLevel={PRIORITY_LEVELS.CUSTOM} />

View File

@ -28,22 +28,24 @@ const MOCK_FEE_ESTIMATE = {
low: {
minWaitTimeEstimate: 360000,
maxWaitTimeEstimate: 300000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '53',
suggestedMaxPriorityFeePerGas: 3,
suggestedMaxFeePerGas: 53,
},
medium: {
minWaitTimeEstimate: 30000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '7',
suggestedMaxFeePerGas: '70',
suggestedMaxPriorityFeePerGas: 7,
suggestedMaxFeePerGas: 70,
},
high: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '10',
suggestedMaxFeePerGas: '100',
suggestedMaxPriorityFeePerGas: 10,
suggestedMaxFeePerGas: 100,
},
estimatedBaseFee: '50',
latestPriorityFeeRange: [2, 6],
estimatedBaseFee: 50,
networkCongestion: 0.7,
};
const render = ({ txProps, contextProps } = {}) => {
@ -141,4 +143,47 @@ describe('EditGasFeePopover', () => {
expect(screen.queryByText('Time')).not.toBeInTheDocument();
expect(screen.queryByText('Max fee')).toBeInTheDocument();
});
it('should show correct header for edit gas mode', () => {
render({
contextProps: { editGasMode: EDIT_GAS_MODES.SWAPS },
});
expect(screen.queryByText('Edit gas fee')).toBeInTheDocument();
render({
contextProps: { editGasMode: EDIT_GAS_MODES.CANCEL },
});
expect(screen.queryByText('Edit cancellation gas fee')).toBeInTheDocument();
render({
contextProps: { editGasMode: EDIT_GAS_MODES.SPEED_UP },
});
expect(screen.queryByText('Edit speed up gas fee')).toBeInTheDocument();
});
it('should not show low option for cancel mode', () => {
render({
contextProps: { editGasMode: EDIT_GAS_MODES.CANCEL },
});
expect(screen.queryByText('Low')).not.toBeInTheDocument();
});
it('should not show low option for speedup mode', () => {
render({
contextProps: { editGasMode: EDIT_GAS_MODES.SPEED_UP },
});
expect(screen.queryByText('Low')).not.toBeInTheDocument();
});
it('should show minimum option for cancel gas mode', () => {
render({
contextProps: { editGasMode: EDIT_GAS_MODES.CANCEL },
});
expect(screen.queryByText('(minimum)')).toBeInTheDocument();
});
it('should show minimum option for speedup gas mode', () => {
render({
contextProps: { editGasMode: EDIT_GAS_MODES.SPEED_UP },
});
expect(screen.queryByText('(minimum)')).toBeInTheDocument();
});
});

View File

@ -1,109 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils';
import {
EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../../../shared/constants/gas';
import {
ALIGN_ITEMS,
DISPLAY,
} from '../../../../helpers/constants/design-system';
import { PRIORITY_LEVEL_ICON_MAP } from '../../../../helpers/constants/gas';
import { PRIMARY } from '../../../../helpers/constants/common';
import {
decGWEIToHexWEI,
decimalToHex,
hexWEIToDecGWEI,
} from '../../../../helpers/utils/conversions.util';
import LoadingHeartBeat from '../../../ui/loading-heartbeat';
import { getAdvancedGasFeeValues } from '../../../../selectors';
import { toHumanReadableTime } from '../../../../helpers/utils/util';
import { useGasFeeContext } from '../../../../contexts/gasFee';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { useTransactionModalContext } from '../../../../contexts/transaction-modal';
import I18nValue from '../../../ui/i18n-value';
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display';
import Box from '../../../ui/box';
import EditGasToolTip from '../edit-gas-tooltip/edit-gas-tooltip';
import InfoTooltip from '../../../ui/info-tooltip';
import { useCustomTimeEstimate } from './useCustomTimeEstimate';
import LoadingHeartBeat from '../../../ui/loading-heartbeat';
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display';
import { useGasItemFeeDetails } from './useGasItemFeeDetails';
const getTitleAndIcon = (priorityLevel, t, editGasMode) => {
let icon = priorityLevel;
let title = t(priorityLevel);
if (priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED) {
title = t('dappSuggestedShortLabel');
} else if (priorityLevel === PRIORITY_LEVELS.MINIMUM) {
icon = null;
title = (
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}>
{t('minimumCancelSpeedupGasFee')}
<span className="edit-gas-item__name__sufix">({t('minimum')})</span>
</Box>
);
} else if (
priorityLevel === PRIORITY_LEVELS.HIGH &&
editGasMode === EDIT_GAS_MODES.SWAPS
) {
icon = 'swapSuggested';
title = t('swapSuggested');
}
return { title, icon };
};
const EditGasItem = ({ priorityLevel }) => {
const {
editGasMode,
estimateUsed,
gasFeeEstimates,
gasLimit,
maxFeePerGas: maxFeePerGasValue,
maxPriorityFeePerGas: maxPriorityFeePerGasValue,
updateTransactionUsingGasFeeEstimates,
updateTransactionToMinimumGasFee,
updateTransactionUsingDAPPSuggestedValues,
updateTransactionUsingEstimate,
transaction,
} = useGasFeeContext();
const t = useI18nContext();
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues);
const { closeModal, openModal } = useTransactionModalContext();
const { dappSuggestedGasFees } = transaction;
let maxFeePerGas;
let maxPriorityFeePerGas;
let minWaitTime;
if (gasFeeEstimates?.[priorityLevel]) {
maxFeePerGas = gasFeeEstimates[priorityLevel].suggestedMaxFeePerGas;
maxPriorityFeePerGas =
gasFeeEstimates[priorityLevel].suggestedMaxPriorityFeePerGas;
} else if (
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
dappSuggestedGasFees
) {
maxFeePerGas = hexWEIToDecGWEI(
dappSuggestedGasFees.maxFeePerGas || dappSuggestedGasFees.gasPrice,
);
maxPriorityFeePerGas = hexWEIToDecGWEI(
dappSuggestedGasFees.maxPriorityFeePerGas || maxFeePerGas,
);
} 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,
const {
// for cancel or speedup estimateGreaterThaGasUse is true if previous gas used
// was more than estimate for the priorityLevel
estimateGreaterThanGasUse,
hexMaximumTransactionFee,
maxFeePerGas,
maxPriorityFeePerGas,
});
if (gasFeeEstimates[priorityLevel]) {
minWaitTime =
priorityLevel === PRIORITY_LEVELS.HIGH
? gasFeeEstimates?.high.minWaitTimeEstimate
: gasFeeEstimates?.low.maxWaitTimeEstimate;
} else {
minWaitTime = waitTimeEstimate;
}
const hexMaximumTransactionFee = maxFeePerGas
? getMaximumGasTotalInHexWei({
gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas),
})
: null;
const onOptionSelect = () => {
if (priorityLevel === PRIORITY_LEVELS.CUSTOM) {
openModal('advancedGasFee');
} else {
updateTransactionUsingGasFeeEstimates(priorityLevel);
closeModal('editGasFee');
}
};
minWaitTime,
} = useGasItemFeeDetails(priorityLevel);
if (
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
@ -112,34 +78,44 @@ const EditGasItem = ({ priorityLevel }) => {
return null;
}
let icon = priorityLevel;
let title = priorityLevel;
if (priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED) {
title = 'dappSuggestedShortLabel';
} else if (
priorityLevel === PRIORITY_LEVELS.HIGH &&
editGasMode === EDIT_GAS_MODES.SWAPS
) {
icon = 'swapSuggested';
title = 'swapSuggested';
const onOptionSelect = () => {
if (priorityLevel === PRIORITY_LEVELS.CUSTOM) {
openModal('advancedGasFee');
} else {
closeModal('editGasFee');
if (priorityLevel === PRIORITY_LEVELS.MINIMUM) {
updateTransactionToMinimumGasFee();
} else if (priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED) {
updateTransactionUsingDAPPSuggestedValues();
} else {
updateTransactionUsingEstimate(priorityLevel);
}
}
};
const { title, icon } = getTitleAndIcon(priorityLevel, t, editGasMode);
return (
<button
className={classNames('edit-gas-item', {
'edit-gas-item--selected': priorityLevel === estimateUsed,
'edit-gas-item--disabled': estimateGreaterThanGasUse,
})}
onClick={onOptionSelect}
aria-label={priorityLevel}
autoFocus={priorityLevel === estimateUsed}
disabled={estimateGreaterThanGasUse}
>
<span className="edit-gas-item__name">
{icon && (
<span
className={`edit-gas-item__icon edit-gas-item__icon-${priorityLevel}`}
>
{PRIORITY_LEVEL_ICON_MAP[icon]}
</span>
<I18nValue messageKey={title} />
)}
{title}
</span>
<span
className={`edit-gas-item__time-estimate edit-gas-item__time-estimate-${priorityLevel}`}
@ -174,6 +150,7 @@ const EditGasItem = ({ priorityLevel }) => {
editGasMode={editGasMode}
gasLimit={gasLimit}
transaction={transaction}
estimateGreaterThanGasUse={estimateGreaterThanGasUse}
/>
}
position="top"

View File

@ -42,7 +42,7 @@ const MOCK_FEE_ESTIMATE = {
estimatedBaseFee: '50',
};
const DAPP_SUGGESTED_ESTIMATE = {
const ESTIMATE_MOCK = {
maxFeePerGas: '0x59682f10',
maxPriorityFeePerGas: '0x59682f00',
};
@ -65,6 +65,7 @@ const renderComponent = ({
},
selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true },
gasEstimateType: 'fee-market',
gasFeeEstimates: MOCK_FEE_ESTIMATE,
advancedGasFee: {
maxBaseFee: '1.5',
@ -139,7 +140,7 @@ describe('EditGasItem', () => {
it('should renders site gas estimate option for priorityLevel dappSuggested', () => {
renderComponent({
componentProps: { priorityLevel: 'dappSuggested' },
transactionProps: { dappSuggestedGasFees: DAPP_SUGGESTED_ESTIMATE },
transactionProps: { dappSuggestedGasFees: ESTIMATE_MOCK },
});
expect(
screen.queryByRole('button', { name: 'dappSuggested' }),
@ -162,4 +163,21 @@ describe('EditGasItem', () => {
// below value of custom gas fee estimate is default obtained from state.metamask.advancedGasFee
expect(screen.queryByTitle('0.001575 ETH')).toBeInTheDocument();
});
it('should renders +10% gas estimate option for priorityLevel minimum', () => {
renderComponent({
componentProps: { priorityLevel: 'minimum' },
transactionProps: {
userFeeLevel: 'minimum',
previousGas: ESTIMATE_MOCK,
},
contextProps: { editGasMode: EDIT_GAS_MODES.CANCEL },
});
expect(
screen.queryByRole('button', { name: 'minimum' }),
).toBeInTheDocument();
expect(screen.queryByText('+10%')).toBeInTheDocument();
expect(screen.queryByText('(minimum)')).toBeInTheDocument();
expect(screen.queryByTitle('0.00003465 ETH')).toBeInTheDocument();
});
});

View File

@ -19,6 +19,11 @@
background-color: $ui-1;
}
button.edit-gas-item--disabled[disabled] {
opacity: 0.25;
pointer-events: none;
}
&__name {
display: inline-flex;
align-items: center;
@ -27,6 +32,11 @@
font-weight: bold;
white-space: nowrap;
width: 36%;
&__sufix {
font-weight: 400;
margin-left: 4px;
}
}
&__icon {

View File

@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import {
EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../../../shared/constants/gas';
import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils';
import {
decGWEIToHexWEI,
decimalToHex,
hexWEIToDecGWEI,
} from '../../../../helpers/utils/conversions.util';
import {
addTenPercentAndRound,
gasEstimateGreaterThanGasUsedPlusTenPercent,
} from '../../../../helpers/utils/gas';
import { getAdvancedGasFeeValues } from '../../../../selectors';
import { useGasFeeContext } from '../../../../contexts/gasFee';
import { useCustomTimeEstimate } from './useCustomTimeEstimate';
export const useGasItemFeeDetails = (priorityLevel) => {
const {
editGasMode,
estimateUsed,
gasFeeEstimates,
gasLimit,
maxFeePerGas: maxFeePerGasValue,
maxPriorityFeePerGas: maxPriorityFeePerGasValue,
transaction,
} = useGasFeeContext();
const [estimateGreaterThanGasUse, setEstimateGreaterThanGasUse] = useState(
false,
);
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues);
let maxFeePerGas;
let maxPriorityFeePerGas;
let minWaitTime;
const { dappSuggestedGasFees } = transaction;
if (gasFeeEstimates?.[priorityLevel]) {
maxFeePerGas = gasFeeEstimates[priorityLevel].suggestedMaxFeePerGas;
maxPriorityFeePerGas =
gasFeeEstimates[priorityLevel].suggestedMaxPriorityFeePerGas;
} else if (
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
dappSuggestedGasFees
) {
maxFeePerGas = hexWEIToDecGWEI(
dappSuggestedGasFees.maxFeePerGas || dappSuggestedGasFees.gasPrice,
);
maxPriorityFeePerGas = hexWEIToDecGWEI(
dappSuggestedGasFees.maxPriorityFeePerGas || maxFeePerGas,
);
} 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;
}
} else if (
priorityLevel === PRIORITY_LEVELS.MINIMUM &&
transaction.previousGas
) {
maxFeePerGas = hexWEIToDecGWEI(
addTenPercentAndRound(transaction.previousGas?.maxFeePerGas),
);
maxPriorityFeePerGas = hexWEIToDecGWEI(
addTenPercentAndRound(transaction.previousGas?.maxPriorityFeePerGas),
);
}
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({
gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas),
})
: null;
useEffect(() => {
// For cancel and speed-up medium / high option is disabled if
// gas used in transaction + 10% is greater tham estimate
if (
(editGasMode === EDIT_GAS_MODES.CANCEL ||
editGasMode === EDIT_GAS_MODES.SPEED_UP) &&
(priorityLevel === PRIORITY_LEVELS.MEDIUM ||
priorityLevel === PRIORITY_LEVELS.HIGH)
) {
const estimateGreater = !gasEstimateGreaterThanGasUsedPlusTenPercent(
transaction,
gasFeeEstimates,
priorityLevel,
);
setEstimateGreaterThanGasUse(estimateGreater);
}
}, [editGasMode, gasFeeEstimates, priorityLevel, transaction]);
return {
estimateGreaterThanGasUse,
maxFeePerGas,
maxPriorityFeePerGas,
minWaitTime,
hexMaximumTransactionFee,
};
};

View File

@ -12,6 +12,8 @@ import {
import Typography from '../../../ui/typography';
const EditGasToolTip = ({
editGasMode,
estimateGreaterThanGasUse,
gasLimit,
priorityLevel,
// maxFeePerGas & maxPriorityFeePerGas are derived from conditional logic
@ -19,7 +21,6 @@ const EditGasToolTip = ({
// the parent component (edit-gas-item) rather than recalculate them
maxFeePerGas,
maxPriorityFeePerGas,
editGasMode,
transaction,
t,
}) => {
@ -32,12 +33,26 @@ const EditGasToolTip = ({
</span>,
]);
case PRIORITY_LEVELS.MEDIUM:
if (estimateGreaterThanGasUse) {
return t('disabledGasOptionToolTipMessage', [
<span key={`disabled-priority-level-${priorityLevel}`}>
{t(priorityLevel)}
</span>,
]);
}
return t('mediumGasSettingToolTipMessage', [
<span key={priorityLevel}>
<b>{t('medium')}</b>
</span>,
]);
case PRIORITY_LEVELS.HIGH:
if (estimateGreaterThanGasUse) {
return t('disabledGasOptionToolTipMessage', [
<span key={`disabled-priority-level-${priorityLevel}`}>
{t(priorityLevel)}
</span>,
]);
}
if (editGasMode === EDIT_GAS_MODES.SWAPS) {
return t('swapSuggestedGasSettingToolTipMessage');
}
@ -69,11 +84,13 @@ const EditGasToolTip = ({
!(
priorityLevel === PRIORITY_LEVELS.HIGH &&
editGasMode === EDIT_GAS_MODES.SWAPS
) ? (
) &&
!estimateGreaterThanGasUse ? (
<img alt="" src={`./images/curve-${priorityLevel}.svg`} />
) : null}
{priorityLevel === PRIORITY_LEVELS.HIGH &&
editGasMode !== EDIT_GAS_MODES.SWAPS ? (
editGasMode !== EDIT_GAS_MODES.SWAPS &&
!estimateGreaterThanGasUse ? (
<div className="edit-gas-tooltip__container__dialog">
<Typography variant={TYPOGRAPHY.H7} color={COLORS.WHITE}>
{t('highGasSettingToolTipDialog')}
@ -83,7 +100,8 @@ const EditGasToolTip = ({
<div className="edit-gas-tooltip__container__message">
<Typography variant={TYPOGRAPHY.H7}>{toolTipMessage()}</Typography>
</div>
{priorityLevel === PRIORITY_LEVELS.CUSTOM ? null : (
{priorityLevel === PRIORITY_LEVELS.CUSTOM ||
estimateGreaterThanGasUse ? null : (
<div className="edit-gas-tooltip__container__values">
<div>
<Typography
@ -140,9 +158,13 @@ const EditGasToolTip = ({
};
EditGasToolTip.propTypes = {
estimateGreaterThanGasUse: PropTypes.bool,
priorityLevel: PropTypes.string,
maxFeePerGas: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string,
maxFeePerGas: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxPriorityFeePerGas: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
t: PropTypes.func,
editGasMode: PropTypes.string,
gasLimit: PropTypes.number,

View File

@ -4,10 +4,10 @@ import { useSelector } from 'react-redux';
import { TYPOGRAPHY } from '../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { getIsMainnet } from '../../../../selectors';
import Box from '../../../../components/ui/box';
import I18nValue from '../../../../components/ui/i18n-value';
import InfoTooltip from '../../../../components/ui/info-tooltip/info-tooltip';
import Typography from '../../../../components/ui/typography/typography';
import Box from '../../../ui/box';
import I18nValue from '../../../ui/i18n-value';
import InfoTooltip from '../../../ui/info-tooltip/info-tooltip';
import Typography from '../../../ui/typography/typography';
const GasDetailsItemTitle = () => {
const t = useI18nContext();
@ -16,7 +16,7 @@ const GasDetailsItemTitle = () => {
return (
<Box display="flex">
<Box marginRight={1}>
<I18nValue messageKey="transactionDetailGasHeadingV2" />
<I18nValue messageKey="gas" />
</Box>
<span className="gas-details-item-title__estimate">
(<I18nValue messageKey="transactionDetailGasInfoV2" />)

View File

@ -1,29 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { COLORS } from '../../../helpers/constants/design-system';
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common';
import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util';
import Box from '../../../components/ui/box';
import GasTiming from '../../../components/app/gas-timing/gas-timing.component';
import I18nValue from '../../../components/ui/i18n-value';
import LoadingHeartBeat from '../../../components/ui/loading-heartbeat';
import TransactionDetailItem from '../../../components/app/transaction-detail-item/transaction-detail-item.component';
import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display';
import { getPreferences } from '../../../selectors';
import { useGasFeeContext } from '../../../contexts/gasFee';
import Box from '../../ui/box';
import I18nValue from '../../ui/i18n-value';
import LoadingHeartBeat from '../../ui/loading-heartbeat';
import GasTiming from '../gas-timing/gas-timing.component';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display';
import GasDetailsItemTitle from './gas-details-item-title';
const GasDetailsItem = ({
hexMaximumTransactionFee,
hexMinimumTransactionFee,
maxFeePerGas,
maxPriorityFeePerGas,
userAcknowledgedGasMissing,
useNativeCurrencyAsPrimaryCurrency,
}) => {
const { estimateUsed, hasSimulationError, transaction } = useGasFeeContext();
const GasDetailsItem = ({ userAcknowledgedGasMissing = false }) => {
const {
estimateUsed,
hasSimulationError,
maximumCostInHexWei: hexMaximumTransactionFee,
minimumCostInHexWei: hexMinimumTransactionFee,
transaction,
} = useGasFeeContext();
const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences);
if (hasSimulationError && !userAcknowledgedGasMissing) return null;
@ -86,11 +89,9 @@ const GasDetailsItem = ({
subTitle={
<GasTiming
maxPriorityFeePerGas={hexWEIToDecGWEI(
maxPriorityFeePerGas || transaction.txParams.maxPriorityFeePerGas,
)}
maxFeePerGas={hexWEIToDecGWEI(
maxFeePerGas || transaction.txParams.maxFeePerGas,
transaction.txParams.maxPriorityFeePerGas,
)}
maxFeePerGas={hexWEIToDecGWEI(transaction.txParams.maxFeePerGas)}
/>
}
/>
@ -98,12 +99,7 @@ const GasDetailsItem = ({
};
GasDetailsItem.propTypes = {
hexMaximumTransactionFee: PropTypes.string,
hexMinimumTransactionFee: PropTypes.string,
maxFeePerGas: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string,
userAcknowledgedGasMissing: PropTypes.bool.isRequired,
useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
userAcknowledgedGasMissing: PropTypes.bool,
};
export default GasDetailsItem;

View File

@ -1,7 +1,9 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { ETH } from '../../../helpers/constants/common';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import mockEstimates from '../../../../test/data/mock-estimates.json';
import mockState from '../../../../test/data/mock-state.json';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import { renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
@ -17,28 +19,36 @@ jest.mock('../../../store/actions', () => ({
getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()),
}));
const render = ({ componentProps, contextProps } = {}) => {
const render = ({ contextProps } = {}) => {
const store = configureStore({
metamask: {
nativeCurrency: ETH,
...mockState.metamask,
accounts: {
[mockState.metamask.selectedAddress]: {
address: mockState.metamask.selectedAddress,
balance: '0x1F4',
},
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
provider: {},
cachedBalances: {},
accounts: {
'0xAddress': {
address: '0xAddress',
balance: '0x176e5b6f173ebe66',
},
},
selectedAddress: '0xAddress',
gasFeeEstimates: mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET],
},
});
return renderWithProvider(
<GasFeeContextProvider transaction={{ txParams: {} }} {...contextProps}>
<GasDetailsItem userAcknowledgedGasMissing={false} {...componentProps} />
<GasFeeContextProvider
transaction={{
txParams: {
gas: '0x5208',
maxFeePerGas: '0x59682f10',
maxPriorityFeePerGas: '0x59682f00',
},
userFeeLevel: 'medium',
}}
{...contextProps}
>
<GasDetailsItem userAcknowledgedGasMissing={false} />
</GasFeeContextProvider>,
store,
);
@ -51,7 +61,7 @@ describe('GasDetailsItem', () => {
expect(screen.queryByText('Gas')).toBeInTheDocument();
expect(screen.queryByText('(estimated)')).toBeInTheDocument();
expect(screen.queryByText('Max fee:')).toBeInTheDocument();
expect(screen.queryByText('ETH')).toBeInTheDocument();
expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0);
});
});
@ -93,17 +103,11 @@ describe('GasDetailsItem', () => {
});
});
it('should should render gas fee details', async () => {
render({
componentProps: {
hexMinimumTransactionFee: '0x1ca62a4f7800',
hexMaximumTransactionFee: '0x290ee75e3d900',
},
});
it('should render gas fee details', async () => {
render();
await waitFor(() => {
expect(screen.queryByTitle('0.0000315 ETH')).toBeInTheDocument();
expect(screen.queryByText('ETH')).toBeInTheDocument();
expect(screen.queryByTitle('0.0007223')).toBeInTheDocument();
expect(screen.queryAllByTitle('0.0000315 ETH').length).toBeGreaterThan(0);
expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0);
});
});
});

View File

@ -6,5 +6,6 @@
&__currency-container,
&__gasfee-label {
position: relative;
white-space: nowrap;
}
}

View File

@ -2,6 +2,8 @@ import React, { useMemo, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import ListItem from '../../ui/list-item';
import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData';
import { useI18nContext } from '../../../hooks/useI18nContext';
@ -16,13 +18,27 @@ import {
TRANSACTION_STATUSES,
} from '../../../../shared/constants/transaction';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import EditGasPopover from '../edit-gas-popover';
import {
GasFeeContextProvider,
useGasFeeContext,
} from '../../../contexts/gasFee';
import {
TransactionModalContextProvider,
useTransactionModalContext,
} from '../../../contexts/transaction-modal';
import { checkNetworkAndAccountSupports1559 } from '../../../selectors';
import { isLegacyTransaction } from '../../../helpers/utils/transactions.util';
import { useMetricEvent } from '../../../hooks/useMetricEvent';
import Button from '../../ui/button';
import AdvancedGasFeePopover from '../advanced-gas-fee-popover';
import CancelButton from '../cancel-button';
import CancelSpeedupPopover from '../cancel-speedup-popover';
import EditGasFeePopover from '../edit-gas-fee-popover';
import EditGasPopover from '../edit-gas-popover';
export default function TransactionListItem({
function TransactionListItemInner({
transactionGroup,
setEditGasMode,
isEarliestNonce = false,
}) {
const t = useI18nContext();
@ -33,6 +49,8 @@ export default function TransactionListItem({
false,
);
const [showRetryEditGasPopover, setShowRetryEditGasPopover] = useState(false);
const { supportsEIP1559V2 } = useGasFeeContext();
const { openModal } = useTransactionModalContext();
const {
initialTransaction: { id },
@ -58,19 +76,29 @@ export default function TransactionListItem({
const retryTransaction = useCallback(
async (event) => {
event.stopPropagation();
setShowRetryEditGasPopover(true);
speedUpMetricsEvent();
if (supportsEIP1559V2) {
setEditGasMode(EDIT_GAS_MODES.SPEED_UP);
openModal('cancelSpeedUpTransaction');
} else {
setShowRetryEditGasPopover(true);
}
},
[speedUpMetricsEvent],
[openModal, setEditGasMode, speedUpMetricsEvent, supportsEIP1559V2],
);
const cancelTransaction = useCallback(
(event) => {
event.stopPropagation();
setShowCancelEditGasPopover(true);
cancelMetricsEvent();
if (supportsEIP1559V2) {
setEditGasMode(EDIT_GAS_MODES.CANCEL);
openModal('cancelSpeedUpTransaction');
} else {
setShowCancelEditGasPopover(true);
}
},
[cancelMetricsEvent],
[cancelMetricsEvent, openModal, setEditGasMode, supportsEIP1559V2],
);
const shouldShowSpeedUp = useShouldShowSpeedUp(
@ -224,14 +252,14 @@ export default function TransactionListItem({
)}
/>
)}
{showRetryEditGasPopover && (
{!supportsEIP1559V2 && showRetryEditGasPopover && (
<EditGasPopover
onClose={() => setShowRetryEditGasPopover(false)}
mode={EDIT_GAS_MODES.SPEED_UP}
transaction={transactionGroup.primaryTransaction}
/>
)}
{showCancelEditGasPopover && (
{!supportsEIP1559V2 && showCancelEditGasPopover && (
<EditGasPopover
onClose={() => setShowCancelEditGasPopover(false)}
mode={EDIT_GAS_MODES.CANCEL}
@ -242,7 +270,46 @@ export default function TransactionListItem({
);
}
TransactionListItem.propTypes = {
TransactionListItemInner.propTypes = {
transactionGroup: PropTypes.object.isRequired,
isEarliestNonce: PropTypes.bool,
setEditGasMode: PropTypes.func,
};
const TransactionListItem = (props) => {
const { transactionGroup } = props;
const [editGasMode, setEditGasMode] = useState();
const transaction = transactionGroup.primaryTransaction;
const EIP_1559_V2_ENABLED =
process.env.EIP_1559_V2 === true || process.env.EIP_1559_V2 === 'true';
const supportsEIP1559 =
useSelector(checkNetworkAndAccountSupports1559) &&
!isLegacyTransaction(transaction?.txParams);
const supportsEIP1559V2 = EIP_1559_V2_ENABLED && supportsEIP1559;
return (
<GasFeeContextProvider
transaction={transactionGroup.primaryTransaction}
editGasMode={editGasMode}
>
<TransactionModalContextProvider captureEventEnabled={false}>
<TransactionListItemInner {...props} setEditGasMode={setEditGasMode} />
{supportsEIP1559V2 && (
<>
<CancelSpeedupPopover />
<EditGasFeePopover />
<AdvancedGasFeePopover />
</>
)}
</TransactionModalContextProvider>
</GasFeeContextProvider>
);
};
TransactionListItem.propTypes = {
transactionGroup: PropTypes.object.isRequired,
};
export default TransactionListItem;

View File

@ -41,7 +41,7 @@ const Popover = ({
centerTitle ? 'center' : '',
)}
>
<h2 title={title}>
<h2 title="popover">
{onBack ? (
<button
className="fas fa-chevron-left popover-header__button"
@ -84,7 +84,7 @@ Popover.propTypes = {
/**
* Show title of the popover
*/
title: PropTypes.string,
title: PropTypes.node,
/**
* Show subtitle label on popover
*/

View File

@ -63,6 +63,7 @@ export const TransactionModalContextProvider = ({
closeAllModals,
currentModal: openModals[openModals.length - 1],
openModal,
openModalCount: openModals.length,
}}
>
{children}

52
ui/helpers/utils/gas.js Normal file
View File

@ -0,0 +1,52 @@
import BigNumber from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util';
import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { bnGreaterThan } from './util';
import { hexWEIToDecGWEI } from './conversions.util';
export const gasEstimateGreaterThanGasUsedPlusTenPercent = (
transaction,
gasFeeEstimates,
estimate,
) => {
let { maxFeePerGas: maxFeePerGasInTransaction } = transaction.txParams;
maxFeePerGasInTransaction = new BigNumber(
hexWEIToDecGWEI(addTenPercentAndRound(maxFeePerGasInTransaction)),
);
const maxFeePerGasFromEstimate =
gasFeeEstimates[estimate]?.suggestedMaxFeePerGas;
return bnGreaterThan(maxFeePerGasFromEstimate, maxFeePerGasInTransaction);
};
/**
* Simple helper to save on duplication to multiply the supplied wei hex string
* by 1.10 to get bare minimum new gas fee.
*
* @param {string | undefined} hexStringValue - hex value in wei to be incremented
* @returns {string | undefined} - hex value in WEI 10% higher than the param.
*/
export function addTenPercent(hexStringValue, conversionOptions = {}) {
if (hexStringValue === undefined) return undefined;
return addHexPrefix(
multiplyCurrencies(hexStringValue, 1.1, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 10,
numberOfDecimals: 0,
...conversionOptions,
}),
);
}
/**
* Simple helper to save on duplication to multiply the supplied wei hex string
* by 1.10 to get bare minimum new gas fee.
*
* @param {string | undefined} hexStringValue - hex value in wei to be incremented
* @returns {string | undefined} - hex value in WEI 10% higher than the param.
*/
export function addTenPercentAndRound(hexStringValue) {
return addTenPercent(hexStringValue, { numberOfDecimals: 0 });
}

View File

@ -0,0 +1,52 @@
import { PRIORITY_LEVELS } from '../../../shared/constants/gas';
import {
addTenPercent,
gasEstimateGreaterThanGasUsedPlusTenPercent,
} from './gas';
describe('Gas utils', () => {
describe('gasEstimateGreaterThanGasUsedPlusTenPercent', () => {
const compareGas = (estimateValues) => {
return gasEstimateGreaterThanGasUsedPlusTenPercent(
{
txParams: {
maxFeePerGas: '0x59682f10',
maxPriorityFeePerGas: '0x59682f00',
},
},
{
medium: estimateValues,
},
PRIORITY_LEVELS.MEDIUM,
);
};
it('should return true if gas used in transaction + 10% is greater that estimate', () => {
const result = compareGas({
suggestedMaxPriorityFeePerGas: '7',
suggestedMaxFeePerGas: '70',
});
expect(result).toStrictEqual(true);
});
it('should return false if gas used in transaction + 10% is less that estimate', () => {
const result = compareGas({
suggestedMaxPriorityFeePerGas: '.5',
suggestedMaxFeePerGas: '1',
});
expect(result).toStrictEqual(false);
});
});
describe('addTenPercent', () => {
it('should add 10% to hex value passed', () => {
const result = addTenPercent('0x59682f00');
expect(result).toStrictEqual('0x62590080');
});
it('should return undefined if undefined value is passed', () => {
const result = addTenPercent(undefined);
expect(result).toBeUndefined();
});
});
});

View File

@ -143,6 +143,7 @@ export function useGasEstimates({
estimatedBaseFee: supportsEIP1559
? decGWEIToHexWEI(gasFeeEstimates.estimatedBaseFee ?? '0')
: undefined,
maximumCostInHexWei,
minimumCostInHexWei,
};
}

View File

@ -198,6 +198,7 @@ export function useGasFeeInputs(
estimatedMinimumFiat,
estimatedMaximumNative,
estimatedMinimumNative,
maximumCostInHexWei,
minimumCostInHexWei,
} = useGasEstimates({
editGasMode,
@ -244,13 +245,19 @@ export function useGasFeeInputs(
}, [minimumGasLimit, gasErrors.gasLimit, transaction]);
const {
cancelTransaction,
speedUpTransaction,
updateTransaction,
updateTransactionUsingGasFeeEstimates,
updateTransactionToMinimumGasFee,
updateTransactionUsingDAPPSuggestedValues,
updateTransactionUsingEstimate,
} = useTransactionFunctions({
defaultEstimateToUse,
editGasMode,
gasFeeEstimates,
gasLimit,
maxPriorityFeePerGas,
minimumGasLimit,
transaction,
});
@ -322,6 +329,8 @@ export function useGasFeeInputs(
estimatedMaximumNative,
estimatedMinimumNative,
isGasEstimatesLoading,
maximumCostInHexWei,
minimumCostInHexWei,
estimateUsed,
gasFeeEstimates,
gasEstimateType,
@ -338,7 +347,11 @@ export function useGasFeeInputs(
minimumGasLimitDec: hexToDecimal(minimumGasLimit),
supportsEIP1559,
supportsEIP1559V2,
cancelTransaction,
speedUpTransaction,
updateTransaction,
updateTransactionUsingGasFeeEstimates,
updateTransactionToMinimumGasFee,
updateTransactionUsingDAPPSuggestedValues,
updateTransactionUsingEstimate,
};
}

View File

@ -0,0 +1,144 @@
import React from 'react';
import { Provider } from 'react-redux';
import { renderHook } from '@testing-library/react-hooks';
import {
CUSTOM_GAS_ESTIMATE,
EDIT_GAS_MODES,
GAS_RECOMMENDATIONS,
} from '../../../shared/constants/gas';
import mockState from '../../../test/data/mock-state.json';
import * as Actions from '../../store/actions';
import configureStore from '../../store/store';
import { useGasFeeEstimates } from '../useGasFeeEstimates';
import { FEE_MARKET_ESTIMATE_RETURN_VALUE } from './test-utils';
import { useTransactionFunctions } from './useTransactionFunctions';
jest.mock('../useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
useGasFeeEstimates.mockImplementation(() => FEE_MARKET_ESTIMATE_RETURN_VALUE);
jest.mock('../../selectors', () => ({
checkNetworkAndAccountSupports1559: () => true,
}));
const wrapper = ({ children }) => (
<Provider store={configureStore(mockState)}>{children}</Provider>
);
const renderUseTransactionFunctions = (props) => {
return renderHook(
() =>
useTransactionFunctions({
defaultEstimateToUse: GAS_RECOMMENDATIONS.MEDIUM,
editGasMode: EDIT_GAS_MODES.MODIFY_IN_PLACE,
estimatedBaseFee: '0x59682f10',
gasFeeEstimates: FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates,
gasLimit: '21000',
maxPriorityFeePerGas: '0x59682f10',
transaction: {
userFeeLevel: CUSTOM_GAS_ESTIMATE,
txParams: { maxFeePerGas: '0x5028', maxPriorityFeePerGas: '0x5028' },
},
...props,
}),
{ wrapper },
);
};
describe('useMaxPriorityFeePerGasInput', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should invoke action createCancelTransaction when cancelTransaction callback is invoked', () => {
const mock = jest
.spyOn(Actions, 'createCancelTransaction')
.mockImplementation(() => ({ type: '' }));
const { result } = renderUseTransactionFunctions();
result.current.cancelTransaction();
expect(mock).toHaveBeenCalledTimes(1);
});
it('should invoke action createSpeedUpTransaction when speedUpTransaction callback is invoked', () => {
const mock = jest
.spyOn(Actions, 'createSpeedUpTransaction')
.mockImplementation(() => ({ type: '' }));
const { result } = renderUseTransactionFunctions();
result.current.speedUpTransaction();
expect(mock).toHaveBeenCalledTimes(1);
});
it('should invoke action updateTransaction with 10% increased fee when updateTransactionToMinimumGasFee callback is invoked', () => {
const mock = jest
.spyOn(Actions, 'updateTransaction')
.mockImplementation(() => ({ type: '' }));
const { result } = renderUseTransactionFunctions();
result.current.updateTransactionToMinimumGasFee();
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith({
txParams: {
estimateSuggested: 'medium',
estimateUsed: 'minimum',
gas: '5208',
gasLimit: '5208',
maxFeePerGas: '0x582c',
maxPriorityFeePerGas: '0x582c',
},
userFeeLevel: 'minimum',
});
});
it('should invoke action updateTransaction with estimate gas values fee when updateTransactionUsingEstimate callback is invoked', () => {
const mock = jest
.spyOn(Actions, 'updateTransaction')
.mockImplementation(() => ({ type: '' }));
const { result } = renderUseTransactionFunctions();
result.current.updateTransactionUsingEstimate(GAS_RECOMMENDATIONS.LOW);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith({
txParams: {
estimateSuggested: 'medium',
estimateUsed: 'low',
gas: '5208',
gasLimit: '5208',
maxFeePerGas: 'c570bd200',
maxPriorityFeePerGas: 'b2d05e00',
},
userFeeLevel: 'low',
});
});
it('should invoke action updateTransaction with dappSuggestedValues values fee when updateTransactionUsingDAPPSuggestedValues callback is invoked', () => {
const mock = jest
.spyOn(Actions, 'updateTransaction')
.mockImplementation(() => ({ type: '' }));
const { result } = renderUseTransactionFunctions({
transaction: {
userFeeLevel: CUSTOM_GAS_ESTIMATE,
dappSuggestedGasFees: {
maxFeePerGas: '0x5028',
maxPriorityFeePerGas: '0x5028',
},
},
});
result.current.updateTransactionUsingDAPPSuggestedValues();
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith({
dappSuggestedGasFees: {
maxFeePerGas: '0x5028',
maxPriorityFeePerGas: '0x5028',
},
txParams: {
estimateSuggested: 'medium',
estimateUsed: 'dappSuggested',
gas: '5208',
gasLimit: '5208',
maxFeePerGas: '0x5028',
maxPriorityFeePerGas: '0x5028',
},
userFeeLevel: 'dappSuggested',
});
});
});

View File

@ -6,7 +6,10 @@ import {
decimalToHex,
decGWEIToHexWEI,
} from '../../helpers/utils/conversions.util';
import { addTenPercentAndRound } from '../../helpers/utils/gas';
import {
createCancelTransaction,
createSpeedUpTransaction,
updateCustomSwapsEIP1559GasParams,
updateSwapsUserFeeLevel,
updateTransaction as updateTransactionFn,
@ -15,22 +18,41 @@ import {
export const useTransactionFunctions = ({
defaultEstimateToUse,
editGasMode,
estimatedBaseFee,
gasFeeEstimates,
gasLimit: gasLimitInTransaction,
gasLimit: gasLimitValue,
maxPriorityFeePerGas: maxPriorityFeePerGasValue,
transaction,
}) => {
const dispatch = useDispatch();
const updateTransaction = useCallback(
({
estimateUsed,
const getTxMeta = useCallback(() => {
if (
(editGasMode !== EDIT_GAS_MODES.CANCEL &&
editGasMode !== EDIT_GAS_MODES.SPEED_UP) ||
transaction.previousGas
) {
return {};
}
const {
maxFeePerGas,
maxPriorityFeePerGas,
gasLimit = gasLimitInTransaction,
}) => {
gasLimit,
} = transaction?.txParams;
return {
previousGas: {
maxFeePerGas,
maxPriorityFeePerGas,
gasLimit,
},
};
}, [editGasMode, transaction?.previousGas, transaction?.txParams]);
const updateTransaction = useCallback(
({ estimateUsed, gasLimit, maxFeePerGas, maxPriorityFeePerGas }) => {
const newGasSettings = {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
gas: decimalToHex(gasLimit || gasLimitValue),
gasLimit: decimalToHex(gasLimit || gasLimitValue),
estimateSuggested: defaultEstimateToUse,
estimateUsed,
};
@ -38,8 +60,10 @@ export const useTransactionFunctions = ({
newGasSettings.maxFeePerGas = maxFeePerGas;
}
if (maxPriorityFeePerGas) {
newGasSettings.maxPriorityFeePerGas = maxPriorityFeePerGas;
newGasSettings.maxPriorityFeePerGas =
maxPriorityFeePerGas || decGWEIToHexWEI(maxPriorityFeePerGasValue);
}
const txMeta = getTxMeta();
const updatedTxMeta = {
...transaction,
@ -48,6 +72,7 @@ export const useTransactionFunctions = ({
...transaction.txParams,
...newGasSettings,
},
...txMeta,
};
if (editGasMode === EDIT_GAS_MODES.SWAPS) {
@ -63,24 +88,46 @@ export const useTransactionFunctions = ({
defaultEstimateToUse,
dispatch,
editGasMode,
gasLimitInTransaction,
gasLimitValue,
getTxMeta,
maxPriorityFeePerGasValue,
transaction,
],
);
const updateTransactionUsingGasFeeEstimates = useCallback(
(gasFeeEstimateToUse) => {
if (gasFeeEstimateToUse === PRIORITY_LEVELS.DAPP_SUGGESTED) {
const {
maxFeePerGas,
maxPriorityFeePerGas,
} = transaction?.dappSuggestedGasFees;
const cancelTransaction = useCallback(() => {
dispatch(
createCancelTransaction(transaction.id, transaction.txParams, {
estimatedBaseFee,
}),
);
}, [dispatch, estimatedBaseFee, transaction]);
const speedUpTransaction = useCallback(() => {
dispatch(
createSpeedUpTransaction(transaction.id, transaction.txParams, {
estimatedBaseFee,
}),
);
}, [dispatch, estimatedBaseFee, transaction]);
const updateTransactionToMinimumGasFee = useCallback(() => {
const { gas: gasLimit, maxFeePerGas, maxPriorityFeePerGas } =
transaction.previousGas || transaction.txParams;
updateTransaction({
estimateUsed: PRIORITY_LEVELS.DAPP_SUGGESTED,
maxFeePerGas,
maxPriorityFeePerGas,
estimateUsed: PRIORITY_LEVELS.MINIMUM,
gasLimit,
maxFeePerGas: addTenPercentAndRound(maxFeePerGas),
maxPriorityFeePerGas: addTenPercentAndRound(maxPriorityFeePerGas),
});
} else {
}, [transaction, updateTransaction]);
const updateTransactionUsingEstimate = useCallback(
(gasFeeEstimateToUse) => {
if (!gasFeeEstimates[gasFeeEstimateToUse]) {
return;
}
const {
suggestedMaxFeePerGas,
suggestedMaxPriorityFeePerGas,
@ -90,10 +137,28 @@ export const useTransactionFunctions = ({
maxFeePerGas: decGWEIToHexWEI(suggestedMaxFeePerGas),
maxPriorityFeePerGas: decGWEIToHexWEI(suggestedMaxPriorityFeePerGas),
});
}
},
[gasFeeEstimates, transaction?.dappSuggestedGasFees, updateTransaction],
[gasFeeEstimates, updateTransaction],
);
return { updateTransaction, updateTransactionUsingGasFeeEstimates };
const updateTransactionUsingDAPPSuggestedValues = useCallback(() => {
const {
maxFeePerGas,
maxPriorityFeePerGas,
} = transaction?.dappSuggestedGasFees;
updateTransaction({
estimateUsed: PRIORITY_LEVELS.DAPP_SUGGESTED,
maxFeePerGas,
maxPriorityFeePerGas,
});
}, [transaction, updateTransaction]);
return {
cancelTransaction,
speedUpTransaction,
updateTransaction,
updateTransactionToMinimumGasFee,
updateTransactionUsingDAPPSuggestedValues,
updateTransactionUsingEstimate,
};
};

View File

@ -1,28 +1,10 @@
import BigNumber from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util';
import { useMemo } from 'react';
import { multiplyCurrencies } from '../../shared/modules/conversion.utils';
import { isEIP1559Transaction } from '../../shared/modules/transaction.utils';
import { decGWEIToHexWEI } from '../helpers/utils/conversions.util';
import { addTenPercent } from '../helpers/utils/gas';
import { useGasFeeEstimates } from './useGasFeeEstimates';
/**
* Simple helper to save on duplication to multiply the supplied wei hex string
* by 1.10 to get bare minimum new gas fee.
*
* @param {string} hexStringValue - hex value in wei to be incremented
* @returns {string} - hex value in WEI 10% higher than the param.
*/
function addTenPercent(hexStringValue) {
return addHexPrefix(
multiplyCurrencies(hexStringValue, 1.1, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 10,
}),
);
}
/**
* Helper that returns the higher of two options for a new gas fee:
* The original fee + 10% or

View File

@ -41,6 +41,7 @@ import TransactionDetail from '../../components/app/transaction-detail/transacti
import TransactionDetailItem from '../../components/app/transaction-detail-item/transaction-detail-item.component';
import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
import GasDetailsItem from '../../components/app/gas-details-item';
import GasTiming from '../../components/app/gas-timing/gas-timing.component';
import LedgerInstructionField from '../../components/app/ledger-instruction-field';
import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message';
@ -60,11 +61,11 @@ import {
import Typography from '../../components/ui/typography/typography';
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants';
import GasDetailsItem from './gas-details-item';
import TransactionAlerts from './transaction-alerts';
// eslint-disable-next-line prefer-destructuring
const EIP_1559_V2_ENABLED = process.env.EIP_1559_V2;
const EIP_1559_V2_ENABLED =
process.env.EIP_1559_V2 === true || process.env.EIP_1559_V2 === 'true';
const renderHeartBeatIfNotInTest = () =>
process.env.IN_TEST ? null : <LoadingHeartBeat />;
@ -437,15 +438,6 @@ export default class ConfirmTransactionBase extends Component {
return this.supportsEIP1559V2 ? (
<GasDetailsItem
key="gas_details"
hexMaximumTransactionFee={hexMaximumTransactionFee}
hexMinimumTransactionFee={hexMinimumTransactionFee}
maxFeePerGas={maxFeePerGas}
maxPriorityFeePerGas={maxPriorityFeePerGas}
supportsEIP1559={supportsEIP1559}
txData={txData}
useNativeCurrencyAsPrimaryCurrency={
useNativeCurrencyAsPrimaryCurrency
}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
) : (

View File

@ -5,8 +5,6 @@
@import 'confirm-approve/index';
@import 'confirm-decrypt-message/confirm-decrypt-message';
@import 'confirm-encryption-public-key/confirm-encryption-public-key';
@import 'confirm-transaction-base/gas-details-item/gas-details-item';
@import 'confirm-transaction-base/gas-details-item/gas-details-item-title/gas-details-item-title';
@import 'confirm-transaction-base/transaction-alerts/transaction-alerts';
@import 'confirmation/confirmation';
@import 'connected-sites/index';

View File

@ -18,7 +18,7 @@ import {
TYPOGRAPHY,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
import GasDetailsItemTitle from '../../confirm-transaction-base/gas-details-item/gas-details-item-title';
import GasDetailsItemTitle from '../../../components/app/gas-details-item/gas-details-item-title';
const GAS_FEES_LEARN_MORE_URL =
'https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172';

View File

@ -408,6 +408,10 @@ export function getGasIsLoading(state) {
return state.appState.gasIsLoading;
}
export function getAppIsLoading(state) {
return state.appState.isLoading;
}
export function getCurrentCurrency(state) {
return state.metamask.currentCurrency;
}

View File

@ -240,4 +240,8 @@ describe('Selectors', () => {
);
expect(isAdvancedGasFeeDefault).toStrictEqual(true);
});
it('#getAppIsLoading', () => {
const appIsLoading = selectors.getAppIsLoading(mockState);
expect(appIsLoading).toStrictEqual(false);
});
});