mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Alert users when the network is busy (#12268)
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".
This commit is contained in:
parent
0bada3abf1
commit
7b963cabd7
@ -1785,6 +1785,9 @@
|
|||||||
"networkDetails": {
|
"networkDetails": {
|
||||||
"message": "Network Details"
|
"message": "Network Details"
|
||||||
},
|
},
|
||||||
|
"networkIsBusy": {
|
||||||
|
"message": "Network is busy. Gas prices are high and estimates are less accurate."
|
||||||
|
},
|
||||||
"networkName": {
|
"networkName": {
|
||||||
"message": "Network Name"
|
"message": "Network Name"
|
||||||
},
|
},
|
||||||
|
@ -56,3 +56,15 @@ export const EDIT_GAS_MODES = {
|
|||||||
MODIFY_IN_PLACE: 'modify-in-place',
|
MODIFY_IN_PLACE: 'modify-in-place',
|
||||||
SWAPS: 'swaps',
|
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,
|
||||||
|
};
|
||||||
|
@ -7,7 +7,7 @@ import FormField from '../../ui/form-field';
|
|||||||
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
|
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
|
||||||
import { getGasFormErrorText } from '../../../helpers/constants/gas';
|
import { getGasFormErrorText } from '../../../helpers/constants/gas';
|
||||||
import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask';
|
import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask';
|
||||||
import { getNetworkSupportsSettingGasPrice } from '../../../selectors/selectors';
|
import { getNetworkSupportsSettingGasPrice } from '../../../selectors';
|
||||||
|
|
||||||
export default function AdvancedGasControls({
|
export default function AdvancedGasControls({
|
||||||
gasEstimateType,
|
gasEstimateType,
|
||||||
|
@ -63,7 +63,6 @@ export default function EditGasDisplay({
|
|||||||
estimatedMaximumFiat,
|
estimatedMaximumFiat,
|
||||||
dappSuggestedGasFeeAcknowledged,
|
dappSuggestedGasFeeAcknowledged,
|
||||||
setDappSuggestedGasFeeAcknowledged,
|
setDappSuggestedGasFeeAcknowledged,
|
||||||
warning,
|
|
||||||
gasErrors,
|
gasErrors,
|
||||||
gasWarnings,
|
gasWarnings,
|
||||||
onManualChange,
|
onManualChange,
|
||||||
@ -72,6 +71,7 @@ export default function EditGasDisplay({
|
|||||||
estimatesUnavailableWarning,
|
estimatesUnavailableWarning,
|
||||||
hasGasErrors,
|
hasGasErrors,
|
||||||
txParamsHaveBeenCustomized,
|
txParamsHaveBeenCustomized,
|
||||||
|
isNetworkBusy,
|
||||||
}) {
|
}) {
|
||||||
const t = useContext(I18nContext);
|
const t = useContext(I18nContext);
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
@ -93,7 +93,7 @@ export default function EditGasDisplay({
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (showAdvancedForm && scrollRef.current) {
|
if (showAdvancedForm && scrollRef.current) {
|
||||||
scrollRef.current.scrollIntoView();
|
scrollRef.current.scrollIntoView?.();
|
||||||
}
|
}
|
||||||
}, [showAdvancedForm]);
|
}, [showAdvancedForm]);
|
||||||
|
|
||||||
@ -133,14 +133,6 @@ export default function EditGasDisplay({
|
|||||||
return (
|
return (
|
||||||
<div className="edit-gas-display">
|
<div className="edit-gas-display">
|
||||||
<div className="edit-gas-display__content">
|
<div className="edit-gas-display__content">
|
||||||
{warning && !isGasEstimatesLoading && (
|
|
||||||
<div className="edit-gas-display__warning">
|
|
||||||
<ActionableMessage
|
|
||||||
className="actionable-message--warning"
|
|
||||||
message={warning}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showTopError && (
|
{showTopError && (
|
||||||
<div className="edit-gas-display__warning">
|
<div className="edit-gas-display__warning">
|
||||||
<ErrorMessage errorKey={errorKey} />
|
<ErrorMessage errorKey={errorKey} />
|
||||||
@ -156,6 +148,16 @@ export default function EditGasDisplay({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{mode === EDIT_GAS_MODES.SPEED_UP && (
|
||||||
<div className="edit-gas-display__top-tooltip">
|
<div className="edit-gas-display__top-tooltip">
|
||||||
<Typography
|
<Typography
|
||||||
@ -336,7 +338,6 @@ EditGasDisplay.propTypes = {
|
|||||||
estimatedMaximumFiat: PropTypes.string,
|
estimatedMaximumFiat: PropTypes.string,
|
||||||
dappSuggestedGasFeeAcknowledged: PropTypes.bool,
|
dappSuggestedGasFeeAcknowledged: PropTypes.bool,
|
||||||
setDappSuggestedGasFeeAcknowledged: PropTypes.func,
|
setDappSuggestedGasFeeAcknowledged: PropTypes.func,
|
||||||
warning: PropTypes.string,
|
|
||||||
transaction: PropTypes.object,
|
transaction: PropTypes.object,
|
||||||
gasErrors: PropTypes.object,
|
gasErrors: PropTypes.object,
|
||||||
gasWarnings: PropTypes.object,
|
gasWarnings: PropTypes.object,
|
||||||
@ -346,4 +347,5 @@ EditGasDisplay.propTypes = {
|
|||||||
estimatesUnavailableWarning: PropTypes.bool,
|
estimatesUnavailableWarning: PropTypes.bool,
|
||||||
hasGasErrors: PropTypes.bool,
|
hasGasErrors: PropTypes.bool,
|
||||||
txParamsHaveBeenCustomized: PropTypes.bool,
|
txParamsHaveBeenCustomized: PropTypes.bool,
|
||||||
|
isNetworkBusy: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
38
ui/components/app/edit-gas-display/edit-gas-display.test.js
Normal file
38
ui/components/app/edit-gas-display/edit-gas-display.test.js
Normal file
@ -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 React from 'react';
|
||||||
|
|
||||||
|
import { NETWORK_CONGESTION_THRESHOLDS } from '../../../../../../shared/constants/gas';
|
||||||
import { useGasFeeContext } from '../../../../../contexts/gasFee';
|
import { useGasFeeContext } from '../../../../../contexts/gasFee';
|
||||||
import I18nValue from '../../../../ui/i18n-value';
|
import I18nValue from '../../../../ui/i18n-value';
|
||||||
import { NetworkStabilityTooltip } from '../tooltips';
|
import { NetworkStabilityTooltip } from '../tooltips';
|
||||||
@ -24,24 +25,24 @@ const determineStatusInfo = (givenNetworkCongestion) => {
|
|||||||
const color = GRADIENT_COLORS[colorIndex];
|
const color = GRADIENT_COLORS[colorIndex];
|
||||||
const sliderTickValue = colorIndex * 10;
|
const sliderTickValue = colorIndex * 10;
|
||||||
|
|
||||||
if (networkCongestion <= 0.33) {
|
if (networkCongestion >= NETWORK_CONGESTION_THRESHOLDS.BUSY) {
|
||||||
return {
|
|
||||||
statusLabel: 'notBusy',
|
|
||||||
tooltipLabel: 'lowLowercase',
|
|
||||||
color,
|
|
||||||
sliderTickValue,
|
|
||||||
};
|
|
||||||
} else if (networkCongestion > 0.66) {
|
|
||||||
return {
|
return {
|
||||||
statusLabel: 'busy',
|
statusLabel: 'busy',
|
||||||
tooltipLabel: 'highLowercase',
|
tooltipLabel: 'highLowercase',
|
||||||
color,
|
color,
|
||||||
sliderTickValue,
|
sliderTickValue,
|
||||||
};
|
};
|
||||||
|
} else if (networkCongestion >= NETWORK_CONGESTION_THRESHOLDS.STABLE) {
|
||||||
|
return {
|
||||||
|
statusLabel: 'stable',
|
||||||
|
tooltipLabel: 'stableLowercase',
|
||||||
|
color,
|
||||||
|
sliderTickValue,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
statusLabel: 'stable',
|
statusLabel: 'notBusy',
|
||||||
tooltipLabel: 'stableLowercase',
|
tooltipLabel: 'lowLowercase',
|
||||||
color,
|
color,
|
||||||
sliderTickValue,
|
sliderTickValue,
|
||||||
};
|
};
|
||||||
|
@ -20,31 +20,31 @@ const renderComponent = ({ networkCongestion }) => {
|
|||||||
|
|
||||||
describe('StatusSlider', () => {
|
describe('StatusSlider', () => {
|
||||||
it('should show "Not busy" when networkCongestion is less than 0.33', () => {
|
it('should show "Not busy" when networkCongestion is less than 0.33', () => {
|
||||||
const { queryByText } = renderComponent({ networkCongestion: 0.32 });
|
const { getByText } = renderComponent({ networkCongestion: 0.32 });
|
||||||
expect(queryByText('Not busy')).toBeInTheDocument();
|
expect(getByText('Not busy')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show "Not busy" when networkCongestion is 0.33', () => {
|
it('should show "Stable" when networkCongestion is 0.33', () => {
|
||||||
const { queryByText } = renderComponent({ networkCongestion: 0.33 });
|
const { getByText } = renderComponent({ networkCongestion: 0.33 });
|
||||||
expect(queryByText('Not busy')).toBeInTheDocument();
|
expect(getByText('Stable')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show "Stable" when networkCongestion is between 0.33 and 0.66', () => {
|
it('should show "Stable" when networkCongestion is between 0.33 and 0.66', () => {
|
||||||
const { queryByText } = renderComponent({ networkCongestion: 0.5 });
|
const { getByText } = renderComponent({ networkCongestion: 0.5 });
|
||||||
expect(queryByText('Stable')).toBeInTheDocument();
|
expect(getByText('Stable')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show "Stable" when networkCongestion is 0.66', () => {
|
it('should show "Busy" when networkCongestion is 0.66', () => {
|
||||||
const { queryByText } = renderComponent({ networkCongestion: 0.66 });
|
const { getByText } = renderComponent({ networkCongestion: 0.66 });
|
||||||
expect(queryByText('Stable')).toBeInTheDocument();
|
expect(getByText('Busy')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show "Busy" when networkCongestion is greater than 0.66', () => {
|
it('should show "Busy" when networkCongestion is greater than 0.66', () => {
|
||||||
const { queryByText } = renderComponent({ networkCongestion: 0.67 });
|
const { getByText } = renderComponent({ networkCongestion: 0.67 });
|
||||||
expect(queryByText('Busy')).toBeInTheDocument();
|
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({});
|
const { getByText } = renderComponent({});
|
||||||
expect(getByText('Stable')).toBeInTheDocument();
|
expect(getByText('Stable')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -61,8 +61,6 @@ export default function EditGasPopover({
|
|||||||
supportsEIP1559;
|
supportsEIP1559;
|
||||||
const [showEducationContent, setShowEducationContent] = useState(false);
|
const [showEducationContent, setShowEducationContent] = useState(false);
|
||||||
|
|
||||||
const [warning] = useState(null);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
dappSuggestedGasFeeAcknowledged,
|
dappSuggestedGasFeeAcknowledged,
|
||||||
setDappSuggestedGasFeeAcknowledged,
|
setDappSuggestedGasFeeAcknowledged,
|
||||||
@ -109,6 +107,7 @@ export default function EditGasPopover({
|
|||||||
balanceError,
|
balanceError,
|
||||||
estimatesUnavailableWarning,
|
estimatesUnavailableWarning,
|
||||||
estimatedBaseFee,
|
estimatedBaseFee,
|
||||||
|
isNetworkBusy,
|
||||||
} = useGasFeeInputs(
|
} = useGasFeeInputs(
|
||||||
defaultEstimateToUse,
|
defaultEstimateToUse,
|
||||||
updatedTransaction,
|
updatedTransaction,
|
||||||
@ -264,7 +263,6 @@ export default function EditGasPopover({
|
|||||||
{process.env.IN_TEST ? null : <LoadingHeartBeat />}
|
{process.env.IN_TEST ? null : <LoadingHeartBeat />}
|
||||||
<EditGasDisplay
|
<EditGasDisplay
|
||||||
showEducationButton={showEducationButton}
|
showEducationButton={showEducationButton}
|
||||||
warning={warning}
|
|
||||||
dappSuggestedGasFeeAcknowledged={dappSuggestedGasFeeAcknowledged}
|
dappSuggestedGasFeeAcknowledged={dappSuggestedGasFeeAcknowledged}
|
||||||
setDappSuggestedGasFeeAcknowledged={
|
setDappSuggestedGasFeeAcknowledged={
|
||||||
setDappSuggestedGasFeeAcknowledged
|
setDappSuggestedGasFeeAcknowledged
|
||||||
@ -298,6 +296,7 @@ export default function EditGasPopover({
|
|||||||
estimatesUnavailableWarning={estimatesUnavailableWarning}
|
estimatesUnavailableWarning={estimatesUnavailableWarning}
|
||||||
hasGasErrors={hasGasErrors}
|
hasGasErrors={hasGasErrors}
|
||||||
txParamsHaveBeenCustomized={txParamsHaveBeenCustomized}
|
txParamsHaveBeenCustomized={txParamsHaveBeenCustomized}
|
||||||
|
isNetworkBusy={isNetworkBusy}
|
||||||
{...editGasDisplayProps}
|
{...editGasDisplayProps}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { addHexPrefix, isHexString, stripHexPrefix } from 'ethereumjs-util';
|
import { addHexPrefix, isHexString, stripHexPrefix } from 'ethereumjs-util';
|
||||||
import * as actionConstants from '../../store/actionConstants';
|
import * as actionConstants from '../../store/actionConstants';
|
||||||
import { ALERT_TYPES } from '../../../shared/constants/alerts';
|
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 { NETWORK_TYPE_RPC } from '../../../shared/constants/network';
|
||||||
import {
|
import {
|
||||||
accountsWithSendEtherInfoSelector,
|
accountsWithSendEtherInfoSelector,
|
||||||
@ -11,7 +15,7 @@ import { updateTransaction } from '../../store/actions';
|
|||||||
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
|
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
|
||||||
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
|
import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util';
|
||||||
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
|
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
|
||||||
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
|
|
||||||
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
|
import { KEYRING_TYPES } from '../../../shared/constants/hardware-wallets';
|
||||||
|
|
||||||
export default function reduceMetamask(state = {}, action) {
|
export default function reduceMetamask(state = {}, action) {
|
||||||
@ -361,6 +365,13 @@ export function getIsGasEstimatesLoading(state) {
|
|||||||
return isGasEstimatesLoading;
|
return isGasEstimatesLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIsNetworkBusy(state) {
|
||||||
|
const gasFeeEstimates = getGasFeeEstimates(state);
|
||||||
|
return (
|
||||||
|
gasFeeEstimates?.networkCongestion >= NETWORK_CONGESTION_THRESHOLDS.BUSY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCompletedOnboarding(state) {
|
export function getCompletedOnboarding(state) {
|
||||||
return state.metamask.completedOnboarding;
|
return state.metamask.completedOnboarding;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import * as actionConstants from '../../store/actionConstants';
|
|||||||
import reduceMetamask, {
|
import reduceMetamask, {
|
||||||
getBlockGasLimit,
|
getBlockGasLimit,
|
||||||
getConversionRate,
|
getConversionRate,
|
||||||
|
getIsNetworkBusy,
|
||||||
getNativeCurrency,
|
getNativeCurrency,
|
||||||
getSendHexDataFeatureFlagState,
|
getSendHexDataFeatureFlagState,
|
||||||
getSendToAccounts,
|
getSendToAccounts,
|
||||||
@ -414,4 +415,30 @@ describe('MetaMask Reducers', () => {
|
|||||||
).toStrictEqual(false);
|
).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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -111,6 +111,7 @@ export function useGasFeeInputs(
|
|||||||
gasFeeEstimates,
|
gasFeeEstimates,
|
||||||
isGasEstimatesLoading,
|
isGasEstimatesLoading,
|
||||||
estimatedGasFeeTimeBounds,
|
estimatedGasFeeTimeBounds,
|
||||||
|
isNetworkBusy,
|
||||||
} = useGasFeeEstimates();
|
} = useGasFeeEstimates();
|
||||||
|
|
||||||
const userPrefersAdvancedGas = useSelector(getAdvancedInlineGasShown);
|
const userPrefersAdvancedGas = useSelector(getAdvancedInlineGasShown);
|
||||||
@ -342,6 +343,7 @@ export function useGasFeeInputs(
|
|||||||
gasFeeEstimates,
|
gasFeeEstimates,
|
||||||
gasEstimateType,
|
gasEstimateType,
|
||||||
estimatedGasFeeTimeBounds,
|
estimatedGasFeeTimeBounds,
|
||||||
|
isNetworkBusy,
|
||||||
onManualChange,
|
onManualChange,
|
||||||
estimatedBaseFee,
|
estimatedBaseFee,
|
||||||
// error and warnings
|
// error and warnings
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
getGasEstimateType,
|
getGasEstimateType,
|
||||||
getGasFeeEstimates,
|
getGasFeeEstimates,
|
||||||
getIsGasEstimatesLoading,
|
getIsGasEstimatesLoading,
|
||||||
|
getIsNetworkBusy,
|
||||||
} from '../ducks/metamask/metamask';
|
} from '../ducks/metamask/metamask';
|
||||||
import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling';
|
import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling';
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ export function useGasFeeEstimates() {
|
|||||||
shallowEqual,
|
shallowEqual,
|
||||||
);
|
);
|
||||||
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
|
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
|
||||||
|
const isNetworkBusy = useSelector(getIsNetworkBusy);
|
||||||
useSafeGasEstimatePolling();
|
useSafeGasEstimatePolling();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -45,5 +47,6 @@ export function useGasFeeEstimates() {
|
|||||||
gasEstimateType,
|
gasEstimateType,
|
||||||
estimatedGasFeeTimeBounds,
|
estimatedGasFeeTimeBounds,
|
||||||
isGasEstimatesLoading,
|
isGasEstimatesLoading,
|
||||||
|
isNetworkBusy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ const TransactionAlerts = ({
|
|||||||
estimateUsed,
|
estimateUsed,
|
||||||
hasSimulationError,
|
hasSimulationError,
|
||||||
supportsEIP1559V2,
|
supportsEIP1559V2,
|
||||||
|
isNetworkBusy,
|
||||||
} = useGasFeeContext();
|
} = useGasFeeContext();
|
||||||
const pendingTransactions = useSelector(submittedPendingTransactionsSelector);
|
const pendingTransactions = useSelector(submittedPendingTransactionsSelector);
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
@ -107,6 +108,13 @@ const TransactionAlerts = ({
|
|||||||
type="warning"
|
type="warning"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isNetworkBusy ? (
|
||||||
|
<ActionableMessage
|
||||||
|
message={<I18nValue messageKey="networkIsBusy" />}
|
||||||
|
iconFillColor="#f8c000"
|
||||||
|
type="warning"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,172 +1,307 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, screen } from '@testing-library/react';
|
import { fireEvent } from '@testing-library/react';
|
||||||
|
import { renderWithProvider } from '../../../../test/jest';
|
||||||
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
|
import { submittedPendingTransactionsSelector } from '../../../selectors/transactions';
|
||||||
import {
|
import { useGasFeeContext } from '../../../contexts/gasFee';
|
||||||
TRANSACTION_ENVELOPE_TYPES,
|
|
||||||
TRANSACTION_STATUSES,
|
|
||||||
} from '../../../../shared/constants/transaction';
|
|
||||||
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 configureStore from '../../../store/store';
|
||||||
|
|
||||||
import TransactionAlerts from './transaction-alerts';
|
import TransactionAlerts from './transaction-alerts';
|
||||||
|
|
||||||
jest.mock('../../../store/actions', () => ({
|
jest.mock('../../../selectors/transactions', () => {
|
||||||
disconnectGasFeeEstimatePoller: jest.fn(),
|
return {
|
||||||
getGasFeeEstimatesAndStartPolling: jest
|
...jest.requireActual('../../../selectors/transactions'),
|
||||||
.fn()
|
submittedPendingTransactionsSelector: jest.fn(),
|
||||||
.mockImplementation(() => Promise.resolve()),
|
};
|
||||||
addPollingTokenToAppState: jest.fn(),
|
});
|
||||||
}));
|
|
||||||
|
|
||||||
const render = ({ componentProps, transactionProps, state }) => {
|
jest.mock('../../../contexts/gasFee');
|
||||||
const store = configureStore({
|
|
||||||
metamask: {
|
|
||||||
...mockState.metamask,
|
|
||||||
accounts: {
|
|
||||||
[mockState.metamask.selectedAddress]: {
|
|
||||||
address: mockState.metamask.selectedAddress,
|
|
||||||
balance: '0x1F4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gasFeeEstimates: mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET],
|
|
||||||
...state,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return renderWithProvider(
|
function render({
|
||||||
<GasFeeContextProvider
|
componentProps = {},
|
||||||
transaction={{
|
useGasFeeContextValue = {},
|
||||||
txParams: {
|
submittedPendingTransactionsSelectorValue = null,
|
||||||
type: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET,
|
}) {
|
||||||
},
|
useGasFeeContext.mockReturnValue(useGasFeeContextValue);
|
||||||
...transactionProps,
|
submittedPendingTransactionsSelector.mockReturnValue(
|
||||||
}}
|
submittedPendingTransactionsSelectorValue,
|
||||||
>
|
|
||||||
<TransactionAlerts {...componentProps} />
|
|
||||||
</GasFeeContextProvider>,
|
|
||||||
store,
|
|
||||||
);
|
);
|
||||||
};
|
const store = configureStore({});
|
||||||
|
return renderWithProvider(<TransactionAlerts {...componentProps} />, store);
|
||||||
|
}
|
||||||
|
|
||||||
describe('TransactionAlerts', () => {
|
describe('TransactionAlerts', () => {
|
||||||
beforeEach(() => {
|
describe('when supportsEIP1559V2 from useGasFeeContext is truthy', () => {
|
||||||
process.env.EIP_1559_V2 = true;
|
describe('if hasSimulationError from useGasFeeContext is true', () => {
|
||||||
});
|
it('informs the user that a simulation of the transaction failed', () => {
|
||||||
|
const { getByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
hasSimulationError: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
expect(
|
||||||
process.env.EIP_1559_V2 = false;
|
getByText(
|
||||||
});
|
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should returning warning message for low gas estimate', () => {
|
describe('if the user has not acknowledged the failure', () => {
|
||||||
render({ transactionProps: { userFeeLevel: 'low' } });
|
it('offers the user an option to bypass the warning', () => {
|
||||||
expect(
|
const { getByText } = render({
|
||||||
document.getElementsByClassName('actionable-message--warning'),
|
useGasFeeContextValue: {
|
||||||
).toHaveLength(1);
|
supportsEIP1559V2: true,
|
||||||
});
|
hasSimulationError: true,
|
||||||
|
|
||||||
it('should return null for gas estimate other than low', () => {
|
|
||||||
render({ transactionProps: { userFeeLevel: 'high' } });
|
|
||||||
expect(
|
|
||||||
document.getElementsByClassName('actionable-message--warning'),
|
|
||||||
).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show insufficient balance message if transaction value is less than balance', () => {
|
|
||||||
render({
|
|
||||||
transactionProps: {
|
|
||||||
userFeeLevel: 'high',
|
|
||||||
txParams: { value: '0x64' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(screen.queryByText('Insufficient funds.')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show insufficient balance message if transaction value is more than balance', () => {
|
|
||||||
render({
|
|
||||||
transactionProps: {
|
|
||||||
userFeeLevel: 'high',
|
|
||||||
txParams: { value: '0x5208' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(screen.queryByText('Insufficient funds.')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show pending transaction message if there are >= 1 pending transactions', () => {
|
|
||||||
render({
|
|
||||||
state: {
|
|
||||||
currentNetworkTxList: [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
time: 0,
|
|
||||||
txParams: {
|
|
||||||
from: mockState.metamask.selectedAddress,
|
|
||||||
to: '0xRecipient',
|
|
||||||
},
|
},
|
||||||
status: TRANSACTION_STATUSES.SUBMITTED,
|
});
|
||||||
},
|
expect(getByText('I want to proceed anyway')).toBeInTheDocument();
|
||||||
],
|
});
|
||||||
},
|
|
||||||
|
it('calls setUserAcknowledgedGasMissing if the user bypasses the warning', () => {
|
||||||
|
const setUserAcknowledgedGasMissing = jest.fn();
|
||||||
|
const { getByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
hasSimulationError: true,
|
||||||
|
},
|
||||||
|
componentProps: { setUserAcknowledgedGasMissing },
|
||||||
|
});
|
||||||
|
fireEvent.click(getByText('I want to proceed anyway'));
|
||||||
|
expect(setUserAcknowledgedGasMissing).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the user has already acknowledged the failure', () => {
|
||||||
|
it('does not offer the user an option to bypass the warning', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
hasSimulationError: true,
|
||||||
|
},
|
||||||
|
componentProps: { userAcknowledgedGasMissing: true },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText('I want to proceed anyway'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if hasSimulationError from useGasFeeContext is falsy', () => {
|
||||||
|
it('does not inform the user that a simulation of the transaction failed', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the length of pendingTransactions is 1', () => {
|
||||||
|
it('informs the user that they have a pending transaction', () => {
|
||||||
|
const { getByText } = render({
|
||||||
|
useGasFeeContextValue: { supportsEIP1559V2: true },
|
||||||
|
submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }],
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
getByText('You have (1) pending transaction.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the length of pendingTransactions is more than 1', () => {
|
||||||
|
it('informs the user that they have pending transactions', () => {
|
||||||
|
const { getByText } = render({
|
||||||
|
useGasFeeContextValue: { supportsEIP1559V2: true },
|
||||||
|
submittedPendingTransactionsSelectorValue: [
|
||||||
|
{ some: 'transaction' },
|
||||||
|
{ some: 'transaction' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
getByText('You have (2) pending transactions.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the length of pendingTransactions is 0', () => {
|
||||||
|
it('does not inform the user that they have pending transactions', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: { supportsEIP1559V2: true },
|
||||||
|
submittedPendingTransactionsSelectorValue: [],
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText('You have (0) pending transactions.'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if balanceError from useGasFeeContext is true', () => {
|
||||||
|
it('informs the user that they have insufficient funds', () => {
|
||||||
|
const { getByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
balanceError: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByText('Insufficient funds.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if balanceError from useGasFeeContext is falsy', () => {
|
||||||
|
it('does not inform the user that they have insufficient funds', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
balanceError: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(queryByText('Insufficient funds.')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if estimateUsed from useGasFeeContext is "low"', () => {
|
||||||
|
it('informs the user that the current transaction is queued', () => {
|
||||||
|
const { getByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
estimateUsed: 'low',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
getByText(
|
||||||
|
'Future transactions will queue after this one. This price was last seen was some time ago.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if estimateUsed from useGasFeeContext is not "low"', () => {
|
||||||
|
it('does not inform the user that the current transaction is queued', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
estimateUsed: 'something_else',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'Future transactions will queue after this one. This price was last seen was some time ago.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if isNetworkBusy from useGasFeeContext is truthy', () => {
|
||||||
|
it('informs the user that the network is busy', () => {
|
||||||
|
const { getByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
isNetworkBusy: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
getByText(
|
||||||
|
'Network is busy. Gas prices are high and estimates are less accurate.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if isNetworkBusy from useGasFeeContext is falsy', () => {
|
||||||
|
it('does not inform the user that the network is busy', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: true,
|
||||||
|
isNetworkBusy: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'Network is busy. Gas prices are high and estimates are less accurate.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
expect(
|
|
||||||
screen.queryByText('You have (1) pending transaction.'),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SimulationError Message', () => {
|
describe('when supportsEIP1559V2 from useGasFeeContext is falsy', () => {
|
||||||
it('should show simulation error message along with option to proceed anyway if transaction.simulationFails is true', () => {
|
describe('if hasSimulationError from useGasFeeContext is true', () => {
|
||||||
render({ transactionProps: { simulationFails: true } });
|
it('does not inform the user that a simulation of the transaction failed', () => {
|
||||||
expect(
|
const { queryByText } = render({
|
||||||
screen.queryByText(
|
useGasFeeContextValue: {
|
||||||
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
|
supportsEIP1559V2: false,
|
||||||
),
|
hasSimulationError: true,
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText('I want to proceed anyway'),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show options to acknowledge gas-missing warning if component prop userAcknowledgedGasMissing is already true', () => {
|
|
||||||
render({
|
|
||||||
componentProps: {
|
|
||||||
userAcknowledgedGasMissing: true,
|
|
||||||
},
|
|
||||||
transactionProps: { simulationFails: true },
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
screen.queryByText(
|
|
||||||
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText('I want to proceed anyway'),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call prop setUserAcknowledgedGasMissing if option to acknowledge gas-missing warning is clicked', () => {
|
|
||||||
const setUserAcknowledgedGasMissing = jest.fn();
|
|
||||||
render({
|
|
||||||
componentProps: {
|
|
||||||
setUserAcknowledgedGasMissing,
|
|
||||||
},
|
|
||||||
transactionProps: { simulationFails: true },
|
|
||||||
});
|
|
||||||
fireEvent.click(screen.queryByText('I want to proceed anyway'));
|
|
||||||
expect(setUserAcknowledgedGasMissing).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null for legacy transactions', () => {
|
|
||||||
const { container } = render({
|
|
||||||
transactionProps: {
|
|
||||||
txParams: {
|
|
||||||
type: TRANSACTION_ENVELOPE_TYPES.LEGACY,
|
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the length of pendingTransactions is at least 1', () => {
|
||||||
|
it('informs the user that they have a pending transaction', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: { supportsEIP1559V2: false },
|
||||||
|
submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }],
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText('You have (1) pending transaction.'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if balanceError from useGasFeeContext is true', () => {
|
||||||
|
it('informs the user that they have insufficient funds', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: false,
|
||||||
|
balanceError: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(queryByText('Insufficient funds.')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if estimateUsed from useGasFeeContext is "low"', () => {
|
||||||
|
it('informs the user that the current transaction is queued', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: false,
|
||||||
|
estimateUsed: 'low',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'Future transactions will queue after this one. This price was last seen was some time ago.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if isNetworkBusy from useGasFeeContext is truthy', () => {
|
||||||
|
it('does not inform the user that the network is busy', () => {
|
||||||
|
const { queryByText } = render({
|
||||||
|
useGasFeeContextValue: {
|
||||||
|
supportsEIP1559V2: false,
|
||||||
|
isNetworkBusy: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'Network is busy. Gas prices are high and estimates are less accurate.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(container.firstChild).toBeNull();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,8 +10,11 @@ import {
|
|||||||
TRANSACTION_TYPES,
|
TRANSACTION_TYPES,
|
||||||
} from '../../shared/constants/transaction';
|
} from '../../shared/constants/transaction';
|
||||||
import { transactionMatchesNetwork } from '../../shared/modules/transaction.utils';
|
import { transactionMatchesNetwork } from '../../shared/modules/transaction.utils';
|
||||||
import { getCurrentChainId, deprecatedGetCurrentNetworkId } from './selectors';
|
import {
|
||||||
import { getSelectedAddress } from '.';
|
getCurrentChainId,
|
||||||
|
deprecatedGetCurrentNetworkId,
|
||||||
|
getSelectedAddress,
|
||||||
|
} from './selectors';
|
||||||
|
|
||||||
export const incomingTxListSelector = (state) => {
|
export const incomingTxListSelector = (state) => {
|
||||||
const { showIncomingTransactions } = state.metamask.featureFlags;
|
const { showIncomingTransactions } = state.metamask.featureFlags;
|
||||||
|
Loading…
Reference in New Issue
Block a user