Skip to content

Commit 684a40c

Browse files
authored
Merge branch 'main' into rekm/ejrpcp-v2
2 parents 4a63eea + c70fd20 commit 684a40c

17 files changed

Lines changed: 291 additions & 40 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@metamask/core-monorepo",
3-
"version": "660.0.0",
3+
"version": "662.0.0",
44
"private": true,
55
"description": "Monorepo for packages shared between MetaMask clients",
66
"repository": {

packages/account-tree-controller/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module.exports = merge(baseConfig, {
1717
// An object that configures minimum threshold enforcement for coverage results
1818
coverageThreshold: {
1919
global: {
20-
branches: 98.34,
20+
branches: 100,
2121
functions: 100,
2222
lines: 100,
2323
statements: 100,

packages/account-tree-controller/src/AccountTreeController.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,9 @@ export class AccountTreeController extends BaseController<
632632
const group = wallet.groups[groupId];
633633
const persistedGroupMetadata = state.accountGroupsMetadata[groupId];
634634

635+
// Ensure metadata object exists once at the beginning
636+
state.accountGroupsMetadata[groupId] ??= {};
637+
635638
// Apply persisted name if available (including empty strings)
636639
if (persistedGroupMetadata?.name !== undefined) {
637640
state.accountTree.wallets[walletId].groups[groupId].metadata.name =
@@ -660,7 +663,6 @@ export class AccountTreeController extends BaseController<
660663
log(`[${group.id}] Set default name to: "${group.metadata.name}"`);
661664

662665
// Persist the generated name to ensure consistency
663-
state.accountGroupsMetadata[groupId] ??= {};
664666
state.accountGroupsMetadata[groupId].name = {
665667
value: proposedName,
666668
// The `lastUpdatedAt` field is used for backup and sync, when comparing local names
@@ -681,7 +683,6 @@ export class AccountTreeController extends BaseController<
681683
this.#accountOrderCallbacks?.isPinnedAccount?.(account),
682684
);
683685
}
684-
state.accountGroupsMetadata[groupId] ??= {};
685686
state.accountGroupsMetadata[groupId].pinned = {
686687
value: isPinned,
687688
lastUpdatedAt: 0,
@@ -700,7 +701,6 @@ export class AccountTreeController extends BaseController<
700701
this.#accountOrderCallbacks?.isHiddenAccount?.(account),
701702
);
702703
}
703-
state.accountGroupsMetadata[groupId] ??= {};
704704
state.accountGroupsMetadata[groupId].hidden = {
705705
value: isHidden,
706706
lastUpdatedAt: 0,

packages/assets-controllers/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [87.1.0]
11+
12+
### Added
13+
14+
- Added `getTrendingTokens` function to fetch trending tokens for specific chains ([#7054]) (https://github.com/MetaMask/core/pull/7054)
15+
- Added new types `SortTrendingBy` and `TrendingAsset` ([#7054]) (https://github.com/MetaMask/core/pull/7054)
16+
1017
## [87.0.0]
1118

1219
### Added
@@ -2239,7 +2246,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22392246
22402247
- Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845))
22412248
2242-
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@87.0.0...HEAD
2249+
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@87.1.0...HEAD
2250+
[87.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@87.0.0...@metamask/assets-controllers@87.1.0
22432251
[87.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@86.0.0...@metamask/assets-controllers@87.0.0
22442252
[86.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@85.0.0...@metamask/assets-controllers@86.0.0
22452253
[85.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@84.0.0...@metamask/assets-controllers@85.0.0

packages/assets-controllers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@metamask/assets-controllers",
3-
"version": "87.0.0",
3+
"version": "87.1.0",
44
"description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)",
55
"keywords": [
66
"MetaMask",

packages/assets-controllers/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export {
142142
SUPPORTED_CHAIN_IDS,
143143
getNativeTokenAddress,
144144
} from './token-prices-service';
145-
export { searchTokens } from './token-service';
145+
export { searchTokens, getTrendingTokens } from './token-service';
146146
export { RatesController, Cryptocurrency } from './RatesController';
147147
export type {
148148
RatesControllerState,
@@ -231,3 +231,4 @@ export type {
231231
} from './selectors/token-selectors';
232232
export { selectAssetsBySelectedAccountGroup } from './selectors/token-selectors';
233233
export { createFormatters } from './utils/formatters';
234+
export type { SortTrendingBy, TrendingAsset } from './token-service';

packages/assets-controllers/src/token-service.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { toHex } from '@metamask/controller-utils';
22
import type { CaipChainId } from '@metamask/utils';
33
import nock from 'nock';
44

5+
import type { SortTrendingBy } from './token-service';
56
import {
67
fetchTokenListByChainId,
78
fetchTokenMetadata,
9+
getTrendingTokens,
810
searchTokens,
911
TOKEN_END_POINT_API,
1012
TOKEN_METADATA_NO_SUPPORT_ERROR,
@@ -709,4 +711,96 @@ describe('Token service', () => {
709711
});
710712
});
711713
});
714+
715+
describe('getTrendingTokens', () => {
716+
const sampleTrendingTokens = [
717+
{
718+
assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
719+
name: 'USDC',
720+
symbol: 'USDC',
721+
decimals: 6,
722+
price: '1.00294333595976',
723+
aggregatedUsdVolume: 455616484.38,
724+
marketCap: 75877371441.07,
725+
},
726+
{
727+
assetId: 'eip155:1/erc20:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
728+
name: 'Wrapped Ether',
729+
symbol: 'WETH',
730+
decimals: 18,
731+
price: '3406.01599421582',
732+
aggregatedUsdVolume: 358982988.74,
733+
marketCap: 7610628690.4,
734+
},
735+
];
736+
it('returns empty array if no chains are provided', async () => {
737+
const result = await getTrendingTokens({ chainIds: [] });
738+
expect(result).toStrictEqual([]);
739+
});
740+
741+
it('returns empty array if api returns non-array response', async () => {
742+
nock(TOKEN_END_POINT_API)
743+
.get(
744+
`/v3/tokens/trending?chainIds=${encodeURIComponent(sampleCaipChainId)}`,
745+
)
746+
.reply(200, { error: 'Invalid response' })
747+
.persist();
748+
749+
const result = await getTrendingTokens({ chainIds: [sampleCaipChainId] });
750+
expect(result).toStrictEqual([]);
751+
});
752+
753+
it('returns empty array if the fetch fails', async () => {
754+
nock(TOKEN_END_POINT_API)
755+
.get(
756+
`/v3/tokens/trending?chainIds=${encodeURIComponent(sampleCaipChainId)}`,
757+
)
758+
.reply(500)
759+
.persist();
760+
761+
const result = await getTrendingTokens({ chainIds: [sampleCaipChainId] });
762+
expect(result).toStrictEqual([]);
763+
});
764+
765+
it('returns the list of trending tokens if the fetch succeeds', async () => {
766+
const testChainId = 'eip155:1';
767+
const sortBy: SortTrendingBy = 'm5_trending';
768+
const testMinLiquidity = 1000000;
769+
const testMinVolume24hUsd = 1000000;
770+
const testMaxVolume24hUsd = 1000000;
771+
const testMinMarketCap = 1000000;
772+
const testMaxMarketCap = 1000000;
773+
nock(TOKEN_END_POINT_API)
774+
.get(
775+
`/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&sortBy=${sortBy}&minLiquidity=${testMinLiquidity}&minVolume24hUsd=${testMinVolume24hUsd}&maxVolume24hUsd=${testMaxVolume24hUsd}&minMarketCap=${testMinMarketCap}&maxMarketCap=${testMaxMarketCap}`,
776+
)
777+
.reply(200, sampleTrendingTokens)
778+
.persist();
779+
780+
const result = await getTrendingTokens({
781+
chainIds: [testChainId],
782+
sortBy,
783+
minLiquidity: testMinLiquidity,
784+
minVolume24hUsd: testMinVolume24hUsd,
785+
maxVolume24hUsd: testMaxVolume24hUsd,
786+
minMarketCap: testMinMarketCap,
787+
maxMarketCap: testMaxMarketCap,
788+
});
789+
expect(result).toStrictEqual(sampleTrendingTokens);
790+
});
791+
792+
it('returns the list of trending tokens if the fetch succeeds with no query params', async () => {
793+
const testChainId = 'eip155:1';
794+
795+
nock(TOKEN_END_POINT_API)
796+
.get(`/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}`)
797+
.reply(200, sampleTrendingTokens)
798+
.persist();
799+
800+
const result = await getTrendingTokens({
801+
chainIds: [testChainId],
802+
});
803+
expect(result).toStrictEqual(sampleTrendingTokens);
804+
});
805+
});
712806
});

packages/assets-controllers/src/token-service.ts

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ export const TOKEN_METADATA_NO_SUPPORT_ERROR =
2020
*/
2121
function getTokensURL(chainId: Hex) {
2222
const occurrenceFloor = chainId === ChainId['linea-mainnet'] ? 1 : 3;
23-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
24-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
23+
2524
return `${TOKEN_END_POINT_API}/tokens/${convertHexToDecimal(
2625
chainId,
2726
)}?occurrenceFloor=${occurrenceFloor}&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`;
@@ -35,13 +34,20 @@ function getTokensURL(chainId: Hex) {
3534
* @returns The token metadata URL.
3635
*/
3736
function getTokenMetadataURL(chainId: Hex, tokenAddress: string) {
38-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
39-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
4037
return `${TOKEN_END_POINT_API}/token/${convertHexToDecimal(
4138
chainId,
4239
)}?address=${tokenAddress}`;
4340
}
4441

42+
/**
43+
* The sort by field for trending tokens.
44+
*/
45+
export type SortTrendingBy =
46+
| 'm5_trending'
47+
| 'h1_trending'
48+
| 'h6_trending'
49+
| 'h24_trending';
50+
4551
/**
4652
* Get the token search URL for the given networks and search query.
4753
*
@@ -58,6 +64,43 @@ function getTokenSearchURL(chainIds: CaipChainId[], query: string, limit = 10) {
5864
return `${TOKEN_END_POINT_API}/tokens/search?chainIds=${encodedChainIds}&query=${encodedQuery}&limit=${limit}`;
5965
}
6066

67+
/**
68+
* Get the trending tokens URL for the given networks and search query.
69+
*
70+
* @param options - Options for getting trending tokens.
71+
* @param options.chainIds - Array of CAIP format chain IDs (e.g., ['eip155:1', 'eip155:137', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']).
72+
* @param options.sortBy - The sort by field.
73+
* @param options.minLiquidity - The minimum liquidity.
74+
* @param options.minVolume24hUsd - The minimum volume 24h in USD.
75+
* @param options.maxVolume24hUsd - The maximum volume 24h in USD.
76+
* @param options.minMarketCap - The minimum market cap.
77+
* @param options.maxMarketCap - The maximum market cap.
78+
* @returns The trending tokens URL.
79+
*/
80+
function getTrendingTokensURL(options: {
81+
chainIds: CaipChainId[];
82+
sortBy?: SortTrendingBy;
83+
minLiquidity?: number;
84+
minVolume24hUsd?: number;
85+
maxVolume24hUsd?: number;
86+
minMarketCap?: number;
87+
maxMarketCap?: number;
88+
}): string {
89+
const encodedChainIds = options.chainIds
90+
.map((id) => encodeURIComponent(id))
91+
.join(',');
92+
// Add the rest of query params if they are defined
93+
const queryParams = new URLSearchParams();
94+
const { chainIds, ...rest } = options;
95+
Object.entries(rest).forEach(([key, value]) => {
96+
if (value !== undefined) {
97+
queryParams.append(key, String(value));
98+
}
99+
});
100+
101+
return `${TOKEN_END_POINT_API}/v3/tokens/trending?chainIds=${encodedChainIds}${queryParams.toString() ? `&${queryParams.toString()}` : ''}`;
102+
}
103+
61104
const tenSecondsInMilliseconds = 10_000;
62105

63106
// Token list averages 1.6 MB in size
@@ -134,6 +177,91 @@ export async function searchTokens(
134177
}
135178
}
136179

180+
/**
181+
* The trending asset type.
182+
*/
183+
export type TrendingAsset = {
184+
assetId: string;
185+
name: string;
186+
symbol: string;
187+
decimals: number;
188+
price: string;
189+
aggregatedUsdVolume: number;
190+
marketCap: number;
191+
priceChangePct?: {
192+
m5?: string;
193+
m15?: string;
194+
m30?: string;
195+
h1?: string;
196+
h6?: string;
197+
h24?: string;
198+
};
199+
labels?: string[];
200+
};
201+
202+
/**
203+
* Get the trending tokens for the given chains.
204+
*
205+
* @param options - Options for getting trending tokens.
206+
* @param options.chainIds - The chains to get the trending tokens for.
207+
* @param options.sortBy - The sort by field.
208+
* @param options.minLiquidity - The minimum liquidity.
209+
* @param options.minVolume24hUsd - The minimum volume 24h in USD.
210+
* @param options.maxVolume24hUsd - The maximum volume 24h in USD.
211+
* @param options.minMarketCap - The minimum market cap.
212+
* @param options.maxMarketCap - The maximum market cap.
213+
* @returns The trending tokens.
214+
* @throws Will throw if the request fails.
215+
*/
216+
export async function getTrendingTokens({
217+
chainIds,
218+
sortBy,
219+
minLiquidity,
220+
minVolume24hUsd,
221+
maxVolume24hUsd,
222+
minMarketCap,
223+
maxMarketCap,
224+
}: {
225+
chainIds: CaipChainId[];
226+
sortBy?: SortTrendingBy;
227+
minLiquidity?: number;
228+
minVolume24hUsd?: number;
229+
maxVolume24hUsd?: number;
230+
minMarketCap?: number;
231+
maxMarketCap?: number;
232+
}): Promise<TrendingAsset[]> {
233+
if (chainIds.length === 0) {
234+
console.error('No chains provided');
235+
return [];
236+
}
237+
238+
const trendingTokensURL = getTrendingTokensURL({
239+
chainIds,
240+
sortBy,
241+
minLiquidity,
242+
minVolume24hUsd,
243+
maxVolume24hUsd,
244+
minMarketCap,
245+
maxMarketCap,
246+
});
247+
248+
try {
249+
const result = await handleFetch(trendingTokensURL);
250+
251+
// Validate that the API returned an array
252+
if (Array.isArray(result)) {
253+
return result;
254+
}
255+
256+
// Handle non-expected responses
257+
console.error('Trending tokens API returned non-array response:', result);
258+
return [];
259+
} catch (error) {
260+
console.error('Trending tokens request failed:', error);
261+
return [];
262+
}
263+
}
264+
137265
/**
138266
* Fetch metadata for the token address provided for a given network. This request is cancellable
139267
* using the abort signal passed in.
@@ -145,8 +273,6 @@ export async function searchTokens(
145273
* @param options.timeout - The fetch timeout.
146274
* @returns The token metadata, or `undefined` if the request was either aborted or failed.
147275
*/
148-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
149-
// eslint-disable-next-line @typescript-eslint/naming-convention
150276
export async function fetchTokenMetadata<T>(
151277
chainId: Hex,
152278
tokenAddress: string,
@@ -208,8 +334,6 @@ async function parseJsonResponse(apiResponse: Response): Promise<unknown> {
208334
const responseObj = await apiResponse.json();
209335
// api may return errors as json without setting an error http status code
210336
if (responseObj?.error) {
211-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
212-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
213337
throw new Error(`TokenService Error: ${responseObj.error}`);
214338
}
215339
return responseObj;

0 commit comments

Comments
 (0)