1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Degrade gracefully when gas API is down (#13865)

When the gas API is down, the logic we use will no longer compute all of
the data that the gas API returns in order to reduce the burden on
Infura. Specifically, only estimated fees for different priority levels,
as well as the latest base fee, will be available; all other data
points, such as the latest and historical priority fee range and network
stability, will be missing. This commit updates the frontend logic to
account for this lack of data by merely hiding the relevant pieces of
the UI that would otherwise be shown.
This commit is contained in:
Elliot Winkler 2022-03-11 11:59:58 -07:00 committed by GitHub
parent 8e0f71a008
commit f8f4397339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 518 additions and 237 deletions

View File

@ -855,6 +855,9 @@
"dontShowThisAgain": { "dontShowThisAgain": {
"message": "Don't show this again" "message": "Don't show this again"
}, },
"downArrow": {
"message": "down arrow"
},
"downloadGoogleChrome": { "downloadGoogleChrome": {
"message": "Download Google Chrome" "message": "Download Google Chrome"
}, },
@ -1694,6 +1697,9 @@
"letsGoSetUp": { "letsGoSetUp": {
"message": "Yes, lets get set up!" "message": "Yes, lets get set up!"
}, },
"levelArrow": {
"message": "level arrow"
},
"likeToImportTokens": { "likeToImportTokens": {
"message": "Would you like to import these tokens?" "message": "Would you like to import these tokens?"
}, },
@ -3713,6 +3719,9 @@
"unverifiedContractAddressMessage": { "unverifiedContractAddressMessage": {
"message": "We cannot verify this contract. Make sure you trust this address." "message": "We cannot verify this contract. Make sure you trust this address."
}, },
"upArrow": {
"message": "up arrow"
},
"updatedWithDate": { "updatedWithDate": {
"message": "Updated $1" "message": "Updated $1"
}, },

View File

@ -1,42 +1,95 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { isNullish } from '../../../../helpers/utils/util';
import { formatGasFeeOrFeeRange } from '../../../../helpers/utils/gas';
import { I18nContext } from '../../../../contexts/i18n';
import Box from '../../../ui/box'; import Box from '../../../ui/box';
import I18nValue from '../../../ui/i18n-value';
import LoadingHeartBeat from '../../../ui/loading-heartbeat'; import LoadingHeartBeat from '../../../ui/loading-heartbeat';
const AdvancedGasFeeInputSubtext = ({ latest, historical, feeTrend }) => { function determineTrendInfo(trend, t) {
switch (trend) {
case 'up':
return {
className: 'advanced-gas-fee-input-subtext__up',
imageSrc: '/images/up-arrow.svg',
imageAlt: t('upArrow'),
};
case 'down':
return {
className: 'advanced-gas-fee-input-subtext__down',
imageSrc: '/images/down-arrow.svg',
imageAlt: t('downArrow'),
};
case 'level':
return {
className: 'advanced-gas-fee-input-subtext__level',
imageSrc: '/images/level-arrow.svg',
imageAlt: t('levelArrow'),
};
default:
return null;
}
}
const AdvancedGasFeeInputSubtext = ({ latest, historical, trend }) => {
const t = useContext(I18nContext);
const trendInfo = determineTrendInfo(trend, t);
return ( return (
<Box className="advanced-gas-fee-input-subtext"> <Box
<Box display="flex" alignItems="center"> display="flex"
<span className="advanced-gas-fee-input-subtext__label"> alignItems="center"
<I18nValue messageKey="currentTitle" /> gap={4}
</span> className="advanced-gas-fee-input-subtext"
<span className="advanced-gas-fee-input-subtext__value"> >
<LoadingHeartBeat /> {isNullish(latest) ? null : (
{latest} <Box display="flex" alignItems="center" data-testid="latest">
</span> <span className="advanced-gas-fee-input-subtext__label">
<span className={`advanced-gas-fee-input-subtext__${feeTrend}`}> {t('currentTitle')}
<img src={`./images/${feeTrend}-arrow.svg`} alt="feeTrend-arrow" /> </span>
</span> <span className="advanced-gas-fee-input-subtext__value">
</Box> <LoadingHeartBeat />
<Box> {formatGasFeeOrFeeRange(latest)}
<span className="advanced-gas-fee-input-subtext__label"> </span>
<I18nValue messageKey="twelveHrTitle" /> {trendInfo === null ? null : (
</span> <span className={trendInfo.className}>
<span className="advanced-gas-fee-input-subtext__value"> <img
<LoadingHeartBeat /> src={trendInfo.imageSrc}
{historical} alt={trendInfo.imageAlt}
</span> data-testid="fee-arrow"
</Box> />
</span>
)}
</Box>
)}
{isNullish(historical) ? null : (
<Box>
<span
className="advanced-gas-fee-input-subtext__label"
data-testid="historical"
>
{t('twelveHrTitle')}
</span>
<span className="advanced-gas-fee-input-subtext__value">
<LoadingHeartBeat />
{formatGasFeeOrFeeRange(historical)}
</span>
</Box>
)}
</Box> </Box>
); );
}; };
AdvancedGasFeeInputSubtext.propTypes = { AdvancedGasFeeInputSubtext.propTypes = {
latest: PropTypes.string, latest: PropTypes.oneOfType([
historical: PropTypes.string, PropTypes.string,
feeTrend: PropTypes.string.isRequired, PropTypes.arrayOf(PropTypes.string),
]),
historical: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
]),
trend: PropTypes.oneOf(['up', 'down', 'level']),
}; };
export default AdvancedGasFeeInputSubtext; export default AdvancedGasFeeInputSubtext;

