Skip to content

Commit

Permalink
Alert users when the network is busy (#12268)
Browse files Browse the repository at this point in the history
When a lot of transactions are occurring on the network, such as during
an NFT drop, it drives gas fees up. When this happens, we want to not
only inform the user about this, but also dissuade them from using a
higher gas fee (as we have proved in testing that high gas fees can
cause bidding wars and exacerbate the situation).

The method for determining whether the network is "busy" is already
handled by GasFeeController, which exposes a `networkCongestion`
property within the gas fee estimate data. If this number exceeds 0.66 —
meaning that the current base fee is above the 66th percentile among the
base fees over the last several days — then we determine that the
network is "busy".
  • Loading branch information
mcmire committed Jan 7, 2022
1 parent 0bada3a commit 7b963ca
Show file tree
Hide file tree
Showing 15 changed files with 430 additions and 186 deletions.
3 changes: 3 additions & 0 deletions app/_locales/en/messages.json
Expand Up @@ -1785,6 +1785,9 @@
"networkDetails": {
"message": "Network Details"
},
"networkIsBusy": {
"message": "Network is busy. Gas prices are high and estimates are less accurate."
},
"networkName": {
"message": "Network Name"
},
Expand Down
12 changes: 12 additions & 0 deletions shared/constants/gas.js
Expand Up @@ -56,3 +56,15 @@ export const EDIT_GAS_MODES = {
MODIFY_IN_PLACE: 'modify-in-place',
SWAPS: 'swaps',
};

/**
* Represents levels for `networkCongestion` (calculated along with gas fee
* estimates; represents a number between 0 and 1) that we use to render the
* network status slider on the send transaction screen and inform users when
* gas fees are high
*/
export const NETWORK_CONGESTION_THRESHOLDS = {
NOT_BUSY: 0,
STABLE: 0.33,
BUSY: 0.66,
};
Expand Up @@ -7,7 +7,7 @@ import FormField from '../../ui/form-field';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import { getGasFormErrorText } from '../../../helpers/constants/gas';
import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask';
import { getNetworkSupportsSettingGasPrice } from '../../../selectors/selectors';
import { getNetworkSupportsSettingGasPrice } from '../../../selectors';

export default function AdvancedGasControls({
gasEstimateType,
Expand Down
24 changes: 13 additions & 11 deletions ui/components/app/edit-gas-display/edit-gas-display.component.js
Expand Up @@ -63,7 +63,6 @@ export default function EditGasDisplay({
estimatedMaximumFiat,
dappSuggestedGasFeeAcknowledged,
setDappSuggestedGasFeeAcknowledged,
warning,
gasErrors,
gasWarnings,
onManualChange,
Expand All @@ -72,6 +71,7 @@ export default function EditGasDisplay({
estimatesUnavailableWarning,
hasGasErrors,
txParamsHaveBeenCustomized,
isNetworkBusy,
}) {
const t = useContext(I18nContext);
const scrollRef = useRef(null);
Expand All @@ -93,7 +93,7 @@ export default function EditGasDisplay({

useLayoutEffect(() => {
if (showAdvancedForm && scrollRef.current) {
scrollRef.current.scrollIntoView();
scrollRef.current.scrollIntoView?.();
}
}, [showAdvancedForm]);

Expand Down Expand Up @@ -133,14 +133,6 @@ export default function EditGasDisplay({
return (
<div className="edit-gas-display">
<div className="edit-gas-display__content">
{warning && !isGasEstimatesLoading && (
<div className="edit-gas-display__warning">
<ActionableMessage
className="actionable-message--warning"
message={warning}
/>
</div>
)}
{showTopError && (
<div className="edit-gas-display__warning">
<ErrorMessage errorKey={errorKey} />
Expand All @@ -156,6 +148,16 @@ export default function EditGasDisplay({
/>
</div>
)}
{isNetworkBusy ? (
<div className="edit-gas-display__warning">
<ActionableMessage
className="actionable-message--warning"
message={t('networkIsBusy')}
iconFillColor="#f8c000"
useIcon
/>
</div>
) : null}
{mode === EDIT_GAS_MODES.SPEED_UP && (
<div className="edit-gas-display__top-tooltip">
<Typography
Expand Down Expand Up @@ -336,7 +338,6 @@ EditGasDisplay.propTypes = {
estimatedMaximumFiat: PropTypes.string,
dappSuggestedGasFeeAcknowledged: PropTypes.bool,
setDappSuggestedGasFeeAcknowledged: PropTypes.func,
warning: PropTypes.string,
transaction: PropTypes.object,
gasErrors: PropTypes.object,
gasWarnings: PropTypes.object,
Expand All @@ -346,4 +347,5 @@ EditGasDisplay.propTypes = {
estimatesUnavailableWarning: PropTypes.bool,
hasGasErrors: PropTypes.bool,
txParamsHaveBeenCustomized: PropTypes.bool,
isNetworkBusy: PropTypes.bool,
};
38 changes: 38 additions & 0 deletions ui/components/app/edit-gas-display/edit-gas-display.test.js
@@ -0,0 +1,38 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import EditGasDisplay from '.';

jest.mock('../../../selectors');
jest.mock('../../../helpers/utils/confirm-tx.util');
jest.mock('../../../helpers/utils/transactions.util');

function render({ componentProps = {} } = {}) {
const store = configureStore({});
return renderWithProvider(<EditGasDisplay {...componentProps} />, store);
}

describe('EditGasDisplay', () => {
describe('if getIsNetworkBusy returns a truthy value', () => {
it('informs the user', () => {
render({ componentProps: { isNetworkBusy: true } });
expect(
screen.getByText(
'Network is busy. Gas prices are high and estimates are less accurate.',
),
).toBeInTheDocument();
});
});

describe('if getIsNetworkBusy does not return a truthy value', () => {
it('does not inform the user', () => {
render({ componentProps: { isNetworkBusy: false } });
expect(
screen.queryByText(
'Network is busy. Gas prices are high and estimates are less accurate.',
),
).not.toBeInTheDocument();
});
});
});
@@ -1,5 +1,6 @@
import React from 'react';

import { NETWORK_CONGESTION_THRESHOLDS } from '../../../../../../shared/constants/gas';
import { useGasFeeContext } from '../../../../../contexts/gasFee';
import I18nValue from '../../../../ui/i18n-value';
import { NetworkStabilityTooltip } from '../tooltips';
Expand All @@ -24,24 +25,24 @@ const determineStatusInfo = (givenNetworkCongestion) => {
const color = GRADIENT_COLORS[colorIndex];
const sliderTickValue = colorIndex * 10;

if (networkCongestion <= 0.33) {
if (networkCongestion >= NETWORK_CONGESTION_THRESHOLDS.BUSY) {
return {
statusLabel: 'notBusy',
tooltipLabel: 'lowLowercase',
statusLabel: 'busy',
tooltipLabel: 'highLowercase',
color,
sliderTickValue,
};
} else if (networkCongestion > 0.66) {
} else if (networkCongestion >= NETWORK_CONGESTION_THRESHOLDS.STABLE) {
return {
statusLabel: 'busy',
tooltipLabel: 'highLowercase',
statusLabel: 'stable',
tooltipLabel: 'stableLowercase',
color,
sliderTickValue,
};
}
return {
statusLabel: 'stable',
tooltipLabel: 'stableLowercase',
statusLabel: 'notBusy',
tooltipLabel: 'lowLowercase',
color,
sliderTickValue,
};
Expand Down
Expand Up @@ -20,31 +20,31 @@ const renderComponent = ({ networkCongestion }) => {

describe('StatusSlider', () => {
it('should show "Not busy" when networkCongestion is less than 0.33', () => {
const { queryByText } = renderComponent({ networkCongestion: 0.32 });
expect(queryByText('Not busy')).toBeInTheDocument();
const { getByText } = renderComponent({ networkCongestion: 0.32 });
expect(getByText('Not busy')).toBeInTheDocument();
});

it('should show "Not busy" when networkCongestion is 0.33', () => {
const { queryByText } = renderComponent({ networkCongestion: 0.33 });
expect(queryByText('Not busy')).toBeInTheDocument();
it('should show "Stable" when networkCongestion is 0.33', () => {
const { getByText } = renderComponent({ networkCongestion: 0.33 });
expect(getByText('Stable')).toBeInTheDocument();
});

it('should show "Stable" when networkCongestion is between 0.33 and 0.66', () => {
const { queryByText } = renderComponent({ networkCongestion: 0.5 });
expect(queryByText('Stable')).toBeInTheDocument();
const { getByText } = renderComponent({ networkCongestion: 0.5 });
expect(getByText('Stable')).toBeInTheDocument();
});

it('should show "Stable" when networkCongestion is 0.66', () => {
const { queryByText } = renderComponent({ networkCongestion: 0.66 });
expect(queryByText('Stable')).toBeInTheDocument();
it('should show "Busy" when networkCongestion is 0.66', () => {
const { getByText } = renderComponent({ networkCongestion: 0.66 });
expect(getByText('Busy')).toBeInTheDocument();
});

it('should show "Busy" when networkCongestion is greater than 0.66', () => {
const { queryByText } = renderComponent({ networkCongestion: 0.67 });
expect(queryByText('Busy')).toBeInTheDocument();
const { getByText } = renderComponent({ networkCongestion: 0.67 });
expect(getByText('Busy')).toBeInTheDocument();
});

it('should show "Stable" if networkCongestion has not been set yet', () => {
it('should show "Stable" if networkCongestion is not available yet', () => {
const { getByText } = renderComponent({});
expect(getByText('Stable')).toBeInTheDocument();
});
Expand Down
Expand Up @@ -61,8 +61,6 @@ export default function EditGasPopover({
supportsEIP1559;
const [showEducationContent, setShowEducationContent] = useState(false);

const [warning] = useState(null);

const [
dappSuggestedGasFeeAcknowledged,
setDappSuggestedGasFeeAcknowledged,
Expand Down Expand Up @@ -109,6 +107,7 @@ export default function EditGasPopover({
balanceError,
estimatesUnavailableWarning,
estimatedBaseFee,
isNetworkBusy,
} = useGasFeeInputs(
defaultEstimateToUse,
updatedTransaction,
Expand Down Expand Up @@ -264,7 +263,6 @@ export default function EditGasPopover({
{process.env.IN_TEST ? null : <LoadingHeartBeat />}
<EditGasDisplay
showEducationButton={showEducationButton}
warning={warning}
dappSuggestedGasFeeAcknowledged={dappSuggestedGasFeeAcknowledged}
setDappSuggestedGasFeeAcknowledged={
setDappSuggestedGasFeeAcknowledged
Expand Down Expand Up @@ -298,6 +296,7 @@ export default function EditGasPopover({
estimatesUnavailableWarning={estimatesUnavailableWarning}
hasGasErrors={hasGasErrors}
txParamsHaveBeenCustomized={txParamsHaveBeenCustomized}
isNetworkBusy={isNetworkBusy}
{...editGasDisplayProps}
/>
</>
Expand Down
13 changes: 12 additions & 1 deletion ui/ducks/metamask/metamask.js
@@ -1,6 +1,10 @@
import { addHexPrefix, isHexString, stripHexPrefix } from 'ethereumjs-util';
import * as actionConstants from '../../store/actionConstants';
import { ALERT_TYPES } from '../../../shared/constants/alerts';
import {
GAS_ESTIMATE_TYPES,
NETWORK_CONGESTION_THRESHOLDS,
} from '../../../shared/constants/gas';
import { NETWORK_TYPE_RPC } from '../../../shared/constants/network';
import {
accountsWithSendEtherInfoSelector,
Expand All @@ -11,7 +15,7 @@ import { updateTransaction } from '../../store/actions';
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';

import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';

export default function reduceMetamask(state = {}, action) {
Expand Down Expand Up @@ -361,6 +365,13 @@ export function getIsGasEstimatesLoading(state) {
return isGasEstimatesLoading;
}

export function getIsNetworkBusy(state) {
const gasFeeEstimates = getGasFeeEstimates(state);
return (
gasFeeEstimates?.networkCongestion >= NETWORK_CONGESTION_THRESHOLDS.BUSY
);
}

export function getCompletedOnboarding(state) {
return state.metamask.completedOnboarding;
}
Expand Down
27 changes: 27 additions & 0 deletions ui/ducks/metamask/metamask.test.js
Expand Up @@ -3,6 +3,7 @@ import * as actionConstants from '../../store/actionConstants';
import reduceMetamask, {
getBlockGasLimit,
getConversionRate,
getIsNetworkBusy,
getNativeCurrency,
getSendHexDataFeatureFlagState,
getSendToAccounts,
Expand Down Expand Up @@ -414,4 +415,30 @@ describe('MetaMask Reducers', () => {
).toStrictEqual(false);
});
});

describe('getIsNetworkBusy', () => {
it('should return true if state.metamask.gasFeeEstimates.networkCongestion is over the "busy" threshold', () => {
expect(
getIsNetworkBusy({
metamask: { gasFeeEstimates: { networkCongestion: 0.67 } },
}),
).toBe(true);
});

it('should return true if state.metamask.gasFeeEstimates.networkCongestion is right at the "busy" threshold', () => {
expect(
getIsNetworkBusy({
metamask: { gasFeeEstimates: { networkCongestion: 0.66 } },
}),
).toBe(true);
});

it('should return false if state.metamask.gasFeeEstimates.networkCongestion is not over the "busy" threshold', () => {
expect(
getIsNetworkBusy({
metamask: { gasFeeEstimates: { networkCongestion: 0.65 } },
}),
).toBe(false);
});
});
});
2 changes: 2 additions & 0 deletions ui/hooks/gasFeeInput/useGasFeeInputs.js
Expand Up @@ -111,6 +111,7 @@ export function useGasFeeInputs(
gasFeeEstimates,
isGasEstimatesLoading,
estimatedGasFeeTimeBounds,
isNetworkBusy,
} = useGasFeeEstimates();

const userPrefersAdvancedGas = useSelector(getAdvancedInlineGasShown);
Expand Down Expand Up @@ -342,6 +343,7 @@ export function useGasFeeInputs(
gasFeeEstimates,
gasEstimateType,
estimatedGasFeeTimeBounds,
isNetworkBusy,
onManualChange,
estimatedBaseFee,
// error and warnings
Expand Down
3 changes: 3 additions & 0 deletions ui/hooks/useGasFeeEstimates.js
Expand Up @@ -5,6 +5,7 @@ import {
getGasEstimateType,
getGasFeeEstimates,
getIsGasEstimatesLoading,
getIsNetworkBusy,
} from '../ducks/metamask/metamask';
import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling';

Expand Down Expand Up @@ -38,12 +39,14 @@ export function useGasFeeEstimates() {
shallowEqual,
);
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
const isNetworkBusy = useSelector(getIsNetworkBusy);
useSafeGasEstimatePolling();

return {
gasFeeEstimates,
gasEstimateType,
estimatedGasFeeTimeBounds,
isGasEstimatesLoading,
isNetworkBusy,
};
}
Expand Up @@ -22,6 +22,7 @@ const TransactionAlerts = ({
estimateUsed,
hasSimulationError,
supportsEIP1559V2,
isNetworkBusy,
} = useGasFeeContext();
const pendingTransactions = useSelector(submittedPendingTransactionsSelector);
const t = useI18nContext();
Expand Down Expand Up @@ -107,6 +108,13 @@ const TransactionAlerts = ({
type="warning"
/>
)}
{isNetworkBusy ? (
<ActionableMessage
message={<I18nValue messageKey="networkIsBusy" />}
iconFillColor="#f8c000"
type="warning"
/>
) : null}
</div>
);
};
Expand Down

0 comments on commit 7b963ca

Please sign in to comment.