diff --git a/app/components/Views/confirmations/components/UI/info-row/alert-row/constants.ts b/app/components/Views/confirmations/components/UI/info-row/alert-row/constants.ts
index ce91f38650b9..65dcaf3ed72a 100755
--- a/app/components/Views/confirmations/components/UI/info-row/alert-row/constants.ts
+++ b/app/components/Views/confirmations/components/UI/info-row/alert-row/constants.ts
@@ -2,6 +2,7 @@ export enum RowAlertKey {
Amount = 'amount',
AccountTypeUpgrade = 'accountTypeUpgrade',
BatchedApprovals = 'batchedApprovals',
+ BurnAddress = 'burnAddress',
Blockaid = 'blockaid',
EstimatedFee = 'estimatedFee',
PayWith = 'payWith',
diff --git a/app/components/Views/confirmations/components/UI/info-row/info-row.styles.ts b/app/components/Views/confirmations/components/UI/info-row/info-row.styles.ts
index dcc88fb4bfc6..5eb1380b517d 100644
--- a/app/components/Views/confirmations/components/UI/info-row/info-row.styles.ts
+++ b/app/components/Views/confirmations/components/UI/info-row/info-row.styles.ts
@@ -33,6 +33,9 @@ const styleSheet = (params: { theme: Theme }) => {
paddingBottom: 8,
paddingHorizontal: 8,
},
+ labelContainerWithoutLabel: {
+ marginLeft: -4,
+ },
});
};
diff --git a/app/components/Views/confirmations/components/UI/info-row/info-row.tsx b/app/components/Views/confirmations/components/UI/info-row/info-row.tsx
index c6792c3a6312..833378372d5f 100644
--- a/app/components/Views/confirmations/components/UI/info-row/info-row.tsx
+++ b/app/components/Views/confirmations/components/UI/info-row/info-row.tsx
@@ -15,7 +15,7 @@ import styleSheet from './info-row.styles';
import CopyIcon from './copy-icon/copy-icon';
export interface InfoRowProps {
- label: string;
+ label?: string;
children?: ReactNode | string;
onTooltipPress?: () => void;
tooltip?: ReactNode;
@@ -48,6 +48,7 @@ const InfoRow = ({
withIcon,
}: InfoRowProps) => {
const { styles } = useStyles(styleSheet, {});
+ const hasLabel = Boolean(label);
const ValueComponent =
typeof children === 'string' ? (
@@ -62,7 +63,7 @@ const InfoRow = ({
style={{ ...styles.container, ...style }}
testID={testID ?? 'info-row'}
>
- {Boolean(label) && (
+ {hasLabel && (
{label}
@@ -77,6 +78,16 @@ const InfoRow = ({
)}
)}
+ {!hasLabel && labelChildren && (
+
+ {labelChildren}
+
+ )}
{valueOnNewLine ? null : ValueComponent}
{copyText && (
diff --git a/app/components/Views/confirmations/components/rows/transactions/from-to-row/from-to-row.tsx b/app/components/Views/confirmations/components/rows/transactions/from-to-row/from-to-row.tsx
index 2b782f1e4695..b3ab3735c00d 100644
--- a/app/components/Views/confirmations/components/rows/transactions/from-to-row/from-to-row.tsx
+++ b/app/components/Views/confirmations/components/rows/transactions/from-to-row/from-to-row.tsx
@@ -12,7 +12,9 @@ import Icon, {
} from '../../../../../../../component-library/components/Icons/Icon';
import { NameType } from '../../../../../../UI/Name/Name.types';
import { useTransferRecipient } from '../../../../hooks/transactions/useTransferRecipient';
+import { RowAlertKey } from '../../../UI/info-row/alert-row/constants';
import InfoSection from '../../../UI/info-row/info-section';
+import AlertRow from '../../../UI/info-row/alert-row';
import styleSheet from './from-to-row.styles';
const FromToRow = () => {
@@ -54,12 +56,15 @@ const FromToRow = () => {
-
+ {/* Intentional empty label to trigger the alert row without a label */}
+
+
+
diff --git a/app/components/Views/confirmations/constants/alerts.ts b/app/components/Views/confirmations/constants/alerts.ts
index 08977a67eb3e..fab61e762fc6 100644
--- a/app/components/Views/confirmations/constants/alerts.ts
+++ b/app/components/Views/confirmations/constants/alerts.ts
@@ -1,6 +1,7 @@
export enum AlertKeys {
BatchedUnusedApprovals = 'batched_unused_approvals',
Blockaid = 'blockaid',
+ BurnAddress = 'burn_address',
DomainMismatch = 'domain_mismatch',
InsufficientBalance = 'insufficient_balance',
InsufficientPayTokenBalance = 'insufficient_pay_token_balance',
diff --git a/app/components/Views/confirmations/hooks/alerts/useBurnAddressAlert.test.ts b/app/components/Views/confirmations/hooks/alerts/useBurnAddressAlert.test.ts
new file mode 100644
index 000000000000..5d1041754e5f
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/alerts/useBurnAddressAlert.test.ts
@@ -0,0 +1,142 @@
+import { useBurnAddressAlert } from './useBurnAddressAlert';
+import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
+import {
+ useNestedTransactionTransferRecipients,
+ useTransferRecipient,
+} from '../transactions/useTransferRecipient';
+import { Severity } from '../../types/alerts';
+import { AlertKeys } from '../../constants/alerts';
+import { RowAlertKey } from '../../components/UI/info-row/alert-row/constants';
+
+jest.mock('../transactions/useTransferRecipient');
+
+describe('useBurnAddressAlert', () => {
+ const BURN_ADDRESS_1 = '0x0000000000000000000000000000000000000000';
+ const BURN_ADDRESS_2 = '0x000000000000000000000000000000000000dead';
+ const NORMAL_ADDRESS = '0x1234567890123456789012345678901234567890';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useTransferRecipient as jest.Mock).mockReturnValue(undefined);
+ (useNestedTransactionTransferRecipients as jest.Mock).mockReturnValue([]);
+ });
+
+ it('returns empty array when no recipient addresses are present', () => {
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toEqual([]);
+ });
+
+ it('returns empty array when transaction recipient is a normal address', () => {
+ (useTransferRecipient as jest.Mock).mockReturnValue(NORMAL_ADDRESS);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toEqual([]);
+ });
+
+ it('returns empty array when nested transaction recipients are normal addresses', () => {
+ (useNestedTransactionTransferRecipients as jest.Mock).mockReturnValue([
+ NORMAL_ADDRESS,
+ '0xabcdef1234567890abcdef1234567890abcdef12',
+ ]);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toEqual([]);
+ });
+
+ it('returns burn address alert when transaction recipient is first burn address', () => {
+ (useTransferRecipient as jest.Mock).mockReturnValue(BURN_ADDRESS_1);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0]).toMatchObject({
+ key: AlertKeys.BurnAddress,
+ field: RowAlertKey.BurnAddress,
+ severity: Severity.Danger,
+ isBlocking: false,
+ });
+ expect(result.current[0].title).toBeDefined();
+ expect(result.current[0].message).toBeDefined();
+ });
+
+ it('returns burn address alert when transaction recipient is second burn address', () => {
+ (useTransferRecipient as jest.Mock).mockReturnValue(BURN_ADDRESS_2);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0]).toMatchObject({
+ key: AlertKeys.BurnAddress,
+ field: RowAlertKey.BurnAddress,
+ severity: Severity.Danger,
+ isBlocking: false,
+ });
+ });
+
+ it('returns burn address alert when transaction recipient is burn address with uppercase letters', () => {
+ (useTransferRecipient as jest.Mock).mockReturnValue(
+ '0x0000000000000000000000000000000000000000'.toUpperCase(),
+ );
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0].key).toBe(AlertKeys.BurnAddress);
+ });
+
+ it('returns burn address alert when nested transaction contains burn address', () => {
+ (useNestedTransactionTransferRecipients as jest.Mock).mockReturnValue([
+ NORMAL_ADDRESS,
+ BURN_ADDRESS_1,
+ ]);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0]).toMatchObject({
+ key: AlertKeys.BurnAddress,
+ field: RowAlertKey.BurnAddress,
+ severity: Severity.Danger,
+ isBlocking: false,
+ });
+ });
+
+ it('returns burn address alert when nested transaction contains burn address with mixed case', () => {
+ (useNestedTransactionTransferRecipients as jest.Mock).mockReturnValue([
+ '0x000000000000000000000000000000000000dEaD',
+ ]);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0].key).toBe(AlertKeys.BurnAddress);
+ });
+
+ it('returns single burn address alert when both transaction and nested transactions contain burn addresses', () => {
+ (useTransferRecipient as jest.Mock).mockReturnValue(BURN_ADDRESS_1);
+ (useNestedTransactionTransferRecipients as jest.Mock).mockReturnValue([
+ BURN_ADDRESS_2,
+ ]);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0].key).toBe(AlertKeys.BurnAddress);
+ });
+
+ it('returns single burn address alert when nested transactions contain multiple burn addresses', () => {
+ (useNestedTransactionTransferRecipients as jest.Mock).mockReturnValue([
+ BURN_ADDRESS_1,
+ NORMAL_ADDRESS,
+ BURN_ADDRESS_2,
+ ]);
+
+ const { result } = renderHookWithProvider(() => useBurnAddressAlert());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0].key).toBe(AlertKeys.BurnAddress);
+ });
+});
diff --git a/app/components/Views/confirmations/hooks/alerts/useBurnAddressAlert.ts b/app/components/Views/confirmations/hooks/alerts/useBurnAddressAlert.ts
new file mode 100644
index 000000000000..c243d018fd7a
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/alerts/useBurnAddressAlert.ts
@@ -0,0 +1,48 @@
+import { useMemo } from 'react';
+import { Alert, Severity } from '../../types/alerts';
+import { RowAlertKey } from '../../components/UI/info-row/alert-row/constants';
+import { AlertKeys } from '../../constants/alerts';
+import { LOWER_CASED_BURN_ADDRESSES } from '../../utils/send-address-validations';
+import { strings } from '../../../../../../locales/i18n';
+import {
+ useNestedTransactionTransferRecipients,
+ useTransferRecipient,
+} from '../transactions/useTransferRecipient';
+
+export function useBurnAddressAlert(): Alert[] {
+ const transactionMetaRecipient = useTransferRecipient();
+ const nestedTransactionRecipients = useNestedTransactionTransferRecipients();
+
+ const hasBurnAddressRecipient = useMemo(() => {
+ const hasBurnAddressInTransactionMetaRecipient =
+ LOWER_CASED_BURN_ADDRESSES.includes(
+ transactionMetaRecipient?.toLowerCase() ?? '',
+ );
+ const hasBurnAddressNestedTransactionRecipient =
+ nestedTransactionRecipients.some((recipient) =>
+ LOWER_CASED_BURN_ADDRESSES.includes(recipient.toLowerCase()),
+ );
+
+ return (
+ hasBurnAddressInTransactionMetaRecipient ||
+ hasBurnAddressNestedTransactionRecipient
+ );
+ }, [transactionMetaRecipient, nestedTransactionRecipients]);
+
+ return useMemo(() => {
+ if (!hasBurnAddressRecipient) {
+ return [];
+ }
+
+ return [
+ {
+ key: AlertKeys.BurnAddress,
+ field: RowAlertKey.BurnAddress,
+ message: strings('alert_system.burn_address.message'),
+ title: strings('alert_system.burn_address.title'),
+ severity: Severity.Danger,
+ isBlocking: false,
+ },
+ ];
+ }, [hasBurnAddressRecipient]);
+}
diff --git a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts
index db0c044218f5..9b3f5173f185 100644
--- a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts
+++ b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts
@@ -18,6 +18,7 @@ import { useInsufficientPayTokenBalanceAlert } from './useInsufficientPayTokenBa
import { useNoPayTokenQuotesAlert } from './useNoPayTokenQuotesAlert';
import { useInsufficientPayTokenNativeAlert } from './useInsufficientPayTokenNativeAlert';
import { useInsufficientPredictBalanceAlert } from './useInsufficientPredictBalanceAlert';
+import { useBurnAddressAlert } from './useBurnAddressAlert';
jest.mock('./useBlockaidAlerts');
jest.mock('./useDomainMismatchAlerts');
@@ -31,6 +32,7 @@ jest.mock('./useInsufficientPayTokenBalanceAlert');
jest.mock('./useNoPayTokenQuotesAlert');
jest.mock('./useInsufficientPayTokenNativeAlert');
jest.mock('./useInsufficientPredictBalanceAlert');
+jest.mock('./useBurnAddressAlert');
describe('useConfirmationAlerts', () => {
const ALERT_MESSAGE_MOCK = 'This is a test alert message.';
@@ -144,6 +146,14 @@ describe('useConfirmationAlerts', () => {
severity: Severity.Danger,
},
];
+ const mockBurnAddressAlert: Alert[] = [
+ {
+ key: 'BurnAddressAlert',
+ title: 'Test Burn Address Alert',
+ message: ALERT_MESSAGE_MOCK,
+ severity: Severity.Danger,
+ },
+ ];
beforeEach(() => {
jest.clearAllMocks();
@@ -159,6 +169,7 @@ describe('useConfirmationAlerts', () => {
(useNoPayTokenQuotesAlert as jest.Mock).mockReturnValue([]);
(useInsufficientPayTokenNativeAlert as jest.Mock).mockReturnValue([]);
(useInsufficientPredictBalanceAlert as jest.Mock).mockReturnValue([]);
+ (useBurnAddressAlert as jest.Mock).mockReturnValue([]);
});
it('returns empty array if no alerts', () => {
@@ -229,6 +240,7 @@ describe('useConfirmationAlerts', () => {
(useInsufficientPredictBalanceAlert as jest.Mock).mockReturnValue(
mockInsufficientPredictBalanceAlert,
);
+ (useBurnAddressAlert as jest.Mock).mockReturnValue(mockBurnAddressAlert);
const { result } = renderHookWithProvider(() => useConfirmationAlerts(), {
state: siweSignatureConfirmationState,
});
@@ -244,6 +256,7 @@ describe('useConfirmationAlerts', () => {
...mockNoPayTokenQuotesAlert,
...mockInsufficientPayTokenNativeAlert,
...mockInsufficientPredictBalanceAlert,
+ ...mockBurnAddressAlert,
...mockUpgradeAccountAlert,
]);
});
diff --git a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts
index 8e57005efa7b..d57c848fe71b 100644
--- a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts
+++ b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts
@@ -12,6 +12,7 @@ import { useInsufficientPayTokenBalanceAlert } from './useInsufficientPayTokenBa
import { useNoPayTokenQuotesAlert } from './useNoPayTokenQuotesAlert';
import { useInsufficientPayTokenNativeAlert } from './useInsufficientPayTokenNativeAlert';
import { useInsufficientPredictBalanceAlert } from './useInsufficientPredictBalanceAlert';
+import { useBurnAddressAlert } from './useBurnAddressAlert';
function useSignatureAlerts(): Alert[] {
const domainMismatchAlerts = useDomainMismatchAlerts();
@@ -30,6 +31,7 @@ function useTransactionAlerts(): Alert[] {
const noPayTokenQuotesAlert = useNoPayTokenQuotesAlert();
const insufficientPayTokenNativeAlert = useInsufficientPayTokenNativeAlert();
const insufficientPredictBalanceAlert = useInsufficientPredictBalanceAlert();
+ const burnAddressAlert = useBurnAddressAlert();
return useMemo(
() => [
@@ -42,6 +44,7 @@ function useTransactionAlerts(): Alert[] {
...noPayTokenQuotesAlert,
...insufficientPayTokenNativeAlert,
...insufficientPredictBalanceAlert,
+ ...burnAddressAlert,
],
[
insufficientBalanceAlert,
@@ -53,6 +56,7 @@ function useTransactionAlerts(): Alert[] {
noPayTokenQuotesAlert,
insufficientPayTokenNativeAlert,
insufficientPredictBalanceAlert,
+ burnAddressAlert,
],
);
}
diff --git a/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts b/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts
index 6b047b263366..3a810a93a098 100644
--- a/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts
+++ b/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts
@@ -108,6 +108,7 @@ function getAlertNames(alerts: Alert[]): string[] {
const ALERTS_NAME_METRICS: AlertNameMetrics = {
[AlertKeys.BatchedUnusedApprovals]: 'batched_unused_approvals',
[AlertKeys.Blockaid]: 'blockaid',
+ [AlertKeys.BurnAddress]: 'burn_address',
[AlertKeys.DomainMismatch]: 'domain_mismatch',
[AlertKeys.InsufficientBalance]: 'insufficient_balance',
[AlertKeys.InsufficientPayTokenBalance]: 'insufficient_funds',
diff --git a/app/components/Views/confirmations/hooks/send/useContacts.test.ts b/app/components/Views/confirmations/hooks/send/useContacts.test.ts
index dd9a5562b500..4afd02b3201f 100644
--- a/app/components/Views/confirmations/hooks/send/useContacts.test.ts
+++ b/app/components/Views/confirmations/hooks/send/useContacts.test.ts
@@ -292,5 +292,33 @@ describe('useContacts', () => {
'0x1234567890123456789012345678901234567890',
);
});
+
+ it('filters out zero address burn address', () => {
+ const burnAddressBook = {
+ '1': {
+ contact1: mockEvmContact1,
+ burnContact: {
+ name: 'Burn Address',
+ address: '0x0000000000000000000000000000000000000000',
+ },
+ },
+ };
+ mockUseSelector.mockImplementation((selector) => {
+ if (selector === selectAddressBook) {
+ return burnAddressBook;
+ }
+ return {};
+ });
+
+ const { result } = renderHook(() => useContacts());
+
+ expect(result.current).toHaveLength(1);
+ expect(result.current[0].address).toBe(mockEvmContact1.address);
+ expect(
+ result.current.find(
+ (c) => c.address === '0x0000000000000000000000000000000000000000',
+ ),
+ ).toBeUndefined();
+ });
});
});
diff --git a/app/components/Views/confirmations/hooks/send/useContacts.ts b/app/components/Views/confirmations/hooks/send/useContacts.ts
index e552a1f910f7..132ec0bb350c 100644
--- a/app/components/Views/confirmations/hooks/send/useContacts.ts
+++ b/app/components/Views/confirmations/hooks/send/useContacts.ts
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import { selectAddressBook } from '../../../../../selectors/addressBookController';
import { type RecipientType } from '../../components/UI/recipient';
+import { LOWER_CASED_BURN_ADDRESSES } from '../../utils/send-address-validations';
import { useSendType } from './useSendType';
export const useContacts = () => {
@@ -26,10 +27,11 @@ export const useContacts = () => {
});
return flattenedContacts.filter((contact) => {
- // Only possibility to check if the address is EVM compatible because contacts are only EVM compatible as of now
if (isEvmSendType) {
return (
- contact.address.startsWith('0x') && contact.address.length === 42
+ contact.address.startsWith('0x') &&
+ contact.address.length === 42 &&
+ !LOWER_CASED_BURN_ADDRESSES.includes(contact.address.toLowerCase())
);
}
return true;
diff --git a/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.test.ts b/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.test.ts
index 6a9329dccb76..0f8e4f31c3b6 100644
--- a/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.test.ts
+++ b/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.test.ts
@@ -3,7 +3,10 @@ import { merge } from 'lodash';
import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
import { transferConfirmationState } from '../../../../../util/test/confirm-data-helpers';
-import { useTransferRecipient } from './useTransferRecipient';
+import {
+ useNestedTransactionTransferRecipients,
+ useTransferRecipient,
+} from './useTransferRecipient';
const nativeTransferState = merge({}, transferConfirmationState, {
engine: {
@@ -41,6 +44,138 @@ const erc20TransferState = merge({}, transferConfirmationState, {
},
});
+const noNestedTransactionsState = merge({}, transferConfirmationState, {
+ engine: {
+ backgroundState: {
+ TransactionController: {
+ transactions: [
+ {
+ type: TransactionType.simpleSend,
+ txParams: {
+ from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164',
+ to: '0x97cb1fdd071da9960d38306c07f146bc98b21231',
+ },
+ },
+ ],
+ },
+ },
+ },
+});
+
+const singleNativeNestedTransactionState = merge(
+ {},
+ transferConfirmationState,
+ {
+ engine: {
+ backgroundState: {
+ TransactionController: {
+ transactions: [
+ {
+ type: TransactionType.batch,
+ txParams: {
+ from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164',
+ },
+ nestedTransactions: [
+ {
+ to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
+ data: '0x654365436543',
+ value: '0x3B9ACA00',
+ type: TransactionType.simpleSend,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+);
+
+const singleErc20NestedTransactionState = merge({}, transferConfirmationState, {
+ engine: {
+ backgroundState: {
+ TransactionController: {
+ transactions: [
+ {
+ type: TransactionType.batch,
+ txParams: {
+ from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164',
+ },
+ nestedTransactions: [
+ {
+ to: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ data: '0xa9059cbb00000000000000000000000097cb1fdd071da9960d38306c07f146bc98b2d31700000000000000000000000000000000000000000000000000000000000f4240',
+ type: TransactionType.tokenMethodTransfer,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+});
+
+const multipleNestedTransactionsState = merge({}, transferConfirmationState, {
+ engine: {
+ backgroundState: {
+ TransactionController: {
+ transactions: [
+ {
+ type: TransactionType.batch,
+ txParams: {
+ from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164',
+ },
+ nestedTransactions: [
+ {
+ to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
+ data: '0x654365436543',
+ value: '0x3B9ACA00',
+ type: TransactionType.simpleSend,
+ },
+ {
+ to: '0xbc2114a988e9cef5ba63548d432024f34b487048',
+ data: '0x789078907890',
+ value: '0x1DCD6500',
+ type: TransactionType.simpleSend,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+});
+
+const mixedNestedTransactionsState = merge({}, transferConfirmationState, {
+ engine: {
+ backgroundState: {
+ TransactionController: {
+ transactions: [
+ {
+ type: TransactionType.batch,
+ txParams: {
+ from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164',
+ },
+ nestedTransactions: [
+ {
+ to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
+ data: '0x654365436543',
+ value: '0x3B9ACA00',
+ type: TransactionType.simpleSend,
+ },
+ {
+ to: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ data: '0xa9059cbb00000000000000000000000097cb1fdd071da9960d38306c07f146bc98b2d31700000000000000000000000000000000000000000000000000000000000f4240',
+ type: TransactionType.tokenMethodTransfer,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+});
+
describe('useTransferRecipient', () => {
it('returns the correct recipient for native transfer', async () => {
const { result } = renderHookWithProvider(() => useTransferRecipient(), {
@@ -58,3 +193,70 @@ describe('useTransferRecipient', () => {
expect(result.current).toBe('0x97cb1fdD071da9960d38306C07F146bc98b2D317');
});
});
+
+describe('useNestedTransactionTransferRecipients', () => {
+ it('returns empty array when no nested transactions', async () => {
+ const { result } = renderHookWithProvider(
+ () => useNestedTransactionTransferRecipients(),
+ {
+ state: noNestedTransactionsState,
+ },
+ );
+
+ expect(result.current).toEqual([]);
+ });
+
+ it('returns the correct recipient for single native nested transaction', async () => {
+ const { result } = renderHookWithProvider(
+ () => useNestedTransactionTransferRecipients(),
+ {
+ state: singleNativeNestedTransactionState,
+ },
+ );
+
+ expect(result.current).toEqual([
+ '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
+ ]);
+ });
+
+ it('returns the correct recipient for single erc20 nested transaction', async () => {
+ const { result } = renderHookWithProvider(
+ () => useNestedTransactionTransferRecipients(),
+ {
+ state: singleErc20NestedTransactionState,
+ },
+ );
+
+ expect(result.current).toEqual([
+ '0x97cb1fdD071da9960d38306C07F146bc98b2D317',
+ ]);
+ });
+
+ it('returns the correct recipients for multiple native nested transactions', async () => {
+ const { result } = renderHookWithProvider(
+ () => useNestedTransactionTransferRecipients(),
+ {
+ state: multipleNestedTransactionsState,
+ },
+ );
+
+ expect(result.current).toEqual([
+ '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
+ '0xbc2114a988e9cef5ba63548d432024f34b487048',
+ ]);
+ });
+
+ it('returns the correct recipients for mixed nested transactions', async () => {
+ const { result } = renderHookWithProvider(
+ () => useNestedTransactionTransferRecipients(),
+ {
+ state: mixedNestedTransactionsState,
+ },
+ );
+
+ expect(result.current).toEqual([
+ '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb',
+ '0x97cb1fdD071da9960d38306C07F146bc98b2D317',
+ ]);
+ });
+});
diff --git a/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts b/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts
index e872cb6d5f0f..03b428ab63be 100644
--- a/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts
+++ b/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts
@@ -1,22 +1,66 @@
-import { TransactionType } from '@metamask/transaction-controller';
+import {
+ TransactionMeta,
+ TransactionType,
+ NestedTransactionMetadata,
+} from '@metamask/transaction-controller';
import { parseStandardTokenTransactionData } from '../../utils/transaction';
import { useTransactionMetadataRequest } from './useTransactionMetadataRequest';
-export function useTransferRecipient() {
+export function useTransferRecipient(): string | undefined {
const transactionMetadata = useTransactionMetadataRequest();
- const transactionData = parseStandardTokenTransactionData(
- transactionMetadata?.txParams?.data,
- );
if (!transactionMetadata) {
- return null;
+ return undefined;
}
+ return getRecipientFromTransactionMetadata(transactionMetadata);
+}
+
+export function useNestedTransactionTransferRecipients(): string[] {
+ const transactionMetadata = useTransactionMetadataRequest();
+
+ if (!transactionMetadata?.nestedTransactions?.length) {
+ return [];
+ }
+
+ return transactionMetadata.nestedTransactions
+ .map(getRecipientFromNestedTransactionMetadata)
+ .filter((recipient): recipient is string => recipient !== undefined);
+}
+
+function getRecipientFromNestedTransactionMetadata(
+ nestedTransactionMetadata: NestedTransactionMetadata,
+): string | undefined {
+ const { type, data, to } = nestedTransactionMetadata;
+ return getRecipientByType(type as TransactionType, data ?? '', to ?? '');
+}
+
+function getRecipientFromTransactionMetadata(
+ transactionMetadata: TransactionMeta,
+): string | undefined {
const { type, txParams } = transactionMetadata;
- const { to: transactionTo } = txParams;
+ return getRecipientByType(
+ type as TransactionType,
+ txParams?.data ?? '',
+ txParams?.to ?? '',
+ );
+}
+
+function getRecipientByType(
+ type: TransactionType,
+ data: string,
+ transactionTo: string,
+): string | undefined {
+ const dataRecipient = getTransactionDataRecipient(data);
+ const paramsRecipient = transactionTo;
+
+ return type === TransactionType.simpleSend ? paramsRecipient : dataRecipient;
+}
+
+function getTransactionDataRecipient(data: string): string | undefined {
+ const transactionData = parseStandardTokenTransactionData(data);
- const transferTo =
- transactionData?.args?._to || transactionData?.args?.to || transactionTo;
+ const transferTo = transactionData?.args?._to || transactionData?.args?.to;
- return type === TransactionType.simpleSend ? transactionTo : transferTo;
+ return transferTo;
}
diff --git a/app/components/Views/confirmations/utils/generic.test.ts b/app/components/Views/confirmations/utils/generic.test.ts
index 2a1e9db9cf53..898d64905152 100644
--- a/app/components/Views/confirmations/utils/generic.test.ts
+++ b/app/components/Views/confirmations/utils/generic.test.ts
@@ -1,24 +1,125 @@
+import { CHAIN_IDS } from '@metamask/transaction-controller';
import { TokenI } from '../../../UI/Tokens/types';
import { getHostFromUrl, isNativeToken } from './generic';
describe('generic utils', () => {
describe('getHostFromUrl', () => {
- it('should return correct value', async () => {
- expect(getHostFromUrl('')).toBe(undefined);
- expect(getHostFromUrl('https://www.dummy.com')).toBe('www.dummy.com');
+ it('returns undefined when url is empty', () => {
+ const result = getHostFromUrl('');
+
+ expect(result).toBe(undefined);
+ });
+
+ it('returns host when url is valid', () => {
+ const result = getHostFromUrl('https://www.dummy.com');
+
+ expect(result).toBe('www.dummy.com');
});
});
+
describe('isNativeToken', () => {
- it('should return correct value', async () => {
- expect(isNativeToken({ isNative: true, isETH: false } as TokenI)).toBe(
- true,
- );
- expect(isNativeToken({ isNative: false, isETH: true } as TokenI)).toBe(
- true,
- );
- expect(isNativeToken({ isNative: false, isETH: false } as TokenI)).toBe(
- false,
- );
+ it('returns true when isNative is true', () => {
+ const token = {
+ isNative: true,
+ isETH: false,
+ address: '0x1234',
+ chainId: CHAIN_IDS.MAINNET,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(true);
+ });
+
+ it('returns true when isETH is true', () => {
+ const token = {
+ isNative: false,
+ isETH: true,
+ address: '0x1234',
+ chainId: CHAIN_IDS.MAINNET,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(true);
+ });
+
+ it('returns true when address matches native token address for Ethereum mainnet', () => {
+ const token = {
+ isNative: false,
+ isETH: false,
+ address: '0x0000000000000000000000000000000000000000',
+ chainId: CHAIN_IDS.MAINNET,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(true);
+ });
+
+ it('returns true when address matches native token address for Polygon', () => {
+ const token = {
+ isNative: false,
+ isETH: false,
+ address: '0x0000000000000000000000000000000000001010',
+ chainId: CHAIN_IDS.POLYGON,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(true);
+ });
+
+ it('returns false when all conditions are false', () => {
+ const token = {
+ isNative: false,
+ isETH: false,
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ chainId: CHAIN_IDS.MAINNET,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(false);
+ });
+
+ it('returns false when address does not match native token address', () => {
+ const token = {
+ isNative: false,
+ isETH: false,
+ address: '0x1234567890123456789012345678901234567890',
+ chainId: CHAIN_IDS.MAINNET,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(false);
+ });
+
+ it('returns true when isNative is true regardless of address', () => {
+ const token = {
+ isNative: true,
+ isETH: false,
+ address: '0x1234567890123456789012345678901234567890',
+ chainId: CHAIN_IDS.MAINNET,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(true);
+ });
+
+ it('returns true when isETH is true regardless of address', () => {
+ const token = {
+ isNative: false,
+ isETH: true,
+ address: '0x1234567890123456789012345678901234567890',
+ chainId: CHAIN_IDS.MAINNET,
+ } as TokenI;
+
+ const result = isNativeToken(token);
+
+ expect(result).toBe(true);
});
});
});
diff --git a/app/components/Views/confirmations/utils/generic.ts b/app/components/Views/confirmations/utils/generic.ts
index c2c7b7100395..47f4e3354ede 100644
--- a/app/components/Views/confirmations/utils/generic.ts
+++ b/app/components/Views/confirmations/utils/generic.ts
@@ -1,7 +1,9 @@
+import { Hex } from '@metamask/utils';
import Routes from '../../../../constants/navigation/Routes';
import Engine from '../../../../core/Engine';
import { NavigationRoute } from '../../../UI/Carousel/types';
import { TokenI } from '../../../UI/Tokens/types';
+import { getNativeTokenAddress } from './asset';
export const getHostFromUrl = (url: string) => {
if (!url) {
@@ -15,8 +17,13 @@ export const getHostFromUrl = (url: string) => {
return;
};
-export const isNativeToken = (selectedAsset: TokenI) =>
- selectedAsset.isNative || selectedAsset.isETH;
+export const isNativeToken = (selectedAsset: TokenI) => {
+ const { isNative, isETH, chainId } = selectedAsset;
+ const nativeTokenAddress = getNativeTokenAddress(chainId as Hex);
+ const isNativeTokenAddress = selectedAsset.address === nativeTokenAddress;
+
+ return isNative || isETH || isNativeTokenAddress;
+};
export function createSmartAccountNavigationDetails(): NavigationRoute {
if (Engine.context.PreferencesController.state.smartAccountOptIn === true) {
diff --git a/app/components/Views/confirmations/utils/send-address-validations.ts b/app/components/Views/confirmations/utils/send-address-validations.ts
index 160b61d9d919..accf771042c3 100644
--- a/app/components/Views/confirmations/utils/send-address-validations.ts
+++ b/app/components/Views/confirmations/utils/send-address-validations.ts
@@ -12,7 +12,7 @@ import {
import { memoizedGetTokenStandardAndDetails } from './token';
import { isBtcMainnetAddress } from '../../../../core/Multichain/utils';
-const LOWER_CASED_BURN_ADDRESSES = [
+export const LOWER_CASED_BURN_ADDRESSES = [
'0x0000000000000000000000000000000000000000',
'0x000000000000000000000000000000000000dead',
];
diff --git a/app/components/Views/confirmations/utils/send.ts b/app/components/Views/confirmations/utils/send.ts
index 4df739f27e08..669ecf5822e4 100644
--- a/app/components/Views/confirmations/utils/send.ts
+++ b/app/components/Views/confirmations/utils/send.ts
@@ -244,7 +244,7 @@ export const submitEvmTransaction = async ({
const trxnParams = prepareEVMTransaction(asset, { from, to, value });
let transactionType;
- if (asset.isNative) {
+ if (isNativeToken(asset)) {
transactionType = TransactionType.simpleSend;
} else if (asset.standard === TokenStandard.ERC20) {
transactionType = TransactionType.tokenMethodTransfer;
diff --git a/locales/languages/en.json b/locales/languages/en.json
index b8c12cd80311..1c63b4b45d2b 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -95,6 +95,10 @@
"perps_hardware_account": {
"title": "Wallet not supported",
"message": "Perps doesn't support hardware wallets.\nSwitch accounts to continue funding."
+ },
+ "burn_address": {
+ "message": "You're sending your assets to a burn address. If you continue, you'll lose your assets.",
+ "title": "Sending Assets to Burn Address"
}
},
"blockaid_banner": {