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".feature/default_network_editable
parent
0bada3abf1
commit
7b963cabd7
@ -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,172 +1,307 @@ |
||||
import React from 'react'; |
||||
import { fireEvent, screen } from '@testing-library/react'; |
||||
|
||||
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; |
||||
import { |
||||
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 { fireEvent } from '@testing-library/react'; |
||||
import { renderWithProvider } from '../../../../test/jest'; |
||||
import { submittedPendingTransactionsSelector } from '../../../selectors/transactions'; |
||||
import { useGasFeeContext } from '../../../contexts/gasFee'; |
||||
import configureStore from '../../../store/store'; |
||||
|
||||
import TransactionAlerts from './transaction-alerts'; |
||||
|
||||
jest.mock('../../../store/actions', () => ({ |
||||
disconnectGasFeeEstimatePoller: jest.fn(), |
||||
getGasFeeEstimatesAndStartPolling: jest |
||||
.fn() |
||||
.mockImplementation(() => Promise.resolve()), |
||||
addPollingTokenToAppState: jest.fn(), |
||||
})); |
||||
|
||||
const render = ({ componentProps, transactionProps, state }) => { |
||||
const store = configureStore({ |
||||
metamask: { |
||||
...mockState.metamask, |
||||
accounts: { |
||||
[mockState.metamask.selectedAddress]: { |
||||
address: mockState.metamask.selectedAddress, |
||||
balance: '0x1F4', |
||||
}, |
||||
}, |
||||
gasFeeEstimates: mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET], |
||||
...state, |
||||
}, |
||||
}); |
||||
jest.mock('../../../selectors/transactions', () => { |
||||
return { |
||||
...jest.requireActual('../../../selectors/transactions'), |
||||
submittedPendingTransactionsSelector: jest.fn(), |
||||
}; |
||||
}); |
||||
|
||||
return renderWithProvider( |
||||
<GasFeeContextProvider |
||||
transaction={{ |
||||
txParams: { |
||||
type: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, |
||||
}, |
||||
...transactionProps, |
||||
}} |
||||
> |
||||
<TransactionAlerts {...componentProps} /> |
||||
</GasFeeContextProvider>, |
||||
store, |
||||
jest.mock('../../../contexts/gasFee'); |
||||
|
||||
function render({ |
||||
componentProps = {}, |
||||
useGasFeeContextValue = {}, |
||||
submittedPendingTransactionsSelectorValue = null, |
||||
}) { |
||||
useGasFeeContext.mockReturnValue(useGasFeeContextValue); |
||||
submittedPendingTransactionsSelector.mockReturnValue( |
||||
submittedPendingTransactionsSelectorValue, |
||||
); |
||||
}; |
||||
const store = configureStore({}); |
||||
return renderWithProvider(<TransactionAlerts {...componentProps} />, store); |
||||
} |
||||
|
||||
describe('TransactionAlerts', () => { |
||||
beforeEach(() => { |
||||
process.env.EIP_1559_V2 = true; |
||||
}); |
||||
describe('when supportsEIP1559V2 from useGasFeeContext is truthy', () => { |
||||
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(() => { |
||||
process.env.EIP_1559_V2 = false; |
||||
}); |
||||
expect( |
||||
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', () => { |
||||
render({ transactionProps: { userFeeLevel: 'low' } }); |
||||
expect( |
||||
document.getElementsByClassName('actionable-message--warning'), |
||||
).toHaveLength(1); |
||||
}); |
||||
describe('if the user has not acknowledged the failure', () => { |
||||
it('offers the user an option to bypass the warning', () => { |
||||
const { getByText } = render({ |
||||
useGasFeeContextValue: { |
||||
supportsEIP1559V2: true, |
||||
hasSimulationError: true, |
||||
}, |
||||
}); |
||||
expect(getByText('I want to proceed anyway')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should return null for gas estimate other than low', () => { |
||||
render({ transactionProps: { userFeeLevel: 'high' } }); |
||||
expect( |
||||
document.getElementsByClassName('actionable-message--warning'), |
||||
).toHaveLength(0); |
||||
}); |
||||
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(); |
||||
}); |
||||
}); |
||||
|
||||
it('should not show insufficient balance message if transaction value is less than balance', () => { |
||||
render({ |
||||
transactionProps: { |
||||
userFeeLevel: 'high', |
||||
txParams: { value: '0x64' }, |
||||
}, |
||||
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(); |
||||
}); |
||||
}); |
||||
}); |
||||
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' }, |
||||
}, |
||||
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(); |
||||
}); |
||||
}); |
||||
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, |
||||
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', () => { |
||||
it('should show simulation error message along with option to proceed anyway if transaction.simulationFails is true', () => { |
||||
render({ 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'), |
||||
).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, |
||||
describe('when supportsEIP1559V2 from useGasFeeContext is falsy', () => { |
||||
describe('if hasSimulationError from useGasFeeContext is true', () => { |
||||
it('does not inform the user that a simulation of the transaction failed', () => { |
||||
const { queryByText } = render({ |
||||
useGasFeeContextValue: { |
||||
supportsEIP1559V2: false, |
||||
hasSimulationError: 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 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(); |
||||
}); |
||||
}); |
||||
}); |
||||
|
Loading…
Reference in new issue