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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/core-monorepo",
"version": "661.0.0",
"version": "662.0.0",
"private": true,
"description": "Monorepo for packages shared between MetaMask clients",
"repository": {
Expand Down
10 changes: 9 additions & 1 deletion packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [87.1.0]

### Added

- Added `getTrendingTokens` function to fetch trending tokens for specific chains ([#7054]) (https://github.com/MetaMask/core/pull/7054)
- Added new types `SortTrendingBy` and `TrendingAsset` ([#7054]) (https://github.com/MetaMask/core/pull/7054)

## [87.0.0]

### Added
Expand Down Expand Up @@ -2239,7 +2246,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845))

[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@87.0.0...HEAD
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@87.1.0...HEAD
[87.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@87.0.0...@metamask/assets-controllers@87.1.0
[87.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@86.0.0...@metamask/assets-controllers@87.0.0
[86.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@85.0.0...@metamask/assets-controllers@86.0.0
[85.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@84.0.0...@metamask/assets-controllers@85.0.0
Expand Down
2 changes: 1 addition & 1 deletion packages/assets-controllers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/assets-controllers",
"version": "87.0.0",
"version": "87.1.0",
"description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)",
"keywords": [
"MetaMask",
Expand Down
3 changes: 2 additions & 1 deletion packages/assets-controllers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export {
SUPPORTED_CHAIN_IDS,
getNativeTokenAddress,
} from './token-prices-service';
export { searchTokens } from './token-service';
export { searchTokens, getTrendingTokens } from './token-service';
export { RatesController, Cryptocurrency } from './RatesController';
export type {
RatesControllerState,
Expand Down Expand Up @@ -231,3 +231,4 @@ export type {
} from './selectors/token-selectors';
export { selectAssetsBySelectedAccountGroup } from './selectors/token-selectors';
export { createFormatters } from './utils/formatters';
export type { SortTrendingBy, TrendingAsset } from './token-service';
94 changes: 94 additions & 0 deletions packages/assets-controllers/src/token-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { toHex } from '@metamask/controller-utils';
import type { CaipChainId } from '@metamask/utils';
import nock from 'nock';

import type { SortTrendingBy } from './token-service';
import {
fetchTokenListByChainId,
fetchTokenMetadata,
getTrendingTokens,
searchTokens,
TOKEN_END_POINT_API,
TOKEN_METADATA_NO_SUPPORT_ERROR,
Expand Down Expand Up @@ -709,4 +711,96 @@ describe('Token service', () => {
});
});
});

describe('getTrendingTokens', () => {
const sampleTrendingTokens = [
{
assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
name: 'USDC',
symbol: 'USDC',
decimals: 6,
price: '1.00294333595976',
aggregatedUsdVolume: 455616484.38,
marketCap: 75877371441.07,
},
{
assetId: 'eip155:1/erc20:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
name: 'Wrapped Ether',
symbol: 'WETH',
decimals: 18,
price: '3406.01599421582',
aggregatedUsdVolume: 358982988.74,
marketCap: 7610628690.4,
},
];
it('returns empty array if no chains are provided', async () => {
const result = await getTrendingTokens({ chainIds: [] });
expect(result).toStrictEqual([]);
});

it('returns empty array if api returns non-array response', async () => {
nock(TOKEN_END_POINT_API)
.get(
`/v3/tokens/trending?chainIds=${encodeURIComponent(sampleCaipChainId)}`,
)
.reply(200, { error: 'Invalid response' })
.persist();

const result = await getTrendingTokens({ chainIds: [sampleCaipChainId] });
expect(result).toStrictEqual([]);
});

it('returns empty array if the fetch fails', async () => {
nock(TOKEN_END_POINT_API)
.get(
`/v3/tokens/trending?chainIds=${encodeURIComponent(sampleCaipChainId)}`,
)
.reply(500)
.persist();

const result = await getTrendingTokens({ chainIds: [sampleCaipChainId] });
expect(result).toStrictEqual([]);
});

it('returns the list of trending tokens if the fetch succeeds', async () => {
const testChainId = 'eip155:1';
const sortBy: SortTrendingBy = 'm5_trending';
const testMinLiquidity = 1000000;
const testMinVolume24hUsd = 1000000;
const testMaxVolume24hUsd = 1000000;
const testMinMarketCap = 1000000;
const testMaxMarketCap = 1000000;
nock(TOKEN_END_POINT_API)
.get(
`/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&sortBy=${sortBy}&minLiquidity=${testMinLiquidity}&minVolume24hUsd=${testMinVolume24hUsd}&maxVolume24hUsd=${testMaxVolume24hUsd}&minMarketCap=${testMinMarketCap}&maxMarketCap=${testMaxMarketCap}`,
)
.reply(200, sampleTrendingTokens)
.persist();

const result = await getTrendingTokens({
chainIds: [testChainId],
sortBy,
minLiquidity: testMinLiquidity,
minVolume24hUsd: testMinVolume24hUsd,
maxVolume24hUsd: testMaxVolume24hUsd,
minMarketCap: testMinMarketCap,
maxMarketCap: testMaxMarketCap,
});
expect(result).toStrictEqual(sampleTrendingTokens);
});

it('returns the list of trending tokens if the fetch succeeds with no query params', async () => {
const testChainId = 'eip155:1';

nock(TOKEN_END_POINT_API)
.get(`/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}`)
.reply(200, sampleTrendingTokens)
.persist();

const result = await getTrendingTokens({
chainIds: [testChainId],
});
expect(result).toStrictEqual(sampleTrendingTokens);
});
});
});
140 changes: 132 additions & 8 deletions packages/assets-controllers/src/token-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export const TOKEN_METADATA_NO_SUPPORT_ERROR =
*/
function getTokensURL(chainId: Hex) {
const occurrenceFloor = chainId === ChainId['linea-mainnet'] ? 1 : 3;
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions

return `${TOKEN_END_POINT_API}/tokens/${convertHexToDecimal(
chainId,
)}?occurrenceFloor=${occurrenceFloor}&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`;
Expand All @@ -35,13 +34,20 @@ function getTokensURL(chainId: Hex) {
* @returns The token metadata URL.
*/
function getTokenMetadataURL(chainId: Hex, tokenAddress: string) {
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `${TOKEN_END_POINT_API}/token/${convertHexToDecimal(
chainId,
)}?address=${tokenAddress}`;
}

/**
* The sort by field for trending tokens.
*/
export type SortTrendingBy =
| 'm5_trending'
| 'h1_trending'
| 'h6_trending'
| 'h24_trending';

/**
* Get the token search URL for the given networks and search query.
*
Expand All @@ -58,6 +64,43 @@ function getTokenSearchURL(chainIds: CaipChainId[], query: string, limit = 10) {
return `${TOKEN_END_POINT_API}/tokens/search?chainIds=${encodedChainIds}&query=${encodedQuery}&limit=${limit}`;
}

/**
* Get the trending tokens URL for the given networks and search query.
*
* @param options - Options for getting trending tokens.
* @param options.chainIds - Array of CAIP format chain IDs (e.g., ['eip155:1', 'eip155:137', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']).
* @param options.sortBy - The sort by field.
* @param options.minLiquidity - The minimum liquidity.
* @param options.minVolume24hUsd - The minimum volume 24h in USD.
* @param options.maxVolume24hUsd - The maximum volume 24h in USD.
* @param options.minMarketCap - The minimum market cap.
* @param options.maxMarketCap - The maximum market cap.
* @returns The trending tokens URL.
*/
function getTrendingTokensURL(options: {
chainIds: CaipChainId[];
sortBy?: SortTrendingBy;
minLiquidity?: number;
minVolume24hUsd?: number;
maxVolume24hUsd?: number;
minMarketCap?: number;
maxMarketCap?: number;
}): string {
const encodedChainIds = options.chainIds
.map((id) => encodeURIComponent(id))
.join(',');
// Add the rest of query params if they are defined
const queryParams = new URLSearchParams();
const { chainIds, ...rest } = options;
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, String(value));
}
});

return `${TOKEN_END_POINT_API}/v3/tokens/trending?chainIds=${encodedChainIds}${queryParams.toString() ? `&${queryParams.toString()}` : ''}`;
}

const tenSecondsInMilliseconds = 10_000;

// Token list averages 1.6 MB in size
Expand Down Expand Up @@ -134,6 +177,91 @@ export async function searchTokens(
}
}

/**
* The trending asset type.
*/
export type TrendingAsset = {
assetId: string;
name: string;
symbol: string;
decimals: number;
price: string;
aggregatedUsdVolume: number;
marketCap: number;
priceChangePct?: {
m5?: string;
m15?: string;
m30?: string;
h1?: string;
h6?: string;
h24?: string;
};
labels?: string[];
};

/**
* Get the trending tokens for the given chains.
*
* @param options - Options for getting trending tokens.
* @param options.chainIds - The chains to get the trending tokens for.
* @param options.sortBy - The sort by field.
* @param options.minLiquidity - The minimum liquidity.
* @param options.minVolume24hUsd - The minimum volume 24h in USD.
* @param options.maxVolume24hUsd - The maximum volume 24h in USD.
* @param options.minMarketCap - The minimum market cap.
* @param options.maxMarketCap - The maximum market cap.
* @returns The trending tokens.
* @throws Will throw if the request fails.
*/
export async function getTrendingTokens({
chainIds,
sortBy,
minLiquidity,
minVolume24hUsd,
maxVolume24hUsd,
minMarketCap,
maxMarketCap,
}: {
chainIds: CaipChainId[];
sortBy?: SortTrendingBy;
minLiquidity?: number;
minVolume24hUsd?: number;
maxVolume24hUsd?: number;
minMarketCap?: number;
maxMarketCap?: number;
}): Promise<TrendingAsset[]> {
if (chainIds.length === 0) {
console.error('No chains provided');
return [];
}

const trendingTokensURL = getTrendingTokensURL({
chainIds,
sortBy,
minLiquidity,
minVolume24hUsd,
maxVolume24hUsd,
minMarketCap,
maxMarketCap,
});

try {
const result = await handleFetch(trendingTokensURL);

// Validate that the API returned an array
if (Array.isArray(result)) {
return result;
}

// Handle non-expected responses
console.error('Trending tokens API returned non-array response:', result);
return [];
} catch (error) {
console.error('Trending tokens request failed:', error);
return [];
}
}

/**
* Fetch metadata for the token address provided for a given network. This request is cancellable
* using the abort signal passed in.
Expand All @@ -145,8 +273,6 @@ export async function searchTokens(
* @param options.timeout - The fetch timeout.
* @returns The token metadata, or `undefined` if the request was either aborted or failed.
*/
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
export async function fetchTokenMetadata<T>(
chainId: Hex,
tokenAddress: string,
Expand Down Expand Up @@ -208,8 +334,6 @@ async function parseJsonResponse(apiResponse: Response): Promise<unknown> {
const responseObj = await apiResponse.json();
// api may return errors as json without setting an error http status code
if (responseObj?.error) {
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`TokenService Error: ${responseObj.error}`);
}
return responseObj;
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
},
"devDependencies": {
"@metamask/accounts-controller": "^34.0.0",
"@metamask/assets-controllers": "^87.0.0",
"@metamask/assets-controllers": "^87.1.0",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/eth-json-rpc-provider": "^5.0.1",
"@metamask/network-controller": "^25.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-pay-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"lodash": "^4.17.21"
},
"devDependencies": {
"@metamask/assets-controllers": "^87.0.0",
"@metamask/assets-controllers": "^87.1.0",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/bridge-controller": "^59.0.0",
"@metamask/bridge-status-controller": "^59.0.0",
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2761,7 +2761,7 @@ __metadata:
languageName: unknown
linkType: soft

"@metamask/assets-controllers@npm:^87.0.0, @metamask/assets-controllers@workspace:packages/assets-controllers":
"@metamask/assets-controllers@npm:^87.1.0, @metamask/assets-controllers@workspace:packages/assets-controllers":
version: 0.0.0-use.local
resolution: "@metamask/assets-controllers@workspace:packages/assets-controllers"
dependencies:
Expand Down Expand Up @@ -2953,7 +2953,7 @@ __metadata:
"@ethersproject/contracts": "npm:^5.7.0"
"@ethersproject/providers": "npm:^5.7.0"
"@metamask/accounts-controller": "npm:^34.0.0"
"@metamask/assets-controllers": "npm:^87.0.0"
"@metamask/assets-controllers": "npm:^87.1.0"
"@metamask/auto-changelog": "npm:^3.4.4"
"@metamask/base-controller": "npm:^9.0.0"
"@metamask/controller-utils": "npm:^11.15.0"
Expand Down Expand Up @@ -5219,7 +5219,7 @@ __metadata:
dependencies:
"@ethersproject/abi": "npm:^5.7.0"
"@ethersproject/contracts": "npm:^5.7.0"
"@metamask/assets-controllers": "npm:^87.0.0"
"@metamask/assets-controllers": "npm:^87.1.0"
"@metamask/auto-changelog": "npm:^3.4.4"
"@metamask/base-controller": "npm:^9.0.0"
"@metamask/bridge-controller": "npm:^59.0.0"
Expand Down