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.
feature/default_network_editable
Elliot Winkler 3 years ago committed by GitHub
parent 8e0f71a008
commit f8f4397339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      app/_locales/en/messages.json
  2. 109
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js
  3. 167
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js
  4. 2
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss
  5. 16
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js
  6. 7
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js
  7. 13
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/utils.js
  8. 49
      ui/components/app/edit-gas-fee-popover/network-statistics/index.scss
  9. 1
      ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/index.js
  10. 35
      ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.js
  11. 31
      ui/components/app/edit-gas-fee-popover/network-statistics/latest-priority-fee-field/latest-priority-fee-field.test.js
  12. 81
      ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.js
  13. 115
      ui/components/app/edit-gas-fee-popover/network-statistics/network-statistics.test.js
  14. 48
      ui/helpers/utils/gas.js
  15. 61
      ui/helpers/utils/gas.test.js
  16. 11
      ui/helpers/utils/util.js

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

@ -1,42 +1,95 @@
import React from 'react';
import React, { useContext } from 'react';
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 I18nValue from '../../../ui/i18n-value';
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 (
<Box className="advanced-gas-fee-input-subtext">
<Box display="flex" alignItems="center">
<span className="advanced-gas-fee-input-subtext__label">
<I18nValue messageKey="currentTitle" />
</span>
<span className="advanced-gas-fee-input-subtext__value">
<LoadingHeartBeat />
{latest}
</span>
<span className={`advanced-gas-fee-input-subtext__${feeTrend}`}>
<img src={`./images/${feeTrend}-arrow.svg`} alt="feeTrend-arrow" />
</span>
</Box>
<Box>
<span className="advanced-gas-fee-input-subtext__label">
<I18nValue messageKey="twelveHrTitle" />
</span>
<span className="advanced-gas-fee-input-subtext__value">
<LoadingHeartBeat />
{historical}
</span>
</Box>
<Box
display="flex"
alignItems="center"
gap={4}
className="advanced-gas-fee-input-subtext"
>
{isNullish(latest) ? null : (
<Box display="flex" alignItems="center" data-testid="latest">
<span className="advanced-gas-fee-input-subtext__label">
{t('currentTitle')}
</span>
<span className="advanced-gas-fee-input-subtext__value">
<LoadingHeartBeat />
{formatGasFeeOrFeeRange(latest)}
</span>
{trendInfo === null ? null : (
<span className={trendInfo.className}>
<img
src={trendInfo.imageSrc}
alt={trendInfo.imageAlt}
data-testid="fee-arrow"
/>
</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>
);
};
AdvancedGasFeeInputSubtext.propTypes = {
latest: PropTypes.string,
historical: PropTypes.string,
feeTrend: PropTypes.string.isRequired,
latest: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
]),
historical: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
]),
trend: PropTypes.oneOf(['up', 'down', 'level']),
};
export default AdvancedGasFeeInputSubtext;

@ -1,55 +1,144 @@
import React from 'react';
import { screen } from '@testing-library/react';
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 { renderWithProvider, screen } from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import AdvancedGasFeeInputSubtext from './advanced-gas-fee-input-subtext';
jest.mock('../../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
getGasFeeEstimatesAndStartPolling: jest.fn().mockResolvedValue(null),
addPollingTokenToAppState: jest.fn(),
removePollingTokenFromAppState: jest.fn(),
}));
const render = () => {
const store = configureStore({
metamask: {
...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,
);
const renderComponent = ({ props = {}, state = {} } = {}) => {
const store = configureStore(state);
return renderWithProvider(<AdvancedGasFeeInputSubtext {...props} />, store);
};
describe('AdvancedGasFeeInputSubtext', () => {
it('should renders latest and historical values passed', () => {
render();
describe('when "latest" is non-nullish', () => {
it('should render the latest fee if given a fee', () => {
renderComponent({
props: {
latest: '123.12345',
},
});
expect(screen.getByText('123.12 GWEI')).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.queryByText('Latest Value')).toBeInTheDocument();
expect(screen.queryByText('Historical value')).toBeInTheDocument();
expect(screen.queryByAltText('feeTrend-arrow')).toBeInTheDocument();
expect(screen.queryByTestId('historical')).not.toBeInTheDocument();
});
});
});

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

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

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

@ -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;
};

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

@ -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>
);
}

@ -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();
});
});

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

@ -1,15 +1,13 @@
import React from 'react';
import { renderWithProvider } from '../../../../../test/jest';
import { GasFeeContext } from '../../../../contexts/gasFee';
import { renderWithProvider, screen } from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import { GasFeeContext } from '../../../../contexts/gasFee';
import NetworkStatistics from './network-statistics';
const renderComponent = (gasFeeEstimates) => {
const store = configureStore({});
const renderComponent = ({ gasFeeContext = {}, state = {} } = {}) => {
const store = configureStore(state);
return renderWithProvider(
<GasFeeContext.Provider value={{ gasFeeEstimates }}>
<GasFeeContext.Provider value={gasFeeContext}>
<NetworkStatistics />
</GasFeeContext.Provider>,
store,
@ -17,17 +15,104 @@ const renderComponent = (gasFeeEstimates) => {
};
describe('NetworkStatistics', () => {
it('should render the latest base fee without decimals', () => {
const { getByText } = renderComponent({
estimatedBaseFee: '50.0112',
it('should render the latest base fee rounded to no decimal places', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: {
estimatedBaseFee: '50.0112',
},
},
});
expect(screen.getByText('50 GWEI')).toBeInTheDocument();
});
it('should not render the latest base fee if it is not present', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: {
estimatedBaseFee: null,
},
},
});
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(getByText('50 GWEI')).toBeInTheDocument();
expect(screen.queryByTestId('status-slider-label')).not.toBeInTheDocument();
});
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'],
it('should not render the network status slider if no gas fee estimates are available', () => {
renderComponent({
gasFeeContext: {
gasFeeEstimates: null,
},
});
expect(getByText('1 - 3 GWEI')).toBeInTheDocument();
expect(screen.queryByTestId('status-slider-label')).not.toBeInTheDocument();
});
});

@ -1,9 +1,13 @@
import { constant, times, uniq, zip } from 'lodash';
import BigNumber from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util';
import { GAS_RECOMMENDATIONS } from '../../../shared/constants/gas';
import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { bnGreaterThan } from './util';
import {
bnGreaterThan,
isNullish,
roundToDecimalPlacesRemovingExtraZeroes,
} from './util';
import { hexWEIToDecGWEI } from './conversions.util';
export const gasEstimateGreaterThanGasUsedPlusTenPercent = (
@ -62,3 +66,43 @@ export function isMetamaskSuggestedGasEstimate(estimate) {
GAS_RECOMMENDATIONS.LOW,
].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`;
}

@ -3,6 +3,7 @@ import { PRIORITY_LEVELS } from '../../../shared/constants/gas';
import {
addTenPercent,
gasEstimateGreaterThanGasUsedPlusTenPercent,
formatGasFeeOrFeeRange,
} from './gas';
describe('Gas utils', () => {
@ -47,4 +48,64 @@ describe('Gas utils', () => {
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();
});
});
});
});

@ -588,3 +588,14 @@ export function coinTypeToProtocolName(coinType) {
}
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;
}

Loading…
Cancel
Save