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
16 changes: 13 additions & 3 deletions app/components/Views/TrendingView/feeds/stocks/useStocksFeed.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useMemo } from 'react';
import type { TrendingAsset } from '@metamask/assets-controllers';
import { useRwaTokens } from '../../../../UI/Trending/hooks/useRwaTokens/useRwaTokens';
import { useFeedRefresh } from '../../hooks/useFeedRefresh';
import type { RefreshConfig } from '../../hooks/useExploreRefresh';

const ETHEREUM_CAIP_CHAIN_ID = 'eip155:1/';

interface UseStocksFeedOptions {
query?: string;
refresh?: RefreshConfig;
Expand All @@ -14,17 +17,24 @@ export interface UseStocksFeedResult {
refetch: () => Promise<void>;
}

/** Tokenized stocks (RWAs on Ethereum mainnet). */
/** Tokenized stocks (RWAs). Only Ethereum mainnet tokens are shown in the section. */
export const useStocksFeed = ({
query,
refresh,
}: UseStocksFeedOptions = {}): UseStocksFeedResult => {
const { data, isLoading, refetch } = useRwaTokens({
searchQuery: query,
...(query ? {} : { chainIds: ['eip155:1'] }),
});

// Keep mainnet filtering here (not in the request) so all surfaces share the same
// RWA cache (server-side); chain-specific params would split the cache and diverge from the main feed.
const ethereumData = useMemo(
() =>
data.filter((asset) => asset.assetId.startsWith(ETHEREUM_CAIP_CHAIN_ID)),
Comment thread
cursor[bot] marked this conversation as resolved.
[data],
);
Comment thread
juanmigdr marked this conversation as resolved.

useFeedRefresh(refresh, refetch);

return { data, isLoading, refetch };
return { data: ethereumData, isLoading, refetch };
};
121 changes: 121 additions & 0 deletions app/components/Views/TrendingView/feeds/tokens/useTokensFeed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,125 @@ describe('useTokensFeed', () => {
enableDebounce: false,
});
});

describe('hideRiskyTokens', () => {
const tokensWithSecurity = [
{
assetId: 'eip155:1/erc20:0x1',
symbol: 'VER',
name: 'Verified Token',
marketCap: 900,
securityData: { resultType: 'Verified' },
},
{
assetId: 'eip155:1/erc20:0x2',
symbol: 'BEN',
name: 'Benign Token',
marketCap: 800,
securityData: { resultType: 'Benign' },
},
{
assetId: 'eip155:1/erc20:0x3',
symbol: 'WRN',
name: 'Warning Token',
marketCap: 700,
securityData: { resultType: 'Warning' },
},
{
assetId: 'eip155:1/erc20:0x4',
symbol: 'SPM',
name: 'Spam Token',
marketCap: 600,
securityData: { resultType: 'Spam' },
},
{
assetId: 'eip155:1/erc20:0x5',
symbol: 'MAL',
name: 'Malicious Token',
marketCap: 500,
securityData: { resultType: 'Malicious' },
},
{
assetId: 'eip155:1/erc20:0x6',
symbol: 'UNS',
name: 'Unscanned Token',
marketCap: 400,
},
] as unknown as TrendingAsset[];

beforeEach(() => {
mockUseTrendingSearch.mockReturnValue({
data: tokensWithSecurity,
isLoading: false,
refetch: mockRefetch,
});
});

it('returns all tokens when hideRiskyTokens is false (default)', () => {
const { result } = renderHook(() => useTokensFeed());

expect(result.current.data.map((t) => t.symbol)).toEqual([
'VER',
'BEN',
'WRN',
'SPM',
'MAL',
'UNS',
]);
});

it('keeps only Verified, Benign, and unscanned tokens when hideRiskyTokens is true', () => {
const { result } = renderHook(() =>
useTokensFeed({ hideRiskyTokens: true }),
);

expect(result.current.data.map((t) => t.symbol)).toEqual([
'VER',
'BEN',
'UNS',
]);
});

it('removes Warning tokens', () => {
mockUseTrendingSearch.mockReturnValue({
data: [tokensWithSecurity[2]],
isLoading: false,
refetch: mockRefetch,
});

const { result } = renderHook(() =>
useTokensFeed({ hideRiskyTokens: true }),
);

expect(result.current.data).toHaveLength(0);
});

it('removes Spam tokens', () => {
mockUseTrendingSearch.mockReturnValue({
data: [tokensWithSecurity[3]],
isLoading: false,
refetch: mockRefetch,
});

const { result } = renderHook(() =>
useTokensFeed({ hideRiskyTokens: true }),
);

expect(result.current.data).toHaveLength(0);
});

it('removes Malicious tokens', () => {
mockUseTrendingSearch.mockReturnValue({
data: [tokensWithSecurity[4]],
isLoading: false,
refetch: mockRefetch,
});

const { result } = renderHook(() =>
useTokensFeed({ hideRiskyTokens: true }),
);

expect(result.current.data).toHaveLength(0);
});
});
});
33 changes: 23 additions & 10 deletions app/components/Views/TrendingView/feeds/tokens/useTokensFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ interface UseTokensFeedOptions {
/** Search query; when present, results are sorted by market cap descending. */
query?: string;
refresh?: RefreshConfig;
/**
* When true, only Verified and Benign tokens (or unscanned ones) are shown.
* Use for surfaces that don't display a security badge.
*/
hideRiskyTokens?: boolean;
}

export interface UseTokensFeedResult {
Expand All @@ -21,6 +26,7 @@ export interface UseTokensFeedResult {
export const useTokensFeed = ({
query,
refresh,
hideRiskyTokens = false,
}: UseTokensFeedOptions = {}): UseTokensFeedResult => {
const { data, isLoading, refetch } = useTrendingSearch({
searchQuery: query,
Expand All @@ -29,16 +35,23 @@ export const useTokensFeed = ({

useFeedRefresh(refresh, refetch);

const filteredData = useMemo(
() =>
fuseSearch(
data,
query,
TOKEN_FUSE_OPTIONS,
(a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0),
),
[data, query],
);
const filteredData = useMemo(() => {
const searched = fuseSearch(
data,
query,
TOKEN_FUSE_OPTIONS,
(a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0),
);

if (!hideRiskyTokens) return searched;

return searched.filter(({ securityData }) => {
const { resultType } = securityData ?? {};
return (
!resultType || resultType === 'Verified' || resultType === 'Benign'
);
});
}, [data, query, hideRiskyTokens]);

return { data: filteredData, isLoading, refetch };
};
2 changes: 1 addition & 1 deletion app/components/Views/TrendingView/tabs/NowTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const NowTab: React.FC<TabProps> = ({ refresh, refreshing, onRefresh }) => {
}, [refresh.trigger]);

const predictions = usePredictionsFeed({ refresh });
const cryptoMovers = useTokensFeed({ refresh });
const cryptoMovers = useTokensFeed({ refresh, hideRiskyTokens: true });
const stocks = useStocksFeed({ refresh });

const renderPredictionItem: ListRenderItem<PredictMarketType> = useCallback(
Expand Down
4 changes: 2 additions & 2 deletions locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8847,8 +8847,8 @@
"search_placeholder": "Search tokens, sites, URLs",
"cancel": "Cancel",
"perps": "Perps",
"rwa_perps_section": "Markets",
"macro_stocks_commodity_perps": "Stocks & commodities",
"rwa_perps_section": "Perps",
"macro_stocks_commodity_perps": "Perps",
"macro_pill_stocks": "Stocks",
"macro_pill_commodities": "Commodities",
"rwa_pill_commodities": "Commodities",
Expand Down
Loading