Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import BigNumber from 'bignumber.js';
import React, { useContext, useMemo } from 'react';
import classNames from 'clsx';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import {
GasEstimateTypes,
PriorityLevels,
} from '../../../../../shared/constants/gas';
import { Box, Text } from '../../../../components/component-library';

Check warning on line 8 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ia&open=AaAS9fPKeqarb4qOy5ia&pullRequest=45590

Check warning on line 8 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Box' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5iZ&open=AaAS9fPKeqarb4qOy5iZ&pullRequest=45590
import { I18nContext } from '../../../../contexts/i18n';
import {
getGasEstimateType,
Expand All @@ -21,34 +19,50 @@
Display,
FlexWrap,
FontWeight,
TextColor,

Check warning on line 22 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextColor' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ib&open=AaAS9fPKeqarb4qOy5ib&pullRequest=45590
TextVariant,

Check warning on line 23 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextVariant' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ic&open=AaAS9fPKeqarb4qOy5ic&pullRequest=45590
} from '../../../../helpers/constants/design-system';
import { GAS_FORM_ERRORS } from '../../../../helpers/constants/gas';
import { usePrevious } from '../../../../hooks/usePrevious';
import { getGasFeeTimeEstimate } from '../../../../store/actions';
import { GAS_FORM_ERRORS as gasFormErrors } from '../../../../helpers/constants/gas';
import { useGasFeeTimeEstimate } from '../../hooks/gas/useGasFeeTimeEstimate';

// Once we reach this second threshold, we switch to minutes as a unit
const SECOND_CUTOFF = 90;
const secondCutoff = 90;