View File

@ -1,55 +1,144 @@
import React from 'react'; import React from 'react';
import { screen } from '@testing-library/react'; import { renderWithProvider, screen } from '../../../../../test/jest';
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 { renderWithProvider } from '../../../../../test/lib/render-helpers';
import configureStore from '../../../../store/store'; import configureStore from '../../../../store/store';
import AdvancedGasFeeInputSubtext from './advanced-gas-fee-input-subtext'; import AdvancedGasFeeInputSubtext from './advanced-gas-fee-input-subtext';
jest.mock('../../../../store/actions', () => ({ jest.mock('../../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(), disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest getGasFeeEstimatesAndStartPolling: jest.fn().mockResolvedValue(null),
.fn()
.mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(), addPollingTokenToAppState: jest.fn(),
removePollingTokenFromAppState: jest.fn(), removePollingTokenFromAppState: jest.fn(),
})); }));
const render = () => { const renderComponent = ({ props = {}, state = {} } = {}) => {
const store = configureStore({ const store = configureStore(state);
metamask: { return renderWithProvider(<AdvancedGasFeeInputSubtext {...props} />, store);
...mockState.metamask,
accounts: {
[mockState.metamask.selectedAddress]: {
address: mockState.metamask.selectedAddress,
balance: '0x1F4',
},
},
advancedGasFee: { priorityFee: 100 },
featureFlags: { advancedInlineGas: true },
gasFeeEstimates:
mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET].gasFeeEstimates,
},
});
return renderWithProvider(
<AdvancedGasFeeInputSubtext
latest="Latest Value"
historical="Historical value"
feeTrend="up"
/>,
store,
);
}; };
describe('AdvancedGasFeeInputSubtext', () => { describe('AdvancedGasFeeInputSubtext', () => {
it('should renders latest and historical values passed', () => { describe('when "latest" is non-nullish', () => {
render(); it('should render the latest fee if given a fee', () => {
renderComponent({
props: {
latest: '123.12345',
},
});
expect(screen.queryByText('Latest Value')).toBeInTheDocument(); expect(screen.getByText('123.12 GWEI')).toBeInTheDocument();
expect(screen.queryByText('Historical value')).toBeInTheDocument(); });
expect(screen.queryByAltText('feeTrend-arrow')).toBeInTheDocument();
it('should render the latest fee range if given a fee range', () => {
renderComponent({
props: {
latest: ['123.456', '456.789'],
},
});
expect(screen.getByText('123.46 - 456.79 GWEI')).toBeInTheDocument();
});
it('should render a fee trend arrow image if given "up" as the trend', () => {
renderComponent({
props: {
latest: '123.12345',
trend: 'up',
},
});
expect(screen.getByAltText('up arrow')).toBeInTheDocument();
});
it('should render a fee trend arrow image if given "down" as the trend', () => {
renderComponent({
props: {
latest: '123.12345',
trend: 'down',
},
});
expect(screen.getByAltText('down arrow')).toBeInTheDocument();
});
it('should render a fee trend arrow image if given "level" as the trend', () => {
renderComponent({
props: {
latest: '123.12345',
trend: 'level',
},
});
expect(screen.getByAltText('level arrow')).toBeInTheDocument();
});
it('should not render a fee trend arrow image if given an invalid trend', () => {
// Suppress warning from PropTypes, which we expect
jest.spyOn(console, 'error').mockImplementation();
renderComponent({
props: {
latest: '123.12345',
trend: 'whatever',
},
});
expect(screen.queryByTestId('fee-arrow')).not.toBeInTheDocument();
});
it('should not render a fee trend arrow image if given a nullish trend', () => {
renderComponent({
props: {
latest: '123.12345',
trend: null,
},
});
expect(screen.queryByTestId('fee-arrow')).not.toBeInTheDocument();
});
});
describe('when "latest" is nullish', () => {
it('should not render the container for the latest fee', () => {
renderComponent({
props: {
latest: null,
},
});
expect(screen.queryByTestId('latest')).not.toBeInTheDocument();
});
});
describe('when "historical" is not nullish', () => {
it('should render the historical fee if given a fee', () => {
renderComponent({
props: {
historical: '123.12345',
},
});
expect(screen.getByText('123.12 GWEI')).toBeInTheDocument();
});
it('should render the historical fee range if given a fee range', () => {
renderComponent({
props: {
historical: ['123.456', '456.789'],
},
});
expect(screen.getByText('123.46 - 456.79 GWEI')).toBeInTheDocument();
});
});
describe('when "historical" is nullish', () => {
it('should not render the container for the historical fee', () => {
renderComponent({
props: {
historical: null,
},
});
expect(screen.queryByTestId('historical')).not.toBeInTheDocument();
});
}); });
}); });

