diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 91bde59f4f6..72db8441213 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -359,7 +359,7 @@ }, "packages/assets-controllers/src/TokenListController.ts": { "@typescript-eslint/explicit-function-return-type": { - "count": 6 + "count": 1 }, "no-restricted-syntax": { "count": 7 diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index feaba24704e..e36f0303c5a 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add RWA data to asset fetching endpoints ([#7548](https://github.com/MetaMask/core/pull/7548)) - Add Rootstock (0x1e) mapping to eip155:30/erc20:0x542fda317318ebf1d3deaf76e0b632741a7e677d for RBTC ([#7601](https://github.com/MetaMask/core/pull/7601)) ### Changed diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index f16b34f7007..464ed175d23 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -752,7 +752,7 @@ export class TokenDetectionController extends StaticIntervalPollingController { const selectedNetworkClient = this.messenger.call( 'NetworkController:getNetworkClientById', networkControllerState.selectedNetworkClientId, @@ -216,7 +218,7 @@ export class TokenListController extends StaticIntervalPollingController { if (!isTokenListSupportedForNetwork(this.chainId)) { return; } @@ -229,7 +231,7 @@ export class TokenListController extends StaticIntervalPollingController { this.stopPolling(); await this.#startDeprecatedPolling(); } @@ -240,7 +242,7 @@ export class TokenListController extends StaticIntervalPollingController { .get( `/token/${convertHexToDecimal( chainId, - )}?address=${dummyTokenAddress}`, + )}?address=${dummyTokenAddress}&includeRwaData=true`, ) .reply(200, { error }) .persist(); diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index 58606100bf3..672bbb34291 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -457,6 +457,7 @@ export class TokensController extends BaseController< isERC721, aggregators: formatAggregatorNames(tokenMetadata?.aggregators || []), name, + ...(tokenMetadata?.rwaData && { rwaData: tokenMetadata.rwaData }), }; const previousIndex = newTokens.findIndex( (token) => token.address.toLowerCase() === address.toLowerCase(), @@ -525,7 +526,7 @@ export class TokensController extends BaseController< }, {}); try { tokensToImport.forEach((tokenToAdd) => { - const { address, symbol, decimals, image, aggregators, name } = + const { address, symbol, decimals, image, aggregators, name, rwaData } = tokenToAdd; const checksumAddress = toChecksumHexAddress(address); const formattedToken: Token = { @@ -535,6 +536,7 @@ export class TokensController extends BaseController< image, aggregators, name, + ...(rwaData && { rwaData }), }; newTokensMap[checksumAddress] = formattedToken; importedTokensMap[address.toLowerCase()] = true; @@ -664,6 +666,7 @@ export class TokensController extends BaseController< aggregators, isERC721, name, + rwaData, } = tokenToAdd; const checksumAddress = toChecksumHexAddress(address); const newEntry: Token = { @@ -674,6 +677,7 @@ export class TokensController extends BaseController< isERC721, aggregators, name, + ...(rwaData && { rwaData }), }; const previousImportedIndex = newTokens.findIndex( diff --git a/packages/assets-controllers/src/selectors/token-selectors.ts b/packages/assets-controllers/src/selectors/token-selectors.ts index a1772dfa6cd..65b0c9c2b98 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.ts @@ -8,6 +8,7 @@ import type { NetworkState } from '@metamask/network-controller'; import { hexToBigInt, parseCaipAssetType } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; import { createSelector, weakMapMemoize } from 'reselect'; +import { TokenRwaData } from 'src/token-service'; import { parseBalanceWithDecimals, @@ -84,6 +85,7 @@ export type Asset = ( conversionRate: number; } | undefined; + rwaData?: TokenRwaData; }; export type AssetListState = { @@ -325,6 +327,7 @@ const selectAllEvmAssets = createAssetListSelector( } : undefined, chainId, + ...(token.rwaData && { rwaData: token.rwaData }), }); } } diff --git a/packages/assets-controllers/src/token-service.test.ts b/packages/assets-controllers/src/token-service.test.ts index 00f48428193..964031d9279 100644 --- a/packages/assets-controllers/src/token-service.test.ts +++ b/packages/assets-controllers/src/token-service.test.ts @@ -201,7 +201,7 @@ const sampleTokenListLinea = [ decimals: 18, occurrences: 11, aggregators: [ - 'paraswap', + 'lineaTeam', 'pmm', 'airswapLight', 'zeroEx', @@ -320,6 +320,52 @@ describe('Token service', () => { expect(tokens).toStrictEqual(sampleTokenListLinea); }); + it('should correctly filter linea tokens: include if has lineaTeam OR >= 3 aggregators', async () => { + const { signal } = new AbortController(); + const lineaChainId = 59144; + const lineaHexChain = toHex(lineaChainId); + + const mixedTokens = [ + { + // Should be included (has lineaTeam) + address: '0x1', + symbol: 'T1', + decimals: 18, + aggregators: ['lineaTeam', 'other'], + }, + { + // Should be included (no lineaTeam, but 3 aggregators) + address: '0x2', + symbol: 'T2', + decimals: 18, + aggregators: ['a1', 'a2', 'a3'], + }, + { + // Should be excluded (no lineaTeam, only 2 aggregators) + address: '0x3', + symbol: 'T3', + decimals: 18, + aggregators: ['a1', 'a2'], + }, + ]; + + nock(TOKEN_END_POINT_API) + .get( + `/tokens/${lineaChainId}?occurrenceFloor=1&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false&includeRwaData=true`, + ) + .reply(200, mixedTokens) + .persist(); + + const tokens = (await fetchTokenListByChainId(lineaHexChain, signal)) as { + address: string; + }[]; + + expect(tokens).toHaveLength(2); + expect(tokens.find((token) => token.address === '0x1')).toBeDefined(); + expect(tokens.find((token) => token.address === '0x2')).toBeDefined(); + expect(tokens.find((token) => token.address === '0x3')).toBeUndefined(); + }); + it('should return undefined if the fetch is aborted', async () => { const abortController = new AbortController(); nock(TOKEN_END_POINT_API) @@ -392,7 +438,7 @@ describe('Token service', () => { const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get( - `/token/${sampleDecimalChainId}?address=0x514910771af9ca656af840dff83e8264ecf986ca`, + `/token/${sampleDecimalChainId}?address=0x514910771af9ca656af840dff83e8264ecf986ca&includeRwaData=true`, ) .reply(200, sampleToken) .persist(); @@ -499,7 +545,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -523,7 +569,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=${customLimit}&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=${customLimit}&includeMarketData=false&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -549,7 +595,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${encodedQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${encodedQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -575,7 +621,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodedChainIds}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodedChainIds}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -595,7 +641,7 @@ describe('Token service', () => { const searchQuery = 'USD'; nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .replyWithError('Example network error') .persist(); @@ -609,7 +655,7 @@ describe('Token service', () => { const searchQuery = 'USD'; nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(400, { error: 'Bad Request' }) .persist(); @@ -623,7 +669,7 @@ describe('Token service', () => { const searchQuery = 'USD'; nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(500) .persist(); @@ -643,7 +689,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -663,7 +709,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -678,7 +724,7 @@ describe('Token service', () => { const errorResponse = { error: 'Invalid search query' }; nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(200, errorResponse) .persist(); @@ -711,7 +757,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodedChainIds}&query=${searchQuery}&limit=10&includeMarketData=false`, + `/tokens/search?networks=${encodedChainIds}&query=${searchQuery}&limit=10&includeMarketData=false&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -734,7 +780,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=true`, + `/tokens/search?networks=${encodeURIComponent(sampleCaipChainId)}&query=${searchQuery}&limit=10&includeMarketData=true&includeRwaData=true`, ) .reply(200, mockResponse) .persist(); @@ -810,7 +856,7 @@ describe('Token service', () => { const testMaxMarketCap = 1000000; nock(TOKEN_END_POINT_API) .get( - `/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&sort=${sortBy}&minLiquidity=${testMinLiquidity}&minVolume24hUsd=${testMinVolume24hUsd}&maxVolume24hUsd=${testMaxVolume24hUsd}&minMarketCap=${testMinMarketCap}&maxMarketCap=${testMaxMarketCap}`, + `/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&sort=${sortBy}&minLiquidity=${testMinLiquidity}&minVolume24hUsd=${testMinVolume24hUsd}&maxVolume24hUsd=${testMaxVolume24hUsd}&minMarketCap=${testMinMarketCap}&maxMarketCap=${testMaxMarketCap}&includeRwaData=true`, ) .reply(200, sampleTrendingTokens) .persist(); @@ -831,7 +877,9 @@ describe('Token service', () => { const testChainId = 'eip155:1'; nock(TOKEN_END_POINT_API) - .get(`/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}`) + .get( + `/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&includeRwaData=true`, + ) .reply(200, sampleTrendingTokens) .persist(); @@ -847,7 +895,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&excludeLabels=${testExcludeLabels.join(',')}`, + `/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&excludeLabels=${testExcludeLabels.join(',')}&includeRwaData=true`, ) .reply(200, sampleTrendingTokens) .persist(); @@ -858,5 +906,22 @@ describe('Token service', () => { }); expect(result).toStrictEqual(sampleTrendingTokens); }); + + it('returns the list of trending tokens with includeRwaData', async () => { + const testChainId = 'eip155:1'; + + nock(TOKEN_END_POINT_API) + .get( + `/v3/tokens/trending?chainIds=${encodeURIComponent(testChainId)}&includeRwaData=true`, + ) + .reply(200, sampleTrendingTokens) + .persist(); + + const result = await getTrendingTokens({ + chainIds: [testChainId], + includeRwaData: true, + }); + expect(result).toStrictEqual(sampleTrendingTokens); + }); }); }); diff --git a/packages/assets-controllers/src/token-service.ts b/packages/assets-controllers/src/token-service.ts index e5aa5722fe9..bcf22e06fc2 100644 --- a/packages/assets-controllers/src/token-service.ts +++ b/packages/assets-controllers/src/token-service.ts @@ -36,7 +36,7 @@ function getTokensURL(chainId: Hex): string { function getTokenMetadataURL(chainId: Hex, tokenAddress: string): string { return `${TOKEN_END_POINT_API}/token/${convertHexToDecimal( chainId, - )}?address=${tokenAddress}`; + )}?address=${tokenAddress}&includeRwaData=true`; } /** @@ -210,7 +210,7 @@ export async function searchTokens( { limit = 10, includeMarketData = false, - includeRwaData, + includeRwaData = true, }: SearchTokenOptions = {}, ): Promise<{ count: number; data: TokenSearchItem[] }> { const tokenSearchURL = getTokenSearchURL({ @@ -228,7 +228,7 @@ export async function searchTokens( // The API returns an object with structure: { count: number, data: array, pageInfo: object } if (result && typeof result === 'object' && Array.isArray(result.data)) { return { - count: result.count || result.data.length, + count: result.count ?? result.data.length, data: result.data, }; } @@ -291,7 +291,7 @@ export async function getTrendingTokens({ minMarketCap, maxMarketCap, excludeLabels, - includeRwaData, + includeRwaData = true, }: { chainIds: CaipChainId[]; sortBy?: SortTrendingBy;