@metamask/assets-controller 10.0.1 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/dist/AssetsController.cjs +23 -5
  3. package/dist/AssetsController.cjs.map +1 -1
  4. package/dist/AssetsController.d.cts +7 -1
  5. package/dist/AssetsController.d.cts.map +1 -1
  6. package/dist/AssetsController.d.mts +7 -1
  7. package/dist/AssetsController.d.mts.map +1 -1
  8. package/dist/AssetsController.mjs +23 -5
  9. package/dist/AssetsController.mjs.map +1 -1
  10. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.cjs +56 -13
  11. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.cjs.map +1 -1
  12. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.cts.map +1 -1
  13. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.mts.map +1 -1
  14. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.mjs +56 -13
  15. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.mjs.map +1 -1
  16. package/dist/migrations/healAssetsInfoMetadata.cjs +385 -0
  17. package/dist/migrations/healAssetsInfoMetadata.cjs.map +1 -0
  18. package/dist/migrations/healAssetsInfoMetadata.d.cts +106 -0
  19. package/dist/migrations/healAssetsInfoMetadata.d.cts.map +1 -0
  20. package/dist/migrations/healAssetsInfoMetadata.d.mts +106 -0
  21. package/dist/migrations/healAssetsInfoMetadata.d.mts.map +1 -0
  22. package/dist/migrations/healAssetsInfoMetadata.mjs +381 -0
  23. package/dist/migrations/healAssetsInfoMetadata.mjs.map +1 -0
  24. package/dist/types.cjs.map +1 -1
  25. package/dist/types.d.cts +3 -0
  26. package/dist/types.d.cts.map +1 -1
  27. package/dist/types.d.mts +3 -0
  28. package/dist/types.d.mts.map +1 -1
  29. package/dist/types.mjs.map +1 -1
  30. package/package.json +7 -7
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healAssetsInfoMetadata.cjs","sourceRoot":"","sources":["../../src/migrations/healAssetsInfoMetadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccountsControllerState } from '@metamask/accounts-controller';\nimport type { TokensControllerState } from '@metamask/assets-controllers';\nimport type { Hex } from '@metamask/utils';\nimport {\n getChecksumAddress,\n getErrorMessage,\n hasProperty,\n isObject,\n} from '@metamask/utils';\nimport { cloneDeep } from 'lodash';\n\nimport { createModuleLogger, projectLogger } from '../logger';\nimport type {\n AccountId,\n Caip19AssetId,\n FungibleAssetMetadata,\n AssetsControllerStateInternal,\n} from '../types';\n\n/**\n * TEMPORARY MODULE — remove in a future release.\n *\n * Port of extension migration #215 (Assets Controller Metadata Healing).\n * Issue: https://consensyssoftware.atlassian.net/browse/ASSETS-3346\n * Incident: #incident-metamask-1731\n *\n * Background: after a prior defect in AssetsController, metadata for custom\n * tokens was wiped. Most popular chains support auto-detection and can\n * self-heal, however not all.\n *\n * This module computes the state additions needed to restore metadata for\n * custom tokens on niche EVM chains (chains that cannot auto-detect and\n * self-heal), using the legacy `TokensController.allTokens` state as the\n * source of truth. It is a pure function so it can be tested in isolation;\n * the controller applies the returned patch in its constructor.\n *\n * The legacy state is treated as fully untrusted (`unknown`): every shape is\n * validated before use, and any unexpected input results in that entry being\n * skipped (or `null` being returned when nothing is restorable).\n */\n\n/**\n * The slices of current `AssetsController` state the healing computation\n * reads. Never mutated.\n */\nexport type CurrentAssetsState = Pick<\n AssetsControllerStateInternal,\n 'assetsInfo' | 'assetsBalance' | 'customAssets' | 'assetPreferences'\n>;\n\n/**\n * TEMPORARY — will be removed in a future release.\n *\n * Shape of the legacy persisted state the ASSETS-3346 healing reads, derived\n * from the real `TokensControllerState` and `AccountsControllerState` types.\n * Use it to type the `tempMigrateAssetsInfoMetadataAssets3346` getter when\n * integrating in a client.\n *\n * Everything is optional and only the listed slices are read:\n *\n * - `TokensController.allTokens` — required for any healing to happen.\n * - `TokensController.allIgnoredTokens` — tokens the user hid/removed\n * (skipped during healing).\n * - `AccountsController.internalAccounts.accounts` — maps addresses to\n * account IDs so healed tokens are also tracked in `customAssets`\n * (without it only `assetsInfo` is healed).\n *\n * At runtime the value still crosses the boundary as `unknown`: every shape\n * is re-validated by the migration, so a partial or malformed object is safe\n * and simply skipped.\n */\nexport type Assets3346MigrationState = {\n // eslint-disable-next-line @typescript-eslint/naming-convention -- must match the persisted controller state key\n TokensController?: Partial<\n Pick<TokensControllerState, 'allTokens' | 'allIgnoredTokens'>\n >;\n // eslint-disable-next-line @typescript-eslint/naming-convention -- must match the persisted controller state key\n AccountsController?: {\n internalAccounts?: {\n accounts?: Record<\n string,\n Pick<\n AccountsControllerState['internalAccounts']['accounts'][string],\n 'address'\n >\n >;\n };\n };\n};\n\nexport type AssetsInfoHealingPatch = {\n assetsInfo: Record<Caip19AssetId, FungibleAssetMetadata>;\n customAssets: Record<AccountId, Caip19AssetId[]>;\n};\n\nconst log = createModuleLogger(projectLogger, 'tempHealAssetsInfoMetadata');\n\nexport type TempHealAssetsInfoMetadataOptions = {\n /** Current `AssetsController` state the healing patch is computed against. */\n state: AssetsControllerStateInternal;\n /**\n * Host-provided getter for the untrusted legacy state root (see\n * `AssetsControllerOptions.tempMigrateAssetsInfoMetadataAssets3346`).\n */\n getMigrationState: () => unknown;\n /** Optional Sentry-compatible reporter for healing failures. */\n captureException?: (error: Error) => void;\n};\n\n/**\n * TEMPORARY — will be removed in a future release.\n *\n * @param options - The options bag.\n * @param options.state - Current controller state to compute the patch against.\n * @param options.getMigrationState - Getter for the untrusted legacy state root.\n * @param options.captureException - Optional reporter for healing failures.\n * @returns Updated controller state with the healing patch applied, or the\n * original state when there is nothing to heal or healing fails.\n */\nexport function tempHealAssetsInfoMetadata({\n state,\n getMigrationState,\n captureException,\n}: TempHealAssetsInfoMetadataOptions): AssetsControllerStateInternal {\n const reportError = (error: unknown): void => {\n log('Failed to heal assetsInfo metadata', error);\n captureException?.(\n new Error(\n `AssetsController: temporary assetsInfo metadata healing failed: ${getErrorMessage(\n error,\n )}`,\n ),\n );\n };\n\n let patch: AssetsInfoHealingPatch | null = null;\n try {\n patch = healAssetsInfoMetadata(getMigrationState(), state);\n } catch (error) {\n reportError(error);\n }\n\n if (!patch) {\n return state;\n }\n\n try {\n const nextState = cloneDeep(state);\n applyHealingPatch(nextState, patch);\n\n log('Healed wiped assetsInfo metadata for niche-chain tokens', {\n healedAssetsInfoCount: Object.keys(patch.assetsInfo).length,\n healedCustomAssetsAccounts: Object.keys(patch.customAssets).length,\n });\n\n return nextState;\n } catch (error) {\n reportError(error);\n return state;\n }\n}\n\n/**\n * Apply a healing patch to (draft) controller state. Defensive against\n * concurrent writes: fills `assetsInfo` gaps only and dedupes `customAssets`\n * against the current draft rather than trusting the patch blindly.\n *\n * @param state - Mutable controller state copy.\n * @param patch - The additions computed by {@link healAssetsInfoMetadata}.\n */\nfunction applyHealingPatch(\n state: CurrentAssetsState,\n patch: AssetsInfoHealingPatch,\n): void {\n for (const [assetId, metadata] of Object.entries(patch.assetsInfo)) {\n state.assetsInfo[assetId as Caip19AssetId] ??= metadata;\n }\n\n for (const [accountId, assetIds] of Object.entries(patch.customAssets)) {\n const existing = state.customAssets[accountId] ?? [];\n state.customAssets[accountId] = [\n ...existing,\n ...assetIds.filter((assetId) => !existing.includes(assetId)),\n ];\n }\n}\n\nconst EVM_CHAIN_NAMESPACE = 'eip155';\n\nconst EVM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/u;\n\n/**\n * Popular networks (covered by the Accounts API) can self-heal through\n * auto-detection, so their tokens must not be touched. Frozen snapshot from\n * extension migration #215.\n */\nconst ACCOUNT_API_SUPPORTED_CHAIN_IDS: ReadonlySet<string> = new Set([\n 'eip155:1', // Ethereum Mainnet\n 'eip155:10', // Optimism\n 'eip155:56', // BNB Smart Chain\n 'eip155:137', // Polygon\n 'eip155:143', // Monad\n 'eip155:999', // HyperEVM\n 'eip155:1329', // Sei\n 'eip155:5042', // Arc\n 'eip155:8453', // Base\n 'eip155:42161', // Arbitrum One\n 'eip155:43114', // Avalanche\n 'eip155:59144', // Linea\n]);\n\n/**\n * Compute the additions required to heal `assetsInfo` metadata (and custom\n * asset tracking) for tokens on niche EVM chains, based on the legacy\n * `TokensController` state.\n *\n * What it deliberately skips:\n *\n * - Accounts-API-supported chains — see `ACCOUNT_API_SUPPORTED_CHAIN_IDS`.\n * - Tokens the user hid/removed, detected via either the legacy\n * `TokensController.allIgnoredTokens` or the current\n * `assetPreferences` (`hidden: true`).\n * - Non-EVM assets, ERC-721s, and tokens with invalid addresses or metadata.\n *\n * @param legacyState - Untrusted legacy state root. Expected (but not\n * required) to contain `TokensController.allTokens`,\n * `TokensController.allIgnoredTokens`, and\n * `AccountsController.internalAccounts.accounts`.\n * @param currentState - Current `AssetsController` state slices (read-only).\n * @returns The additions to apply, or `null` when there is nothing to heal.\n */\nexport function healAssetsInfoMetadata(\n legacyState: unknown,\n currentState: CurrentAssetsState,\n): AssetsInfoHealingPatch | null {\n const allTokens = readPath(legacyState, ['TokensController', 'allTokens']);\n if (!isObject(allTokens)) {\n return null;\n }\n\n const addressToAccountId = buildAddressToAccountIdMap(legacyState);\n const hiddenAssetIds = collectHiddenAssetIds(currentState);\n const allIgnoredTokens = readPath(legacyState, [\n 'TokensController',\n 'allIgnoredTokens',\n ]);\n\n const assetsInfoAdditions: Record<Caip19AssetId, FungibleAssetMetadata> = {};\n const customAssetAdditions: Record<AccountId, Caip19AssetId[]> = {};\n\n for (const [hexChainId, accountTokens] of Object.entries(allTokens)) {\n if (!isObject(accountTokens)) {\n continue;\n }\n\n const caip2 = hexChainIdToCaip2(hexChainId);\n // Skip chains we can't parse or that self-heal via the accounts API.\n if (!caip2 || ACCOUNT_API_SUPPORTED_CHAIN_IDS.has(caip2)) {\n continue;\n }\n\n for (const [rawAddress, tokens] of Object.entries(accountTokens)) {\n if (!Array.isArray(tokens) || tokens.length === 0) {\n continue;\n }\n\n const accountId = addressToAccountId[rawAddress.toLowerCase()];\n const ignoredAddresses = collectIgnoredAddresses(\n allIgnoredTokens,\n hexChainId,\n rawAddress,\n );\n\n for (const token of tokens) {\n const restorable = getRestorableAsset(\n token,\n caip2,\n ignoredAddresses,\n hiddenAssetIds,\n );\n if (!restorable) {\n continue;\n }\n const { assetId, info } = restorable;\n\n // `assetsInfo` is a global registry; fill gaps only, never overwrite.\n if (\n currentState.assetsInfo[assetId] === undefined &&\n assetsInfoAdditions[assetId] === undefined\n ) {\n assetsInfoAdditions[assetId] = info;\n }\n\n // Ensure the asset is tracked for its account.\n if (accountId) {\n addCustomAssetAddition(\n currentState,\n customAssetAdditions,\n accountId,\n assetId,\n );\n }\n }\n }\n }\n\n if (\n Object.keys(assetsInfoAdditions).length === 0 &&\n Object.keys(customAssetAdditions).length === 0\n ) {\n return null;\n }\n\n return {\n assetsInfo: assetsInfoAdditions,\n customAssets: customAssetAdditions,\n };\n}\n\n/**\n * Resolve a raw `TokensController` token entry to the CAIP-19 asset ID and\n * metadata that should be healed, or `null` when the token must be skipped\n * (not an object, missing/invalid address or symbol, an ERC-721, or hidden\n * in either the legacy or the current controller).\n *\n * @param token - Raw token entry from `allTokens`.\n * @param caip2 - The CAIP-2 chain ID of the token's chain (e.g. 'eip155:14').\n * @param ignoredAddresses - Lowercase addresses hidden via legacy `allIgnoredTokens`.\n * @param hiddenAssetIds - Lowercase asset IDs hidden via current `assetPreferences`.\n * @returns The restorable asset, or `null` to skip.\n */\nfunction getRestorableAsset(\n token: unknown,\n caip2: string,\n ignoredAddresses: Set<string>,\n hiddenAssetIds: Set<string>,\n): { assetId: Caip19AssetId; info: FungibleAssetMetadata } | null {\n if (!isObject(token)) {\n return null;\n }\n // Skip NFTs — AssetsController tracks them separately.\n if (token.isERC721 === true) {\n return null;\n }\n if (\n typeof token.address !== 'string' ||\n !EVM_ADDRESS_REGEX.test(token.address)\n ) {\n return null;\n }\n // Healed metadata with no symbol would be unusable in the UI; skip it.\n if (typeof token.symbol !== 'string' || token.symbol.length === 0) {\n return null;\n }\n\n const assetId = buildErc20AssetId(caip2, token.address as Hex);\n if (!assetId) {\n return null;\n }\n\n // Skip tokens the user hid/removed — in the legacy controller…\n if (ignoredAddresses.has(token.address.toLowerCase())) {\n return null;\n }\n // …or in the current controller.\n if (hiddenAssetIds.has(assetId.toLowerCase())) {\n return null;\n }\n\n return { assetId, info: buildEvmAssetInfo(token) };\n}\n\n/**\n * Build a `FungibleAssetMetadata` from a raw `TokensController` token entry.\n * The caller has already validated `symbol` as a non-empty string.\n *\n * @param token - Raw token object.\n * @returns The metadata to write into `assetsInfo`.\n */\nfunction buildEvmAssetInfo(\n token: Record<string, unknown>,\n): FungibleAssetMetadata {\n const symbol = token.symbol as string;\n const name =\n typeof token.name === 'string' && token.name.length > 0\n ? token.name\n : symbol;\n const decimals =\n typeof token.decimals === 'number' && Number.isFinite(token.decimals)\n ? token.decimals\n : 0;\n\n const image =\n typeof token.image === 'string' && token.image.length > 0\n ? token.image\n : undefined;\n const aggregators = Array.isArray(token.aggregators)\n ? token.aggregators.filter(\n (aggregator): aggregator is string => typeof aggregator === 'string',\n )\n : undefined;\n\n return {\n type: 'erc20',\n symbol,\n name,\n decimals,\n ...(image ? { image } : {}),\n ...(aggregators ? { aggregators } : {}),\n };\n}\n\n/**\n * Build a map from lowercase account address to account UUID using the\n * legacy `AccountsController.internalAccounts.accounts`.\n *\n * @param legacyState - Untrusted legacy state root.\n * @returns Map of lowercase address to account ID (empty on invalid input).\n */\nfunction buildAddressToAccountIdMap(\n legacyState: unknown,\n): Record<string, AccountId> {\n const accounts = readPath(legacyState, [\n 'AccountsController',\n 'internalAccounts',\n 'accounts',\n ]);\n if (!isObject(accounts)) {\n return {};\n }\n\n const map: Record<string, AccountId> = {};\n for (const [id, account] of Object.entries(accounts)) {\n if (\n isObject(account) &&\n typeof account.address === 'string' &&\n account.address.length > 0\n ) {\n map[account.address.toLowerCase()] = id;\n }\n }\n return map;\n}\n\n/**\n * Collect the set of CAIP-19 asset IDs (lowercased) marked `hidden: true` in\n * the current `assetPreferences`. Lowercasing lets callers compare against\n * checksummed asset IDs case-insensitively.\n *\n * @param currentState - Current `AssetsController` state slices.\n * @returns Set of lowercase hidden asset IDs.\n */\nfunction collectHiddenAssetIds(currentState: CurrentAssetsState): Set<string> {\n const hidden = new Set<string>();\n for (const [assetId, preference] of Object.entries(\n currentState.assetPreferences,\n )) {\n if (isObject(preference) && preference.hidden === true) {\n hidden.add(assetId.toLowerCase());\n }\n }\n return hidden;\n}\n\n/**\n * Collect the lowercase token addresses ignored (hidden) in the legacy\n * `TokensController.allIgnoredTokens` for a given chain and account. The\n * account key is matched case-insensitively.\n *\n * @param allIgnoredTokens - The legacy `allIgnoredTokens` map (possibly missing).\n * @param hexChainId - The hex chain ID being processed.\n * @param accountAddress - The account address whose ignored list to read.\n * @returns Set of lowercase ignored token addresses.\n */\nfunction collectIgnoredAddresses(\n allIgnoredTokens: unknown,\n hexChainId: string,\n accountAddress: string,\n): Set<string> {\n const result = new Set<string>();\n if (!isObject(allIgnoredTokens)) {\n return result;\n }\n\n const chainEntry = allIgnoredTokens[hexChainId];\n if (!isObject(chainEntry)) {\n return result;\n }\n\n const lowerAccount = accountAddress.toLowerCase();\n for (const [address, list] of Object.entries(chainEntry)) {\n if (address.toLowerCase() !== lowerAccount || !Array.isArray(list)) {\n continue;\n }\n for (const ignored of list) {\n if (typeof ignored === 'string') {\n result.add(ignored.toLowerCase());\n }\n }\n }\n return result;\n}\n\n/**\n * Convert a hex chain ID (e.g. '0x1') to a CAIP-2 chain ID (e.g. 'eip155:1').\n *\n * @param hexChainId - The hex-encoded EVM chain ID.\n * @returns The CAIP-2 chain ID, or `null` when the input cannot be parsed.\n */\nfunction hexChainIdToCaip2(hexChainId: string): string | null {\n if (!/^0x[0-9a-fA-F]+$/u.test(hexChainId)) {\n return null;\n }\n const decimal = Number.parseInt(hexChainId, 16);\n if (!Number.isFinite(decimal)) {\n return null;\n }\n return `${EVM_CHAIN_NAMESPACE}:${decimal}`;\n}\n\n/**\n * Build the checksummed CAIP-19 asset ID for an ERC-20 token.\n *\n * @param caip2 - The CAIP-2 chain ID (e.g. 'eip155:14').\n * @param tokenAddress - The ERC-20 contract address.\n * @returns The asset ID, or `null` when the address cannot be checksummed.\n */\nfunction buildErc20AssetId(\n caip2: string,\n tokenAddress: Hex,\n): Caip19AssetId | null {\n let checksummed: string;\n try {\n checksummed = getChecksumAddress(tokenAddress);\n } catch {\n return null;\n }\n return `${caip2}/erc20:${checksummed}` as Caip19AssetId;\n}\n\n/**\n * Read a nested path on an unknown value, returning `undefined` if any\n * intermediate key is missing or not an object.\n *\n * @param root - The value to read from.\n * @param path - Sequence of keys to traverse.\n * @returns The value at the path, or `undefined`.\n */\nfunction readPath(root: unknown, path: string[]): unknown {\n return path.reduce<unknown>((cursor, key) => {\n if (!isObject(cursor) || !hasProperty(cursor, key)) {\n return undefined;\n }\n return cursor[key];\n }, root);\n}\n\n/**\n * Record `assetId` as a custom-asset addition for `accountId` unless it is\n * already tracked in the current `assetsBalance` (mutual exclusion), already\n * present in the current `customAssets`, or already queued as an addition.\n *\n * @param currentState - Current `AssetsController` state slices.\n * @param additions - The custom-asset additions accumulated so far (mutated).\n * @param accountId - The account UUID.\n * @param assetId - CAIP-19 asset identifier.\n */\nfunction addCustomAssetAddition(\n currentState: CurrentAssetsState,\n additions: Record<AccountId, Caip19AssetId[]>,\n accountId: AccountId,\n assetId: Caip19AssetId,\n): void {\n if (currentState.assetsBalance[accountId]?.[assetId]) {\n return;\n }\n if (currentState.customAssets[accountId]?.includes(assetId)) {\n return;\n }\n\n const queued = additions[accountId] ?? [];\n if (!queued.includes(assetId)) {\n queued.push(assetId);\n }\n additions[accountId] = queued;\n}\n"]}
@@ -0,0 +1,106 @@
1
+ import type { AccountsControllerState } from "@metamask/accounts-controller";
2
+ import type { TokensControllerState } from "@metamask/assets-controllers";
3
+ import type { AccountId, Caip19AssetId, FungibleAssetMetadata, AssetsControllerStateInternal } from "../types.cjs";
4
+ /**
5
+ * TEMPORARY MODULE — remove in a future release.
6
+ *
7
+ * Port of extension migration #215 (Assets Controller Metadata Healing).
8
+ * Issue: https://consensyssoftware.atlassian.net/browse/ASSETS-3346
9
+ * Incident: #incident-metamask-1731
10
+ *
11
+ * Background: after a prior defect in AssetsController, metadata for custom
12
+ * tokens was wiped. Most popular chains support auto-detection and can
13
+ * self-heal, however not all.
14
+ *
15
+ * This module computes the state additions needed to restore metadata for
16
+ * custom tokens on niche EVM chains (chains that cannot auto-detect and
17
+ * self-heal), using the legacy `TokensController.allTokens` state as the
18
+ * source of truth. It is a pure function so it can be tested in isolation;
19
+ * the controller applies the returned patch in its constructor.
20
+ *
21
+ * The legacy state is treated as fully untrusted (`unknown`): every shape is
22
+ * validated before use, and any unexpected input results in that entry being
23
+ * skipped (or `null` being returned when nothing is restorable).
24
+ */
25
+ /**
26
+ * The slices of current `AssetsController` state the healing computation
27
+ * reads. Never mutated.
28
+ */
29
+ export type CurrentAssetsState = Pick<AssetsControllerStateInternal, 'assetsInfo' | 'assetsBalance' | 'customAssets' | 'assetPreferences'>;
30
+ /**
31
+ * TEMPORARY — will be removed in a future release.
32
+ *
33
+ * Shape of the legacy persisted state the ASSETS-3346 healing reads, derived
34
+ * from the real `TokensControllerState` and `AccountsControllerState` types.
35
+ * Use it to type the `tempMigrateAssetsInfoMetadataAssets3346` getter when
36
+ * integrating in a client.
37
+ *
38
+ * Everything is optional and only the listed slices are read:
39
+ *
40
+ * - `TokensController.allTokens` — required for any healing to happen.
41
+ * - `TokensController.allIgnoredTokens` — tokens the user hid/removed
42
+ * (skipped during healing).
43
+ * - `AccountsController.internalAccounts.accounts` — maps addresses to
44
+ * account IDs so healed tokens are also tracked in `customAssets`
45
+ * (without it only `assetsInfo` is healed).
46
+ *
47
+ * At runtime the value still crosses the boundary as `unknown`: every shape
48
+ * is re-validated by the migration, so a partial or malformed object is safe
49
+ * and simply skipped.
50
+ */
51
+ export type Assets3346MigrationState = {
52
+ TokensController?: Partial<Pick<TokensControllerState, 'allTokens' | 'allIgnoredTokens'>>;
53
+ AccountsController?: {
54
+ internalAccounts?: {
55
+ accounts?: Record<string, Pick<AccountsControllerState['internalAccounts']['accounts'][string], 'address'>>;
56
+ };
57
+ };
58
+ };
59
+ export type AssetsInfoHealingPatch = {
60
+ assetsInfo: Record<Caip19AssetId, FungibleAssetMetadata>;
61
+ customAssets: Record<AccountId, Caip19AssetId[]>;
62
+ };
63
+ export type TempHealAssetsInfoMetadataOptions = {
64
+ /** Current `AssetsController` state the healing patch is computed against. */
65
+ state: AssetsControllerStateInternal;
66
+ /**
67
+ * Host-provided getter for the untrusted legacy state root (see
68
+ * `AssetsControllerOptions.tempMigrateAssetsInfoMetadataAssets3346`).
69
+ */
70
+ getMigrationState: () => unknown;
71
+ /** Optional Sentry-compatible reporter for healing failures. */
72
+ captureException?: (error: Error) => void;
73
+ };
74
+ /**
75
+ * TEMPORARY — will be removed in a future release.
76
+ *
77
+ * @param options - The options bag.
78
+ * @param options.state - Current controller state to compute the patch against.
79
+ * @param options.getMigrationState - Getter for the untrusted legacy state root.
80
+ * @param options.captureException - Optional reporter for healing failures.
81
+ * @returns Updated controller state with the healing patch applied, or the
82
+ * original state when there is nothing to heal or healing fails.
83
+ */
84
+ export declare function tempHealAssetsInfoMetadata({ state, getMigrationState, captureException, }: TempHealAssetsInfoMetadataOptions): AssetsControllerStateInternal;
85
+ /**
86
+ * Compute the additions required to heal `assetsInfo` metadata (and custom
87
+ * asset tracking) for tokens on niche EVM chains, based on the legacy
88
+ * `TokensController` state.
89
+ *
90
+ * What it deliberately skips:
91
+ *
92
+ * - Accounts-API-supported chains — see `ACCOUNT_API_SUPPORTED_CHAIN_IDS`.
93
+ * - Tokens the user hid/removed, detected via either the legacy
94
+ * `TokensController.allIgnoredTokens` or the current
95
+ * `assetPreferences` (`hidden: true`).
96
+ * - Non-EVM assets, ERC-721s, and tokens with invalid addresses or metadata.
97
+ *
98
+ * @param legacyState - Untrusted legacy state root. Expected (but not
99
+ * required) to contain `TokensController.allTokens`,
100
+ * `TokensController.allIgnoredTokens`, and
101
+ * `AccountsController.internalAccounts.accounts`.
102
+ * @param currentState - Current `AssetsController` state slices (read-only).
103
+ * @returns The additions to apply, or `null` when there is nothing to heal.
104
+ */
105
+ export declare function healAssetsInfoMetadata(legacyState: unknown, currentState: CurrentAssetsState): AssetsInfoHealingPatch | null;
106
+ //# sourceMappingURL=healAssetsInfoMetadata.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healAssetsInfoMetadata.d.cts","sourceRoot":"","sources":["../../src/migrations/healAssetsInfoMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sCAAsC;AAC7E,OAAO,KAAK,EAAE,qBAAqB,EAAE,qCAAqC;AAW1E,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,qBAAqB,EACrB,6BAA6B,EAC9B,qBAAiB;AAElB;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,6BAA6B,EAC7B,YAAY,GAAG,eAAe,GAAG,cAAc,GAAG,kBAAkB,CACrE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,wBAAwB,GAAG;IAErC,gBAAgB,CAAC,EAAE,OAAO,CACxB,IAAI,CAAC,qBAAqB,EAAE,WAAW,GAAG,kBAAkB,CAAC,CAC9D,CAAC;IAEF,kBAAkB,CAAC,EAAE;QACnB,gBAAgB,CAAC,EAAE;YACjB,QAAQ,CAAC,EAAE,MAAM,CACf,MAAM,EACN,IAAI,CACF,uBAAuB,CAAC,kBAAkB,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAC/D,SAAS,CACV,CACF,CAAC;SACH,CAAC;KACH,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC;IACzD,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;CAClD,CAAC;AAIF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,8EAA8E;IAC9E,KAAK,EAAE,6BAA6B,CAAC;IACrC;;;OAGG;IACH,iBAAiB,EAAE,MAAM,OAAO,CAAC;IACjC,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,KAAK,EACL,iBAAiB,EACjB,gBAAgB,GACjB,EAAE,iCAAiC,GAAG,6BAA6B,CAqCnE;AAmDD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,kBAAkB,GAC/B,sBAAsB,GAAG,IAAI,CAmF/B"}
@@ -0,0 +1,106 @@
1
+ import type { AccountsControllerState } from "@metamask/accounts-controller";
2
+ import type { TokensControllerState } from "@metamask/assets-controllers";
3
+ import type { AccountId, Caip19AssetId, FungibleAssetMetadata, AssetsControllerStateInternal } from "../types.mjs";
4
+ /**
5
+ * TEMPORARY MODULE — remove in a future release.
6
+ *
7
+ * Port of extension migration #215 (Assets Controller Metadata Healing).
8
+ * Issue: https://consensyssoftware.atlassian.net/browse/ASSETS-3346
9
+ * Incident: #incident-metamask-1731
10
+ *
11
+ * Background: after a prior defect in AssetsController, metadata for custom
12
+ * tokens was wiped. Most popular chains support auto-detection and can
13
+ * self-heal, however not all.
14
+ *
15
+ * This module computes the state additions needed to restore metadata for
16
+ * custom tokens on niche EVM chains (chains that cannot auto-detect and
17
+ * self-heal), using the legacy `TokensController.allTokens` state as the
18
+ * source of truth. It is a pure function so it can be tested in isolation;
19
+ * the controller applies the returned patch in its constructor.
20
+ *
21
+ * The legacy state is treated as fully untrusted (`unknown`): every shape is
22
+ * validated before use, and any unexpected input results in that entry being
23
+ * skipped (or `null` being returned when nothing is restorable).
24
+ */
25
+ /**
26
+ * The slices of current `AssetsController` state the healing computation
27
+ * reads. Never mutated.
28
+ */
29
+ export type CurrentAssetsState = Pick<AssetsControllerStateInternal, 'assetsInfo' | 'assetsBalance' | 'customAssets' | 'assetPreferences'>;
30
+ /**
31
+ * TEMPORARY — will be removed in a future release.
32
+ *
33
+ * Shape of the legacy persisted state the ASSETS-3346 healing reads, derived
34
+ * from the real `TokensControllerState` and `AccountsControllerState` types.
35
+ * Use it to type the `tempMigrateAssetsInfoMetadataAssets3346` getter when
36
+ * integrating in a client.
37
+ *
38
+ * Everything is optional and only the listed slices are read:
39
+ *
40
+ * - `TokensController.allTokens` — required for any healing to happen.
41
+ * - `TokensController.allIgnoredTokens` — tokens the user hid/removed
42
+ * (skipped during healing).
43
+ * - `AccountsController.internalAccounts.accounts` — maps addresses to
44
+ * account IDs so healed tokens are also tracked in `customAssets`
45
+ * (without it only `assetsInfo` is healed).
46
+ *
47
+ * At runtime the value still crosses the boundary as `unknown`: every shape
48
+ * is re-validated by the migration, so a partial or malformed object is safe
49
+ * and simply skipped.
50
+ */
51
+ export type Assets3346MigrationState = {
52
+ TokensController?: Partial<Pick<TokensControllerState, 'allTokens' | 'allIgnoredTokens'>>;
53
+ AccountsController?: {
54
+ internalAccounts?: {
55
+ accounts?: Record<string, Pick<AccountsControllerState['internalAccounts']['accounts'][string], 'address'>>;
56
+ };
57
+ };
58
+ };
59
+ export type AssetsInfoHealingPatch = {
60
+ assetsInfo: Record<Caip19AssetId, FungibleAssetMetadata>;
61
+ customAssets: Record<AccountId, Caip19AssetId[]>;
62
+ };
63
+ export type TempHealAssetsInfoMetadataOptions = {
64
+ /** Current `AssetsController` state the healing patch is computed against. */
65
+ state: AssetsControllerStateInternal;
66
+ /**
67
+ * Host-provided getter for the untrusted legacy state root (see
68
+ * `AssetsControllerOptions.tempMigrateAssetsInfoMetadataAssets3346`).
69
+ */
70
+ getMigrationState: () => unknown;
71
+ /** Optional Sentry-compatible reporter for healing failures. */
72
+ captureException?: (error: Error) => void;
73
+ };
74
+ /**
75
+ * TEMPORARY — will be removed in a future release.
76
+ *
77
+ * @param options - The options bag.
78
+ * @param options.state - Current controller state to compute the patch against.
79
+ * @param options.getMigrationState - Getter for the untrusted legacy state root.
80
+ * @param options.captureException - Optional reporter for healing failures.
81
+ * @returns Updated controller state with the healing patch applied, or the
82
+ * original state when there is nothing to heal or healing fails.
83
+ */
84
+ export declare function tempHealAssetsInfoMetadata({ state, getMigrationState, captureException, }: TempHealAssetsInfoMetadataOptions): AssetsControllerStateInternal;
85
+ /**
86
+ * Compute the additions required to heal `assetsInfo` metadata (and custom
87
+ * asset tracking) for tokens on niche EVM chains, based on the legacy
88
+ * `TokensController` state.
89
+ *
90
+ * What it deliberately skips:
91
+ *
92
+ * - Accounts-API-supported chains — see `ACCOUNT_API_SUPPORTED_CHAIN_IDS`.
93
+ * - Tokens the user hid/removed, detected via either the legacy
94
+ * `TokensController.allIgnoredTokens` or the current
95
+ * `assetPreferences` (`hidden: true`).
96
+ * - Non-EVM assets, ERC-721s, and tokens with invalid addresses or metadata.
97
+ *
98
+ * @param legacyState - Untrusted legacy state root. Expected (but not
99
+ * required) to contain `TokensController.allTokens`,
100
+ * `TokensController.allIgnoredTokens`, and
101
+ * `AccountsController.internalAccounts.accounts`.
102
+ * @param currentState - Current `AssetsController` state slices (read-only).
103
+ * @returns The additions to apply, or `null` when there is nothing to heal.
104
+ */
105
+ export declare function healAssetsInfoMetadata(legacyState: unknown, currentState: CurrentAssetsState): AssetsInfoHealingPatch | null;
106
+ //# sourceMappingURL=healAssetsInfoMetadata.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healAssetsInfoMetadata.d.mts","sourceRoot":"","sources":["../../src/migrations/healAssetsInfoMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sCAAsC;AAC7E,OAAO,KAAK,EAAE,qBAAqB,EAAE,qCAAqC;AAW1E,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,qBAAqB,EACrB,6BAA6B,EAC9B,qBAAiB;AAElB;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,6BAA6B,EAC7B,YAAY,GAAG,eAAe,GAAG,cAAc,GAAG,kBAAkB,CACrE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,wBAAwB,GAAG;IAErC,gBAAgB,CAAC,EAAE,OAAO,CACxB,IAAI,CAAC,qBAAqB,EAAE,WAAW,GAAG,kBAAkB,CAAC,CAC9D,CAAC;IAEF,kBAAkB,CAAC,EAAE;QACnB,gBAAgB,CAAC,EAAE;YACjB,QAAQ,CAAC,EAAE,MAAM,CACf,MAAM,EACN,IAAI,CACF,uBAAuB,CAAC,kBAAkB,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAC/D,SAAS,CACV,CACF,CAAC;SACH,CAAC;KACH,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC;IACzD,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;CAClD,CAAC;AAIF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,8EAA8E;IAC9E,KAAK,EAAE,6BAA6B,CAAC;IACrC;;;OAGG;IACH,iBAAiB,EAAE,MAAM,OAAO,CAAC;IACjC,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,KAAK,EACL,iBAAiB,EACjB,gBAAgB,GACjB,EAAE,iCAAiC,GAAG,6BAA6B,CAqCnE;AAmDD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,kBAAkB,GAC/B,sBAAsB,GAAG,IAAI,CAmF/B"}
@@ -0,0 +1,381 @@
1
+ import { getChecksumAddress, getErrorMessage, hasProperty, isObject } from "@metamask/utils";
2
+ import $lodash from "lodash";
3
+ const { cloneDeep } = $lodash;
4
+ import { createModuleLogger, projectLogger } from "../logger.mjs";
5
+ const log = createModuleLogger(projectLogger, 'tempHealAssetsInfoMetadata');
6
+ /**
7
+ * TEMPORARY — will be removed in a future release.
8
+ *
9
+ * @param options - The options bag.
10
+ * @param options.state - Current controller state to compute the patch against.
11
+ * @param options.getMigrationState - Getter for the untrusted legacy state root.
12
+ * @param options.captureException - Optional reporter for healing failures.
13
+ * @returns Updated controller state with the healing patch applied, or the
14
+ * original state when there is nothing to heal or healing fails.
15
+ */
16
+ export function tempHealAssetsInfoMetadata({ state, getMigrationState, captureException, }) {
17
+ const reportError = (error) => {
18
+ log('Failed to heal assetsInfo metadata', error);
19
+ captureException?.(new Error(`AssetsController: temporary assetsInfo metadata healing failed: ${getErrorMessage(error)}`));
20
+ };
21
+ let patch = null;
22
+ try {
23
+ patch = healAssetsInfoMetadata(getMigrationState(), state);
24
+ }
25
+ catch (error) {
26
+ reportError(error);
27
+ }
28
+ if (!patch) {
29
+ return state;
30
+ }
31
+ try {
32
+ const nextState = cloneDeep(state);
33
+ applyHealingPatch(nextState, patch);
34
+ log('Healed wiped assetsInfo metadata for niche-chain tokens', {
35
+ healedAssetsInfoCount: Object.keys(patch.assetsInfo).length,
36
+ healedCustomAssetsAccounts: Object.keys(patch.customAssets).length,
37
+ });
38
+ return nextState;
39
+ }
40
+ catch (error) {
41
+ reportError(error);
42
+ return state;
43
+ }
44
+ }
45
+ /**
46
+ * Apply a healing patch to (draft) controller state. Defensive against
47
+ * concurrent writes: fills `assetsInfo` gaps only and dedupes `customAssets`
48
+ * against the current draft rather than trusting the patch blindly.
49
+ *
50
+ * @param state - Mutable controller state copy.
51
+ * @param patch - The additions computed by {@link healAssetsInfoMetadata}.
52
+ */
53
+ function applyHealingPatch(state, patch) {
54
+ var _a, _b;
55
+ for (const [assetId, metadata] of Object.entries(patch.assetsInfo)) {
56
+ (_a = state.assetsInfo)[_b = assetId] ?? (_a[_b] = metadata);
57
+ }
58
+ for (const [accountId, assetIds] of Object.entries(patch.customAssets)) {
59
+ const existing = state.customAssets[accountId] ?? [];
60
+ state.customAssets[accountId] = [
61
+ ...existing,
62
+ ...assetIds.filter((assetId) => !existing.includes(assetId)),
63
+ ];
64
+ }
65
+ }
66
+ const EVM_CHAIN_NAMESPACE = 'eip155';
67
+ const EVM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/u;
68
+ /**
69
+ * Popular networks (covered by the Accounts API) can self-heal through
70
+ * auto-detection, so their tokens must not be touched. Frozen snapshot from
71
+ * extension migration #215.
72
+ */
73
+ const ACCOUNT_API_SUPPORTED_CHAIN_IDS = new Set([
74
+ 'eip155:1', // Ethereum Mainnet
75
+ 'eip155:10', // Optimism
76
+ 'eip155:56', // BNB Smart Chain
77
+ 'eip155:137', // Polygon
78
+ 'eip155:143', // Monad
79
+ 'eip155:999', // HyperEVM
80
+ 'eip155:1329', // Sei
81
+ 'eip155:5042', // Arc
82
+ 'eip155:8453', // Base
83
+ 'eip155:42161', // Arbitrum One
84
+ 'eip155:43114', // Avalanche
85
+ 'eip155:59144', // Linea
86
+ ]);
87
+ /**
88
+ * Compute the additions required to heal `assetsInfo` metadata (and custom
89
+ * asset tracking) for tokens on niche EVM chains, based on the legacy
90
+ * `TokensController` state.
91
+ *
92
+ * What it deliberately skips:
93
+ *
94
+ * - Accounts-API-supported chains — see `ACCOUNT_API_SUPPORTED_CHAIN_IDS`.
95
+ * - Tokens the user hid/removed, detected via either the legacy
96
+ * `TokensController.allIgnoredTokens` or the current
97
+ * `assetPreferences` (`hidden: true`).
98
+ * - Non-EVM assets, ERC-721s, and tokens with invalid addresses or metadata.
99
+ *
100
+ * @param legacyState - Untrusted legacy state root. Expected (but not
101
+ * required) to contain `TokensController.allTokens`,
102
+ * `TokensController.allIgnoredTokens`, and
103
+ * `AccountsController.internalAccounts.accounts`.
104
+ * @param currentState - Current `AssetsController` state slices (read-only).
105
+ * @returns The additions to apply, or `null` when there is nothing to heal.
106
+ */
107
+ export function healAssetsInfoMetadata(legacyState, currentState) {
108
+ const allTokens = readPath(legacyState, ['TokensController', 'allTokens']);
109
+ if (!isObject(allTokens)) {
110
+ return null;
111
+ }
112
+ const addressToAccountId = buildAddressToAccountIdMap(legacyState);
113
+ const hiddenAssetIds = collectHiddenAssetIds(currentState);
114
+ const allIgnoredTokens = readPath(legacyState, [
115
+ 'TokensController',
116
+ 'allIgnoredTokens',
117
+ ]);
118
+ const assetsInfoAdditions = {};
119
+ const customAssetAdditions = {};
120
+ for (const [hexChainId, accountTokens] of Object.entries(allTokens)) {
121
+ if (!isObject(accountTokens)) {
122
+ continue;
123
+ }
124
+ const caip2 = hexChainIdToCaip2(hexChainId);
125
+ // Skip chains we can't parse or that self-heal via the accounts API.
126
+ if (!caip2 || ACCOUNT_API_SUPPORTED_CHAIN_IDS.has(caip2)) {
127
+ continue;
128
+ }
129
+ for (const [rawAddress, tokens] of Object.entries(accountTokens)) {
130
+ if (!Array.isArray(tokens) || tokens.length === 0) {
131
+ continue;
132
+ }
133
+ const accountId = addressToAccountId[rawAddress.toLowerCase()];
134
+ const ignoredAddresses = collectIgnoredAddresses(allIgnoredTokens, hexChainId, rawAddress);
135
+ for (const token of tokens) {
136
+ const restorable = getRestorableAsset(token, caip2, ignoredAddresses, hiddenAssetIds);
137
+ if (!restorable) {
138
+ continue;
139
+ }
140
+ const { assetId, info } = restorable;
141
+ // `assetsInfo` is a global registry; fill gaps only, never overwrite.
142
+ if (currentState.assetsInfo[assetId] === undefined &&
143
+ assetsInfoAdditions[assetId] === undefined) {
144
+ assetsInfoAdditions[assetId] = info;
145
+ }
146
+ // Ensure the asset is tracked for its account.
147
+ if (accountId) {
148
+ addCustomAssetAddition(currentState, customAssetAdditions, accountId, assetId);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ if (Object.keys(assetsInfoAdditions).length === 0 &&
154
+ Object.keys(customAssetAdditions).length === 0) {
155
+ return null;
156
+ }
157
+ return {
158
+ assetsInfo: assetsInfoAdditions,
159
+ customAssets: customAssetAdditions,
160
+ };
161
+ }
162
+ /**
163
+ * Resolve a raw `TokensController` token entry to the CAIP-19 asset ID and
164
+ * metadata that should be healed, or `null` when the token must be skipped
165
+ * (not an object, missing/invalid address or symbol, an ERC-721, or hidden
166
+ * in either the legacy or the current controller).
167
+ *
168
+ * @param token - Raw token entry from `allTokens`.
169
+ * @param caip2 - The CAIP-2 chain ID of the token's chain (e.g. 'eip155:14').
170
+ * @param ignoredAddresses - Lowercase addresses hidden via legacy `allIgnoredTokens`.
171
+ * @param hiddenAssetIds - Lowercase asset IDs hidden via current `assetPreferences`.
172
+ * @returns The restorable asset, or `null` to skip.
173
+ */
174
+ function getRestorableAsset(token, caip2, ignoredAddresses, hiddenAssetIds) {
175
+ if (!isObject(token)) {
176
+ return null;
177
+ }
178
+ // Skip NFTs — AssetsController tracks them separately.
179
+ if (token.isERC721 === true) {
180
+ return null;
181
+ }
182
+ if (typeof token.address !== 'string' ||
183
+ !EVM_ADDRESS_REGEX.test(token.address)) {
184
+ return null;
185
+ }
186
+ // Healed metadata with no symbol would be unusable in the UI; skip it.
187
+ if (typeof token.symbol !== 'string' || token.symbol.length === 0) {
188
+ return null;
189
+ }
190
+ const assetId = buildErc20AssetId(caip2, token.address);
191
+ if (!assetId) {
192
+ return null;
193
+ }
194
+ // Skip tokens the user hid/removed — in the legacy controller…
195
+ if (ignoredAddresses.has(token.address.toLowerCase())) {
196
+ return null;
197
+ }
198
+ // …or in the current controller.
199
+ if (hiddenAssetIds.has(assetId.toLowerCase())) {
200
+ return null;
201
+ }
202
+ return { assetId, info: buildEvmAssetInfo(token) };
203
+ }
204
+ /**
205
+ * Build a `FungibleAssetMetadata` from a raw `TokensController` token entry.
206
+ * The caller has already validated `symbol` as a non-empty string.
207
+ *
208
+ * @param token - Raw token object.
209
+ * @returns The metadata to write into `assetsInfo`.
210
+ */
211
+ function buildEvmAssetInfo(token) {
212
+ const symbol = token.symbol;
213
+ const name = typeof token.name === 'string' && token.name.length > 0
214
+ ? token.name
215
+ : symbol;
216
+ const decimals = typeof token.decimals === 'number' && Number.isFinite(token.decimals)
217
+ ? token.decimals
218
+ : 0;
219
+ const image = typeof token.image === 'string' && token.image.length > 0
220
+ ? token.image
221
+ : undefined;
222
+ const aggregators = Array.isArray(token.aggregators)
223
+ ? token.aggregators.filter((aggregator) => typeof aggregator === 'string')
224
+ : undefined;
225
+ return {
226
+ type: 'erc20',
227
+ symbol,
228
+ name,
229
+ decimals,
230
+ ...(image ? { image } : {}),
231
+ ...(aggregators ? { aggregators } : {}),
232
+ };
233
+ }
234
+ /**
235
+ * Build a map from lowercase account address to account UUID using the
236
+ * legacy `AccountsController.internalAccounts.accounts`.
237
+ *
238
+ * @param legacyState - Untrusted legacy state root.
239
+ * @returns Map of lowercase address to account ID (empty on invalid input).
240
+ */
241
+ function buildAddressToAccountIdMap(legacyState) {
242
+ const accounts = readPath(legacyState, [
243
+ 'AccountsController',
244
+ 'internalAccounts',
245
+ 'accounts',
246
+ ]);
247
+ if (!isObject(accounts)) {
248
+ return {};
249
+ }
250
+ const map = {};
251
+ for (const [id, account] of Object.entries(accounts)) {
252
+ if (isObject(account) &&
253
+ typeof account.address === 'string' &&
254
+ account.address.length > 0) {
255
+ map[account.address.toLowerCase()] = id;
256
+ }
257
+ }
258
+ return map;
259
+ }
260
+ /**
261
+ * Collect the set of CAIP-19 asset IDs (lowercased) marked `hidden: true` in
262
+ * the current `assetPreferences`. Lowercasing lets callers compare against
263
+ * checksummed asset IDs case-insensitively.
264
+ *
265
+ * @param currentState - Current `AssetsController` state slices.
266
+ * @returns Set of lowercase hidden asset IDs.
267
+ */
268
+ function collectHiddenAssetIds(currentState) {
269
+ const hidden = new Set();
270
+ for (const [assetId, preference] of Object.entries(currentState.assetPreferences)) {
271
+ if (isObject(preference) && preference.hidden === true) {
272
+ hidden.add(assetId.toLowerCase());
273
+ }
274
+ }
275
+ return hidden;
276
+ }
277
+ /**
278
+ * Collect the lowercase token addresses ignored (hidden) in the legacy
279
+ * `TokensController.allIgnoredTokens` for a given chain and account. The
280
+ * account key is matched case-insensitively.
281
+ *
282
+ * @param allIgnoredTokens - The legacy `allIgnoredTokens` map (possibly missing).
283
+ * @param hexChainId - The hex chain ID being processed.
284
+ * @param accountAddress - The account address whose ignored list to read.
285
+ * @returns Set of lowercase ignored token addresses.
286
+ */
287
+ function collectIgnoredAddresses(allIgnoredTokens, hexChainId, accountAddress) {
288
+ const result = new Set();
289
+ if (!isObject(allIgnoredTokens)) {
290
+ return result;
291
+ }
292
+ const chainEntry = allIgnoredTokens[hexChainId];
293
+ if (!isObject(chainEntry)) {
294
+ return result;
295
+ }
296
+ const lowerAccount = accountAddress.toLowerCase();
297
+ for (const [address, list] of Object.entries(chainEntry)) {
298
+ if (address.toLowerCase() !== lowerAccount || !Array.isArray(list)) {
299
+ continue;
300
+ }
301
+ for (const ignored of list) {
302
+ if (typeof ignored === 'string') {
303
+ result.add(ignored.toLowerCase());
304
+ }
305
+ }
306
+ }
307
+ return result;
308
+ }
309
+ /**
310
+ * Convert a hex chain ID (e.g. '0x1') to a CAIP-2 chain ID (e.g. 'eip155:1').
311
+ *
312
+ * @param hexChainId - The hex-encoded EVM chain ID.
313
+ * @returns The CAIP-2 chain ID, or `null` when the input cannot be parsed.
314
+ */
315
+ function hexChainIdToCaip2(hexChainId) {
316
+ if (!/^0x[0-9a-fA-F]+$/u.test(hexChainId)) {
317
+ return null;
318
+ }
319
+ const decimal = Number.parseInt(hexChainId, 16);
320
+ if (!Number.isFinite(decimal)) {
321
+ return null;
322
+ }
323
+ return `${EVM_CHAIN_NAMESPACE}:${decimal}`;
324
+ }
325
+ /**
326
+ * Build the checksummed CAIP-19 asset ID for an ERC-20 token.
327
+ *
328
+ * @param caip2 - The CAIP-2 chain ID (e.g. 'eip155:14').
329
+ * @param tokenAddress - The ERC-20 contract address.
330
+ * @returns The asset ID, or `null` when the address cannot be checksummed.
331
+ */
332
+ function buildErc20AssetId(caip2, tokenAddress) {
333
+ let checksummed;
334
+ try {
335
+ checksummed = getChecksumAddress(tokenAddress);
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ return `${caip2}/erc20:${checksummed}`;
341
+ }
342
+ /**
343
+ * Read a nested path on an unknown value, returning `undefined` if any
344
+ * intermediate key is missing or not an object.
345
+ *
346
+ * @param root - The value to read from.
347
+ * @param path - Sequence of keys to traverse.
348
+ * @returns The value at the path, or `undefined`.
349
+ */
350
+ function readPath(root, path) {
351
+ return path.reduce((cursor, key) => {
352
+ if (!isObject(cursor) || !hasProperty(cursor, key)) {
353
+ return undefined;
354
+ }
355
+ return cursor[key];
356
+ }, root);
357
+ }
358
+ /**
359
+ * Record `assetId` as a custom-asset addition for `accountId` unless it is
360
+ * already tracked in the current `assetsBalance` (mutual exclusion), already
361
+ * present in the current `customAssets`, or already queued as an addition.
362
+ *
363
+ * @param currentState - Current `AssetsController` state slices.
364
+ * @param additions - The custom-asset additions accumulated so far (mutated).
365
+ * @param accountId - The account UUID.
366
+ * @param assetId - CAIP-19 asset identifier.
367
+ */
368
+ function addCustomAssetAddition(currentState, additions, accountId, assetId) {
369
+ if (currentState.assetsBalance[accountId]?.[assetId]) {
370
+ return;
371
+ }
372
+ if (currentState.customAssets[accountId]?.includes(assetId)) {
373
+ return;
374
+ }
375
+ const queued = additions[accountId] ?? [];
376
+ if (!queued.includes(assetId)) {
377
+ queued.push(assetId);
378
+ }
379
+ additions[accountId] = queued;
380
+ }
381
+ //# sourceMappingURL=healAssetsInfoMetadata.mjs.map