Skip to content
Draft
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
137 changes: 104 additions & 33 deletions packages/assets-controllers/src/TokenListController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import type {
} from '@metamask/messenger';
import type { NetworkState } from '@metamask/network-controller';
import type { Hex } from '@metamask/utils';
import { clone } from 'lodash';
import nock from 'nock';
import * as sinon from 'sinon';

import { MOCK_ETHEREUM_TOKENS_METADATA } from './__fixtures__/tokens-api-mocks';
import * as tokenService from './token-service';
import type {
TokenListMap,
Expand Down Expand Up @@ -234,7 +236,6 @@ const sampleSepoliaTokenList = [
name: 'Wrapped BTC',
iconUrl:
'https://static.cx.metamask.io/api/v1/tokenIcons/11155111/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599.png',
type: 'erc20',
aggregators: [
'Metamask',
'Aave',
Expand All @@ -253,10 +254,6 @@ const sampleSepoliaTokenList = [
'Coinmarketcap',
],
occurrences: 15,
fees: {},
storage: {
balance: 0,
},
},
{
address: '0x04fa0d235c4abf4bcf4787af4cf447de572ef828',
Expand All @@ -265,7 +262,6 @@ const sampleSepoliaTokenList = [
name: 'UMA',
iconUrl:
'https://static.cx.metamask.io/api/v1/tokenIcons/11155111/0x04fa0d235c4abf4bcf4787af4cf447de572ef828.png',
type: 'erc20',
aggregators: [
'Metamask',
'Bancor',
Expand All @@ -282,7 +278,6 @@ const sampleSepoliaTokenList = [
'Coinmarketcap',
],
occurrences: 13,
fees: {},
},
{
address: '0x6810e776880c02933d47db1b9fc05908e5386b96',
Expand All @@ -291,7 +286,6 @@ const sampleSepoliaTokenList = [
name: 'Gnosis Token',
iconUrl:
'https://static.cx.metamask.io/api/v1/tokenIcons/11155111/0x6810e776880c02933d47db1b9fc05908e5386b96.png',
type: 'erc20',
aggregators: [
'Metamask',
'Bancor',
Expand All @@ -307,10 +301,44 @@ const sampleSepoliaTokenList = [
'Coinmarketcap',
],
occurrences: 12,
fees: {},
},
];

const createNockEndpoint = (
chainId: number,
opts?: {
nockIntercept?: (
intercept: nock.Interceptor,
) => nock.Interceptor | nock.Scope;
queryParams?: Record<string, string>;
response?: unknown;
},
): nock.Scope => {
const nockPartial = nock(tokenService.TOKENS_END_POINT_API)
.get(`/tokens/${chainId}`)
.query({
occurrenceFloor: '3',
includeTokenFees: 'false',
includeAssetType: 'false',
includeERC20Permit: 'false',
includeStorage: 'false',
includeAggregators: 'true',
includeOccurrences: 'true',
includeIconUrl: 'true',
includeRwaData: 'true',
first: '3000',
...opts?.queryParams,
});

const finalNock = opts?.nockIntercept?.(nockPartial) ?? nockPartial;

return 'isDone' in finalNock
? finalNock
: finalNock
.reply(200, opts?.response ?? MOCK_ETHEREUM_TOKENS_METADATA)
.persist();
};

const sampleSepoliaTokensChainCache =
sampleSepoliaTokenList.reduce<TokenListMap>((output, current) => {
output[current.address] = current;
Expand Down Expand Up @@ -606,10 +634,14 @@ describe('TokenListController', () => {
});

it('should update tokensChainsCache state when network updates are passed via onNetworkStateChange callback', async () => {
nock(tokenService.TOKEN_END_POINT_API)
.get(getTokensPath(ChainId.mainnet))
.reply(200, sampleMainnetTokenList)
.persist();
const mainnetEndpointResponse = clone(MOCK_ETHEREUM_TOKENS_METADATA);
mainnetEndpointResponse.data = sampleMainnetTokenList;
const mainnetEndpoint = createNockEndpoint(
convertHexToDecimal(ChainId.mainnet),
{
response: mainnetEndpointResponse,
},
);

jest.spyOn(Date, 'now').mockImplementation(() => 100);
const selectedNetworkClientId = 'selectedNetworkClientId';
Expand Down Expand Up @@ -718,6 +750,7 @@ describe('TokenListController', () => {
},
'0x539': { timestamp: 100, data: {} },
});
expect(mainnetEndpoint.isDone()).toBe(true);
controller.destroy();
});

Expand Down Expand Up @@ -851,10 +884,14 @@ describe('TokenListController', () => {
});

it('should update tokensChainsCache from api', async () => {
nock(tokenService.TOKEN_END_POINT_API)
.get(getTokensPath(ChainId.mainnet))
.reply(200, sampleMainnetTokenList)
.persist();
const mainnetEndpointResponse = clone(MOCK_ETHEREUM_TOKENS_METADATA);
mainnetEndpointResponse.data = sampleMainnetTokenList;
const mainnetEndpoint = createNockEndpoint(
convertHexToDecimal(ChainId.mainnet),
{
response: mainnetEndpointResponse,
},
);

const messenger = getMessenger();
const restrictedMessenger = getRestrictedMessenger(messenger);
Expand All @@ -879,6 +916,7 @@ describe('TokenListController', () => {
).toBeGreaterThanOrEqual(
sampleSingleChainState.tokensChainsCache[ChainId.mainnet].timestamp,
);
expect(mainnetEndpoint.isDone()).toBe(true);
controller.destroy();
} finally {
controller.destroy();
Expand Down Expand Up @@ -939,10 +977,14 @@ describe('TokenListController', () => {
});

it('should update the cache when the timestamp expires', async () => {
nock(tokenService.TOKEN_END_POINT_API)
.get(getTokensPath(ChainId.mainnet))
.reply(200, sampleMainnetTokenList)
.persist();
const mainnetEndpointResponse = clone(MOCK_ETHEREUM_TOKENS_METADATA);
mainnetEndpointResponse.data = sampleMainnetTokenList;
const mainnetEndpoint = createNockEndpoint(
convertHexToDecimal(ChainId.mainnet),
{
response: mainnetEndpointResponse,
},
);

const messenger = getMessenger();
const restrictedMessenger = getRestrictedMessenger(messenger);
Expand All @@ -965,22 +1007,46 @@ describe('TokenListController', () => {
).toStrictEqual(
sampleSingleChainState.tokensChainsCache[ChainId.mainnet].data,
);
expect(mainnetEndpoint.isDone()).toBe(true);
controller.destroy();
});

it('should update tokensChainsCache when the chainId change', async () => {
nock(tokenService.TOKEN_END_POINT_API)
.get(getTokensPath(ChainId.mainnet))
.reply(200, sampleMainnetTokenList)
.get(getTokensPath(ChainId.sepolia))
.reply(200, {
error: `ChainId ${convertHexToDecimal(
ChainId.sepolia,
)} is not supported`,
})
.get(getTokensPath(toHex(56)))
.reply(200, sampleBinanceTokenList)
.persist();
const sepoliaEndpoint = createNockEndpoint(
convertHexToDecimal(ChainId.sepolia),
{
nockIntercept: (intercept) =>
intercept.reply(200, {
error: `ChainId ${convertHexToDecimal(
ChainId.sepolia,
)} is not supported`,
}),
},
);
const binanceEndpointResponse = clone(MOCK_ETHEREUM_TOKENS_METADATA);
binanceEndpointResponse.data = sampleBinanceTokenList;
const binanceEndpoint = createNockEndpoint(56, {
response: binanceEndpointResponse,
});

type Endpoint = 'sepolia' | 'binance';
const assertEndpointCalls = (opts: {
done: Endpoint[];
notDone: Endpoint[];
}): void => {
const endpointMap = {
sepolia: sepoliaEndpoint,
binance: binanceEndpoint,
};

opts.done.forEach((endpoint) => {
expect(endpointMap[endpoint].isDone()).toBe(true);
});
opts.notDone.forEach((endpoint) => {
expect(endpointMap[endpoint].isDone()).toBe(false);
});
};

const selectedCustomNetworkClientId = 'selectedCustomNetworkClientId';
const messenger = getMessenger();
const getNetworkClientById = buildMockGetNetworkClientById({
Expand Down Expand Up @@ -1012,6 +1078,7 @@ describe('TokenListController', () => {
sampleTwoChainState.tokensChainsCache[ChainId.mainnet].data,
);

// Change to sepolia
messenger.publish(
'NetworkController:stateChange',
{
Expand All @@ -1032,6 +1099,8 @@ describe('TokenListController', () => {
sampleTwoChainState.tokensChainsCache[ChainId.mainnet].data,
);

assertEndpointCalls({ done: ['sepolia'], notDone: ['binance'] });

messenger.publish(
'NetworkController:stateChange',
{
Expand All @@ -1052,6 +1121,8 @@ describe('TokenListController', () => {
sampleTwoChainState.tokensChainsCache[ChainId.mainnet].data,
);

assertEndpointCalls({ done: ['sepolia', 'binance'], notDone: [] });

expect(controller.state.tokensChainsCache[toHex(56)].data).toStrictEqual(
sampleTwoChainState.tokensChainsCache[toHex(56)].data,
);
Expand Down
37 changes: 22 additions & 15 deletions packages/assets-controllers/src/TokenListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
formatAggregatorNames,
formatIconUrlWithProxy,
} from './assetsUtil';
import { fetchTokenListByChainId } from './token-service';
import { TokenRwaData, fetchTokenListByChainId } from './token-service';

const DEFAULT_INTERVAL = 24 * 60 * 60 * 1000;
const DEFAULT_THRESHOLD = 24 * 60 * 60 * 1000;
Expand All @@ -34,6 +34,7 @@ export type TokenListToken = {
occurrences: number;
aggregators: string[];
iconUrl: string;
rwaData?: TokenRwaData;
};

export type TokenListMap = Record<string, TokenListToken>;
Expand Down Expand Up @@ -305,27 +306,33 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
}

// Fetch fresh token list from the API
const tokensFromAPI = await safelyExecute(
() =>
fetchTokenListByChainId(
chainId,
this.abortController.signal,
) as Promise<TokenListToken[]>,
const tokensFromAPI = await safelyExecute(() =>
fetchTokenListByChainId(chainId, this.abortController.signal),
);

// Have response - process and update list
if (tokensFromAPI) {
if (tokensFromAPI && tokensFromAPI.length > 0) {
// Format tokens from API (HTTP) and update tokenList
const tokenList: TokenListMap = {};
for (const token of tokensFromAPI) {
tokenList[token.address] = {
...token,
aggregators: formatAggregatorNames(token.aggregators),
iconUrl: formatIconUrlWithProxy({
chainId,
tokenAddress: token.address,
}),
address: token.address,
symbol: token.symbol,
decimals: token.decimals,
name: token.name,
occurrences: token.occurrences,
aggregators: formatAggregatorNames(token.aggregators ?? []),
iconUrl:
token.iconUrl ??
formatIconUrlWithProxy({
chainId,
tokenAddress: token.address,
}),
};

if (token.rwaData) {
tokenList[token.address].rwaData = token.rwaData;
}
}

this.update((state) => {
Expand All @@ -338,7 +345,7 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
}

// No response - fallback to previous state, or initialise empty
if (!tokensFromAPI) {
if (!tokensFromAPI || tokensFromAPI.length === 0) {
this.update((state) => {
const newDataCache: DataCache = { data: {}, timestamp: Date.now() };
state.tokensChainsCache[chainId] ??= newDataCache;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,23 @@ export class TokenSearchDiscoveryDataController extends BaseController<

let tokenMetadata: TokenListToken | undefined;
try {
tokenMetadata = await fetchTokenMetadata<TokenListToken>(
const tokenMetadataResult = await fetchTokenMetadata(
chainId,
address,
this.#abortController.signal,
);
if (tokenMetadataResult) {
tokenMetadata = {
name: tokenMetadataResult.name,
symbol: tokenMetadataResult.symbol,
decimals: tokenMetadataResult.decimals,
address: tokenMetadataResult.address,
aggregators: tokenMetadataResult.aggregators,
iconUrl: tokenMetadataResult.iconUrl,
rwaData: tokenMetadataResult.rwaData,
occurrences: tokenMetadataResult.occurrences,
};
}
} catch (error) {
if (
!(error instanceof Error) ||
Expand Down
Loading
Loading