Skip to content

Commit 8b576cf

Browse files
committed
create new token search discovery data controller
- add token fetching functionality - add swap data functionality - add market data fetching
1 parent 1e5b340 commit 8b576cf

File tree

4 files changed

+279
-0
lines changed

4 files changed

+279
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import {
2+
BaseController,
3+
type ControllerGetStateAction,
4+
type ControllerStateChangeEvent,
5+
type RestrictedMessenger,
6+
} from '@metamask/base-controller';
7+
8+
import type {
9+
TokenDisplayData
10+
} from './types';
11+
import { fetchTokenMetadata, TOKEN_METADATA_NO_SUPPORT_ERROR } from '../token-service';
12+
import { Hex } from '@metamask/utils';
13+
import { TokenListToken } from '../TokenListController';
14+
import { formatIconUrlWithProxy } from '../assetsUtil';
15+
import { AbstractTokenPricesService } from '../token-prices-service';
16+
import type {GetCurrencyRateState} from '../CurrencyRateController';
17+
import { TokenPrice } from '../token-prices-service/abstract-token-prices-service';
18+
19+
// === GENERAL ===
20+
21+
const controllerName = 'TokenSearchDiscoveryDataController';
22+
23+
const MAX_TOKEN_DISPLAY_DATA_LENGTH = 10;
24+
25+
// === STATE ===
26+
27+
export type TokenSearchDiscoveryDataControllerState = {
28+
tokenDisplayData: TokenDisplayData[];
29+
swapsTokenAddressesByChainId: Record<Hex, { lastFetched: number; addresses: string[] }>;
30+
};
31+
32+
const tokenSearchDiscoveryDataControllerMetadata = {
33+
tokenDisplayData: { persist: true, anonymous: false },
34+
swapsTokenAddressesByChainId: { persist: true, anonymous: false },
35+
} as const;
36+
37+
// === MESSENGER ===
38+
39+
/**
40+
* The action which can be used to retrieve the state of the
41+
* {@link TokenSearchDiscoveryDataController}.
42+
*/
43+
export type TokenSearchDiscoveryDataControllerGetStateAction =
44+
ControllerGetStateAction<
45+
typeof controllerName,
46+
TokenSearchDiscoveryDataControllerState
47+
>;
48+
49+
/**
50+
* All actions that {@link TokenSearchDiscoveryDataController} registers, to be
51+
* called externally.
52+
*/
53+
export type TokenSearchDiscoveryDataControllerActions =
54+
TokenSearchDiscoveryDataControllerGetStateAction;
55+
56+
/**
57+
* All actions that {@link TokenSearchDiscoveryDataController} calls internally.
58+
*/
59+
type AllowedActions = GetCurrencyRateState;
60+
61+
/**
62+
* The event that {@link TokenSearchDiscoveryDataController} publishes when updating
63+
* state.
64+
*/
65+
export type TokenSearchDiscoveryDataControllerStateChangeEvent =
66+
ControllerStateChangeEvent<
67+
typeof controllerName,
68+
TokenSearchDiscoveryDataControllerState
69+
>;
70+
71+
/**
72+
* All events that {@link TokenSearchDiscoveryDataController} publishes, to be
73+
* subscribed to externally.
74+
*/
75+
export type TokenSearchDiscoveryDataControllerEvents =
76+
TokenSearchDiscoveryDataControllerStateChangeEvent;
77+
78+
/**
79+
* All events that {@link TokenSearchDiscoveryDataController} subscribes to internally.
80+
*/
81+
type AllowedEvents = never;
82+
83+
/**
84+
* The messenger which is restricted to actions and events accessed by
85+
* {@link TokenSearchDiscoveryDataController}.
86+
*/
87+
export type TokenSearchDiscoveryDataControllerMessenger = RestrictedMessenger<
88+
typeof controllerName,
89+
TokenSearchDiscoveryDataControllerActions | AllowedActions,
90+
TokenSearchDiscoveryDataControllerEvents | AllowedEvents,
91+
AllowedActions['type'],
92+
AllowedEvents['type']
93+
>;
94+
95+
/**
96+
* Constructs the default {@link TokenSearchDiscoveryDataController} state. This allows
97+
* consumers to provide a partial state object when initializing the controller
98+
* and also helps in constructing complete state objects for this controller in
99+
* tests.
100+
*
101+
* @returns The default {@link TokenSearchDiscoveryDataController} state.
102+
*/
103+
export function getDefaultTokenSearchDiscoveryDataControllerState(): TokenSearchDiscoveryDataControllerState {
104+
return {
105+
tokenDisplayData: [],
106+
swapsTokenAddressesByChainId: {},
107+
};
108+
}
109+
110+
/**
111+
* The TokenSearchDiscoveryDataController manages the retrieval of token search results and token discovery.
112+
* It fetches token search results and discovery data from the Portfolio API.
113+
*/
114+
export class TokenSearchDiscoveryDataController extends BaseController<
115+
typeof controllerName,
116+
TokenSearchDiscoveryDataControllerState,
117+
TokenSearchDiscoveryDataControllerMessenger
118+
> {
119+
#abortController: AbortController;
120+
121+
#tokenPricesService: AbstractTokenPricesService;
122+
123+
#swapsSupportedChainIds: Hex[];
124+
125+
#fetchTokens: (chainId: Hex) => Promise<{ address: string; }[]>;
126+
127+
#fetchSwapsTokensThresholdMs: number;
128+
129+
constructor({
130+
state = {},
131+
messenger,
132+
tokenPricesService,
133+
swapsSupportedChainIds,
134+
fetchTokens,
135+
fetchSwapsTokensThresholdMs,
136+
}: {
137+
state?: Partial<TokenSearchDiscoveryDataControllerState>;
138+
messenger: TokenSearchDiscoveryDataControllerMessenger;
139+
tokenPricesService: AbstractTokenPricesService;
140+
swapsSupportedChainIds: Hex[];
141+
fetchTokens: (chainId: Hex) => Promise<{ address: string; }[]>;
142+
fetchSwapsTokensThresholdMs: number;
143+
}) {
144+
super({
145+
name: controllerName,
146+
metadata: tokenSearchDiscoveryDataControllerMetadata,
147+
messenger,
148+
state: { ...getDefaultTokenSearchDiscoveryDataControllerState(), ...state },
149+
});
150+
151+
this.#abortController = new AbortController();
152+
this.#tokenPricesService = tokenPricesService;
153+
this.#swapsSupportedChainIds = swapsSupportedChainIds;
154+
this.#fetchTokens = fetchTokens;
155+
this.#fetchSwapsTokensThresholdMs = fetchSwapsTokensThresholdMs;
156+
}
157+
158+
async #fetchPriceData(chainId: Hex, address: string): Promise<TokenPrice<Hex, string> | null> {
159+
const { currentCurrency } = this.messagingSystem.call('CurrencyRateController:getState');
160+
161+
try {
162+
const pricesData = await this.#tokenPricesService.fetchTokenPrices({
163+
chainId,
164+
tokenAddresses: [address as Hex],
165+
currency: currentCurrency,
166+
});
167+
168+
return pricesData[address as Hex] ?? null;
169+
} catch (error) {
170+
return null;
171+
}
172+
}
173+
174+
async #fetchSwapsTokens(chainId: Hex): Promise<void> {
175+
if (!this.#swapsSupportedChainIds.includes(chainId)) {
176+
return;
177+
}
178+
179+
const swapsTokens = this.state.swapsTokenAddressesByChainId[chainId];
180+
if (!swapsTokens || swapsTokens.lastFetched < Date.now() - this.#fetchSwapsTokensThresholdMs) {
181+
try {
182+
const tokens = await this.#fetchTokens(chainId);
183+
this.update((state) => {
184+
state.swapsTokenAddressesByChainId[chainId] = {
185+
lastFetched: Date.now(),
186+
addresses: tokens.map((token) => token.address),
187+
};
188+
});
189+
} catch (error) {
190+
console.error(error);
191+
}
192+
}
193+
}
194+
195+
async fetchTokenDisplayData(chainId: Hex, address: string): Promise<void> {
196+
await this.#fetchSwapsTokens(chainId);
197+
198+
let tokenMetadata: TokenListToken | undefined;
199+
try {
200+
tokenMetadata = await fetchTokenMetadata<TokenListToken>(chainId, address, this.#abortController.signal);
201+
} catch (error) {
202+
if (
203+
!(error instanceof Error) ||
204+
!error.message.includes(TOKEN_METADATA_NO_SUPPORT_ERROR)
205+
) {
206+
throw error;
207+
}
208+
}
209+
210+
const { currentCurrency } = this.messagingSystem.call('CurrencyRateController:getState');
211+
212+
let tokenDisplayData: TokenDisplayData;
213+
if (!tokenMetadata) {
214+
tokenDisplayData = {
215+
found: false,
216+
address,
217+
chainId,
218+
currency: currentCurrency,
219+
};
220+
} else {
221+
const priceData = await this.#fetchPriceData(chainId, address);
222+
tokenDisplayData = {
223+
found: true,
224+
address,
225+
chainId,
226+
currency: currentCurrency,
227+
token: {
228+
...tokenMetadata,
229+
isERC721: false,
230+
image: formatIconUrlWithProxy({
231+
chainId,
232+
tokenAddress: address,
233+
}),
234+
},
235+
price: priceData,
236+
};
237+
}
238+
239+
this.update((state) => {
240+
state.tokenDisplayData = [
241+
tokenDisplayData,
242+
...state.tokenDisplayData.filter(token => token.address !== address && token.chainId !== chainId && token.currency !== currentCurrency)
243+
].slice(0, MAX_TOKEN_DISPLAY_DATA_LENGTH);
244+
});
245+
}
246+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './TokenSearchDiscoveryDataController';
2+
export * from './types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Hex } from "@metamask/utils";
2+
import { TokenPrice } from "../token-prices-service/abstract-token-prices-service";
3+
import { Token } from "../TokenRatesController";
4+
5+
export type NotFoundTokenDisplayData = {
6+
found: false;
7+
chainId: Hex;
8+
address: string;
9+
currency: string;
10+
};
11+
12+
export type FoundTokenDisplayData = {
13+
found: true;
14+
chainId: Hex;
15+
address: string;
16+
currency: string;
17+
token: Token;
18+
price: TokenPrice<Hex, string> | null;
19+
};
20+
21+
export type TokenDisplayData = NotFoundTokenDisplayData | FoundTokenDisplayData;

packages/assets-controllers/src/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,13 @@ export type {
185185
MultichainAssetsRatesControllerStateChange,
186186
MultichainAssetsRatesControllerMessenger,
187187
} from './MultichainAssetsRatesController';
188+
export { TokenSearchDiscoveryDataController } from './TokenSearchDiscoveryDataController';
189+
export type {
190+
TokenDisplayData,
191+
TokenSearchDiscoveryDataControllerState,
192+
TokenSearchDiscoveryDataControllerGetStateAction,
193+
TokenSearchDiscoveryDataControllerEvents,
194+
TokenSearchDiscoveryDataControllerStateChangeEvent,
195+
TokenSearchDiscoveryDataControllerActions,
196+
TokenSearchDiscoveryDataControllerMessenger,
197+
} from './TokenSearchDiscoveryDataController';

0 commit comments

Comments
 (0)