// Shows "seconds" as unit of time if under SECOND_CUTOFF, otherwise "minutes"
const toHumanReadableTime = (milliseconds = 1, t) => {
// Shows "seconds" as unit of time if under secondCutoff, otherwise "minutes"
const toHumanReadableTime = (
milliseconds = 1,

Check warning on line 33 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Default parameters should be last.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5id&open=AaAS9fPKeqarb4qOy5id&pullRequest=45590
t: (key: string, args?: unknown[]) => string,
) => {
const seconds = Math.ceil(milliseconds / 1000);
if (seconds <= SECOND_CUTOFF) {
if (seconds <= secondCutoff) {
return t('gasTimingSecondsShort', [seconds]);
}
return t('gasTimingMinutesShort', [Math.ceil(seconds / 60)]);
};
// Preset levels show their label; others (e.g. PriorityLevels.tenPercentIncreased, custom) show as "Advanced"
const PRESET_ESTIMATES = new Set(['low', 'medium', 'high']);

// Preset levels show their label; others (e.g. tenPercentIncreased, custom) show as "Advanced"
const presetEstimates = new Set(['low', 'medium', 'high']);

type GasTimingProps = {
chainId?: string;
networkClientId?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
gasWarnings?: {
maxPriorityFee?: string;
maxFee?: string;
};
userFeeLevelOverride?: string;
};

export default function GasTiming({

Check failure on line 58 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ie&open=AaAS9fPKeqarb4qOy5ie&pullRequest=45590
chainId,
networkClientId,
maxFeePerGas = '0',
maxPriorityFeePerGas = '0',
gasWarnings,
userFeeLevelOverride,
}) {
}: GasTimingProps) {

Check warning on line 65 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5if&open=AaAS9fPKeqarb4qOy5if&pullRequest=45590
const chainGasEstimateType = useSelector((state) =>
getGasEstimateTypeByChainId(state, chainId),
);
Expand All @@ -70,20 +84,21 @@
chainIsGasEstimatesLoading ?? rootIsGasEstimatesLoading;

const gasFeeEstimates = chainGasFeeEstimates || gasFeeEstimatesFromRoot;
const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
const t = useContext(I18nContext);

// 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 =
// we'll need to fetch a custom time estimate via useGasFeeTimeEstimate
const isUnknownLow = Boolean(
gasFeeEstimates?.low &&
Number(maxPriorityFeePerGas) <
Number(gasFeeEstimates.low.suggestedMaxPriorityFeePerGas);
Number(maxPriorityFeePerGas) <
Number(gasFeeEstimates.low.suggestedMaxPriorityFeePerGas),
);

const previousMaxFeePerGas = usePrevious(maxFeePerGas);
const previousMaxPriorityFeePerGas = usePrevious(maxPriorityFeePerGas);
const previousIsUnknownLow = usePrevious(isUnknownLow);
const { data: customEstimatedTime } = useGasFeeTimeEstimate({
maxPriorityFeePerGas,
maxFeePerGas,
enabled: isUnknownLow,
});

const estimateTextMap = useMemo(
() => ({
Expand All @@ -94,83 +109,46 @@
[t],
);

useEffect(() => {
let isMounted = true;
const priority = maxPriorityFeePerGas;
const fee = maxFeePerGas;

if (
isUnknownLow ||
(priority && priority !== previousMaxPriorityFeePerGas) ||
(fee && fee !== previousMaxFeePerGas)
) {
// getGasFeeTimeEstimate requires parameters in string format
getGasFeeTimeEstimate(
new BigNumber(priority, 10).toString(10),
new BigNumber(fee, 10).toString(10),
).then((result) => {
if (
maxFeePerGas === fee &&
maxPriorityFeePerGas === priority &&
isMounted
) {
setCustomEstimatedTime(result);
}
});
}

if (isUnknownLow !== false && previousIsUnknownLow === true) {
setCustomEstimatedTime(null);
}

return () => {
isMounted = false;
};
}, [
maxPriorityFeePerGas,
maxFeePerGas,
isUnknownLow,
previousMaxFeePerGas,
previousMaxPriorityFeePerGas,
previousIsUnknownLow,
]);

if (
gasWarnings?.maxPriorityFee === GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW ||
gasWarnings?.maxFee === GAS_FORM_ERRORS.MAX_FEE_TOO_LOW
gasWarnings?.maxPriorityFee === gasFormErrors.MAX_PRIORITY_FEE_TOO_LOW ||
gasWarnings?.maxFee === gasFormErrors.MAX_FEE_TOO_LOW
) {
return (
<Text

Check warning on line 117 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ig&open=AaAS9fPKeqarb4qOy5ig&pullRequest=45590
variant={TextVariant.bodySm}

Check warning on line 118 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextVariant' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ih&open=AaAS9fPKeqarb4qOy5ih&pullRequest=45590
fontWeight={FontWeight.Bold}
color={TextColor.textAlternative}

Check warning on line 120 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextColor' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ii&open=AaAS9fPKeqarb4qOy5ii&pullRequest=45590
className={classNames('gas-timing', 'gas-timing--negative')}
>
{t('editGasTooLow')}
</Text>

Check warning on line 124 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ij&open=AaAS9fPKeqarb4qOy5ij&pullRequest=45590
);
}

// Don't show anything if we don't have enough information
if (isGasEstimatesLoading || gasEstimateType !== GasEstimateTypes.feeMarket) {
if (
isGasEstimatesLoading ||
gasEstimateType !== GasEstimateTypes.feeMarket
) {
return null;
}

const { low = {}, medium = {}, high = {} } = gasFeeEstimates;
const { low = {}, medium = {}, high = {} } = gasFeeEstimates ?? {};

const estimateToUse = userFeeLevelOverride ?? 'medium';

const isPresetEstimate = PRESET_ESTIMATES.has(estimateToUse);
const isPresetEstimate = presetEstimates.has(estimateToUse);
const textTKey = estimateToUse === 'low' ? 'gasTimingLow' : estimateToUse;
let text =
estimateTextMap[estimateToUse] ??
estimateTextMap[estimateToUse as keyof typeof estimateTextMap] ??
(isPresetEstimate ? t(textTKey) : t('custom'));
let time = '';
let timeMs = 0;

// Anything medium or faster is positive
if (
Number(maxPriorityFeePerGas) >= Number(medium.suggestedMaxPriorityFeePerGas)
Number(maxPriorityFeePerGas) >=
Number(medium.suggestedMaxPriorityFeePerGas)
) {
// High+ is very likely, medium is likely
if (
Expand All @@ -187,17 +165,23 @@
} else if (isUnknownLow) {
// If the user has chosen a value less than our low estimate,
// calculate a potential wait time
const upperTimeBound =
customEstimatedTime &&
typeof customEstimatedTime === 'object' &&
'upperTimeBound' in customEstimatedTime
? customEstimatedTime.upperTimeBound
: undefined;

// If we didn't get any useful information, show the
// "unknown processing time" message
if (
!customEstimatedTime ||
customEstimatedTime === 'unknown' ||
customEstimatedTime?.upperTimeBound === 'unknown'
(customEstimatedTime as unknown) === 'unknown' ||
upperTimeBound === 'unknown'
) {
text = t('editGasTooLow');
} else {
timeMs = Number(customEstimatedTime?.upperTimeBound);
timeMs = Number(upperTimeBound);
time = toHumanReadableTime(timeMs, t);
}
} else {
Expand All @@ -206,33 +190,24 @@
}

return (
<Box display={Display.Flex} marginBottom={1} flexWrap={FlexWrap.Wrap}>

Check warning on line 193 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Box' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ik&open=AaAS9fPKeqarb4qOy5ik&pullRequest=45590
{text && (
<Text

Check warning on line 195 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5il&open=AaAS9fPKeqarb4qOy5il&pullRequest=45590
color={TextColor.textAlternative}

Check warning on line 196 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextColor' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5im&open=AaAS9fPKeqarb4qOy5im&pullRequest=45590
variant={TextVariant.bodyMd}

Check warning on line 197 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextVariant' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5in&open=AaAS9fPKeqarb4qOy5in&pullRequest=45590
paddingInlineEnd={2}
>
{text}
</Text>

Check warning on line 201 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5io&open=AaAS9fPKeqarb4qOy5io&pullRequest=45590
)}

{time && (
<Text variant={TextVariant.bodyMd} color={TextColor.textDefault}>

Check warning on line 205 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextVariant' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5iq&open=AaAS9fPKeqarb4qOy5iq&pullRequest=45590

Check warning on line 205 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ip&open=AaAS9fPKeqarb4qOy5ip&pullRequest=45590

Check warning on line 205 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'TextColor' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5ir&open=AaAS9fPKeqarb4qOy5ir&pullRequest=45590
<span data-testid="gas-timing-time">
{timeMs > 0 && timeMs < 1000 ? `<${time}` : `~${time}`}
</span>
</Text>

Check warning on line 209 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5is&open=AaAS9fPKeqarb4qOy5is&pullRequest=45590
)}
</Box>

Check warning on line 211 in ui/pages/confirmations/components/gas-timing/gas-timing.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Box' is deprecated.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-extension&issues=AaAS9fPKeqarb4qOy5it&open=AaAS9fPKeqarb4qOy5it&pullRequest=45590
);
}

GasTiming.propTypes = {
chainId: PropTypes.string,
networkClientId: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string,
maxFeePerGas: PropTypes.string,
gasWarnings: PropTypes.object,
userFeeLevelOverride: PropTypes.string,
};
75 changes: 75 additions & 0 deletions ui/pages/confirmations/hooks/gas/useGasFeeTimeEstimate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getGasFeeTimeEstimate } from '../../../../store/actions';
import { useGasFeeTimeEstimate } from './useGasFeeTimeEstimate';

jest.mock('../../../../store/actions', () => ({
getGasFeeTimeEstimate: jest.fn(),
}));

const mockedGetGasFeeTimeEstimate = jest.mocked(getGasFeeTimeEstimate);

describe('useGasFeeTimeEstimate', () => {
let queryClient: QueryClient;

const createWrapper = () => {
return ({ children }: { children: React.ReactNode }) =>
React.createElement(
QueryClientProvider,
{ client: queryClient },
children,
);
};

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
mockedGetGasFeeTimeEstimate.mockResolvedValue({
lowerTimeBound: 1000,
upperTimeBound: 5000,
});
});

afterEach(() => {
queryClient.clear();
jest.clearAllMocks();
});

it('does not fetch when disabled', () => {
renderHook(
() =>
useGasFeeTimeEstimate({
maxPriorityFeePerGas: '1',
maxFeePerGas: '2',
enabled: false,
}),
{ wrapper: createWrapper() },
);

expect(mockedGetGasFeeTimeEstimate).not.toHaveBeenCalled();
});

it('fetches when enabled and fee strings are present', async () => {
const { result } = renderHook(
() =>
useGasFeeTimeEstimate({
maxPriorityFeePerGas: '1',
maxFeePerGas: '2',
enabled: true,
}),
{ wrapper: createWrapper() },
);

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(mockedGetGasFeeTimeEstimate).toHaveBeenCalledWith('1', '2');
expect(result.current.data).toEqual({
lowerTimeBound: 1000,
upperTimeBound: 5000,
});
});
});
26 changes: 26 additions & 0 deletions ui/pages/confirmations/hooks/gas/useGasFeeTimeEstimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import BigNumber from 'bignumber.js';
import { useQuery } from '@tanstack/react-query';
import { getGasFeeTimeEstimate } from '../../../../store/actions';

export function useGasFeeTimeEstimate({
maxPriorityFeePerGas,
maxFeePerGas,
enabled,
}: {
maxPriorityFeePerGas?: string;
maxFeePerGas?: string;
enabled: boolean;
}) {
const priorityFee = maxPriorityFeePerGas
? new BigNumber(maxPriorityFeePerGas, 10).toString(10)
: '';
const fee = maxFeePerGas
? new BigNumber(maxFeePerGas, 10).toString(10)
: '';

return useQuery({
queryKey: ['gasFeeTimeEstimate', priorityFee, fee],
queryFn: () => getGasFeeTimeEstimate(priorityFee, fee),
enabled: Boolean(priorityFee && fee && enabled),
});
}
Loading