Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export enum RowAlertKey {
Amount = 'amount',
AccountTypeUpgrade = 'accountTypeUpgrade',
BatchedApprovals = 'batchedApprovals',
BurnAddress = 'burnAddress',
Blockaid = 'blockaid',
EstimatedFee = 'estimatedFee',
PayWith = 'payWith',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const styleSheet = (params: { theme: Theme }) => {
paddingBottom: 8,
paddingHorizontal: 8,
},
labelContainerWithoutLabel: {
marginLeft: -4,
},
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,6 +48,7 @@ const InfoRow = ({
withIcon,
}: InfoRowProps) => {
const { styles } = useStyles(styleSheet, {});
const hasLabel = Boolean(label);

const ValueComponent =
typeof children === 'string' ? (
Expand All @@ -62,7 +63,7 @@ const InfoRow = ({
style={{ ...styles.container, ...style }}
testID={testID ?? 'info-row'}
>
{Boolean(label) && (
{hasLabel && (
<View style={styles.labelContainer}>
<Text variant={TextVariant.BodyMDMedium} color={variant}>
{label}
Expand All @@ -77,6 +78,16 @@ const InfoRow = ({
)}
</View>
)}
{!hasLabel && labelChildren && (
<View
style={{
...styles.labelContainer,
...styles.labelContainerWithoutLabel,
}}
>
{labelChildren}
</View>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2 label children created now can be merged by adding condition.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{(hasLabel || labelChildren) && (
  <View style={{
    ...styles.labelContainer,
    ...(!hasLabel && styles.labelContainerWithoutLabel),
  }}>
    {hasLabel && (
      <>
        <Text variant={TextVariant.BodyMDMedium} color={variant}>
          {label}
        </Text>
        {labelChildren}
        {!labelChildren && tooltip && (
          <Tooltip
            content={tooltip}
            onPress={onTooltipPress}
            title={tooltipTitle ?? label}
          />
        )}
      </>
    )}
    {!hasLabel && labelChildren}
  </View>
)}

Wouldn't it look more complicated if we merge conditions here?

{valueOnNewLine ? null : ValueComponent}
{copyText && (
<CopyIcon textToCopy={copyText ?? ''} color={IconColor.Muted} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: sorting imports

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not going to address sortings anymore as lint rules of the repo doesn't dictate it.

import styleSheet from './from-to-row.styles';

const FromToRow = () => {
Expand Down Expand Up @@ -54,12 +56,15 @@ const FromToRow = () => {
</View>

<View style={[styles.nameContainer, styles.rightNameContainer]}>
<Name
type={NameType.EthereumAddress}
value={toAddress}
variation={chainId}
maxCharLength={MAX_CHAR_LENGTH}
/>
{/* Intentional empty label to trigger the alert row without a label */}
<AlertRow alertField={RowAlertKey.BurnAddress}>
<Name
type={NameType.EthereumAddress}
value={toAddress as string}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Type Assertion Bypasses Safety

The toAddress variable, which can be undefined from useTransferRecipient(), is unsafely type-asserted as string when passed to the Name component's value prop. This as string assertion bypasses TypeScript's type safety, potentially causing runtime errors if Name receives undefined.

Fix in Cursor Fix in Web

variation={chainId}
maxCharLength={MAX_CHAR_LENGTH}
/>
</AlertRow>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Undefined Address Handling

The component does not guard against toAddress being undefined before passing it to the Name component. The useTransferRecipient() hook returns string | undefined, but the code casts it to string (line 63: value={toAddress as string}) without checking if it's defined. While the reviewer comment suggests this "will never be the case", a proper guard condition should still be added to handle the undefined case safely and prevent potential crashes if the assumption changes. Consider adding: if (!toAddress) return null; before rendering, or wrapping the AlertRow/Name component in a conditional.

Fix in Cursor Fix in Web

</View>
</View>
</InfoSection>
Expand Down
1 change: 1 addition & 0 deletions app/components/Views/confirmations/constants/alerts.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.';
Expand Down Expand Up @@ -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();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -229,6 +240,7 @@ describe('useConfirmationAlerts', () => {
(useInsufficientPredictBalanceAlert as jest.Mock).mockReturnValue(
mockInsufficientPredictBalanceAlert,
);
(useBurnAddressAlert as jest.Mock).mockReturnValue(mockBurnAddressAlert);
const { result } = renderHookWithProvider(() => useConfirmationAlerts(), {
state: siweSignatureConfirmationState,
});
Expand All @@ -244,6 +256,7 @@ describe('useConfirmationAlerts', () => {
...mockNoPayTokenQuotesAlert,
...mockInsufficientPayTokenNativeAlert,
...mockInsufficientPredictBalanceAlert,
...mockBurnAddressAlert,
...mockUpgradeAccountAlert,
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -30,6 +31,7 @@ function useTransactionAlerts(): Alert[] {
const noPayTokenQuotesAlert = useNoPayTokenQuotesAlert();
const insufficientPayTokenNativeAlert = useInsufficientPayTokenNativeAlert();
const insufficientPredictBalanceAlert = useInsufficientPredictBalanceAlert();
const burnAddressAlert = useBurnAddressAlert();

return useMemo(
() => [
Expand All @@ -42,6 +44,7 @@ function useTransactionAlerts(): Alert[] {
...noPayTokenQuotesAlert,
...insufficientPayTokenNativeAlert,
...insufficientPredictBalanceAlert,
...burnAddressAlert,
],
[
insufficientBalanceAlert,
Expand All @@ -53,6 +56,7 @@ function useTransactionAlerts(): Alert[] {
noPayTokenQuotesAlert,
insufficientPayTokenNativeAlert,
insufficientPredictBalanceAlert,
burnAddressAlert,
],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading