Skip to content

Commit ec11b69

Browse files
authored
chore: align data fetch on stocks (#29795)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** The `useStocksFeed` hook was passing `chainIds: ['eip155:1']` to `useRwaTokens` when no search query was active, restricting the API fetch to Ethereum mainnet only. The `RWATokensFullView` (full-screen Stocks view) fetches from all RWA-supported chains (Ethereum + BNB Chain) by default. This mismatch caused the two views to operate on different datasets, leading to inconsistent sort order: tokens available on BNB Chain (e.g. Cipher Mining Ondo Tokenized) appeared at the top of the full-screen view due to a higher 24h price change, but were completely absent from the section widget, making a lower-ranked Ethereum token incorrectly appear first there. The fix removes the `chainIds` override from `useStocksFeed` so it fetches the same combined dataset as the full-screen view. A client-side filter is then applied to return only Ethereum mainnet tokens to the section, preserving the intended display scope while correctly reflecting the global sort order. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: align data fetch on stocks ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3149 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate UI/data behavior change: alters what assets appear in Trending sections by shifting RWA chain filtering client-side and optionally hiding risky tokens, which could affect ordering/visibility but not security-critical flows. > > **Overview** > Aligns the Trending *Stocks* section with the full RWA dataset by removing the `chainIds` constraint from the `useRwaTokens` request and instead filtering to Ethereum mainnet locally (by `assetId` CAIP prefix) to avoid cache divergence and inconsistent sorting. > > Adds an optional `hideRiskyTokens` flag to `useTokensFeed` to filter out tokens marked `Warning`, `Spam`, or `Malicious` (keeping `Verified`, `Benign`, and unscanned), enables it for the *Crypto movers* pills in `NowTab`, and extends unit tests to cover the new filtering behavior. > > Updates English strings so the RWA perps section/pill labels display "Perps" instead of "Markets"/"Stocks & commodities". > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ed1be03. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 3c59b56 commit ec11b69

5 files changed

Lines changed: 160 additions & 16 deletions

File tree

app/components/Views/TrendingView/feeds/stocks/useStocksFeed.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { useMemo } from 'react';
12
import type { TrendingAsset } from '@metamask/assets-controllers';
23
import { useRwaTokens } from '../../../../UI/Trending/hooks/useRwaTokens/useRwaTokens';
34
import { useFeedRefresh } from '../../hooks/useFeedRefresh';
45
import type { RefreshConfig } from '../../hooks/useExploreRefresh';
56

7+
const ETHEREUM_CAIP_CHAIN_ID = 'eip155:1/';
8+
69
interface UseStocksFeedOptions {
710
query?: string;
811
refresh?: RefreshConfig;
@@ -14,17 +17,24 @@ export interface UseStocksFeedResult {
1417
refetch: () => Promise<void>;
1518
}
1619

17-
/** Tokenized stocks (RWAs on Ethereum mainnet). */
20+
/** Tokenized stocks (RWAs). Only Ethereum mainnet tokens are shown in the section. */
1821
export const useStocksFeed = ({
1922
query,
2023
refresh,
2124
}: UseStocksFeedOptions = {}): UseStocksFeedResult => {
2225
const { data, isLoading, refetch } = useRwaTokens({
2326
searchQuery: query,
24-
...(query ? {} : { chainIds: ['eip155:1'] }),
2527
});
2628

29+
// Keep mainnet filtering here (not in the request) so all surfaces share the same
30+
// RWA cache (server-side); chain-specific params would split the cache and diverge from the main feed.
31+
const ethereumData = useMemo(
32+
() =>
33+
data.filter((asset) => asset.assetId.startsWith(ETHEREUM_CAIP_CHAIN_ID)),
34+
[data],
35+
);
36+
2737
useFeedRefresh(refresh, refetch);
2838

29-
return { data, isLoading, refetch };
39+
return { data: ethereumData, isLoading, refetch };
3040
};

app/components/Views/TrendingView/feeds/tokens/useTokensFeed.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,125 @@ describe('useTokensFeed', () => {
8484
enableDebounce: false,
8585
});
8686
});
87+
88+
describe('hideRiskyTokens', () => {
89+
const tokensWithSecurity = [
90+
{
91+
assetId: 'eip155:1/erc20:0x1',
92+
symbol: 'VER',
93+
name: 'Verified Token',
94+
marketCap: 900,
95+
securityData: { resultType: 'Verified' },
96+
},
97+
{
98+
assetId: 'eip155:1/erc20:0x2',
99+
symbol: 'BEN',
100+
name: 'Benign Token',
101+
marketCap: 800,
102+
securityData: { resultType: 'Benign' },
103+
},
104+
{
105+
assetId: 'eip155:1/erc20:0x3',
106+
symbol: 'WRN',
107+
name: 'Warning Token',
108+
marketCap: 700,
109+
securityData: { resultType: 'Warning' },
110+
},
111+
{
112+
assetId: 'eip155:1/erc20:0x4',
113+
symbol: 'SPM',
114+
name: 'Spam Token',
115+
marketCap: 600,
116+
securityData: { resultType: 'Spam' },
117+
},
118+
{
119+
assetId: 'eip155:1/erc20:0x5',
120+
symbol: 'MAL',
121+
name: 'Malicious Token',
122+
marketCap: 500,
123+
securityData: { resultType: 'Malicious' },
124+
},
125+
{
126+
assetId: 'eip155:1/erc20:0x6',
127+
symbol: 'UNS',
128+
name: 'Unscanned Token',
129+
marketCap: 400,
130+
},
131+
] as unknown as TrendingAsset[];
132+
133+
beforeEach(() => {
134+
mockUseTrendingSearch.mockReturnValue({
135+
data: tokensWithSecurity,
136+
isLoading: false,
137+
refetch: mockRefetch,
138+
});
139+
});
140+
141+
it('returns all tokens when hideRiskyTokens is false (default)', () => {
142+
const { result } = renderHook(() => useTokensFeed());
143+
144+
expect(result.current.data.map((t) => t.symbol)).toEqual([
145+
'VER',
146+
'BEN',
147+
'WRN',
148+
'SPM',
149+
'MAL',
150+
'UNS',
151+
]);
152+
});
153+
154+
it('keeps only Verified, Benign, and unscanned tokens when hideRiskyTokens is true', () => {
155+
const { result } = renderHook(() =>
156+
useTokensFeed({ hideRiskyTokens: true }),
157+
);
158+
159+
expect(result.current.data.map((t) => t.symbol)).toEqual([
160+
'VER',
161+
'BEN',
162+
'UNS',
163+
]);
164+
});
165+
166+
it('removes Warning tokens', () => {
167+
mockUseTrendingSearch.mockReturnValue({
168+
data: [tokensWithSecurity[2]],
169+
isLoading: false,
170+
refetch: mockRefetch,
171+
});
172+
173+
const { result } = renderHook(() =>
174+
useTokensFeed({ hideRiskyTokens: true }),
175+
);
176+
177+
expect(result.current.data).toHaveLength(0);
178+
});
179+
180+
it('removes Spam tokens', () => {
181+
mockUseTrendingSearch.mockReturnValue({
182+
data: [tokensWithSecurity[3]],
183+
isLoading: false,
184+
refetch: mockRefetch,
185+
});
186+
187+
const { result } = renderHook(() =>
188+
useTokensFeed({ hideRiskyTokens: true }),
189+
);
190+
191+
expect(result.current.data).toHaveLength(0);
192+
});
193+
194+
it('removes Malicious tokens', () => {
195+
mockUseTrendingSearch.mockReturnValue({
196+
data: [tokensWithSecurity[4]],
197+
isLoading: false,
198+
refetch: mockRefetch,
199+
});
200+
201+
const { result } = renderHook(() =>
202+
useTokensFeed({ hideRiskyTokens: true }),
203+
);
204+
205+
expect(result.current.data).toHaveLength(0);
206+
});
207+
});
87208
});

