diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index a8b60a6bf0af..4e9c901e3903 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3913,6 +3913,9 @@ "message": "Warning: you are about to send to a token contract which could result in a loss of funds. $1", "description": "$1 is a clickable link with text defined by the 'learnMoreUpperCase' key. The link will open to a support article regarding the known contract address warning" }, + "sendingZeroAmount": { + "message": "You are sending 0 $1." + }, "sepolia": { "message": "Sepolia test network" }, diff --git a/ui/components/app/transaction-alerts/transaction-alerts.js b/ui/components/app/transaction-alerts/transaction-alerts.js index 0738fdc104b4..c72d58bf1b68 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; - import { PriorityLevels } from '../../../../shared/constants/gas'; import { submittedPendingTransactionsSelector } from '../../../selectors'; import { useGasFeeContext } from '../../../contexts/gasFee'; @@ -16,16 +15,44 @@ import { isSuspiciousResponse } from '../../../../shared/modules/security-provid import BlockaidBannerAlert from '../security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert'; ///: END:ONLY_INCLUDE_IN import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; +import { TransactionType } from '../../../../shared/constants/transaction'; +import { parseStandardTokenTransactionData } from '../../../../shared/modules/transaction.utils'; +import { getTokenValueParam } from '../../../../shared/lib/metamask-controller-utils'; const TransactionAlerts = ({ userAcknowledgedGasMissing, setUserAcknowledgedGasMissing, txData, + tokenSymbol, }) => { const { estimateUsed, hasSimulationError, supportsEIP1559, isNetworkBusy } = useGasFeeContext(); const pendingTransactions = useSelector(submittedPendingTransactionsSelector); const t = useI18nContext(); + const nativeCurrency = useSelector(getNativeCurrency); + const transactionData = txData.txParams.data; + const currentTokenSymbol = tokenSymbol || nativeCurrency; + let currentTokenAmount; + + if (txData.type === TransactionType.simpleSend) { + currentTokenAmount = txData.txParams.value; + } + if (txData.type === TransactionType.tokenMethodTransfer) { + const tokenData = parseStandardTokenTransactionData(transactionData); + currentTokenAmount = getTokenValueParam(tokenData); + } + + // isSendingZero is true when either sending native tokens where the value is in txParams + // or sending tokens where the value is in the txData + // We want to only display this warning in the cases where txType is simpleSend || transfer and not contractInteractions + const hasProperTxType = + txData.type === TransactionType.simpleSend || + txData.type === TransactionType.tokenMethodTransfer; + + const isSendingZero = + hasProperTxType && + (currentTokenAmount === '0x0' || currentTokenAmount === '0'); return (
@@ -85,6 +112,11 @@ const TransactionAlerts = ({ {t('networkIsBusy')} ) : null} + {isSendingZero && ( + + {t('sendingZeroAmount', [currentTokenSymbol])} + + )}
); }; @@ -93,6 +125,7 @@ TransactionAlerts.propTypes = { userAcknowledgedGasMissing: PropTypes.bool, setUserAcknowledgedGasMissing: PropTypes.func, txData: PropTypes.object, + tokenSymbol: PropTypes.string, }; export default TransactionAlerts; diff --git a/ui/components/app/transaction-alerts/transaction-alerts.stories.js b/ui/components/app/transaction-alerts/transaction-alerts.stories.js index 72426dd9202e..a53cd7a23ff0 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.stories.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.stories.js @@ -98,6 +98,11 @@ export default { }, args: { userAcknowledgedGasMissing: false, + txData: { + txParams: { + value: '0x1', + }, + }, }, }; @@ -121,6 +126,15 @@ export const DefaultStory = (args) => ( ); DefaultStory.storyName = 'Default'; +DefaultStory.args = { + ...DefaultStory.args, + txData: { + txParams: { + value: '0x0', + }, + type: 'simpleSend', + }, +}; export const SimulationError = (args) => ( @@ -170,3 +184,20 @@ export const BusyNetwork = (args) => ( ); BusyNetwork.storyName = 'BusyNetwork'; + +export const SendingZeroAmount = (args) => ( + + + + + +); +SendingZeroAmount.storyName = 'SendingZeroAmount'; +SendingZeroAmount.args = { + txData: { + txParams: { + value: '0x0', + }, + type: 'simpleSend', + }, +}; diff --git a/ui/components/app/transaction-alerts/transaction-alerts.test.js b/ui/components/app/transaction-alerts/transaction-alerts.test.js index 8bbda90e37a2..0918801b77ab 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.test.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.test.js @@ -1,10 +1,14 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; +import sinon from 'sinon'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../shared/constants/security-provider'; import { renderWithProvider } from '../../../../test/jest'; import { submittedPendingTransactionsSelector } from '../../../selectors/transactions'; import { useGasFeeContext } from '../../../contexts/gasFee'; import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import * as txUtil from '../../../../shared/modules/transaction.utils'; +import * as metamaskControllerUtils from '../../../../shared/lib/metamask-controller-utils'; import TransactionAlerts from './transaction-alerts'; jest.mock('../../../selectors/transactions', () => { @@ -20,12 +24,13 @@ function render({ componentProps = {}, useGasFeeContextValue = {}, submittedPendingTransactionsSelectorValue = null, + mockedStore = mockState, }) { useGasFeeContext.mockReturnValue(useGasFeeContextValue); submittedPendingTransactionsSelector.mockReturnValue( submittedPendingTransactionsSelectorValue, ); - const store = configureStore({}); + const store = configureStore(mockedStore); return renderWithProvider(, store); } @@ -44,6 +49,9 @@ describe('TransactionAlerts', () => { operator: '0x92a3b9773b1763efa556f55ccbeb20441962d9b2', }, }, + txParams: { + value: '0x1', + }, }, }, }); @@ -59,6 +67,9 @@ describe('TransactionAlerts', () => { reason: 'Some reason...', reason_header: 'Some reason header...', }, + txParams: { + value: '0x1', + }, }, }, }); @@ -79,6 +90,9 @@ describe('TransactionAlerts', () => { securityProviderResponse: { flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITY.NOT_MALICIOUS, }, + txParams: { + value: '0x1', + }, }, }, }); @@ -100,6 +114,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( @@ -116,6 +137,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect(getByText('I want to proceed anyway')).toBeInTheDocument(); }); @@ -127,7 +155,14 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, - componentProps: { setUserAcknowledgedGasMissing }, + componentProps: { + setUserAcknowledgedGasMissing, + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); fireEvent.click(getByText('I want to proceed anyway')); expect(setUserAcknowledgedGasMissing).toHaveBeenCalled(); @@ -141,7 +176,14 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, - componentProps: { userAcknowledgedGasMissing: true }, + componentProps: { + userAcknowledgedGasMissing: true, + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('I want to proceed anyway'), @@ -156,6 +198,13 @@ describe('TransactionAlerts', () => { useGasFeeContextValue: { supportsEIP1559: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -170,6 +219,13 @@ describe('TransactionAlerts', () => { const { getByText } = render({ useGasFeeContextValue: { supportsEIP1559: true }, submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText('You have (1) pending transaction.'), @@ -185,6 +241,13 @@ describe('TransactionAlerts', () => { { some: 'transaction' }, { some: 'transaction' }, ], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText('You have (2) pending transactions.'), @@ -197,6 +260,13 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ useGasFeeContextValue: { supportsEIP1559: true }, submittedPendingTransactionsSelectorValue: [], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('You have (0) pending transactions.'), @@ -211,6 +281,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, balanceError: false, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect(queryByText('Insufficient funds.')).not.toBeInTheDocument(); }); @@ -223,6 +300,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, estimateUsed: 'low', }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText('Future transactions will queue after this one.'), @@ -237,6 +321,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, estimateUsed: 'something_else', }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('Future transactions will queue after this one.'), @@ -251,6 +342,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, isNetworkBusy: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText( @@ -267,6 +365,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, isNetworkBusy: false, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -285,6 +390,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, hasSimulationError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( @@ -300,6 +412,13 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ useGasFeeContextValue: { supportsEIP1559: false }, submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('You have (1) pending transaction.'), @@ -314,6 +433,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, balanceError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect(queryByText('Insufficient funds.')).not.toBeInTheDocument(); }); @@ -326,6 +452,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, estimateUsed: 'low', }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -342,6 +475,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, isNetworkBusy: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -351,4 +491,85 @@ describe('TransactionAlerts', () => { }); }); }); + + describe('when sending zero amount it should display a warning', () => { + it('should display alert if sending zero tokens', () => { + // Mock + const testTokenData = { + args: 'decoded-param', + }; + const testTokenValue = '0'; + + const parseStandardTokenTransactionDataStub = sinon.stub( + txUtil, + 'parseStandardTokenTransactionData', + ); + const getTokenValueStub = sinon.stub( + metamaskControllerUtils, + 'getTokenValueParam', + ); + + parseStandardTokenTransactionDataStub.callsFake(() => testTokenData); + getTokenValueStub.callsFake(() => testTokenValue); + // render + const { getByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x0', + }, + type: 'transfer', + }, + tokenSymbol: 'DAI', + }, + }); + // assert + expect(getByText('You are sending 0 DAI.')).toBeInTheDocument(); + }); + + it('should display alert if sending zero of native currency', () => { + const { getByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x0', + }, + type: 'simpleSend', + }, + tokenSymbol: undefined, + }, + }); + expect(getByText('You are sending 0 ETH.')).toBeInTheDocument(); + }); + }); + + describe('when sending amount different than zero should not display alert', () => { + it('should not display alerts if sending amount different than zero in native currency', () => { + const { queryByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x5af3107a4000', + }, + }, + tokenSymbol: undefined, + }, + }); + expect(queryByText('You are sending 0 ETH.')).not.toBeInTheDocument(); + }); + + it('should not display alerts if sending amount different than zero in tokens', () => { + const { queryByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x0', + }, + }, + tokenSymbol: 'DAI', + }, + }); + expect(queryByText('You are sending 0 DAI.')).not.toBeInTheDocument(); + }); + }); }); diff --git a/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap b/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap index cbeb323e9f0a..3b1ab1ed3cb9 100644 --- a/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap +++ b/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap @@ -329,6 +329,21 @@ exports[`ConfirmSendEther should render correct information for for confirm send

+
+ +
+

+ You are sending 0 ETH. +

+
+
); } diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index f610f6b92214..6cd2da349f3b 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -145,6 +145,7 @@ export default class ConfirmTransactionBase extends Component { isNoteToTraderSupported: PropTypes.bool, isMainBetaFlask: PropTypes.bool, displayAccountBalanceHeader: PropTypes.bool, + tokenSymbol: PropTypes.string, }; state = { @@ -324,6 +325,7 @@ export default class ConfirmTransactionBase extends Component { nativeCurrency, isBuyableChain, useCurrencyRateCheck, + tokenSymbol, } = this.props; const { t } = this.context; const { userAcknowledgedGasMissing } = this.state; @@ -461,6 +463,7 @@ export default class ConfirmTransactionBase extends Component { networkName={networkName} type={txData.type} isBuyableChain={isBuyableChain} + tokenSymbol={tokenSymbol} />