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}
/>