View File

@ -1,6 +1,4 @@
.advanced-gas-fee-input-subtext { .advanced-gas-fee-input-subtext {
display: flex;
align-items: center;
margin-top: 2px; margin-top: 2px;
color: var(--ui-4); color: var(--ui-4);
font-size: $font-size-h8; font-size: $font-size-h8;

View File

@ -7,11 +7,7 @@ import {
PRIORITY_LEVELS, PRIORITY_LEVELS,
} from '../../../../../../shared/constants/gas'; } from '../../../../../../shared/constants/gas';
import { PRIMARY } from '../../../../../helpers/constants/common'; import { PRIMARY } from '../../../../../helpers/constants/common';
import { import { bnGreaterThan, bnLessThan } from '../../../../../helpers/utils/util';
bnGreaterThan,
bnLessThan,
roundToDecimalPlacesRemovingExtraZeroes,
} from '../../../../../helpers/utils/util';
import { decGWEIToHexWEI } from '../../../../../helpers/utils/conversions.util'; import { decGWEIToHexWEI } from '../../../../../helpers/utils/conversions.util';
import { getAdvancedGasFeeValues } from '../../../../../selectors'; import { getAdvancedGasFeeValues } from '../../../../../selectors';
import { useGasFeeContext } from '../../../../../contexts/gasFee'; import { useGasFeeContext } from '../../../../../contexts/gasFee';
@ -23,7 +19,6 @@ import FormField from '../../../../ui/form-field';
import { useAdvancedGasFeePopoverContext } from '../../context'; import { useAdvancedGasFeePopoverContext } from '../../context';
import AdvancedGasFeeInputSubtext from '../../advanced-gas-fee-input-subtext'; import AdvancedGasFeeInputSubtext from '../../advanced-gas-fee-input-subtext';
import { renderFeeRange } from '../utils';
const validateBaseFee = (value, gasFeeEstimates, maxPriorityFeePerGas) => { const validateBaseFee = (value, gasFeeEstimates, maxPriorityFeePerGas) => {
if (bnGreaterThan(maxPriorityFeePerGas, value)) { if (bnGreaterThan(maxPriorityFeePerGas, value)) {
@ -133,12 +128,9 @@ const BaseFeeInput = () => {
numeric numeric
/> />
<AdvancedGasFeeInputSubtext <AdvancedGasFeeInputSubtext
latest={`${roundToDecimalPlacesRemovingExtraZeroes( latest={estimatedBaseFee}
estimatedBaseFee, historical={historicalBaseFeeRange}
2, trend={baseFeeTrend}
)} GWEI`}
historical={renderFeeRange(historicalBaseFeeRange)}
feeTrend={baseFeeTrend}
/> />
</Box> </Box>
); );

View File

@ -19,7 +19,6 @@ import { bnGreaterThan, bnLessThan } from '../../../../../helpers/utils/util';
import { useAdvancedGasFeePopoverContext } from '../../context'; import { useAdvancedGasFeePopoverContext } from '../../context';
import AdvancedGasFeeInputSubtext from '../../advanced-gas-fee-input-subtext'; import AdvancedGasFeeInputSubtext from '../../advanced-gas-fee-input-subtext';
import { renderFeeRange } from '../utils';
const validatePriorityFee = (value, gasFeeEstimates) => { const validatePriorityFee = (value, gasFeeEstimates) => {
if (value <= 0) { if (value <= 0) {
@ -117,9 +116,9 @@ const PriorityFeeInput = () => {
numeric numeric
/> />
<AdvancedGasFeeInputSubtext <AdvancedGasFeeInputSubtext
latest={renderFeeRange(latestPriorityFeeRange)} latest={latestPriorityFeeRange}
historical={renderFeeRange(historicalPriorityFeeRange)} historical={historicalPriorityFeeRange}
feeTrend={priorityFeeTrend} trend={priorityFeeTrend}
/> />
</Box> </Box>
); );

View File

@ -1,13 +0,0 @@
import { uniq } from 'lodash';
import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util';
export const renderFeeRange = (feeRange) => {
if (feeRange) {
const formattedRange = uniq(
feeRange.map((fee) => roundToDecimalPlacesRemovingExtraZeroes(fee, 2)),
).join(' - ');
return `${formattedRange} GWEI`;
}
return null;
};

View File

@ -7,37 +7,32 @@
height: 56px; height: 56px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: center;
}
&__separator { &__field {
border-left: 1px solid var(--ui-2); display: flex;
height: 65%; flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
&:not(:last-child) {
border-right: 1px solid var(--ui-2);
} }
}
&__field { &__field-data {
display: flex; color: var(--color-text-alternative);
flex-direction: column; font-size: 12px;
align-items: center; text-align: center;
justify-content: center; }
width: 30%;
&-data { &__field-label {
color: var(--ui-4); color: var(--color-text-default);
font-size: 12px; font-size: 10px;
text-align: center; font-weight: bold;
} margin-top: 4px;
&-label {
color: var(--Black-100);
font-size: 10px;
font-weight: bold;
margin-top: 4px;
}
}
.latest-priority-fee-field {
width: 40%;
}
} }
&__tooltip-label { &__tooltip-label {

View File

@ -1 +0,0 @@
export { default } from './latest-priority-fee-field';

View File

@ -1,35 +0,0 @@
import React, { useMemo } from 'react';
import { uniq } from 'lodash';
import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../../helpers/utils/util';
import { useGasFeeContext } from '../../../../../contexts/gasFee';
import I18nValue from '../../../../ui/i18n-value';
import { PriorityFeeTooltip } from '../tooltips';
export default function LatestPriorityFeeField() {
const { gasFeeEstimates } = useGasFeeContext();
const priorityFeeRange = useMemo(() => {
const { latestPriorityFeeRange } = gasFeeEstimates;
if (latestPriorityFeeRange) {
const formattedRange = uniq([
roundToDecimalPlacesRemovingExtraZeroes(latestPriorityFeeRange[0], 1),
roundToDecimalPlacesRemovingExtraZeroes(latestPriorityFeeRange[1], 0),
]).join(' - ');
return `${formattedRange} GWEI`;
}
return null;
}, [gasFeeEstimates]);
return (
<div className="network-statistics__info__field latest-priority-fee-field">
<PriorityFeeTooltip>
<span className="network-statistics__info__field-data">
{priorityFeeRange}
</span>
<span className="network-statistics__info__field-label">
<I18nValue messageKey="priorityFee" />
</span>
</PriorityFeeTooltip>
</div>
);
}

View File

@ -1,31 +0,0 @@
import React from 'react';
import { renderWithProvider } from '../../../../../../test/jest';
import { GasFeeContext } from '../../../../../contexts/gasFee';
import configureStore from '../../../../../store/store';
import LatestPriorityFeeField from './latest-priority-fee-field';
const renderComponent = (gasFeeEstimates) => {
const store = configureStore({});
return renderWithProvider(
<GasFeeContext.Provider value={{ gasFeeEstimates }}>
<LatestPriorityFeeField />
</GasFeeContext.Provider>,
store,
);
};
describe('LatestPriorityFeeField', () => {
it('should render a version of latest priority fee range pulled from context, lower range rounded to 1 decimal place', () => {
const { getByText } = renderComponent({
latestPriorityFeeRange: ['1.000001668', '2.5634234'],
});
expect(getByText('1 - 3 GWEI')).toBeInTheDocument();
});
it('should render nothing if gasFeeEstimates are empty', () => {
const { queryByText } = renderComponent({});
expect(queryByText('GWEI')).not.toBeInTheDocument();
});
});

View File

@ -1,21 +1,31 @@
import React from 'react'; import React, { useContext } from 'react';
import { import {
COLORS, COLORS,
FONT_WEIGHT, FONT_WEIGHT,
TYPOGRAPHY, TYPOGRAPHY,
} from '../../../../helpers/constants/design-system'; } from '../../../../helpers/constants/design-system';
import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; import { isNullish } from '../../../../helpers/utils/util';
import { formatGasFeeOrFeeRange } from '../../../../helpers/utils/gas';
import { I18nContext } from '../../../../contexts/i18n';
import { useGasFeeContext } from '../../../../contexts/gasFee'; import { useGasFeeContext } from '../../../../contexts/gasFee';
import I18nValue from '../../../ui/i18n-value';
import Typography from '../../../ui/typography/typography'; import Typography from '../../../ui/typography/typography';
import { BaseFeeTooltip, PriorityFeeTooltip } from './tooltips';
import { BaseFeeTooltip } from './tooltips';
import LatestPriorityFeeField from './latest-priority-fee-field';
import StatusSlider from './status-slider'; import StatusSlider from './status-slider';
const NetworkStatistics = () => { const NetworkStatistics = () => {
const t = useContext(I18nContext);
const { gasFeeEstimates } = useGasFeeContext(); const { gasFeeEstimates } = useGasFeeContext();
const formattedLatestBaseFee = formatGasFeeOrFeeRange(
gasFeeEstimates?.estimatedBaseFee,
{
precision: 0,
},
);
const formattedLatestPriorityFeeRange = formatGasFeeOrFeeRange(
gasFeeEstimates?.latestPriorityFeeRange,
{ precision: [1, 0] },
);
const networkCongestion = gasFeeEstimates?.networkCongestion;
return ( return (
<div className="network-statistics"> <div className="network-statistics">
@ -25,29 +35,44 @@ const NetworkStatistics = () => {
margin={[3, 0]} margin={[3, 0]}
variant={TYPOGRAPHY.H8} variant={TYPOGRAPHY.H8}
> >
<I18nValue messageKey="networkStatus" /> {t('networkStatus')}
</Typography> </Typography>
<div className="network-statistics__info"> <div className="network-statistics__info">
<div className="network-statistics__info__field"> {isNullish(formattedLatestBaseFee) ? null : (
<BaseFeeTooltip> <div
<span className="network-statistics__info__field-data"> className="network-statistics__field"
{gasFeeEstimates?.estimatedBaseFee && data-testid="formatted-latest-base-fee"
`${roundToDecimalPlacesRemovingExtraZeroes( >
gasFeeEstimates?.estimatedBaseFee, <BaseFeeTooltip>
0, <span className="network-statistics__field-data">
)} GWEI`} {formattedLatestBaseFee}
</span> </span>
<span className="network-statistics__info__field-label"> <span className="network-statistics__field-label">
<I18nValue messageKey="baseFee" /> {t('baseFee')}
</span> </span>
</BaseFeeTooltip> </BaseFeeTooltip>
</div> </div>
<div className="network-statistics__info__separator" /> )}
<LatestPriorityFeeField /> {isNullish(formattedLatestPriorityFeeRange) ? null : (
<div className="network-statistics__info__separator" /> <div
<div className="network-statistics__info__field"> className="network-statistics__field"
<StatusSlider /> data-testid="formatted-latest-priority-fee-range"
</div> >
<PriorityFeeTooltip>
<span className="network-statistics__field-data">
{formattedLatestPriorityFeeRange}
</span>
<span className="network-statistics__field-label">
{t('priorityFee')}
</span>
</PriorityFeeTooltip>
</div>
)}
{isNullish(networkCongestion) ? null : (
<div className="network-statistics__field">
<StatusSlider />
</div>
)}
</div> </div>
</div> </div>
); );

View File

@ -1,15 +1,13 @@
import React from 'react'; import React from 'react';
import { renderWithProvider, screen } from '../../../../../test/jest';
import { renderWithProvider } from '../../../../../test/jest';
import { GasFeeContext } from '../../../../contexts/gasFee';
import configureStore from '../../../../store/store'; import configureStore from '../../../../store/store';
import { GasFeeContext } from '../../../../contexts/gasFee';
import NetworkStatistics from './network-statistics'; import NetworkStatistics from './network-statistics';
const renderComponent = (gasFeeEstimates) => { const renderComponent = ({ gasFeeContext = {}, state = {} } = {}) => {
const store = configureStore({}); const store = configureStore(state);
return renderWithProvider( return renderWithProvider(
<GasFeeContext.Provider value={{ gasFeeEstimates }}> <GasFeeContext.Provider value={gasFeeContext}>
<NetworkStatistics /> <NetworkStatistics />
</GasFeeContext.Provider>, </GasFeeContext.Provider>,
store, store,
@ -17,17 +15,104 @@ const renderComponent = (gasFeeEstimates) => {
}; };
describe('NetworkStatistics', () => { describe('NetworkStatistics', () => {
it('should render the latest base fee without decimals', () => { it('should render the latest base fee rounded to no decimal places', () => {
const { getByText } = renderComponent({ renderComponent({
estimatedBaseFee: '50.0112', gasFeeContext: {
gasFeeEstimates: {
estimatedBaseFee: '50.0112',
},
},
}); });
expect(getByText('50 GWEI')).toBeInTheDocument(); expect(screen.getByText('50 GWEI')).toBeInTheDocument();
}); });
it('should render a version of latest priority fee range pulled from context, lower range rounded to 1 decimal place', () => { it('should not render the latest base fee if it is not present', () => {
const { getByText } = renderComponent({ renderComponent({
latestPriorityFeeRange: ['1.000001668', '2.5634234'], gasFeeContext: {
gasFeeEstimates: {
estimatedBaseFee: null,
},
},
}); });
expect(getByText('1 - 3 GWEI')).toBeInTheDocument(); expect(
screen.queryByTestId('formatted-latest-base-fee'),
).not.toBeInTheDocument();
});
it('should not render the latest base fee if no gas fee estimates are available', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: null,
},
});
expect(
screen.queryByTestId('formatted-latest-base-fee'),
).not.toBeInTheDocument();
});
it('should render the latest priority fee range, with the low end of the range rounded to 1 decimal place and the high end rounded to no decimal places', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: {
latestPriorityFeeRange: ['1.100001668', '2.5634234'],
},
},
});
expect(screen.getByText('1.1 - 3 GWEI')).toBeInTheDocument();
});
it('should not render the latest priority fee range if it is not present', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: {
latestPriorityFeeRange: null,
},
},
});
expect(
screen.queryByTestId('formatted-latest-priority-fee-range'),
).not.toBeInTheDocument();
});
it('should not render the latest priority fee range if no gas fee estimates are available', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: null,
},
});
expect(
screen.queryByTestId('formatted-latest-priority-fee-range'),
).not.toBeInTheDocument();
});
it('should render the network status slider', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: {
networkCongestion: 0.5,
},
},
});
expect(screen.getByText('Stable')).toBeInTheDocument();
});
it('should not render the network status slider if the network congestion is not available', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: {
networkCongestion: null,
},
},
});
expect(screen.queryByTestId('status-slider-label')).not.toBeInTheDocument();
});
it('should not render the network status slider if no gas fee estimates are available', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: null,
},
});
expect(screen.queryByTestId('status-slider-label')).not.toBeInTheDocument();
}); });
}); });

