diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 8cde5b05021d..541189975532 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1089,6 +1089,9 @@ "bridgeLowestCost": { "message": "Lowest cost" }, + "bridgeMalicious": { + "message": "Malicious" + }, "bridgeMaliciousTokenTitle": { "message": "Malicious token" }, @@ -1264,6 +1267,9 @@ "message": "Swapping $1 for $2", "description": "$1 is the amount of the source asset, $2 is the amount of the destination asset" }, + "bridgeSuspicious": { + "message": "Suspicious" + }, "bridgeSuspiciousTokenTitle": { "message": "Suspicious token" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 8cde5b05021d..541189975532 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1089,6 +1089,9 @@ "bridgeLowestCost": { "message": "Lowest cost" }, + "bridgeMalicious": { + "message": "Malicious" + }, "bridgeMaliciousTokenTitle": { "message": "Malicious token" }, @@ -1264,6 +1267,9 @@ "message": "Swapping $1 for $2", "description": "$1 is the amount of the source asset, $2 is the amount of the destination asset" }, + "bridgeSuspicious": { + "message": "Suspicious" + }, "bridgeSuspiciousTokenTitle": { "message": "Suspicious token" }, diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index 626f2ec4d444..6c8526c4fccb 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -87,6 +87,7 @@ describe('Ducks - Bridge', () => { "isVerified": undefined, "name": "SYMBOL", "rwaData": undefined, + "securityData": undefined, "symbol": "SYMBOL", "tokenFiatAmount": undefined, } @@ -210,6 +211,7 @@ describe('Ducks - Bridge', () => { chainId: 'eip155:10', rwaData: undefined, isVerified: undefined, + securityData: undefined, iconUrl: 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/erc20/0x13341431.png', }); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 28c38cfcd5f2..a54f0688225e 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -612,6 +612,7 @@ describe('Bridge selectors', () => { tokenFiatAmount: undefined, rwaData: undefined, isVerified: undefined, + securityData: undefined, }); }); }); @@ -647,6 +648,7 @@ describe('Bridge selectors', () => { "isVerified": undefined, "name": "DEST", "rwaData": undefined, + "securityData": undefined, "symbol": "DEST", "tokenFiatAmount": undefined, } @@ -680,6 +682,7 @@ describe('Bridge selectors', () => { tokenFiatAmount: undefined, rwaData: undefined, isVerified: undefined, + securityData: undefined, }); }); @@ -748,6 +751,7 @@ describe('Bridge selectors', () => { tokenFiatAmount: undefined, rwaData: undefined, isVerified: undefined, + securityData: undefined, }); }); }); diff --git a/ui/ducks/bridge/utils.test.ts b/ui/ducks/bridge/utils.test.ts index 8e050c5c77e8..4d7b5b55434b 100644 --- a/ui/ducks/bridge/utils.test.ts +++ b/ui/ducks/bridge/utils.test.ts @@ -1,6 +1,7 @@ import { type CaipAssetType } from '@metamask/utils'; import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; import { CHAIN_IDS } from '../../../shared/constants/network'; +import { BridgeAssetSecurityDataType } from '../../pages/bridge/utils/tokens'; import { isSupportedBridgeChain, toBridgeToken } from './utils'; const BASE_PAYLOAD = { @@ -56,6 +57,39 @@ describe('isSupportedBridgeChain', () => { }); describe('toBridgeToken', () => { + describe('securityData', () => { + it('passes securityData from the payload when tokenMetadata has no securityData', () => { + const token = toBridgeToken( + { + ...BASE_PAYLOAD, + securityData: { type: BridgeAssetSecurityDataType.MALICIOUS }, + }, + { balance: '10' }, + ); + expect(token.securityData).toStrictEqual({ + type: BridgeAssetSecurityDataType.MALICIOUS, + }); + }); + + it('tokenMetadata.securityData takes precedence over payload.securityData', () => { + const token = toBridgeToken( + { + ...BASE_PAYLOAD, + securityData: { type: BridgeAssetSecurityDataType.SPAM }, + }, + { securityData: { type: BridgeAssetSecurityDataType.VERIFIED } }, + ); + expect(token.securityData).toStrictEqual({ + type: BridgeAssetSecurityDataType.VERIFIED, + }); + }); + + it('securityData is undefined when absent from both payload and tokenMetadata', () => { + const token = toBridgeToken(BASE_PAYLOAD, { balance: '10' }); + expect(token.securityData).toBeUndefined(); + }); + }); + describe('isVerified', () => { it('includes isVerified: true from the payload', () => { const token = toBridgeToken({ ...BASE_PAYLOAD, isVerified: true }); @@ -87,5 +121,51 @@ describe('toBridgeToken', () => { ); expect(token.isVerified).toBe(true); }); + + describe('securityData', () => { + it('returns true when tokenMetadata.securityData.type is VERIFIED', () => { + const token = toBridgeToken(BASE_PAYLOAD, { + securityData: { type: BridgeAssetSecurityDataType.VERIFIED }, + }); + expect(token.isVerified).toBe(true); + }); + + it('returns true when tokenMetadata.securityData.type is VERIFIED even if tokenMetadata.isVerified is false', () => { + const token = toBridgeToken(BASE_PAYLOAD, { + isVerified: false, + securityData: { type: BridgeAssetSecurityDataType.VERIFIED }, + }); + expect(token.isVerified).toBe(true); + }); + + it('falls back to tokenMetadata.isVerified when securityData.type is not VERIFIED', () => { + const token = toBridgeToken(BASE_PAYLOAD, { + isVerified: true, + securityData: { type: BridgeAssetSecurityDataType.WARNING }, + }); + expect(token.isVerified).toBe(true); + }); + + it('falls back to payload isVerified when securityData.type is not VERIFIED and tokenMetadata.isVerified is absent', () => { + const token = toBridgeToken( + { ...BASE_PAYLOAD, isVerified: true }, + { securityData: { type: BridgeAssetSecurityDataType.BENIGN } }, + ); + expect(token.isVerified).toBe(true); + }); + + it('returns false when securityData.type is not VERIFIED and tokenMetadata.isVerified is false', () => { + const token = toBridgeToken(BASE_PAYLOAD, { + isVerified: false, + securityData: { type: BridgeAssetSecurityDataType.SPAM }, + }); + expect(token.isVerified).toBe(false); + }); + + it('returns undefined when no securityData and no isVerified in either payload or tokenMetadata', () => { + const token = toBridgeToken(BASE_PAYLOAD, { balance: '50' }); + expect(token.isVerified).toBeUndefined(); + }); + }); }); }); diff --git a/ui/ducks/bridge/utils.ts b/ui/ducks/bridge/utils.ts index 752a379d6dc6..560358429dca 100644 --- a/ui/ducks/bridge/utils.ts +++ b/ui/ducks/bridge/utils.ts @@ -21,6 +21,7 @@ import { BRIDGE_CHAINID_COMMON_TOKEN_PAIR, } from '../../../shared/constants/bridge'; import { getAssetImageUrl } from '../../../shared/lib/asset-utils'; +import { BridgeAssetSecurityDataType } from '../../pages/bridge/utils/tokens'; import type { TokenPayload, BridgeToken } from './types'; // Re-export isNonEvmChainId from bridge-controller for backward compatibility @@ -212,6 +213,7 @@ export const toBridgeToken = ( accountType, rwaData, isVerified, + securityData, } = payload; const { chainId } = parseCaipAssetType(assetId); return { @@ -225,7 +227,12 @@ export const toBridgeToken = ( tokenFiatAmount: tokenMetadata?.tokenFiatAmount ?? tokenFiatAmount, accountType: tokenMetadata?.accountType ?? accountType, rwaData: tokenMetadata?.rwaData ?? rwaData, - isVerified: tokenMetadata?.isVerified ?? isVerified, + isVerified: + (tokenMetadata?.securityData?.type === + BridgeAssetSecurityDataType.VERIFIED || + tokenMetadata?.isVerified) ?? + isVerified, + securityData: tokenMetadata?.securityData ?? securityData, }; }; diff --git a/ui/hooks/bridge/__snapshots__/usePrefillFromSearchQuery.test.ts.snap b/ui/hooks/bridge/__snapshots__/usePrefillFromSearchQuery.test.ts.snap index 797684366b60..58063d68a5de 100644 --- a/ui/hooks/bridge/__snapshots__/usePrefillFromSearchQuery.test.ts.snap +++ b/ui/hooks/bridge/__snapshots__/usePrefillFromSearchQuery.test.ts.snap @@ -32,6 +32,7 @@ exports[`usePrefillFromSearchQuery should only set dest token 1`] = ` "isVerified": undefined, "name": "USDC", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, @@ -50,6 +51,7 @@ exports[`usePrefillFromSearchQuery should only set src token and amount 1`] = ` "isVerified": undefined, "name": "USDC", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, @@ -71,6 +73,7 @@ exports[`usePrefillFromSearchQuery should set evm bridge params 1`] = ` "isVerified": undefined, "name": "USDC", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, @@ -86,6 +89,7 @@ exports[`usePrefillFromSearchQuery should set evm bridge params 1`] = ` "isVerified": undefined, "name": "Solana", "rwaData": undefined, + "securityData": undefined, "symbol": "SOL", "tokenFiatAmount": undefined, }, @@ -104,6 +108,7 @@ exports[`usePrefillFromSearchQuery should set solana swap params 1`] = ` "isVerified": undefined, "name": "Solana", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, @@ -118,6 +123,7 @@ exports[`usePrefillFromSearchQuery should set solana swap params 1`] = ` "isVerified": undefined, "name": "Solana", "rwaData": undefined, + "securityData": undefined, "symbol": "SOL", "tokenFiatAmount": undefined, }, @@ -136,6 +142,7 @@ exports[`usePrefillFromSearchQuery should set src token after navigating from ER "isVerified": undefined, "name": "USDC", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, @@ -156,6 +163,7 @@ exports[`usePrefillFromSearchQuery should set src token after navigating from na "isVerified": undefined, "name": "Ethereum", "rwaData": undefined, + "securityData": undefined, "symbol": "ETH", "tokenFiatAmount": undefined, }, diff --git a/ui/hooks/bridge/usePrefillFromBridgeState.test.ts b/ui/hooks/bridge/usePrefillFromBridgeState.test.ts index 338fbfff4f56..dd429a78858c 100644 --- a/ui/hooks/bridge/usePrefillFromBridgeState.test.ts +++ b/ui/hooks/bridge/usePrefillFromBridgeState.test.ts @@ -91,6 +91,7 @@ describe('usePrefillFromBridgeState', () => { "isVerified": undefined, "name": "USDC", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, @@ -154,6 +155,7 @@ describe('usePrefillFromBridgeState', () => { "isVerified": undefined, "name": "USD Coin", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, @@ -168,6 +170,7 @@ describe('usePrefillFromBridgeState', () => { "isVerified": undefined, "name": "Native USD Coin (POS)", "rwaData": undefined, + "securityData": undefined, "symbol": "USDC", "tokenFiatAmount": undefined, }, diff --git a/ui/pages/bridge/prepare/__snapshots__/bridge-input-group.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/bridge-input-group.test.tsx.snap index 01b511e42ea2..a9a4731378c1 100644 --- a/ui/pages/bridge/prepare/__snapshots__/bridge-input-group.test.tsx.snap +++ b/ui/pages/bridge/prepare/__snapshots__/bridge-input-group.test.tsx.snap @@ -371,6 +371,7 @@ exports[`BridgeInputGroup should render popular tokens 2`] = ` "isVerified": undefined, "name": "Ether", "rwaData": undefined, + "securityData": undefined, "symbol": "ETH", "tokenFiatAmount": 25.242128065034784, }, @@ -384,6 +385,7 @@ exports[`BridgeInputGroup should render popular tokens 2`] = ` "isVerified": undefined, "name": "Uniswap", "rwaData": undefined, + "securityData": undefined, "symbol": "UNI", "tokenFiatAmount": 0.0010728914112762384, }, @@ -397,6 +399,7 @@ exports[`BridgeInputGroup should render popular tokens 2`] = ` "isVerified": undefined, "name": "Link", "rwaData": undefined, + "securityData": undefined, "symbol": "LINK", "tokenFiatAmount": 0.0000030290553678041743, }, @@ -1067,6 +1070,7 @@ exports[`BridgeInputGroup should search for tokens 4`] = ` "isVerified": undefined, "name": "Ether", "rwaData": undefined, + "securityData": undefined, "symbol": "ETH", "tokenFiatAmount": 25.242128065034784, }, @@ -1080,6 +1084,7 @@ exports[`BridgeInputGroup should search for tokens 4`] = ` "isVerified": undefined, "name": "Uniswap", "rwaData": undefined, + "securityData": undefined, "symbol": "UNI", "tokenFiatAmount": 0.0010728914112762384, }, @@ -1093,6 +1098,7 @@ exports[`BridgeInputGroup should search for tokens 4`] = ` "isVerified": undefined, "name": "Link", "rwaData": undefined, + "securityData": undefined, "symbol": "LINK", "tokenFiatAmount": 0.0000030290553678041743, }, @@ -1520,6 +1526,7 @@ exports[`BridgeInputGroup should search for tokens with hasNextPage 3`] = ` "isVerified": undefined, "name": "Ether", "rwaData": undefined, + "securityData": undefined, "symbol": "ETH", "tokenFiatAmount": 25.242128065034784, }, @@ -1533,6 +1540,7 @@ exports[`BridgeInputGroup should search for tokens with hasNextPage 3`] = ` "isVerified": undefined, "name": "Uniswap", "rwaData": undefined, + "securityData": undefined, "symbol": "UNI", "tokenFiatAmount": 0.0010728914112762384, }, @@ -1546,6 +1554,7 @@ exports[`BridgeInputGroup should search for tokens with hasNextPage 3`] = ` "isVerified": undefined, "name": "Link", "rwaData": undefined, + "securityData": undefined, "symbol": "LINK", "tokenFiatAmount": 0.0000030290553678041743, }, diff --git a/ui/pages/bridge/prepare/components/bridge-asset-picker/__snapshots__/asset.test.tsx.snap b/ui/pages/bridge/prepare/components/bridge-asset-picker/__snapshots__/asset.test.tsx.snap index 0ce38d768f68..77cc1dccf001 100644 --- a/ui/pages/bridge/prepare/components/bridge-asset-picker/__snapshots__/asset.test.tsx.snap +++ b/ui/pages/bridge/prepare/components/bridge-asset-picker/__snapshots__/asset.test.tsx.snap @@ -396,109 +396,3 @@ exports[`BridgeAsset should render native asset 1`] = ` `; - -exports[`BridgeAsset should render verified badge when isVerified is true 1`] = ` -
-
-
-
- mUSD -
-
-
-
- Ethereum -
-
-
-
-
-
-

- mUSD -

- - - -
-

-

-
-

- MetaMask USD -

-

-

-
- -
-`; diff --git a/ui/pages/bridge/prepare/components/bridge-asset-picker/asset-banner.test.tsx b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset-banner.test.tsx new file mode 100644 index 000000000000..51c6ee7363c6 --- /dev/null +++ b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset-banner.test.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { getNativeAssetForChainId } from '@metamask/bridge-controller'; +import { + en, + renderWithLocalization, +} from '../../../../../../test/lib/render-helpers-navigate'; +import { toBridgeToken } from '../../../../../ducks/bridge/utils'; +import { BridgeAssetSecurityDataType } from '../../../utils/tokens'; +import { AssetBanner } from './asset-banner'; + +const BASE_ASSET = toBridgeToken(getNativeAssetForChainId('0x1')); + +describe('AssetBanner', () => { + describe('verified icon', () => { + it('renders verified badge when isVerified is true', () => { + const { getByTestId } = renderWithLocalization( + , + ); + expect(getByTestId('bridge-asset-verified-badge')).toBeInTheDocument(); + }); + + it('does not render verified badge when isVerified is false', () => { + const { queryByTestId } = renderWithLocalization( + , + ); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + }); + + it('does not render verified badge when isVerified is absent', () => { + const { queryByTestId } = renderWithLocalization( + , + ); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + }); + + it('renders verified badge when securityData.type is VERIFIED', () => { + const { getByTestId } = renderWithLocalization( + , + ); + expect(getByTestId('bridge-asset-verified-badge')).toBeInTheDocument(); + }); + + it('renders verified badge when securityData.type is VERIFIED even if isVerified is false', () => { + const { getByTestId } = renderWithLocalization( + , + ); + expect(getByTestId('bridge-asset-verified-badge')).toBeInTheDocument(); + }); + + it('renders verified badge via isVerified fallback when securityData.type is not VERIFIED', () => { + const { getByTestId } = renderWithLocalization( + , + ); + expect(getByTestId('bridge-asset-verified-badge')).toBeInTheDocument(); + }); + }); + + describe('suspicious tag', () => { + it('renders Suspicious tag when securityData.type is WARNING', () => { + const { getByText, queryByTestId } = renderWithLocalization( + , + ); + expect(getByText(en.bridgeSuspicious.message)).toBeInTheDocument(); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + }); + + it('renders Suspicious tag when securityData.type is SPAM', () => { + const { getByText, queryByTestId } = renderWithLocalization( + , + ); + expect(getByText(en.bridgeSuspicious.message)).toBeInTheDocument(); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + }); + + it('does not render Suspicious tag when securityData.type is SPAM but isVerified is true', () => { + const { getByTestId, queryByText } = renderWithLocalization( + , + ); + expect(getByTestId('bridge-asset-verified-badge')).toBeInTheDocument(); + expect(queryByText(en.bridgeSuspicious.message)).not.toBeInTheDocument(); + }); + }); + + describe('malicious tag', () => { + it('renders Malicious tag when securityData.type is MALICIOUS', () => { + const { getByText, queryByTestId } = renderWithLocalization( + , + ); + expect(getByText(en.bridgeMalicious.message)).toBeInTheDocument(); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + }); + + it('does not render Malicious tag when securityData.type is MALICIOUS but isVerified is true', () => { + const { getByTestId, queryByText } = renderWithLocalization( + , + ); + expect(getByTestId('bridge-asset-verified-badge')).toBeInTheDocument(); + expect(queryByText(en.bridgeMalicious.message)).not.toBeInTheDocument(); + }); + }); + + describe('null cases', () => { + it('renders nothing when securityData.type is INFO', () => { + const { queryByTestId, queryByText } = renderWithLocalization( + , + ); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + expect(queryByText(en.bridgeSuspicious.message)).not.toBeInTheDocument(); + expect(queryByText(en.bridgeMalicious.message)).not.toBeInTheDocument(); + }); + + it('renders nothing when securityData.type is BENIGN', () => { + const { queryByTestId, queryByText } = renderWithLocalization( + , + ); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + expect(queryByText(en.bridgeSuspicious.message)).not.toBeInTheDocument(); + expect(queryByText(en.bridgeMalicious.message)).not.toBeInTheDocument(); + }); + + it('renders nothing when no securityData and isVerified is absent', () => { + const { queryByTestId, queryByText } = renderWithLocalization( + , + ); + expect( + queryByTestId('bridge-asset-verified-badge'), + ).not.toBeInTheDocument(); + expect(queryByText(en.bridgeSuspicious.message)).not.toBeInTheDocument(); + expect(queryByText(en.bridgeMalicious.message)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/ui/pages/bridge/prepare/components/bridge-asset-picker/asset-banner.tsx b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset-banner.tsx new file mode 100644 index 000000000000..2944fb239f6d --- /dev/null +++ b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset-banner.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { + Icon, + IconColor, + IconName, + IconSize, +} from '@metamask/design-system-react'; +import { type BridgeToken } from '../../../../../ducks/bridge/types'; +import { BridgeAssetSecurityDataType } from '../../../utils/tokens'; +import { Tag } from '../../../../../components/component-library'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { + BackgroundColor, + FontWeight, + TextColor, +} from '../../../../../helpers/constants/design-system'; + +type AssetBannerProps = { + asset: BridgeToken; +}; + +export const AssetBanner = ({ asset }: AssetBannerProps) => { + const t = useI18nContext(); + const assetIsVerified = + asset.securityData?.type === BridgeAssetSecurityDataType.VERIFIED || + asset.isVerified; + const assetIsSuspicious = + asset.securityData?.type === BridgeAssetSecurityDataType.WARNING || + asset.securityData?.type === BridgeAssetSecurityDataType.SPAM; + const assetIsMalicious = + asset.securityData?.type === BridgeAssetSecurityDataType.MALICIOUS; + + if (assetIsVerified) { + return ( + + ); + } + + if (assetIsSuspicious) { + return ( + + ); + } + + if (assetIsMalicious) { + return ( + + ); + } + + return null; +}; diff --git a/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.test.tsx b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.test.tsx index cce626a7c412..0069c359c0f3 100644 --- a/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.test.tsx +++ b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.test.tsx @@ -69,56 +69,4 @@ describe('BridgeAsset', () => { expect(getByText('Native SegWit')).toBeInTheDocument(); expect(getByTestId(/^bridge-asset--/u)).toMatchSnapshot(); }); - - it('should render verified badge when isVerified is true', () => { - const { getByTestId } = renderWithProvider( - , - configureStore(createBridgeMockStore({})), - ); - expect(getByTestId('bridge-asset-verified-badge')).toBeInTheDocument(); - expect(getByTestId(/^bridge-asset--/u)).toMatchSnapshot(); - }); - - it('does not render verified badge when isVerified is false', () => { - const { getByTestId, queryByTestId } = renderWithProvider( - , - configureStore(createBridgeMockStore({})), - ); - expect(getByTestId('bridge-asset-symbol')).toBeInTheDocument(); - expect( - queryByTestId('bridge-asset-verified-badge'), - ).not.toBeInTheDocument(); - }); - - it('does not render verified badge when isVerified is absent', () => { - const { getByTestId, queryByTestId } = renderWithProvider( - , - configureStore(createBridgeMockStore({})), - ); - expect(getByTestId('bridge-asset-symbol')).toBeInTheDocument(); - expect( - queryByTestId('bridge-asset-verified-badge'), - ).not.toBeInTheDocument(); - }); }); diff --git a/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.tsx b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.tsx index 3ca7b088ce7f..b84a7db8ebbc 100644 --- a/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.tsx +++ b/ui/pages/bridge/prepare/components/bridge-asset-picker/asset.tsx @@ -41,6 +41,7 @@ import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { Column, Row } from '../../../layout'; import { formatCurrencyAmount, formatTokenAmount } from '../../../utils/quote'; import { useRWAToken } from '../../../hooks/useRWAToken'; +import { AssetBanner } from './asset-banner'; export const BridgeAsset = React.forwardRef( ( @@ -130,14 +131,7 @@ export const BridgeAsset = React.forwardRef( {asset.symbol} - {asset.isVerified && ( - - )} + {asset.accountType && ACCOUNT_TYPE_LABELS[asset.accountType] && ( )} diff --git a/ui/pages/bridge/utils/tokens.ts b/ui/pages/bridge/utils/tokens.ts index 07343d19d395..5c63f62e07f3 100644 --- a/ui/pages/bridge/utils/tokens.ts +++ b/ui/pages/bridge/utils/tokens.ts @@ -9,6 +9,8 @@ import { nullable, optional, intersection, + array, + enums, } from '@metamask/superstruct'; import { CaipAssetTypeStruct, type CaipChainId } from '@metamask/utils'; import { getClientHeaders } from '@metamask/bridge-controller'; @@ -30,8 +32,38 @@ const MinimalAssetSchema = type({ decimals: number(), }); +export enum BridgeAssetSecurityDataType { + INFO = 'Info', + BENIGN = 'Benign', + VERIFIED = 'Verified', + WARNING = 'Warning', + SPAM = 'Spam', + MALICIOUS = 'Malicious', +} + +const BridgeAssetSecurityData = type({ + isVerified: optional(boolean()), + securityData: optional( + type({ + type: enums(Object.values(BridgeAssetSecurityDataType)), + metadata: optional( + type({ + features: array( + type({ + featureId: string(), + type: enums(Object.values(BridgeAssetSecurityDataType)), + description: string(), + }), + ), + }), + ), + }), + ), +}); + const BridgeAssetV2Schema = intersection([ MinimalAssetSchema, + BridgeAssetSecurityData, type({ /** * URL for token icon @@ -43,7 +75,6 @@ const BridgeAssetV2Schema = intersection([ isSource: nullable(optional(boolean())), }), ), - isVerified: optional(boolean()), }), ]);