From 033bba66cece97884cc749f922eedf5de75c9cc5 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 10 Oct 2023 11:09:59 -0500 Subject: [PATCH] Parameterize `TokenListController` by `NetworkClientId` + Integrate `PollingController` (#1763) Resolves: https://github.com/MetaMask/MetaMask-planning/issues/1032 Integrates new `PollingController` abstraction into the `TokenListController`, implements the `executePoll` method and parameterizes the `fetchTokenList` method by `networkClientId` such that tokenLists can be fetched and cached for different networks via concurrent polling sessions. **A note:** As is our current controller refactor strategy, these changes are additive, backwards compatible and coexist alongside the existing globally selected network pattern. Once we fully adopt this polling pattern we should no longer access the root `tokenList` state but rather access from the cache with a chainId selector. --- .../src/TokenListController.test.ts | 323 +++++++++++++++++- .../src/TokenListController.ts | 57 ++-- .../src/token-service.test.ts | 16 +- .../assets-controllers/src/token-service.ts | 2 +- .../assets-controllers/tsconfig.build.json | 3 +- packages/assets-controllers/tsconfig.json | 3 +- 6 files changed, 360 insertions(+), 44 deletions(-) diff --git a/packages/assets-controllers/src/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts index 4b44f1961d..87bbbd5a77 100644 --- a/packages/assets-controllers/src/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -7,6 +7,7 @@ import { toHex, } from '@metamask/controller-utils'; import type { + NetworkControllerGetNetworkClientByIdAction, NetworkControllerStateChangeEvent, NetworkState, ProviderConfig, @@ -15,7 +16,7 @@ import { NetworkStatus } from '@metamask/network-controller'; import nock from 'nock'; import * as sinon from 'sinon'; -import { TOKEN_END_POINT_API } from './token-service'; +import * as tokenService from './token-service'; import type { TokenListStateChange, GetTokenListState, @@ -27,6 +28,10 @@ import { TokenListController } from './TokenListController'; const name = 'TokenListController'; const timestamp = Date.now(); +const flushPromises = () => { + return new Promise(jest.requireActual('timers').setImmediate); +}; + const sampleMainnetTokenList = [ { address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', @@ -223,6 +228,15 @@ const sampleBinanceTokenList = [ 'https://static.metafi.codefi.network/api/v1/tokenIcons/56/0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3.png', }, ]; + +const sampleBinanceTokensChainsCache = sampleBinanceTokenList.reduce( + (output, current) => { + output[current.address] = current; + return output; + }, + {} as TokenListMap, +); + const sampleSingleChainState = { tokenList: { '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f': { @@ -299,7 +313,92 @@ const sampleSingleChainState = { }, }; -const sampleBinanceTokensChainsCache = sampleBinanceTokenList.reduce( +const sampleSepoliaTokenList = [ + { + address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', + symbol: 'WBTC', + decimals: 8, + name: 'Wrapped BTC', + iconUrl: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/11155111/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599.png', + type: 'erc20', + aggregators: [ + 'Metamask', + 'Aave', + 'Bancor', + 'Cmc', + 'Cryptocom', + 'CoinGecko', + 'OneInch', + 'Pmm', + 'Sushiswap', + 'Zerion', + 'Lifi', + 'Openswap', + 'Sonarwatch', + 'UniswapLabs', + 'Coinmarketcap', + ], + occurrences: 15, + fees: {}, + storage: { + balance: 0, + }, + }, + { + address: '0x04fa0d235c4abf4bcf4787af4cf447de572ef828', + symbol: 'UMA', + decimals: 18, + name: 'UMA', + iconUrl: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/11155111/0x04fa0d235c4abf4bcf4787af4cf447de572ef828.png', + type: 'erc20', + aggregators: [ + 'Metamask', + 'Bancor', + 'CMC', + 'Crypto.com', + 'CoinGecko', + '1inch', + 'PMM', + 'Sushiswap', + 'Zerion', + 'Openswap', + 'Sonarwatch', + 'UniswapLabs', + 'Coinmarketcap', + ], + occurrences: 13, + fees: {}, + }, + { + address: '0x6810e776880c02933d47db1b9fc05908e5386b96', + symbol: 'GNO', + decimals: 18, + name: 'Gnosis Token', + iconUrl: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/11155111/0x6810e776880c02933d47db1b9fc05908e5386b96.png', + type: 'erc20', + aggregators: [ + 'Metamask', + 'Bancor', + 'CMC', + 'CoinGecko', + '1inch', + 'Sushiswap', + 'Zerion', + 'Lifi', + 'Openswap', + 'Sonarwatch', + 'UniswapLabs', + 'Coinmarketcap', + ], + occurrences: 12, + fees: {}, + }, +]; + +const sampleSepoliaTokensChainCache = sampleSepoliaTokenList.reduce( (output, current) => { output[current.address] = current; return output; @@ -481,7 +580,7 @@ const expiredCacheExistingState: TokenListState = { }; type MainControllerMessenger = ControllerMessenger< - GetTokenListState, + GetTokenListState | NetworkControllerGetNetworkClientByIdAction, TokenListStateChange | NetworkControllerStateChangeEvent >; @@ -494,7 +593,7 @@ const getRestrictedMessenger = ( ) => { const messenger = controllerMessenger.getRestricted({ name, - allowedActions: [], + allowedActions: ['NetworkController:getNetworkClientById'], allowedEvents: [ 'TokenListController:stateChange', 'NetworkController:stateChange', @@ -531,6 +630,8 @@ function buildNetworkControllerStateWithProviderConfig( describe('TokenListController', () => { afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllTimers(); sinon.restore(); }); @@ -638,7 +739,7 @@ describe('TokenListController', () => { }); it('should update tokenList state when network updates are passed via onNetworkStateChange callback', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleMainnetTokenList) .persist(); @@ -801,7 +902,7 @@ describe('TokenListController', () => { }); it('should update token list from api', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleMainnetTokenList) .persist(); @@ -839,12 +940,12 @@ describe('TokenListController', () => { }); it('should update the cache before threshold time if the current data is undefined', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .once() .reply(200, undefined); - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleMainnetTokenList) .persist(); @@ -894,7 +995,7 @@ describe('TokenListController', () => { }); it('should update token list after removing data with duplicate symbols', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleWithDuplicateSymbols) .persist(); @@ -937,7 +1038,7 @@ describe('TokenListController', () => { }); it('should update token list after removing data less than 3 occurrences', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleWithLessThan3OccurencesResponse) .persist(); @@ -961,7 +1062,7 @@ describe('TokenListController', () => { }); it('should update token list when the token property changes', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleMainnetTokenList) .persist(); @@ -989,7 +1090,7 @@ describe('TokenListController', () => { }); it('should update the cache when the timestamp expires', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleMainnetTokenList) .persist(); @@ -1019,7 +1120,7 @@ describe('TokenListController', () => { }); it('should update token list when the chainId change', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleMainnetTokenList) .get(`/tokens/${convertHexToDecimal(ChainId.goerli)}`) @@ -1116,7 +1217,7 @@ describe('TokenListController', () => { }); it('should update preventPollingOnNetworkRestart and restart the polling on network restart', async () => { - nock(TOKEN_END_POINT_API) + nock(tokenService.TOKEN_END_POINT_API) .get(`/tokens/${convertHexToDecimal(ChainId.mainnet)}`) .reply(200, sampleMainnetTokenList) .get(`/tokens/${convertHexToDecimal(ChainId.goerli)}`) @@ -1192,4 +1293,198 @@ describe('TokenListController', () => { ); }); }); + + describe('executePoll', () => { + it('should call fetchTokenListByChainId with the correct chainId', async () => { + nock(tokenService.TOKEN_END_POINT_API) + .get(`/tokens/${convertHexToDecimal(ChainId.sepolia)}`) + .reply(200, sampleSepoliaTokenList) + .persist(); + + const fetchTokenListByChainIdSpy = jest.spyOn( + tokenService, + 'fetchTokenListByChainId', + ); + const controllerMessenger = getControllerMessenger(); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + jest.fn().mockReturnValue({ + configuration: { + type: NetworkType.sepolia, + chainId: ChainId.sepolia, + }, + }), + ); + const messenger = getRestrictedMessenger(controllerMessenger); + const controller = new TokenListController({ + chainId: ChainId.mainnet, + preventPollingOnNetworkRestart: false, + messenger, + state: expiredCacheExistingState, + }); + expect(controller.state.tokenList).toStrictEqual( + expiredCacheExistingState.tokenList, + ); + + await controller.executePoll('sepolia'); + expect(fetchTokenListByChainIdSpy.mock.calls[0]).toStrictEqual( + expect.arrayContaining([ChainId.sepolia]), + ); + expect(controller.state.tokenList).toStrictEqual( + sampleSepoliaTokensChainCache, + ); + }); + }); + + describe('startPollingByNetworkClient', () => { + it('should start polling against the token list API at the interval passed to the constructor', async () => { + jest.useFakeTimers(); + const pollingIntervalTime = 1000; + const fetchTokenListByChainIdSpy = jest.spyOn( + tokenService, + 'fetchTokenListByChainId', + ); + + const controllerMessenger = getControllerMessenger(); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + jest.fn().mockReturnValue({ + configuration: { + type: NetworkType.goerli, + chainId: ChainId.goerli, + }, + }), + ); + const messenger = getRestrictedMessenger(controllerMessenger); + const controller = new TokenListController({ + chainId: ChainId.mainnet, + preventPollingOnNetworkRestart: false, + messenger, + state: expiredCacheExistingState, + interval: pollingIntervalTime, + }); + expect(controller.state.tokenList).toStrictEqual( + expiredCacheExistingState.tokenList, + ); + + controller.startPollingByNetworkClientId('goerli'); + jest.advanceTimersByTime(pollingIntervalTime / 2); + await flushPromises(); + expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(0); + jest.advanceTimersByTime(pollingIntervalTime / 2); + await flushPromises(); + + expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); + await Promise.all([ + jest.advanceTimersByTime(pollingIntervalTime), + flushPromises(), + ]); + + await Promise.all([jest.runOnlyPendingTimers(), flushPromises()]); + expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(2); + }); + + it('should update tokenList state and tokensChainsCache', async () => { + jest.useFakeTimers(); + const startingState: TokenListState = { + tokenList: {}, + tokensChainsCache: {}, + preventPollingOnNetworkRestart: false, + }; + + const fetchTokenListByChainIdSpy = jest + .spyOn(tokenService, 'fetchTokenListByChainId') + .mockImplementation(async (chainId) => { + switch (chainId) { + case ChainId.sepolia: + return sampleSepoliaTokenList; + case toHex(56): + return sampleBinanceTokenList; + default: + throw new Error('Invalid chainId'); + } + }); + const controllerMessenger = getControllerMessenger(); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + jest.fn().mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'sepolia': + return { + configuration: { + type: NetworkType.sepolia, + chainId: ChainId.sepolia, + }, + }; + case 'binance-network-client-id': + return { + configuration: { + type: NetworkType.rpc, + chainId: toHex(56), + }, + }; + default: + throw new Error('Invalid networkClientId'); + } + }), + ); + const pollingIntervalTime = 1000; + const messenger = getRestrictedMessenger(controllerMessenger); + const controller = new TokenListController({ + chainId: ChainId.mainnet, + preventPollingOnNetworkRestart: false, + messenger, + state: startingState, + interval: pollingIntervalTime, + }); + + expect(controller.state).toStrictEqual(startingState); + + // start polling for sepolia + await controller.startPollingByNetworkClientId('sepolia'); + // wait a polling interval + jest.advanceTimersByTime(pollingIntervalTime); + await flushPromises(); + + expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); + // expect the state to be updated with the sepolia token list + expect(controller.state.tokenList).toStrictEqual( + sampleSepoliaTokensChainCache, + ); + expect(controller.state.tokensChainsCache).toStrictEqual({ + [ChainId.sepolia]: { + timestamp: expect.any(Number), + data: sampleSepoliaTokensChainCache, + }, + }); + // start polling for binance + await controller.startPollingByNetworkClientId( + 'binance-network-client-id', + ); + jest.advanceTimersByTime(pollingIntervalTime); + await flushPromises(); + + // expect fetchTokenListByChain to be called for binance, but not for sepolia + // because the cache for the recently fetched sepolia token list is still valid + expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(2); + + // expect tokenList to be updated with the binance token list + // and the cache to now contain both the binance token list and the sepolia token list + expect(controller.state.tokenList).toStrictEqual( + sampleBinanceTokensChainsCache, + ); + // once we adopt this polling pattern we should no longer access the root tokenList state + // but rather access from the cache with a chainId selector. + expect(controller.state.tokensChainsCache).toStrictEqual({ + [toHex(56)]: { + timestamp: expect.any(Number), + data: sampleBinanceTokensChainsCache, + }, + [ChainId.sepolia]: { + timestamp: expect.any(Number), + data: sampleSepoliaTokensChainCache, + }, + }); + }); + }); }); diff --git a/packages/assets-controllers/src/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts index eef38badaa..e113ca9f1b 100644 --- a/packages/assets-controllers/src/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -1,10 +1,12 @@ import type { RestrictedControllerMessenger } from '@metamask/base-controller'; -import { BaseControllerV2 } from '@metamask/base-controller'; import { safelyExecute } from '@metamask/controller-utils'; import type { + NetworkClientId, NetworkControllerStateChangeEvent, NetworkState, + NetworkControllerGetNetworkClientByIdAction, } from '@metamask/network-controller'; +import { PollingController } from '@metamask/polling-controller'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import type { Patch } from 'immer'; @@ -14,7 +16,7 @@ import { formatAggregatorNames, formatIconUrlWithProxy, } from './assetsUtil'; -import { fetchTokenList } from './token-service'; +import { fetchTokenListByChainId } from './token-service'; const DEFAULT_INTERVAL = 24 * 60 * 60 * 1000; const DEFAULT_THRESHOLD = 24 * 60 * 60 * 1000; @@ -56,12 +58,11 @@ export type GetTokenListState = { type: `${typeof name}:getState`; handler: () => TokenListState; }; - type TokenListMessenger = RestrictedControllerMessenger< typeof name, - GetTokenListState, + GetTokenListState | NetworkControllerGetNetworkClientByIdAction, TokenListStateChange | NetworkControllerStateChangeEvent, - never, + NetworkControllerGetNetworkClientByIdAction['type'], TokenListStateChange['type'] | NetworkControllerStateChangeEvent['type'] >; @@ -80,7 +81,7 @@ const defaultState: TokenListState = { /** * Controller that passively polls on a set interval for the list of tokens from metaswaps api */ -export class TokenListController extends BaseControllerV2< +export class TokenListController extends PollingController< typeof name, TokenListState, TokenListMessenger @@ -232,28 +233,46 @@ export class TokenListController extends BaseControllerV2< /** * Fetching token list from the Token Service API. + * + * @param networkClientId - The ID of the network client triggering the fetch. + * @returns A promise that resolves when this operation completes. + */ + async executePoll(networkClientId: string): Promise { + return this.fetchTokenList(networkClientId); + } + + /** + * Fetching token list from the Token Service API. + * + * @param networkClientId - The ID of the network client triggering the fetch. */ - async fetchTokenList(): Promise { + async fetchTokenList(networkClientId?: NetworkClientId): Promise { const releaseLock = await this.mutex.acquire(); + let networkClient; + if (networkClientId) { + networkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + } + const chainId = networkClient?.configuration.chainId ?? this.chainId; try { const { tokensChainsCache } = this.state; let tokenList: TokenListMap = {}; const cachedTokens: TokenListMap = await safelyExecute(() => - this.fetchFromCache(), + this.#fetchFromCache(chainId), ); if (cachedTokens) { // Use non-expired cached tokens tokenList = { ...cachedTokens }; } else { // Fetch fresh token list - const tokensFromAPI: TokenListToken[] = await safelyExecute(() => - fetchTokenList(this.chainId, this.abortController.signal), - ); - + const tokensFromAPI: TokenListToken[] = await safelyExecute(() => { + return fetchTokenListByChainId(chainId, this.abortController.signal); + }); if (!tokensFromAPI) { // Fallback to expired cached tokens - tokenList = { ...(tokensChainsCache[this.chainId]?.data || {}) }; - + tokenList = { ...(tokensChainsCache[chainId]?.data || {}) }; this.update(() => { return { ...this.state, @@ -287,7 +306,7 @@ export class TokenListController extends BaseControllerV2< ...token, aggregators: formatAggregatorNames(token.aggregators), iconUrl: formatIconUrlWithProxy({ - chainId: this.chainId, + chainId, tokenAddress: token.address, }), }; @@ -296,7 +315,7 @@ export class TokenListController extends BaseControllerV2< } const updatedTokensChainsCache: TokensChainsCache = { ...tokensChainsCache, - [this.chainId]: { + [chainId]: { timestamp: Date.now(), data: tokenList, }, @@ -317,12 +336,12 @@ export class TokenListController extends BaseControllerV2< * Checks if the Cache timestamp is valid, * if yes data in cache will be returned * otherwise null will be returned. - * + * @param chainId - The chain ID of the network for which to fetch the cache. * @returns The cached data, or `null` if the cache was expired. */ - async fetchFromCache(): Promise { + async #fetchFromCache(chainId: Hex): Promise { const { tokensChainsCache }: TokenListState = this.state; - const dataCache = tokensChainsCache[this.chainId]; + const dataCache = tokensChainsCache[chainId]; const now = Date.now(); if ( dataCache?.data && diff --git a/packages/assets-controllers/src/token-service.test.ts b/packages/assets-controllers/src/token-service.test.ts index 7e168dd6cc..1420d10787 100644 --- a/packages/assets-controllers/src/token-service.test.ts +++ b/packages/assets-controllers/src/token-service.test.ts @@ -2,7 +2,7 @@ import { toHex } from '@metamask/controller-utils'; import nock from 'nock'; import { - fetchTokenList, + fetchTokenListByChainId, fetchTokenMetadata, TOKEN_END_POINT_API, TOKEN_METADATA_NO_SUPPORT_ERROR, @@ -137,7 +137,7 @@ const sampleDecimalChainId = 1; const sampleChainId = toHex(sampleDecimalChainId); describe('Token service', () => { - describe('fetchTokenList', () => { + describe('fetchTokenListByChainId', () => { it('should call the tokens api and return the list of tokens', async () => { const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) @@ -145,7 +145,7 @@ describe('Token service', () => { .reply(200, sampleTokenList) .persist(); - const tokens = await fetchTokenList(sampleChainId, signal); + const tokens = await fetchTokenListByChainId(sampleChainId, signal); expect(tokens).toStrictEqual(sampleTokenList); }); @@ -159,7 +159,7 @@ describe('Token service', () => { .reply(200, sampleTokenList) .persist(); - const fetchPromise = fetchTokenList( + const fetchPromise = fetchTokenListByChainId( sampleChainId, abortController.signal, ); @@ -175,7 +175,7 @@ describe('Token service', () => { .replyWithError('Example network error') .persist(); - const result = await fetchTokenList(sampleChainId, signal); + const result = await fetchTokenListByChainId(sampleChainId, signal); expect(result).toBeUndefined(); }); @@ -187,7 +187,7 @@ describe('Token service', () => { .reply(500) .persist(); - const result = await fetchTokenList(sampleChainId, signal); + const result = await fetchTokenListByChainId(sampleChainId, signal); expect(result).toBeUndefined(); }); @@ -201,7 +201,7 @@ describe('Token service', () => { .reply(200, sampleTokenList) .persist(); - const result = await fetchTokenList(sampleChainId, signal, { + const result = await fetchTokenListByChainId(sampleChainId, signal, { timeout: ONE_MILLISECOND, }); @@ -317,7 +317,7 @@ describe('Token service', () => { .reply(404, undefined) .persist(); - const tokens = await fetchTokenList(sampleChainId, signal); + const tokens = await fetchTokenListByChainId(sampleChainId, signal); expect(tokens).toBeUndefined(); }); diff --git a/packages/assets-controllers/src/token-service.ts b/packages/assets-controllers/src/token-service.ts index 614753acfa..e065459741 100644 --- a/packages/assets-controllers/src/token-service.ts +++ b/packages/assets-controllers/src/token-service.ts @@ -46,7 +46,7 @@ const defaultTimeout = tenSecondsInMilliseconds; * @param options.timeout - The fetch timeout. * @returns The token list, or `undefined` if the request was cancelled. */ -export async function fetchTokenList( +export async function fetchTokenListByChainId( chainId: Hex, abortSignal: AbortSignal, { timeout = defaultTimeout } = {}, diff --git a/packages/assets-controllers/tsconfig.build.json b/packages/assets-controllers/tsconfig.build.json index 44753a0911..93737886c1 100644 --- a/packages/assets-controllers/tsconfig.build.json +++ b/packages/assets-controllers/tsconfig.build.json @@ -10,7 +10,8 @@ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, - { "path": "../preferences-controller/tsconfig.build.json" } + { "path": "../preferences-controller/tsconfig.build.json" }, + { "path": "../polling-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/assets-controllers/tsconfig.json b/packages/assets-controllers/tsconfig.json index e46347555f..2900e14b0e 100644 --- a/packages/assets-controllers/tsconfig.json +++ b/packages/assets-controllers/tsconfig.json @@ -8,7 +8,8 @@ { "path": "../base-controller" }, { "path": "../controller-utils" }, { "path": "../network-controller" }, - { "path": "../preferences-controller" } + { "path": "../preferences-controller" }, + { "path": "../polling-controller" } ], "include": ["../../types", "./src"] }