View File

@ -1,9 +1,13 @@
import { constant, times, uniq, zip } from 'lodash';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util'; import { addHexPrefix } from 'ethereumjs-util';
import { GAS_RECOMMENDATIONS } from '../../../shared/constants/gas'; import { GAS_RECOMMENDATIONS } from '../../../shared/constants/gas';
import { multiplyCurrencies } from '../../../shared/modules/conversion.utils'; import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { bnGreaterThan } from './util'; import {
bnGreaterThan,
isNullish,
roundToDecimalPlacesRemovingExtraZeroes,
} from './util';
import { hexWEIToDecGWEI } from './conversions.util'; import { hexWEIToDecGWEI } from './conversions.util';
export const gasEstimateGreaterThanGasUsedPlusTenPercent = ( export const gasEstimateGreaterThanGasUsedPlusTenPercent = (
@ -62,3 +66,43 @@ export function isMetamaskSuggestedGasEstimate(estimate) {
GAS_RECOMMENDATIONS.LOW, GAS_RECOMMENDATIONS.LOW,
].includes(estimate); ].includes(estimate);
} }
/**
* Formats a singular gas fee or a range of gas fees by rounding them to the
* given precisions and then arranging them as a string.
*
* @param {string | [string, string] | null | undefined} feeOrFeeRange - The fee
* in GWEI or range of fees in GWEI.
* @param {object} options - The options.
* @param {number | [number, number]} options.precision - The precision(s) to
* use when formatting the fee(s).
* @returns A string which represents the formatted version of the fee or fee
* range.
*/
export function formatGasFeeOrFeeRange(
feeOrFeeRange,
{ precision: precisionOrPrecisions = 2 } = {},
) {
if (
isNullish(feeOrFeeRange) ||
(Array.isArray(feeOrFeeRange) && feeOrFeeRange.length === 0)
) {
return null;
}
const range = Array.isArray(feeOrFeeRange)
? feeOrFeeRange.slice(0, 2)
: [feeOrFeeRange];
const precisions = Array.isArray(precisionOrPrecisions)
? precisionOrPrecisions.slice(0, 2)
: times(range.length, constant(precisionOrPrecisions));
const formattedRange = uniq(
zip(range, precisions).map(([fee, precision]) => {
return precision === undefined
? fee
: roundToDecimalPlacesRemovingExtraZeroes(fee, precision);
}),
).join(' - ');
return `${formattedRange} GWEI`;
}

