Skip to content

Commit 057530c

Browse files
committed
add token display data
1 parent adea818 commit 057530c

File tree

7 files changed

+415
-5
lines changed

7 files changed

+415
-5
lines changed

packages/assets-controllers/src/TokensController.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ const controllerName = 'TokensController';
123123

124124
export type TokensControllerActions =
125125
| TokensControllerGetStateAction
126-
| TokensControllerAddDetectedTokensAction;
126+
| TokensControllerAddDetectedTokensAction
127+
| TokensControllerFetchTokenMetadataAction;
127128

128129
export type TokensControllerGetStateAction = ControllerGetStateAction<
129130
typeof controllerName,
@@ -135,6 +136,11 @@ export type TokensControllerAddDetectedTokensAction = {
135136
handler: TokensController['addDetectedTokens'];
136137
};
137138

139+
export type TokensControllerFetchTokenMetadataAction = {
140+
type: `${typeof controllerName}:fetchTokenMetadata`;
141+
handler: TokensController['fetchTokenMetadata'];
142+
};
143+
138144
/**
139145
* The external actions available to the {@link TokensController}.
140146
*/
@@ -334,14 +340,16 @@ export class TokensController extends BaseController<
334340
* Fetch metadata for a token.
335341
*
336342
* @param tokenAddress - The address of the token.
343+
* @param chainId - The chain ID of the token, defaults to the controller's chain ID.
337344
* @returns The token metadata.
338345
*/
339-
async #fetchTokenMetadata(
346+
async fetchTokenMetadata(
340347
tokenAddress: string,
348+
chainId: Hex = this.#chainId,
341349
): Promise<TokenListToken | undefined> {
342350
try {
343351
const token = await fetchTokenMetadata<TokenListToken>(
344-
this.#chainId,
352+
chainId,
345353
tokenAddress,
346354
this.#abortController.signal,
347355
);
@@ -413,7 +421,7 @@ export class TokensController extends BaseController<
413421
const [isERC721, tokenMetadata] = await Promise.all([
414422
this.#detectIsERC721(address, networkClientId),
415423
// TODO parameterize the token metadata fetch by networkClientId
416-
this.#fetchTokenMetadata(address),
424+
this.fetchTokenMetadata(address),
417425
]);
418426
// TODO remove this once this method is fully parameterized by networkClientId
419427
if (!networkClientId && currentChainId !== this.#chainId) {

packages/assets-controllers/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export type {
120120
TokensControllerActions,
121121
TokensControllerGetStateAction,
122122
TokensControllerAddDetectedTokensAction,
123+
TokensControllerFetchTokenMetadataAction,
123124
TokensControllerEvents,
124125
TokensControllerStateChangeEvent,
125126
TokensControllerMessenger,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"name": "@metamask/token-search-discovery-controller",
3+
"version": "2.1.0",
4+
"description": "Manages token search and discovery through the Portfolio API",
5+
"keywords": [
6+
"MetaMask",
7+
"Ethereum"
8+
],
9+
"homepage": "https://github.com/MetaMask/core/tree/main/packages/token-search-discovery-controller#readme",
10+
"bugs": {
11+
"url": "https://github.com/MetaMask/core/issues"
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "https://github.com/MetaMask/core.git"
16+
},
17+
"license": "MIT",
18+
"sideEffects": false,
19+
"exports": {
20+
".": {
21+
"import": {
22+
"types": "./dist/index.d.mts",
23+
"default": "./dist/index.mjs"
24+
},
25+
"require": {
26+
"types": "./dist/index.d.cts",
27+
"default": "./dist/index.cjs"
28+
}
29+
},
30+
"./package.json": "./package.json"
31+
},
32+
"main": "./dist/index.cjs",
33+
"types": "./dist/index.d.cts",
34+
"files": [
35+
"dist/"
36+
],
37+
"scripts": {
38+
"build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references",
39+
"build:docs": "typedoc",
40+
"changelog:update": "../../scripts/update-changelog.sh @metamask/token-search-discovery-controller",
41+
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/token-search-discovery-controller",
42+
"publish:preview": "yarn npm publish --tag preview",
43+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter",
44+
"test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache",
45+
"test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
46+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
47+
"since-latest-release": "../../scripts/since-latest-release.sh"
48+
},
49+
"dependencies": {
50+
"@metamask/base-controller": "^8.0.0",
51+
"@metamask/utils": "^11.1.0"
52+
},
53+
"devDependencies": {
54+
"@metamask/assets-controllers": "workspace:^",
55+
"@metamask/auto-changelog": "^3.4.4",
56+
"@types/jest": "^27.4.1",
57+
"deepmerge": "^4.2.2",
58+
"jest": "^27.5.1",
59+
"nock": "^13.3.1",
60+
"ts-jest": "^27.1.4",
61+
"typedoc": "^0.24.8",
62+
"typedoc-plugin-missing-exports": "^2.0.0",
63+
"typescript": "~5.2.2"
64+
},
65+
"engines": {
66+
"node": "^18.18 || >=20"
67+
},
68+
"publishConfig": {
69+
"access": "public",
70+
"registry": "https://registry.npmjs.org/"
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import type { TokensControllerFetchTokenMetadataAction } from '@metamask/assets-controllers';
2+
import type {
3+
ControllerGetStateAction,
4+
ControllerStateChangeEvent,
5+
RestrictedMessenger,
6+
} from '@metamask/base-controller';
7+
import { BaseController } from '@metamask/base-controller';
8+
import type { Hex } from '@metamask/utils';
9+
10+
import type { AbstractTokenDiscoveryApiService } from './token-discovery-api-service/abstract-token-discovery-api-service';
11+
import type { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service';
12+
import type {
13+
TokenDisplayData,
14+
TokenSearchParams,
15+
TokenSearchResponseItem,
16+
TokenTrendingResponseItem,
17+
TrendingTokensParams,
18+
} from './types';
19+
20+
// === GENERAL ===
21+
22+
const controllerName = 'TokenSearchDiscoveryController';
23+
24+
// === STATE ===
25+
26+
export type TokenSearchDiscoveryControllerState = {
27+
recentSearches: TokenSearchResponseItem[];
28+
lastSearchTimestamp: number | null;
29+
tokenDisplayData: TokenDisplayData[];
30+
};
31+
32+
const tokenSearchDiscoveryControllerMetadata = {
33+
recentSearches: { persist: true, anonymous: false },
34+
lastSearchTimestamp: { persist: true, anonymous: false },
35+
tokenDisplayData: { persist: true, anonymous: false },
36+
} as const;
37+
38+
const MAX_TOKEN_DISPLAY_DATA = 10;
39+
40+
// === MESSENGER ===
41+
42+
/**
43+
* The action which can be used to retrieve the state of the
44+
* {@link TokenSearchDiscoveryController}.
45+
*/
46+
export type TokenSearchDiscoveryControllerGetStateAction =
47+
ControllerGetStateAction<
48+
typeof controllerName,
49+
TokenSearchDiscoveryControllerState
50+
>;
51+
52+
/**
53+
* All actions that {@link TokenSearchDiscoveryController} registers, to be
54+
* called externally.
55+
*/
56+
export type TokenSearchDiscoveryControllerActions =
57+
TokenSearchDiscoveryControllerGetStateAction;
58+
59+
/**
60+
* All actions that {@link TokenSearchDiscoveryController} calls internally.
61+
*/
62+
type AllowedActions = TokensControllerFetchTokenMetadataAction;
63+
64+
/**
65+
* The event that {@link TokenSearchDiscoveryController} publishes when updating
66+
* state.
67+
*/
68+
export type TokenSearchDiscoveryControllerStateChangeEvent =
69+
ControllerStateChangeEvent<
70+
typeof controllerName,
71+
TokenSearchDiscoveryControllerState
72+
>;
73+
74+
/**
75+
* All events that {@link TokenSearchDiscoveryController} publishes, to be
76+
* subscribed to externally.
77+
*/
78+
export type TokenSearchDiscoveryControllerEvents =
79+
TokenSearchDiscoveryControllerStateChangeEvent;
80+
81+
/**
82+
* All events that {@link TokenSearchDiscoveryController} subscribes to internally.
83+
*/
84+
type AllowedEvents = never;
85+
86+
/**
87+
* The messenger which is restricted to actions and events accessed by
88+
* {@link TokenSearchDiscoveryController}.
89+
*/
90+
export type TokenSearchDiscoveryControllerMessenger = RestrictedMessenger<
91+
typeof controllerName,
92+
TokenSearchDiscoveryControllerActions | AllowedActions,
93+
TokenSearchDiscoveryControllerEvents | AllowedEvents,
94+
AllowedActions['type'],
95+
AllowedEvents['type']
96+
>;
97+
98+
/**
99+
* Constructs the default {@link TokenSearchDiscoveryController} state. This allows
100+
* consumers to provide a partial state object when initializing the controller
101+
* and also helps in constructing complete state objects for this controller in
102+
* tests.
103+
*
104+
* @returns The default {@link TokenSearchDiscoveryController} state.
105+
*/
106+
export function getDefaultTokenSearchDiscoveryControllerState(): TokenSearchDiscoveryControllerState {
107+
return {
108+
recentSearches: [],
109+
lastSearchTimestamp: null,
110+
tokenDisplayData: [],
111+
};
112+
}
113+
114+
/**
115+
* The TokenSearchDiscoveryController manages the retrieval of token search results and token discovery.
116+
* It fetches token search results and discovery data from the Portfolio API.
117+
*/
118+
export class TokenSearchDiscoveryController extends BaseController<
119+
typeof controllerName,
120+
TokenSearchDiscoveryControllerState,
121+
TokenSearchDiscoveryControllerMessenger
122+
> {
123+
readonly #tokenSearchService: AbstractTokenSearchApiService;
124+
125+
readonly #tokenDiscoveryService: AbstractTokenDiscoveryApiService;
126+
127+
constructor({
128+
tokenSearchService,
129+
tokenDiscoveryService,
130+
state = {},
131+
messenger,
132+
}: {
133+
tokenSearchService: AbstractTokenSearchApiService;
134+
tokenDiscoveryService: AbstractTokenDiscoveryApiService;
135+
state?: Partial<TokenSearchDiscoveryControllerState>;
136+
messenger: TokenSearchDiscoveryControllerMessenger;
137+
}) {
138+
super({
139+
name: controllerName,
140+
metadata: tokenSearchDiscoveryControllerMetadata,
141+
messenger,
142+
state: { ...getDefaultTokenSearchDiscoveryControllerState(), ...state },
143+
});
144+
145+
this.#tokenSearchService = tokenSearchService;
146+
this.#tokenDiscoveryService = tokenDiscoveryService;
147+
}
148+
149+
async searchTokens(
150+
tokenSearchParams: TokenSearchParams,
151+
): Promise<TokenSearchResponseItem[]> {
152+
const results =
153+
await this.#tokenSearchService.searchTokens(tokenSearchParams);
154+
155+
this.update((state) => {
156+
state.recentSearches = results;
157+
state.lastSearchTimestamp = Date.now();
158+
});
159+
160+
return results;
161+
}
162+
163+
async getTrendingTokens(
164+
params: TrendingTokensParams,
165+
): Promise<TokenTrendingResponseItem[]> {
166+
return this.#tokenDiscoveryService.getTrendingTokensByChains(params);
167+
}
168+
169+
async fetchTokenForDisplay(tokenAddress: string, chainId: Hex) {
170+
const tokenMetadata = await this.messagingSystem.call(
171+
'TokensController:fetchTokenMetadata',
172+
tokenAddress,
173+
chainId,
174+
);
175+
176+
let data: TokenDisplayData;
177+
178+
if (!tokenMetadata) {
179+
data = {
180+
chainId,
181+
tokenAddress,
182+
found: false,
183+
};
184+
} else {
185+
data = {
186+
chainId,
187+
tokenAddress,
188+
found: true,
189+
token: tokenMetadata,
190+
};
191+
}
192+
193+
this.update((state) => {
194+
state.tokenDisplayData = [...state.tokenDisplayData, data].slice(
195+
0,
196+
MAX_TOKEN_DISPLAY_DATA,
197+
);
198+
});
199+
200+
return data;
201+
}
202+
}

0 commit comments

Comments
 (0)