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
127 changes: 127 additions & 0 deletions packages/assets-controllers/src/TokenCacheService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { Hex } from '@metamask/utils';

/**
* Token metadata for a single token
*/
export type TokenListToken = {
name: string;
symbol: string;
decimals: number;
address: string;
occurrences: number;
aggregators: string[];
iconUrl: string;
};

/**
* Map of token addresses to token metadata
*/
export type TokenListMap = Record<string, TokenListToken>;

/**
* Cache entry containing token list data and timestamp
*/
type DataCache = {
timestamp: number;
data: TokenListMap;
};

/**
* Cache structure mapping chain IDs to token lists
*/
export type TokensChainsCache = {
[chainId: Hex]: DataCache;
};

/**
* Service for managing token list cache outside of controller state
* This provides in-memory token metadata storage without persisting to disk
*/
export class TokenCacheService {
readonly #cache: Map<Hex, DataCache> = new Map();

readonly #cacheThreshold: number;

/**
* Creates a new TokenCacheService instance
*
* @param cacheThreshold - Time in milliseconds before cache is considered stale (default: 24 hours)
*/
constructor(cacheThreshold: number = 24 * 60 * 60 * 1000) {
this.#cacheThreshold = cacheThreshold;
}

/**
* Get cache entry for a specific chain
*
* @param chainId - Chain ID in hex format
* @returns Cache entry with token list data and timestamp, or undefined if not cached
*/
get(chainId: Hex): DataCache | undefined {
return this.#cache.get(chainId);
}

/**
* Set cache entry for a specific chain
*
* @param chainId - Chain ID in hex format
* @param data - Token list data to cache
* @param timestamp - Optional timestamp (defaults to current time)
*/
set(chainId: Hex, data: TokenListMap, timestamp: number = Date.now()): void {
this.#cache.set(chainId, { data, timestamp });
}

/**
* Check if cache entry for a chain is still valid (not expired)
*
* @param chainId - Chain ID in hex format
* @returns True if cache exists and is not expired
*/
isValid(chainId: Hex): boolean {
const entry = this.#cache.get(chainId);
if (!entry) {
return false;
}
return Date.now() - entry.timestamp < this.#cacheThreshold;
}

/**
* Get all cached token lists across all chains
*
* @returns Complete cache structure with all chains
*/
getAll(): TokensChainsCache {
const result: TokensChainsCache = {};
this.#cache.forEach((value, key) => {
result[key] = value;
});
return result;
}

/**
* Clear all cached token lists
*/
clear(): void {
this.#cache.clear();
}

/**
* Remove cache entry for a specific chain
*
* @param chainId - Chain ID in hex format
* @returns True if an entry was removed
*/
delete(chainId: Hex): boolean {
return this.#cache.delete(chainId);
}

/**
* Get number of cached chains
*
* @returns Count of cached chains
*/
get size(): number {
return this.#cache.size;
}
}
29 changes: 13 additions & 16 deletions packages/assets-controllers/src/TokenDetectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ import {
} from './multi-chain-accounts-service';
import type {
GetTokenListState,
GetTokenListForChain,
GetAllTokenLists,
TokenListMap,
TokenListStateChange,
TokenListCacheUpdate,
TokensChainsCache,
} from './TokenListController';
import type { Token } from './TokenRatesController';
Expand Down Expand Up @@ -140,6 +142,8 @@ export type AllowedActions =
| NetworkControllerGetNetworkConfigurationByNetworkClientId
| NetworkControllerGetStateAction
| GetTokenListState
| GetTokenListForChain
| GetAllTokenLists
| KeyringControllerGetStateAction
| PreferencesControllerGetStateAction
| TokensControllerGetStateAction
Expand All @@ -157,7 +161,7 @@ export type TokenDetectionControllerEvents =
export type AllowedEvents =
| AccountsControllerSelectedEvmAccountChangeEvent
| NetworkControllerNetworkDidChangeEvent
| TokenListStateChange
| TokenListCacheUpdate
| KeyringControllerLockEvent
| KeyringControllerUnlockEvent
| PreferencesControllerStateChangeEvent
Expand Down Expand Up @@ -337,12 +341,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To

this.#selectedAccountId = this.#getSelectedAccount().id;

const { tokensChainsCache } = this.messenger.call(
'TokenListController:getState',
this.#tokensChainsCache = this.messenger.call(
'TokenListController:getAllTokenLists',
);

this.#tokensChainsCache = tokensChainsCache;

const { useTokenDetection: defaultUseTokenDetection } = this.messenger.call(
'PreferencesController:getState',
);
Expand Down Expand Up @@ -378,8 +380,8 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
});

this.messenger.subscribe(
'TokenListController:stateChange',
async ({ tokensChainsCache }) => {
'TokenListController:cacheUpdate',
async (tokensChainsCache) => {
const isEqualValues = this.#compareTokensChainsCache(
tokensChainsCache,
this.#tokensChainsCache,
Expand Down Expand Up @@ -666,10 +668,8 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
if (isMainnetDetectionInactive) {
this.#tokensChainsCache = this.#getConvertedStaticMainnetTokenList();
} else {
const { tokensChainsCache } = this.messenger.call(
'TokenListController:getState',
);
this.#tokensChainsCache = tokensChainsCache ?? {};
this.#tokensChainsCache =
this.messenger.call('TokenListController:getAllTokenLists') ?? {};
}

return true;
Expand Down Expand Up @@ -894,12 +894,9 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
const isTokenDetectionInactiveInMainnet =
!this.#isDetectionEnabledFromPreferences &&
chainId === ChainId.mainnet;
const { tokensChainsCache } = this.messenger.call(
'TokenListController:getState',
);
this.#tokensChainsCache = isTokenDetectionInactiveInMainnet
? this.#getConvertedStaticMainnetTokenList()
: (tokensChainsCache ?? {});
: (this.messenger.call('TokenListController:getAllTokenLists') ?? {});

// Generate token candidates based on chainId and selectedAddress
const tokenCandidateSlices = this.#getSlicesOfTokensToDetect({
Expand Down
Loading
Loading