View File

@ -3,6 +3,7 @@ import { PRIORITY_LEVELS } from '../../../shared/constants/gas';
import { import {
addTenPercent, addTenPercent,
gasEstimateGreaterThanGasUsedPlusTenPercent, gasEstimateGreaterThanGasUsedPlusTenPercent,
formatGasFeeOrFeeRange,
} from './gas'; } from './gas';
describe('Gas utils', () => { describe('Gas utils', () => {
@ -47,4 +48,64 @@ describe('Gas utils', () => {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
}); });
describe('formatGasFeeOrFeeRange', () => {
describe('given a singular fee', () => {
it('should return a string "X GWEI" where X is the fee rounded to the given precision', () => {
expect(formatGasFeeOrFeeRange('23.43', { precision: 1 })).toStrictEqual(
'23.4 GWEI',
);
});
});
describe('given an array of two fees', () => {
describe('given a single precision', () => {
it('should return a string "X - Y GWEI" where X and Y are the fees rounded to the given precision', () => {
expect(
formatGasFeeOrFeeRange(['23.43', '83.9342'], { precision: 1 }),
).toStrictEqual('23.4 - 83.9 GWEI');
});
});
describe('given two precisions', () => {
it('should return a string "X - Y GWEI" where X and Y are the fees rounded to the given precisions', () => {
expect(
formatGasFeeOrFeeRange(['23.43', '83.9342'], { precision: [1, 0] }),
).toStrictEqual('23.4 - 84 GWEI');
});
});
describe('given more than two precisions', () => {
it('should ignore precisions past 2', () => {
expect(
formatGasFeeOrFeeRange(['23.43', '83.9342'], {
precision: [1, 0, 999],
}),
).toStrictEqual('23.4 - 84 GWEI');
});
});
});
describe('given an array of more than two fees', () => {
it('should ignore fees past two', () => {
expect(
formatGasFeeOrFeeRange(['23.43', '83.9342', '300.3414'], {
precision: 1,
}),
).toStrictEqual('23.4 - 83.9 GWEI');
});
});
describe('if the fee is null', () => {
it('should return null', () => {
expect(formatGasFeeOrFeeRange(null, { precision: 1 })).toBeNull();
});
});
describe('if the fee is undefined', () => {
it('should return null', () => {
expect(formatGasFeeOrFeeRange(null, { precision: 1 })).toBeNull();
});
});
});
}); });

View File

@ -588,3 +588,14 @@ export function coinTypeToProtocolName(coinType) {
} }
return slip44[coinType]?.name || undefined; return slip44[coinType]?.name || undefined;
} }
/**
* Tests "nullishness". Used to guard a section of a component from being
* rendered based on a value.
*
* @param {any} value - A value (literally anything).
* @returns `true` if the value is null or undefined, `false` otherwise.
*/
export function isNullish(value) {
return value === null || value === undefined;
}