app/components/Views/TrendingView/feeds/tokens/useTokensFeed.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ interface UseTokensFeedOptions {
99
/** Search query; when present, results are sorted by market cap descending. */
1010
query?: string;
1111
refresh?: RefreshConfig;
12+
/**
13+
* When true, only Verified and Benign tokens (or unscanned ones) are shown.
14+
* Use for surfaces that don't display a security badge.
15+
*/
16+
hideRiskyTokens?: boolean;
1217
}
1318

1419
export interface UseTokensFeedResult {
@@ -21,6 +26,7 @@ export interface UseTokensFeedResult {
2126
export const useTokensFeed = ({
2227
query,
2328
refresh,
29+
hideRiskyTokens = false,
2430
}: UseTokensFeedOptions = {}): UseTokensFeedResult => {
2531
const { data, isLoading, refetch } = useTrendingSearch({
2632
searchQuery: query,
@@ -29,16 +35,23 @@ export const useTokensFeed = ({
2935

3036
useFeedRefresh(refresh, refetch);
3137

32-
const filteredData = useMemo(
33-
() =>
34-
fuseSearch(
35-
data,
36-
query,
37-
TOKEN_FUSE_OPTIONS,
38-
(a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0),
39-
),
40-
[data, query],
41-
);
38+
const filteredData = useMemo(() => {
39+
const searched = fuseSearch(
40+
data,
41+
query,
42+
TOKEN_FUSE_OPTIONS,
43+
(a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0),
44+
);
45+
46+
if (!hideRiskyTokens) return searched;
47+
48+
return searched.filter(({ securityData }) => {
49+
const { resultType } = securityData ?? {};
50+
return (
51+
!resultType || resultType === 'Verified' || resultType === 'Benign'
52+
);
53+
});
54+
}, [data, query, hideRiskyTokens]);
4255

4356
return { data: filteredData, isLoading, refetch };
4457
};

app/components/Views/TrendingView/tabs/NowTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const NowTab: React.FC<TabProps> = ({ refresh, refreshing, onRefresh }) => {
104104
}, [refresh.trigger]);
105105

106106
const predictions = usePredictionsFeed({ refresh });
107-
const cryptoMovers = useTokensFeed({ refresh });
107+
const cryptoMovers = useTokensFeed({ refresh, hideRiskyTokens: true });
108108
const stocks = useStocksFeed({ refresh });
109109

110110
const renderPredictionItem: ListRenderItem<PredictMarketType> = useCallback(

locales/languages/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8847,8 +8847,8 @@
88478847
"search_placeholder": "Search tokens, sites, URLs",
88488848
"cancel": "Cancel",
88498849
"perps": "Perps",
8850-
"rwa_perps_section": "Markets",
8851-
"macro_stocks_commodity_perps": "Stocks & commodities",
8850+
"rwa_perps_section": "Perps",
8851+
"macro_stocks_commodity_perps": "Perps",
88528852
"macro_pill_stocks": "Stocks",
88538853
"macro_pill_commodities": "Commodities",
88548854
"rwa_pill_commodities": "Commodities",

0 commit comments

Comments
 (0)