@metamask-previews/assets-controllers 99.3.2-preview-b271ae9 → 99.3.2-preview-1a74d12

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Add Blockaid token security scanning to `MultichainAssetsController` to filter out spam, malicious, and warning tokens during automatic asset detection ([#TBD](https://github.com/MetaMask/core/pull/7923))
13
+ - Tokens with `assetNamespace` of "token" (e.g. SPL tokens) are scanned via the Blockaid bulk token scan API
14
+ - Only tokens with a `Benign` result are kept; native assets (e.g. `slip44`) are not scanned
15
+ - The filter fails open: if the API is unreachable or returns an error, all tokens are kept
16
+ - Filtering applies to account-added and asset-list-updated events; `addAssets` (curated list) is not filtered
17
+
10
18
  ## [99.3.2]
11
19
 
12
20
  ### Changed
@@ -10,10 +10,11 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _MultichainAssetsController_instances, _MultichainAssetsController_snaps, _MultichainAssetsController_controllerOperationMutex, _MultichainAssetsController_handleAccountAssetListUpdatedEvent, _MultichainAssetsController_handleOnAccountAddedEvent, _MultichainAssetsController_registerMessageHandlers, _MultichainAssetsController_isAssetIgnored, _MultichainAssetsController_handleAccountAssetListUpdated, _MultichainAssetsController_isNonEvmAccount, _MultichainAssetsController_handleOnAccountAdded, _MultichainAssetsController_handleOnAccountRemovedEvent, _MultichainAssetsController_refreshAssetsMetadata, _MultichainAssetsController_updateAssetsMetadata, _MultichainAssetsController_getAssetSnaps, _MultichainAssetsController_getAssetSnapFor, _MultichainAssetsController_getAllSnaps, _MultichainAssetsController_getSnapsPermissions, _MultichainAssetsController_getAssetsMetadataFrom, _MultichainAssetsController_getAssetsList, _MultichainAssetsController_getClient, _MultichainAssetsController_assertControllerMutexIsLocked, _MultichainAssetsController_withControllerLock;
13
+ var _MultichainAssetsController_instances, _MultichainAssetsController_snaps, _MultichainAssetsController_controllerOperationMutex, _MultichainAssetsController_handleAccountAssetListUpdatedEvent, _MultichainAssetsController_handleOnAccountAddedEvent, _MultichainAssetsController_registerMessageHandlers, _MultichainAssetsController_isAssetIgnored, _MultichainAssetsController_handleAccountAssetListUpdated, _MultichainAssetsController_isNonEvmAccount, _MultichainAssetsController_handleOnAccountAdded, _MultichainAssetsController_handleOnAccountRemovedEvent, _MultichainAssetsController_refreshAssetsMetadata, _MultichainAssetsController_updateAssetsMetadata, _MultichainAssetsController_getAssetSnaps, _MultichainAssetsController_getAssetSnapFor, _MultichainAssetsController_getAllSnaps, _MultichainAssetsController_getSnapsPermissions, _MultichainAssetsController_getAssetsMetadataFrom, _MultichainAssetsController_scanBlockaidTokens, _MultichainAssetsController_filterBlockaidSpamTokens, _MultichainAssetsController_getAssetsList, _MultichainAssetsController_getClient, _MultichainAssetsController_assertControllerMutexIsLocked, _MultichainAssetsController_withControllerLock;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.MultichainAssetsController = exports.getDefaultMultichainAssetsControllerState = void 0;
15
+ exports.MultichainAssetsController = exports.getDefaultMultichainAssetsControllerState = exports.BlockaidResultType = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
+ const controller_utils_1 = require("@metamask/controller-utils");
17
18
  const keyring_api_1 = require("@metamask/keyring-api");
18
19
  const keyring_snap_client_1 = require("@metamask/keyring-snap-client");
19
20
  const snaps_utils_1 = require("@metamask/snaps-utils");
@@ -21,6 +22,19 @@ const utils_1 = require("@metamask/utils");
21
22
  const async_mutex_1 = require("async-mutex");
22
23
  const utils_2 = require("./utils.cjs");
23
24
  const controllerName = 'MultichainAssetsController';
25
+ // Blockaid token security scanning constants
26
+ const SECURITY_ALERTS_BASE_URL = 'https://security-alerts.api.cx.metamask.io';
27
+ const TOKEN_BULK_SCANNING_ENDPOINT = '/token/scan-bulk';
28
+ /**
29
+ * Result type from Blockaid token security scan.
30
+ */
31
+ var BlockaidResultType;
32
+ (function (BlockaidResultType) {
33
+ BlockaidResultType["Benign"] = "Benign";
34
+ BlockaidResultType["Spam"] = "Spam";
35
+ BlockaidResultType["Warning"] = "Warning";
36
+ BlockaidResultType["Malicious"] = "Malicious";
37
+ })(BlockaidResultType || (exports.BlockaidResultType = BlockaidResultType = {}));
24
38
  /**
25
39
  * Constructs the default {@link MultichainAssetsController} state. This allows
26
40
  * consumers to provide a partial state object when initializing the controller
@@ -193,9 +207,11 @@ async function _MultichainAssetsController_handleAccountAssetListUpdated(event)
193
207
  const existing = this.state.accountsAssets[accountId] || [];
194
208
  // In case accountsAndAssetsToUpdate event is fired with "added" assets that already exist, we don't want to add them again
195
209
  // Also filter out ignored assets
196
- const filteredToBeAddedAssets = added.filter((asset) => !existing.includes(asset) &&
210
+ const preFilteredToBeAddedAssets = added.filter((asset) => !existing.includes(asset) &&
197
211
  (0, utils_1.isCaipAssetType)(asset) &&
198
212
  !__classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_isAssetIgnored).call(this, asset, accountId));
213
+ // Filter out tokens flagged by Blockaid as non-benign
214
+ const filteredToBeAddedAssets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_filterBlockaidSpamTokens).call(this, preFilteredToBeAddedAssets);
199
215
  // In case accountsAndAssetsToUpdate event is fired with "removed" assets that don't exist, we don't want to remove them
200
216
  const filteredToBeRemovedAssets = removed.filter((asset) => existing.includes(asset) && (0, utils_1.isCaipAssetType)(asset));
201
217
  if (filteredToBeAddedAssets.length > 0 ||
@@ -251,7 +267,8 @@ async function _MultichainAssetsController_handleOnAccountAdded(account) {
251
267
  __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_assertControllerMutexIsLocked).call(this);
252
268
  // Get assets list
253
269
  if (account.metadata.snap) {
254
- const assets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_getAssetsList).call(this, account.id, account.metadata.snap.id);
270
+ const allAssets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_getAssetsList).call(this, account.id, account.metadata.snap.id);
271
+ const assets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_filterBlockaidSpamTokens).call(this, allAssets);
255
272
  await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_refreshAssetsMetadata).call(this, assets);
256
273
  this.update((state) => {
257
274
  state.accountsAssets[account.id] = assets;
@@ -396,6 +413,77 @@ async function _MultichainAssetsController_getAssetsMetadataFrom(assets, snapId)
396
413
  console.error(error);
397
414
  return undefined;
398
415
  }
416
+ }, _MultichainAssetsController_scanBlockaidTokens =
417
+ /**
418
+ * Calls the Blockaid token bulk scanning endpoint for a given chain and
419
+ * set of token addresses.
420
+ *
421
+ * @param chain - The chain name for the Blockaid API (e.g. "solana").
422
+ * @param tokens - The token addresses to scan.
423
+ * @returns The scan response, or null if the request fails or times out.
424
+ */
425
+ async function _MultichainAssetsController_scanBlockaidTokens(chain, tokens) {
426
+ try {
427
+ return await (0, controller_utils_1.handleFetch)(`${SECURITY_ALERTS_BASE_URL}${TOKEN_BULK_SCANNING_ENDPOINT}`, {
428
+ method: 'POST',
429
+ headers: {
430
+ Accept: 'application/json',
431
+ 'Content-Type': 'application/json',
432
+ },
433
+ body: JSON.stringify({ chain, tokens }),
434
+ });
435
+ }
436
+ catch (error) {
437
+ console.error('Blockaid token scan failed:', error);
438
+ return null;
439
+ }
440
+ }, _MultichainAssetsController_filterBlockaidSpamTokens =
441
+ /**
442
+ * Filters out tokens flagged as non-benign by Blockaid. Only tokens with
443
+ * an `assetNamespace` of "token" are scanned (native assets like slip44 are
444
+ * passed through unfiltered). If the API call fails, all tokens are kept.
445
+ *
446
+ * @param assets - The CAIP asset type list to filter.
447
+ * @returns The filtered list with malicious/spam/warning tokens removed.
448
+ */
449
+ async function _MultichainAssetsController_filterBlockaidSpamTokens(assets) {
450
+ // Group scannable token assets by chain namespace
451
+ const tokensByChain = {};
452
+ for (const asset of assets) {
453
+ const { assetNamespace, assetReference, chain } = (0, utils_1.parseCaipAssetType)(asset);
454
+ // Only scan fungible token assets (e.g. SPL tokens), skip native (slip44)
455
+ if (assetNamespace === 'token') {
456
+ const chainName = chain.namespace;
457
+ if (!tokensByChain[chainName]) {
458
+ tokensByChain[chainName] = [];
459
+ }
460
+ tokensByChain[chainName].push({ asset, address: assetReference });
461
+ }
462
+ }
463
+ // If there are no token assets to scan, return as-is
464
+ if (Object.keys(tokensByChain).length === 0) {
465
+ return assets;
466
+ }
467
+ // Build a set of assets to reject (non-benign tokens)
468
+ const rejectedAssets = new Set();
469
+ for (const [chainName, tokenEntries] of Object.entries(tokensByChain)) {
470
+ const addresses = tokenEntries.map((entry) => entry.address);
471
+ const scanResponse = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_scanBlockaidTokens).call(this, chainName, addresses);
472
+ if (!scanResponse?.results) {
473
+ // If the scan fails, keep all tokens (fail open)
474
+ continue;
475
+ }
476
+ for (const entry of tokenEntries) {
477
+ const result = scanResponse.results[entry.address];
478
+ // Reject the token only if we have a definitive non-benign result
479
+ if (result?.result_type &&
480
+ result.result_type !== BlockaidResultType.Benign) {
481
+ rejectedAssets.add(entry.asset);
482
+ }
483
+ }
484
+ }
485
+ // Filter while preserving original order
486
+ return assets.filter((asset) => !rejectedAssets.has(asset));
399
487
  }, _MultichainAssetsController_getAssetsList =
400
488
  /**
401
489
  * Get assets list for an account
@@ -1 +1 @@
1
- {"version":3,"file":"MultichainAssetsController.cjs","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AccountsControllerAccountAddedEvent,\n AccountsControllerAccountAssetListUpdatedEvent,\n AccountsControllerAccountRemovedEvent,\n AccountsControllerListMultichainAccountsAction,\n} from '@metamask/accounts-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { isEvmAccountType } from '@metamask/keyring-api';\nimport type {\n AccountAssetListUpdatedEventPayload,\n CaipAssetType,\n CaipAssetTypeOrId,\n} from '@metamask/keyring-api';\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\nimport { KeyringClient } from '@metamask/keyring-snap-client';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n GetPermissions,\n PermissionConstraint,\n SubjectPermissions,\n} from '@metamask/permission-controller';\nimport type {\n GetAllSnaps,\n HandleSnapRequest,\n} from '@metamask/snaps-controllers';\nimport type { FungibleAssetMetadata, Snap, SnapId } from '@metamask/snaps-sdk';\nimport { HandlerType } from '@metamask/snaps-utils';\nimport { isCaipAssetType, parseCaipAssetType } from '@metamask/utils';\nimport type { CaipChainId } from '@metamask/utils';\nimport type { Json, JsonRpcRequest } from '@metamask/utils';\nimport type { MutexInterface } from 'async-mutex';\nimport { Mutex } from 'async-mutex';\n\nimport { getChainIdsCaveat } from './utils';\n\nconst controllerName = 'MultichainAssetsController';\n\nexport type MultichainAssetsControllerState = {\n assetsMetadata: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n accountsAssets: { [account: string]: CaipAssetType[] };\n allIgnoredAssets: { [account: string]: CaipAssetType[] };\n};\n\n// Represents the response of the asset snap's onAssetLookup handler\nexport type AssetMetadataResponse = {\n assets: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n};\n\nexport type MultichainAssetsControllerAccountAssetListUpdatedEvent = {\n type: `${typeof controllerName}:accountAssetListUpdated`;\n payload: AccountsControllerAccountAssetListUpdatedEvent['payload'];\n};\n\n/**\n * Constructs the default {@link MultichainAssetsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link MultichainAssetsController} state.\n */\nexport function getDefaultMultichainAssetsControllerState(): MultichainAssetsControllerState {\n return { accountsAssets: {}, assetsMetadata: {}, allIgnoredAssets: {} };\n}\n\nexport type MultichainAssetsControllerGetAssetMetadataAction = {\n type: `${typeof controllerName}:getAssetMetadata`;\n handler: MultichainAssetsController['getAssetMetadata'];\n};\n\nexport type MultichainAssetsControllerIgnoreAssetsAction = {\n type: `${typeof controllerName}:ignoreAssets`;\n handler: MultichainAssetsController['ignoreAssets'];\n};\n\nexport type MultichainAssetsControllerAddAssetsAction = {\n type: `${typeof controllerName}:addAssets`;\n handler: MultichainAssetsController['addAssets'];\n};\n\n/**\n * Returns the state of the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n MultichainAssetsControllerState\n>;\n\n/**\n * Event emitted when the state of the {@link MultichainAssetsController} changes.\n */\nexport type MultichainAssetsControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n MultichainAssetsControllerState\n >;\n\n/**\n * Actions exposed by the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerActions =\n | MultichainAssetsControllerGetStateAction\n | MultichainAssetsControllerGetAssetMetadataAction\n | MultichainAssetsControllerIgnoreAssetsAction\n | MultichainAssetsControllerAddAssetsAction;\n\n/**\n * Events emitted by {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerEvents =\n | MultichainAssetsControllerStateChangeEvent\n | MultichainAssetsControllerAccountAssetListUpdatedEvent;\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback<Result> = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise<Result>;\n\n/**\n * Actions that this controller is allowed to call.\n */\ntype AllowedActions =\n | HandleSnapRequest\n | GetAllSnaps\n | GetPermissions\n | AccountsControllerListMultichainAccountsAction;\n\n/**\n * Events that this controller is allowed to subscribe.\n */\ntype AllowedEvents =\n | AccountsControllerAccountAddedEvent\n | AccountsControllerAccountRemovedEvent\n | AccountsControllerAccountAssetListUpdatedEvent;\n\n/**\n * Messenger type for the MultichainAssetsController.\n */\nexport type MultichainAssetsControllerMessenger = Messenger<\n typeof controllerName,\n MultichainAssetsControllerActions | AllowedActions,\n MultichainAssetsControllerEvents | AllowedEvents\n>;\n\n/**\n * {@link MultichainAssetsController}'s metadata.\n *\n * This allows us to choose if fields of the state should be persisted or not\n * using the `persist` flag; and if they can be sent to Sentry or not, using\n * the `anonymous` flag.\n */\nconst assetsControllerMetadata: StateMetadata<MultichainAssetsControllerState> =\n {\n assetsMetadata: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n accountsAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n allIgnoredAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n };\n\n// TODO: make this controller extends StaticIntervalPollingController and update all assetsMetadata once a day.\n\nexport class MultichainAssetsController extends BaseController<\n typeof controllerName,\n MultichainAssetsControllerState,\n MultichainAssetsControllerMessenger\n> {\n // Mapping of CAIP-2 Chain ID to Asset Snaps.\n #snaps: Record<CaipChainId, Snap[]>;\n\n readonly #controllerOperationMutex = new Mutex();\n\n constructor({\n messenger,\n state = {},\n }: {\n messenger: MultichainAssetsControllerMessenger;\n state?: Partial<MultichainAssetsControllerState>;\n }) {\n super({\n messenger,\n name: controllerName,\n metadata: assetsControllerMetadata,\n state: {\n ...getDefaultMultichainAssetsControllerState(),\n ...state,\n },\n });\n\n this.#snaps = {};\n\n this.messenger.subscribe(\n 'AccountsController:accountAdded',\n async (account) => await this.#handleOnAccountAddedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountRemoved',\n async (account) => await this.#handleOnAccountRemovedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountAssetListUpdated',\n async (event) => await this.#handleAccountAssetListUpdatedEvent(event),\n );\n\n this.#registerMessageHandlers();\n }\n\n async #handleAccountAssetListUpdatedEvent(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n return this.#withControllerLock(async () =>\n this.#handleAccountAssetListUpdated(event),\n );\n }\n\n async #handleOnAccountAddedEvent(account: InternalAccount) {\n return this.#withControllerLock(async () =>\n this.#handleOnAccountAdded(account),\n );\n }\n\n /**\n * Constructor helper for registering the controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:getAssetMetadata',\n this.getAssetMetadata.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:ignoreAssets',\n this.ignoreAssets.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:addAssets',\n this.addAssets.bind(this),\n );\n }\n\n /**\n * Returns the metadata for the given asset\n *\n * @param asset - The asset to get metadata for\n * @returns The metadata for the asset or undefined if not found.\n */\n getAssetMetadata(asset: CaipAssetType): FungibleAssetMetadata | undefined {\n return this.state.assetsMetadata[asset];\n }\n\n /**\n * Ignores a batch of assets for a specific account.\n *\n * @param assetsToIgnore - Array of asset IDs to ignore.\n * @param accountId - The account ID to ignore assets for.\n */\n ignoreAssets(assetsToIgnore: CaipAssetType[], accountId: string): void {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = state.accountsAssets[\n accountId\n ].filter((asset) => !assetsToIgnore.includes(asset));\n }\n\n if (!state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = [];\n }\n\n const newIgnoredAssets = assetsToIgnore.filter(\n (asset) => !state.allIgnoredAssets[accountId].includes(asset),\n );\n state.allIgnoredAssets[accountId].push(...newIgnoredAssets);\n });\n }\n\n /**\n * Adds multiple assets to the stored asset list for a specific account.\n * All assets must belong to the same chain.\n *\n * @param assetIds - Array of CAIP asset IDs to add (must be from same chain).\n * @param accountId - The account ID to add the assets to.\n * @returns The updated asset list for the account.\n * @throws Error if assets are from different chains.\n */\n async addAssets(\n assetIds: CaipAssetType[],\n accountId: string,\n ): Promise<CaipAssetType[]> {\n if (assetIds.length === 0) {\n return this.state.accountsAssets[accountId] || [];\n }\n\n // Validate that all assets are from the same chain\n const chainIds = new Set(\n assetIds.map((assetId) => parseCaipAssetType(assetId).chainId),\n );\n if (chainIds.size > 1) {\n throw new Error(\n `All assets must belong to the same chain. Found assets from chains: ${Array.from(chainIds).join(', ')}`,\n );\n }\n\n return this.#withControllerLock(async () => {\n // Refresh metadata for all assets\n await this.#refreshAssetsMetadata(assetIds);\n\n const addedAssets: CaipAssetType[] = [];\n\n this.update((state) => {\n // Initialize account assets if it doesn't exist\n if (!state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = [];\n }\n\n // Add assets if they don't already exist\n for (const assetId of assetIds) {\n if (!state.accountsAssets[accountId].includes(assetId)) {\n state.accountsAssets[accountId].push(assetId);\n addedAssets.push(assetId);\n }\n }\n\n // Remove from ignored list if they exist there (inline logic like EVM)\n if (state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = state.allIgnoredAssets[\n accountId\n ].filter((asset) => !assetIds.includes(asset));\n\n // Clean up empty arrays\n if (state.allIgnoredAssets[accountId].length === 0) {\n delete state.allIgnoredAssets[accountId];\n }\n }\n });\n\n // Publish event to notify other controllers (balances, rates) about the new assets\n if (addedAssets.length > 0) {\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [accountId]: {\n added: addedAssets,\n removed: [],\n },\n },\n });\n }\n\n return this.state.accountsAssets[accountId] || [];\n });\n }\n\n /**\n * Checks if an asset is ignored for a specific account.\n *\n * @param asset - The asset ID to check.\n * @param accountId - The account ID to check for.\n * @returns True if the asset is ignored, false otherwise.\n */\n #isAssetIgnored(asset: CaipAssetType, accountId: string): boolean {\n return this.state.allIgnoredAssets[accountId]?.includes(asset) ?? false;\n }\n\n /**\n * Function to update the assets list for an account\n *\n * @param event - The list of assets to update\n */\n async #handleAccountAssetListUpdated(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n this.#assertControllerMutexIsLocked();\n\n const assetsForMetadataRefresh = new Set<CaipAssetType>([]);\n const accountsAndAssetsToUpdate: AccountAssetListUpdatedEventPayload['assets'] =\n {};\n for (const [accountId, { added, removed }] of Object.entries(\n event.assets,\n )) {\n if (added.length > 0 || removed.length > 0) {\n const existing = this.state.accountsAssets[accountId] || [];\n\n // In case accountsAndAssetsToUpdate event is fired with \"added\" assets that already exist, we don't want to add them again\n // Also filter out ignored assets\n const filteredToBeAddedAssets = added.filter(\n (asset) =>\n !existing.includes(asset) &&\n isCaipAssetType(asset) &&\n !this.#isAssetIgnored(asset, accountId),\n );\n\n // In case accountsAndAssetsToUpdate event is fired with \"removed\" assets that don't exist, we don't want to remove them\n const filteredToBeRemovedAssets = removed.filter(\n (asset) => existing.includes(asset) && isCaipAssetType(asset),\n );\n\n if (\n filteredToBeAddedAssets.length > 0 ||\n filteredToBeRemovedAssets.length > 0\n ) {\n accountsAndAssetsToUpdate[accountId] = {\n added: filteredToBeAddedAssets,\n removed: filteredToBeRemovedAssets,\n };\n }\n\n for (const asset of existing) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeAddedAssets) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeRemovedAssets) {\n assetsForMetadataRefresh.delete(asset);\n }\n }\n }\n\n this.update((state) => {\n for (const [accountId, { added, removed }] of Object.entries(\n accountsAndAssetsToUpdate,\n )) {\n const assets = new Set([\n ...(state.accountsAssets[accountId] || []),\n ...added,\n ]);\n for (const asset of removed) {\n assets.delete(asset);\n }\n\n state.accountsAssets[accountId] = Array.from(assets);\n }\n });\n\n // Trigger fetching metadata for new assets\n await this.#refreshAssetsMetadata(Array.from(assetsForMetadataRefresh));\n\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: accountsAndAssetsToUpdate,\n });\n }\n\n /**\n * Checks for non-EVM accounts.\n *\n * @param account - The new account to be checked.\n * @returns True if the account is a non-EVM account, false otherwise.\n */\n #isNonEvmAccount(account: InternalAccount): boolean {\n return (\n !isEvmAccountType(account.type) &&\n // Non-EVM accounts are backed by a Snap for now\n account.metadata.snap !== undefined\n );\n }\n\n /**\n * Handles changes when a new account has been added.\n *\n * @param account - The new account being added.\n */\n async #handleOnAccountAdded(account: InternalAccount): Promise<void> {\n if (!this.#isNonEvmAccount(account)) {\n // Nothing to do here for EVM accounts\n return;\n }\n this.#assertControllerMutexIsLocked();\n\n // Get assets list\n if (account.metadata.snap) {\n const assets = await this.#getAssetsList(\n account.id,\n account.metadata.snap.id,\n );\n await this.#refreshAssetsMetadata(assets);\n this.update((state) => {\n state.accountsAssets[account.id] = assets;\n });\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [account.id]: {\n added: assets,\n removed: [],\n },\n },\n });\n }\n }\n\n /**\n * Handles changes when a new account has been removed.\n *\n * @param accountId - The new account id being removed.\n */\n async #handleOnAccountRemovedEvent(accountId: string): Promise<void> {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n delete state.accountsAssets[accountId];\n }\n if (state.allIgnoredAssets[accountId]) {\n delete state.allIgnoredAssets[accountId];\n }\n // TODO: We are not deleting the assetsMetadata because we will soon make this controller extends StaticIntervalPollingController\n // and update all assetsMetadata once a day.\n });\n }\n\n /**\n * Refreshes the assets snaps and metadata for the given list of assets\n *\n * @param assets - The assets to refresh\n */\n async #refreshAssetsMetadata(assets: CaipAssetType[]) {\n this.#assertControllerMutexIsLocked();\n\n const assetsWithoutMetadata: CaipAssetType[] = assets.filter(\n (asset) => !this.state.assetsMetadata[asset],\n );\n\n // Call the snap to get the metadata\n if (assetsWithoutMetadata.length > 0) {\n // Check if for every asset in assetsWithoutMetadata there is a snap in snaps by chainId else call getAssetSnaps\n if (\n !assetsWithoutMetadata.every((asset: CaipAssetType) => {\n const { chainId } = parseCaipAssetType(asset);\n return Boolean(this.#getAssetSnapFor(chainId));\n })\n ) {\n this.#snaps = this.#getAssetSnaps();\n }\n await this.#updateAssetsMetadata(assetsWithoutMetadata);\n }\n }\n\n /**\n * Updates the assets metadata for the given list of assets\n *\n * @param assets - The assets to update\n */\n async #updateAssetsMetadata(assets: CaipAssetType[]) {\n // Creates a mapping of scope to their respective assets list.\n const assetsByScope: Record<CaipChainId, CaipAssetType[]> = {};\n for (const asset of assets) {\n const { chainId } = parseCaipAssetType(asset);\n if (!assetsByScope[chainId]) {\n assetsByScope[chainId] = [];\n }\n assetsByScope[chainId].push(asset);\n }\n\n let newMetadata: Record<CaipAssetType, FungibleAssetMetadata> = {};\n for (const chainId of Object.keys(assetsByScope) as CaipChainId[]) {\n const assetsForChain = assetsByScope[chainId];\n // Now fetch metadata from the associated asset Snaps:\n const snap = this.#getAssetSnapFor(chainId);\n if (snap) {\n const metadata = await this.#getAssetsMetadataFrom(\n assetsForChain,\n snap.id,\n );\n newMetadata = {\n ...newMetadata,\n ...(metadata?.assets ?? {}),\n };\n }\n }\n this.update((state) => {\n state.assetsMetadata = {\n ...this.state.assetsMetadata,\n ...newMetadata,\n };\n });\n }\n\n /**\n * Creates a mapping of CAIP-2 Chain ID to Asset Snaps.\n *\n * @returns A mapping of CAIP-2 Chain ID to Asset Snaps.\n */\n #getAssetSnaps(): Record<CaipChainId, Snap[]> {\n const snaps: Record<CaipChainId, Snap[]> = {};\n const allSnaps = this.#getAllSnaps();\n const allPermissions = allSnaps.map((snap) =>\n this.#getSnapsPermissions(snap.id),\n );\n\n for (const [index, permission] of allPermissions.entries()) {\n let scopes;\n for (const singlePermissionConstraint of Object.values(permission)) {\n scopes = getChainIdsCaveat(singlePermissionConstraint);\n if (!scopes) {\n continue;\n }\n for (const scope of scopes as CaipChainId[]) {\n if (!snaps[scope]) {\n snaps[scope] = [];\n }\n snaps[scope].push(allSnaps[index]);\n }\n }\n }\n return snaps;\n }\n\n /**\n * Returns the first asset snap for the given scope\n *\n * @param scope - The scope to get the asset snap for\n * @returns The asset snap for the given scope\n */\n #getAssetSnapFor(scope: CaipChainId): Snap | undefined {\n const allSnaps = this.#snaps[scope];\n // Pick only the first one, we ignore the other Snaps if there are multiple candidates for now.\n return allSnaps?.[0]; // Will be undefined if there's no Snaps candidate for this scope.\n }\n\n /**\n * Returns all the asset snaps\n *\n * @returns All the asset snaps\n */\n #getAllSnaps(): Snap[] {\n // TODO: Use dedicated SnapController's action once available for this:\n return this.messenger\n .call('SnapController:getAll')\n .filter((snap) => snap.enabled && !snap.blocked);\n }\n\n /**\n * Returns the permissions for the given origin\n *\n * @param origin - The origin to get the permissions for\n * @returns The permissions for the given origin\n */\n #getSnapsPermissions(\n origin: string,\n ): SubjectPermissions<PermissionConstraint> {\n return this.messenger.call(\n 'PermissionController:getPermissions',\n origin,\n ) as SubjectPermissions<PermissionConstraint>;\n }\n\n /**\n * Returns the metadata for the given assets\n *\n * @param assets - The assets to get metadata for\n * @param snapId - The snap ID to get metadata from\n * @returns The metadata for the assets\n */\n async #getAssetsMetadataFrom(\n assets: CaipAssetType[],\n snapId: string,\n ): Promise<AssetMetadataResponse | undefined> {\n try {\n return (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnAssetsLookup,\n request: {\n jsonrpc: '2.0',\n method: 'onAssetLookup',\n params: {\n assets,\n },\n },\n })) as Promise<AssetMetadataResponse>;\n } catch (error) {\n // Ignore\n console.error(error);\n return undefined;\n }\n }\n\n /**\n * Get assets list for an account\n *\n * @param accountId - AccountId to get assets for\n * @param snapId - Snap ID for the account\n * @returns list of assets\n */\n async #getAssetsList(\n accountId: string,\n snapId: string,\n ): Promise<CaipAssetTypeOrId[]> {\n return await this.#getClient(snapId).listAccountAssets(accountId);\n }\n\n /**\n * Gets a `KeyringClient` for a Snap.\n *\n * @param snapId - ID of the Snap to get the client for.\n * @returns A `KeyringClient` for the Snap.\n */\n #getClient(snapId: string): KeyringClient {\n return new KeyringClient({\n send: async (request: JsonRpcRequest) =>\n (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnKeyringRequest,\n request,\n })) as Promise<Json>,\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(\n 'MultichainAssetsControllerError - Attempt to update state',\n );\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param callback - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock<Result>(\n callback: MutuallyExclusiveCallback<Result>,\n ): Promise<Result> {\n return withLock(this.#controllerOperationMutex, callback);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param callback - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock<Result>(\n mutex: Mutex,\n callback: MutuallyExclusiveCallback<Result>,\n): Promise<Result> {\n const releaseLock = await mutex.acquire();\n\n try {\n return await callback({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n"]}
1
+ {"version":3,"file":"MultichainAssetsController.cjs","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AccountsControllerAccountAddedEvent,\n AccountsControllerAccountAssetListUpdatedEvent,\n AccountsControllerAccountRemovedEvent,\n AccountsControllerListMultichainAccountsAction,\n} from '@metamask/accounts-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { handleFetch } from '@metamask/controller-utils';\nimport { isEvmAccountType } from '@metamask/keyring-api';\nimport type {\n AccountAssetListUpdatedEventPayload,\n CaipAssetType,\n CaipAssetTypeOrId,\n} from '@metamask/keyring-api';\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\nimport { KeyringClient } from '@metamask/keyring-snap-client';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n GetPermissions,\n PermissionConstraint,\n SubjectPermissions,\n} from '@metamask/permission-controller';\nimport type {\n GetAllSnaps,\n HandleSnapRequest,\n} from '@metamask/snaps-controllers';\nimport type { FungibleAssetMetadata, Snap, SnapId } from '@metamask/snaps-sdk';\nimport { HandlerType } from '@metamask/snaps-utils';\nimport { isCaipAssetType, parseCaipAssetType } from '@metamask/utils';\nimport type { CaipChainId } from '@metamask/utils';\nimport type { Json, JsonRpcRequest } from '@metamask/utils';\nimport type { MutexInterface } from 'async-mutex';\nimport { Mutex } from 'async-mutex';\n\nimport { getChainIdsCaveat } from './utils';\n\nconst controllerName = 'MultichainAssetsController';\n\n// Blockaid token security scanning constants\nconst SECURITY_ALERTS_BASE_URL = 'https://security-alerts.api.cx.metamask.io';\nconst TOKEN_BULK_SCANNING_ENDPOINT = '/token/scan-bulk';\n\n/**\n * Result type from Blockaid token security scan.\n */\nexport enum BlockaidResultType {\n Benign = 'Benign',\n Spam = 'Spam',\n Warning = 'Warning',\n Malicious = 'Malicious',\n}\n\n/**\n * Response shape from the Blockaid token bulk scanning endpoint.\n */\ntype BlockaidTokenScanResponse = {\n results: Record<\n string,\n {\n /* eslint-disable @typescript-eslint/naming-convention */\n result_type: BlockaidResultType;\n malicious_score: string;\n attack_types: Record<string, unknown>;\n /* eslint-enable @typescript-eslint/naming-convention */\n chain: string;\n address: string;\n }\n >;\n};\n\nexport type MultichainAssetsControllerState = {\n assetsMetadata: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n accountsAssets: { [account: string]: CaipAssetType[] };\n allIgnoredAssets: { [account: string]: CaipAssetType[] };\n};\n\n// Represents the response of the asset snap's onAssetLookup handler\nexport type AssetMetadataResponse = {\n assets: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n};\n\nexport type MultichainAssetsControllerAccountAssetListUpdatedEvent = {\n type: `${typeof controllerName}:accountAssetListUpdated`;\n payload: AccountsControllerAccountAssetListUpdatedEvent['payload'];\n};\n\n/**\n * Constructs the default {@link MultichainAssetsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link MultichainAssetsController} state.\n */\nexport function getDefaultMultichainAssetsControllerState(): MultichainAssetsControllerState {\n return { accountsAssets: {}, assetsMetadata: {}, allIgnoredAssets: {} };\n}\n\nexport type MultichainAssetsControllerGetAssetMetadataAction = {\n type: `${typeof controllerName}:getAssetMetadata`;\n handler: MultichainAssetsController['getAssetMetadata'];\n};\n\nexport type MultichainAssetsControllerIgnoreAssetsAction = {\n type: `${typeof controllerName}:ignoreAssets`;\n handler: MultichainAssetsController['ignoreAssets'];\n};\n\nexport type MultichainAssetsControllerAddAssetsAction = {\n type: `${typeof controllerName}:addAssets`;\n handler: MultichainAssetsController['addAssets'];\n};\n\n/**\n * Returns the state of the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n MultichainAssetsControllerState\n>;\n\n/**\n * Event emitted when the state of the {@link MultichainAssetsController} changes.\n */\nexport type MultichainAssetsControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n MultichainAssetsControllerState\n >;\n\n/**\n * Actions exposed by the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerActions =\n | MultichainAssetsControllerGetStateAction\n | MultichainAssetsControllerGetAssetMetadataAction\n | MultichainAssetsControllerIgnoreAssetsAction\n | MultichainAssetsControllerAddAssetsAction;\n\n/**\n * Events emitted by {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerEvents =\n | MultichainAssetsControllerStateChangeEvent\n | MultichainAssetsControllerAccountAssetListUpdatedEvent;\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback<Result> = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise<Result>;\n\n/**\n * Actions that this controller is allowed to call.\n */\ntype AllowedActions =\n | HandleSnapRequest\n | GetAllSnaps\n | GetPermissions\n | AccountsControllerListMultichainAccountsAction;\n\n/**\n * Events that this controller is allowed to subscribe.\n */\ntype AllowedEvents =\n | AccountsControllerAccountAddedEvent\n | AccountsControllerAccountRemovedEvent\n | AccountsControllerAccountAssetListUpdatedEvent;\n\n/**\n * Messenger type for the MultichainAssetsController.\n */\nexport type MultichainAssetsControllerMessenger = Messenger<\n typeof controllerName,\n MultichainAssetsControllerActions | AllowedActions,\n MultichainAssetsControllerEvents | AllowedEvents\n>;\n\n/**\n * {@link MultichainAssetsController}'s metadata.\n *\n * This allows us to choose if fields of the state should be persisted or not\n * using the `persist` flag; and if they can be sent to Sentry or not, using\n * the `anonymous` flag.\n */\nconst assetsControllerMetadata: StateMetadata<MultichainAssetsControllerState> =\n {\n assetsMetadata: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n accountsAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n allIgnoredAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n };\n\n// TODO: make this controller extends StaticIntervalPollingController and update all assetsMetadata once a day.\n\nexport class MultichainAssetsController extends BaseController<\n typeof controllerName,\n MultichainAssetsControllerState,\n MultichainAssetsControllerMessenger\n> {\n // Mapping of CAIP-2 Chain ID to Asset Snaps.\n #snaps: Record<CaipChainId, Snap[]>;\n\n readonly #controllerOperationMutex = new Mutex();\n\n constructor({\n messenger,\n state = {},\n }: {\n messenger: MultichainAssetsControllerMessenger;\n state?: Partial<MultichainAssetsControllerState>;\n }) {\n super({\n messenger,\n name: controllerName,\n metadata: assetsControllerMetadata,\n state: {\n ...getDefaultMultichainAssetsControllerState(),\n ...state,\n },\n });\n\n this.#snaps = {};\n\n this.messenger.subscribe(\n 'AccountsController:accountAdded',\n async (account) => await this.#handleOnAccountAddedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountRemoved',\n async (account) => await this.#handleOnAccountRemovedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountAssetListUpdated',\n async (event) => await this.#handleAccountAssetListUpdatedEvent(event),\n );\n\n this.#registerMessageHandlers();\n }\n\n async #handleAccountAssetListUpdatedEvent(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n return this.#withControllerLock(async () =>\n this.#handleAccountAssetListUpdated(event),\n );\n }\n\n async #handleOnAccountAddedEvent(account: InternalAccount) {\n return this.#withControllerLock(async () =>\n this.#handleOnAccountAdded(account),\n );\n }\n\n /**\n * Constructor helper for registering the controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:getAssetMetadata',\n this.getAssetMetadata.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:ignoreAssets',\n this.ignoreAssets.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:addAssets',\n this.addAssets.bind(this),\n );\n }\n\n /**\n * Returns the metadata for the given asset\n *\n * @param asset - The asset to get metadata for\n * @returns The metadata for the asset or undefined if not found.\n */\n getAssetMetadata(asset: CaipAssetType): FungibleAssetMetadata | undefined {\n return this.state.assetsMetadata[asset];\n }\n\n /**\n * Ignores a batch of assets for a specific account.\n *\n * @param assetsToIgnore - Array of asset IDs to ignore.\n * @param accountId - The account ID to ignore assets for.\n */\n ignoreAssets(assetsToIgnore: CaipAssetType[], accountId: string): void {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = state.accountsAssets[\n accountId\n ].filter((asset) => !assetsToIgnore.includes(asset));\n }\n\n if (!state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = [];\n }\n\n const newIgnoredAssets = assetsToIgnore.filter(\n (asset) => !state.allIgnoredAssets[accountId].includes(asset),\n );\n state.allIgnoredAssets[accountId].push(...newIgnoredAssets);\n });\n }\n\n /**\n * Adds multiple assets to the stored asset list for a specific account.\n * All assets must belong to the same chain.\n *\n * @param assetIds - Array of CAIP asset IDs to add (must be from same chain).\n * @param accountId - The account ID to add the assets to.\n * @returns The updated asset list for the account.\n * @throws Error if assets are from different chains.\n */\n async addAssets(\n assetIds: CaipAssetType[],\n accountId: string,\n ): Promise<CaipAssetType[]> {\n if (assetIds.length === 0) {\n return this.state.accountsAssets[accountId] || [];\n }\n\n // Validate that all assets are from the same chain\n const chainIds = new Set(\n assetIds.map((assetId) => parseCaipAssetType(assetId).chainId),\n );\n if (chainIds.size > 1) {\n throw new Error(\n `All assets must belong to the same chain. Found assets from chains: ${Array.from(chainIds).join(', ')}`,\n );\n }\n\n return this.#withControllerLock(async () => {\n // Refresh metadata for all assets\n await this.#refreshAssetsMetadata(assetIds);\n\n const addedAssets: CaipAssetType[] = [];\n\n this.update((state) => {\n // Initialize account assets if it doesn't exist\n if (!state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = [];\n }\n\n // Add assets if they don't already exist\n for (const assetId of assetIds) {\n if (!state.accountsAssets[accountId].includes(assetId)) {\n state.accountsAssets[accountId].push(assetId);\n addedAssets.push(assetId);\n }\n }\n\n // Remove from ignored list if they exist there (inline logic like EVM)\n if (state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = state.allIgnoredAssets[\n accountId\n ].filter((asset) => !assetIds.includes(asset));\n\n // Clean up empty arrays\n if (state.allIgnoredAssets[accountId].length === 0) {\n delete state.allIgnoredAssets[accountId];\n }\n }\n });\n\n // Publish event to notify other controllers (balances, rates) about the new assets\n if (addedAssets.length > 0) {\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [accountId]: {\n added: addedAssets,\n removed: [],\n },\n },\n });\n }\n\n return this.state.accountsAssets[accountId] || [];\n });\n }\n\n /**\n * Checks if an asset is ignored for a specific account.\n *\n * @param asset - The asset ID to check.\n * @param accountId - The account ID to check for.\n * @returns True if the asset is ignored, false otherwise.\n */\n #isAssetIgnored(asset: CaipAssetType, accountId: string): boolean {\n return this.state.allIgnoredAssets[accountId]?.includes(asset) ?? false;\n }\n\n /**\n * Function to update the assets list for an account\n *\n * @param event - The list of assets to update\n */\n async #handleAccountAssetListUpdated(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n this.#assertControllerMutexIsLocked();\n\n const assetsForMetadataRefresh = new Set<CaipAssetType>([]);\n const accountsAndAssetsToUpdate: AccountAssetListUpdatedEventPayload['assets'] =\n {};\n for (const [accountId, { added, removed }] of Object.entries(\n event.assets,\n )) {\n if (added.length > 0 || removed.length > 0) {\n const existing = this.state.accountsAssets[accountId] || [];\n\n // In case accountsAndAssetsToUpdate event is fired with \"added\" assets that already exist, we don't want to add them again\n // Also filter out ignored assets\n const preFilteredToBeAddedAssets = added.filter(\n (asset) =>\n !existing.includes(asset) &&\n isCaipAssetType(asset) &&\n !this.#isAssetIgnored(asset, accountId),\n );\n\n // Filter out tokens flagged by Blockaid as non-benign\n const filteredToBeAddedAssets = await this.#filterBlockaidSpamTokens(\n preFilteredToBeAddedAssets,\n );\n\n // In case accountsAndAssetsToUpdate event is fired with \"removed\" assets that don't exist, we don't want to remove them\n const filteredToBeRemovedAssets = removed.filter(\n (asset) => existing.includes(asset) && isCaipAssetType(asset),\n );\n\n if (\n filteredToBeAddedAssets.length > 0 ||\n filteredToBeRemovedAssets.length > 0\n ) {\n accountsAndAssetsToUpdate[accountId] = {\n added: filteredToBeAddedAssets,\n removed: filteredToBeRemovedAssets,\n };\n }\n\n for (const asset of existing) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeAddedAssets) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeRemovedAssets) {\n assetsForMetadataRefresh.delete(asset);\n }\n }\n }\n\n this.update((state) => {\n for (const [accountId, { added, removed }] of Object.entries(\n accountsAndAssetsToUpdate,\n )) {\n const assets = new Set([\n ...(state.accountsAssets[accountId] || []),\n ...added,\n ]);\n for (const asset of removed) {\n assets.delete(asset);\n }\n\n state.accountsAssets[accountId] = Array.from(assets);\n }\n });\n\n // Trigger fetching metadata for new assets\n await this.#refreshAssetsMetadata(Array.from(assetsForMetadataRefresh));\n\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: accountsAndAssetsToUpdate,\n });\n }\n\n /**\n * Checks for non-EVM accounts.\n *\n * @param account - The new account to be checked.\n * @returns True if the account is a non-EVM account, false otherwise.\n */\n #isNonEvmAccount(account: InternalAccount): boolean {\n return (\n !isEvmAccountType(account.type) &&\n // Non-EVM accounts are backed by a Snap for now\n account.metadata.snap !== undefined\n );\n }\n\n /**\n * Handles changes when a new account has been added.\n *\n * @param account - The new account being added.\n */\n async #handleOnAccountAdded(account: InternalAccount): Promise<void> {\n if (!this.#isNonEvmAccount(account)) {\n // Nothing to do here for EVM accounts\n return;\n }\n this.#assertControllerMutexIsLocked();\n\n // Get assets list\n if (account.metadata.snap) {\n const allAssets = await this.#getAssetsList(\n account.id,\n account.metadata.snap.id,\n );\n const assets = await this.#filterBlockaidSpamTokens(allAssets);\n await this.#refreshAssetsMetadata(assets);\n this.update((state) => {\n state.accountsAssets[account.id] = assets;\n });\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [account.id]: {\n added: assets,\n removed: [],\n },\n },\n });\n }\n }\n\n /**\n * Handles changes when a new account has been removed.\n *\n * @param accountId - The new account id being removed.\n */\n async #handleOnAccountRemovedEvent(accountId: string): Promise<void> {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n delete state.accountsAssets[accountId];\n }\n if (state.allIgnoredAssets[accountId]) {\n delete state.allIgnoredAssets[accountId];\n }\n // TODO: We are not deleting the assetsMetadata because we will soon make this controller extends StaticIntervalPollingController\n // and update all assetsMetadata once a day.\n });\n }\n\n /**\n * Refreshes the assets snaps and metadata for the given list of assets\n *\n * @param assets - The assets to refresh\n */\n async #refreshAssetsMetadata(assets: CaipAssetType[]) {\n this.#assertControllerMutexIsLocked();\n\n const assetsWithoutMetadata: CaipAssetType[] = assets.filter(\n (asset) => !this.state.assetsMetadata[asset],\n );\n\n // Call the snap to get the metadata\n if (assetsWithoutMetadata.length > 0) {\n // Check if for every asset in assetsWithoutMetadata there is a snap in snaps by chainId else call getAssetSnaps\n if (\n !assetsWithoutMetadata.every((asset: CaipAssetType) => {\n const { chainId } = parseCaipAssetType(asset);\n return Boolean(this.#getAssetSnapFor(chainId));\n })\n ) {\n this.#snaps = this.#getAssetSnaps();\n }\n await this.#updateAssetsMetadata(assetsWithoutMetadata);\n }\n }\n\n /**\n * Updates the assets metadata for the given list of assets\n *\n * @param assets - The assets to update\n */\n async #updateAssetsMetadata(assets: CaipAssetType[]) {\n // Creates a mapping of scope to their respective assets list.\n const assetsByScope: Record<CaipChainId, CaipAssetType[]> = {};\n for (const asset of assets) {\n const { chainId } = parseCaipAssetType(asset);\n if (!assetsByScope[chainId]) {\n assetsByScope[chainId] = [];\n }\n assetsByScope[chainId].push(asset);\n }\n\n let newMetadata: Record<CaipAssetType, FungibleAssetMetadata> = {};\n for (const chainId of Object.keys(assetsByScope) as CaipChainId[]) {\n const assetsForChain = assetsByScope[chainId];\n // Now fetch metadata from the associated asset Snaps:\n const snap = this.#getAssetSnapFor(chainId);\n if (snap) {\n const metadata = await this.#getAssetsMetadataFrom(\n assetsForChain,\n snap.id,\n );\n newMetadata = {\n ...newMetadata,\n ...(metadata?.assets ?? {}),\n };\n }\n }\n this.update((state) => {\n state.assetsMetadata = {\n ...this.state.assetsMetadata,\n ...newMetadata,\n };\n });\n }\n\n /**\n * Creates a mapping of CAIP-2 Chain ID to Asset Snaps.\n *\n * @returns A mapping of CAIP-2 Chain ID to Asset Snaps.\n */\n #getAssetSnaps(): Record<CaipChainId, Snap[]> {\n const snaps: Record<CaipChainId, Snap[]> = {};\n const allSnaps = this.#getAllSnaps();\n const allPermissions = allSnaps.map((snap) =>\n this.#getSnapsPermissions(snap.id),\n );\n\n for (const [index, permission] of allPermissions.entries()) {\n let scopes;\n for (const singlePermissionConstraint of Object.values(permission)) {\n scopes = getChainIdsCaveat(singlePermissionConstraint);\n if (!scopes) {\n continue;\n }\n for (const scope of scopes as CaipChainId[]) {\n if (!snaps[scope]) {\n snaps[scope] = [];\n }\n snaps[scope].push(allSnaps[index]);\n }\n }\n }\n return snaps;\n }\n\n /**\n * Returns the first asset snap for the given scope\n *\n * @param scope - The scope to get the asset snap for\n * @returns The asset snap for the given scope\n */\n #getAssetSnapFor(scope: CaipChainId): Snap | undefined {\n const allSnaps = this.#snaps[scope];\n // Pick only the first one, we ignore the other Snaps if there are multiple candidates for now.\n return allSnaps?.[0]; // Will be undefined if there's no Snaps candidate for this scope.\n }\n\n /**\n * Returns all the asset snaps\n *\n * @returns All the asset snaps\n */\n #getAllSnaps(): Snap[] {\n // TODO: Use dedicated SnapController's action once available for this:\n return this.messenger\n .call('SnapController:getAll')\n .filter((snap) => snap.enabled && !snap.blocked);\n }\n\n /**\n * Returns the permissions for the given origin\n *\n * @param origin - The origin to get the permissions for\n * @returns The permissions for the given origin\n */\n #getSnapsPermissions(\n origin: string,\n ): SubjectPermissions<PermissionConstraint> {\n return this.messenger.call(\n 'PermissionController:getPermissions',\n origin,\n ) as SubjectPermissions<PermissionConstraint>;\n }\n\n /**\n * Returns the metadata for the given assets\n *\n * @param assets - The assets to get metadata for\n * @param snapId - The snap ID to get metadata from\n * @returns The metadata for the assets\n */\n async #getAssetsMetadataFrom(\n assets: CaipAssetType[],\n snapId: string,\n ): Promise<AssetMetadataResponse | undefined> {\n try {\n return (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnAssetsLookup,\n request: {\n jsonrpc: '2.0',\n method: 'onAssetLookup',\n params: {\n assets,\n },\n },\n })) as Promise<AssetMetadataResponse>;\n } catch (error) {\n // Ignore\n console.error(error);\n return undefined;\n }\n }\n\n /**\n * Calls the Blockaid token bulk scanning endpoint for a given chain and\n * set of token addresses.\n *\n * @param chain - The chain name for the Blockaid API (e.g. \"solana\").\n * @param tokens - The token addresses to scan.\n * @returns The scan response, or null if the request fails or times out.\n */\n async #scanBlockaidTokens(\n chain: string,\n tokens: string[],\n ): Promise<BlockaidTokenScanResponse | null> {\n try {\n return await handleFetch(\n `${SECURITY_ALERTS_BASE_URL}${TOKEN_BULK_SCANNING_ENDPOINT}`,\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ chain, tokens }),\n },\n );\n } catch (error) {\n console.error('Blockaid token scan failed:', error);\n return null;\n }\n }\n\n /**\n * Filters out tokens flagged as non-benign by Blockaid. Only tokens with\n * an `assetNamespace` of \"token\" are scanned (native assets like slip44 are\n * passed through unfiltered). If the API call fails, all tokens are kept.\n *\n * @param assets - The CAIP asset type list to filter.\n * @returns The filtered list with malicious/spam/warning tokens removed.\n */\n async #filterBlockaidSpamTokens(\n assets: CaipAssetType[],\n ): Promise<CaipAssetType[]> {\n // Group scannable token assets by chain namespace\n const tokensByChain: Record<\n string,\n { asset: CaipAssetType; address: string }[]\n > = {};\n\n for (const asset of assets) {\n const { assetNamespace, assetReference, chain } =\n parseCaipAssetType(asset);\n\n // Only scan fungible token assets (e.g. SPL tokens), skip native (slip44)\n if (assetNamespace === 'token') {\n const chainName = chain.namespace;\n if (!tokensByChain[chainName]) {\n tokensByChain[chainName] = [];\n }\n tokensByChain[chainName].push({ asset, address: assetReference });\n }\n }\n\n // If there are no token assets to scan, return as-is\n if (Object.keys(tokensByChain).length === 0) {\n return assets;\n }\n\n // Build a set of assets to reject (non-benign tokens)\n const rejectedAssets = new Set<CaipAssetType>();\n\n for (const [chainName, tokenEntries] of Object.entries(tokensByChain)) {\n const addresses = tokenEntries.map((entry) => entry.address);\n const scanResponse = await this.#scanBlockaidTokens(chainName, addresses);\n\n if (!scanResponse?.results) {\n // If the scan fails, keep all tokens (fail open)\n continue;\n }\n\n for (const entry of tokenEntries) {\n const result = scanResponse.results[entry.address];\n // Reject the token only if we have a definitive non-benign result\n if (\n result?.result_type &&\n result.result_type !== BlockaidResultType.Benign\n ) {\n rejectedAssets.add(entry.asset);\n }\n }\n }\n\n // Filter while preserving original order\n return assets.filter((asset) => !rejectedAssets.has(asset));\n }\n\n /**\n * Get assets list for an account\n *\n * @param accountId - AccountId to get assets for\n * @param snapId - Snap ID for the account\n * @returns list of assets\n */\n async #getAssetsList(\n accountId: string,\n snapId: string,\n ): Promise<CaipAssetTypeOrId[]> {\n return await this.#getClient(snapId).listAccountAssets(accountId);\n }\n\n /**\n * Gets a `KeyringClient` for a Snap.\n *\n * @param snapId - ID of the Snap to get the client for.\n * @returns A `KeyringClient` for the Snap.\n */\n #getClient(snapId: string): KeyringClient {\n return new KeyringClient({\n send: async (request: JsonRpcRequest) =>\n (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnKeyringRequest,\n request,\n })) as Promise<Json>,\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(\n 'MultichainAssetsControllerError - Attempt to update state',\n );\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param callback - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock<Result>(\n callback: MutuallyExclusiveCallback<Result>,\n ): Promise<Result> {\n return withLock(this.#controllerOperationMutex, callback);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param callback - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock<Result>(\n mutex: Mutex,\n callback: MutuallyExclusiveCallback<Result>,\n): Promise<Result> {\n const releaseLock = await mutex.acquire();\n\n try {\n return await callback({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n"]}
@@ -7,6 +7,15 @@ import type { GetPermissions } from "@metamask/permission-controller";
7
7
  import type { GetAllSnaps, HandleSnapRequest } from "@metamask/snaps-controllers";
8
8
  import type { FungibleAssetMetadata } from "@metamask/snaps-sdk";
9
9
  declare const controllerName = "MultichainAssetsController";
10
+ /**
11
+ * Result type from Blockaid token security scan.
12
+ */
13
+ export declare enum BlockaidResultType {
14
+ Benign = "Benign",
15
+ Spam = "Spam",
16
+ Warning = "Warning",
17
+ Malicious = "Malicious"
18
+ }
10
19
  export type MultichainAssetsControllerState = {
11
20
  assetsMetadata: {
12
21
  [asset: CaipAssetType]: FungibleAssetMetadata;
@@ -1 +1 @@
1
- {"version":3,"file":"MultichainAssetsController.d.cts","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MultichainAssetsController.d.cts","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,15 @@ import type { GetPermissions } from "@metamask/permission-controller";
7
7
  import type { GetAllSnaps, HandleSnapRequest } from "@metamask/snaps-controllers";
8
8
  import type { FungibleAssetMetadata } from "@metamask/snaps-sdk";
9
9
  declare const controllerName = "MultichainAssetsController";
10
+ /**
11
+ * Result type from Blockaid token security scan.
12
+ */
13
+ export declare enum BlockaidResultType {
14
+ Benign = "Benign",
15
+ Spam = "Spam",
16
+ Warning = "Warning",
17
+ Malicious = "Malicious"
18
+ }
10
19
  export type MultichainAssetsControllerState = {
11
20
  assetsMetadata: {
12
21
  [asset: CaipAssetType]: FungibleAssetMetadata;
@@ -1 +1 @@
1
- {"version":3,"file":"MultichainAssetsController.d.mts","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MultichainAssetsController.d.mts","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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"}
@@ -9,8 +9,9 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _MultichainAssetsController_instances, _MultichainAssetsController_snaps, _MultichainAssetsController_controllerOperationMutex, _MultichainAssetsController_handleAccountAssetListUpdatedEvent, _MultichainAssetsController_handleOnAccountAddedEvent, _MultichainAssetsController_registerMessageHandlers, _MultichainAssetsController_isAssetIgnored, _MultichainAssetsController_handleAccountAssetListUpdated, _MultichainAssetsController_isNonEvmAccount, _MultichainAssetsController_handleOnAccountAdded, _MultichainAssetsController_handleOnAccountRemovedEvent, _MultichainAssetsController_refreshAssetsMetadata, _MultichainAssetsController_updateAssetsMetadata, _MultichainAssetsController_getAssetSnaps, _MultichainAssetsController_getAssetSnapFor, _MultichainAssetsController_getAllSnaps, _MultichainAssetsController_getSnapsPermissions, _MultichainAssetsController_getAssetsMetadataFrom, _MultichainAssetsController_getAssetsList, _MultichainAssetsController_getClient, _MultichainAssetsController_assertControllerMutexIsLocked, _MultichainAssetsController_withControllerLock;
12
+ var _MultichainAssetsController_instances, _MultichainAssetsController_snaps, _MultichainAssetsController_controllerOperationMutex, _MultichainAssetsController_handleAccountAssetListUpdatedEvent, _MultichainAssetsController_handleOnAccountAddedEvent, _MultichainAssetsController_registerMessageHandlers, _MultichainAssetsController_isAssetIgnored, _MultichainAssetsController_handleAccountAssetListUpdated, _MultichainAssetsController_isNonEvmAccount, _MultichainAssetsController_handleOnAccountAdded, _MultichainAssetsController_handleOnAccountRemovedEvent, _MultichainAssetsController_refreshAssetsMetadata, _MultichainAssetsController_updateAssetsMetadata, _MultichainAssetsController_getAssetSnaps, _MultichainAssetsController_getAssetSnapFor, _MultichainAssetsController_getAllSnaps, _MultichainAssetsController_getSnapsPermissions, _MultichainAssetsController_getAssetsMetadataFrom, _MultichainAssetsController_scanBlockaidTokens, _MultichainAssetsController_filterBlockaidSpamTokens, _MultichainAssetsController_getAssetsList, _MultichainAssetsController_getClient, _MultichainAssetsController_assertControllerMutexIsLocked, _MultichainAssetsController_withControllerLock;
13
13
  import { BaseController } from "@metamask/base-controller";
14
+ import { handleFetch } from "@metamask/controller-utils";
14
15
  import { isEvmAccountType } from "@metamask/keyring-api";
15
16
  import { KeyringClient } from "@metamask/keyring-snap-client";
16
17
  import { HandlerType } from "@metamask/snaps-utils";
@@ -18,6 +19,19 @@ import { isCaipAssetType, parseCaipAssetType } from "@metamask/utils";
18
19
  import { Mutex } from "async-mutex";
19
20
  import { getChainIdsCaveat } from "./utils.mjs";
20
21
  const controllerName = 'MultichainAssetsController';
22
+ // Blockaid token security scanning constants
23
+ const SECURITY_ALERTS_BASE_URL = 'https://security-alerts.api.cx.metamask.io';
24
+ const TOKEN_BULK_SCANNING_ENDPOINT = '/token/scan-bulk';
25
+ /**
26
+ * Result type from Blockaid token security scan.
27
+ */
28
+ export var BlockaidResultType;
29
+ (function (BlockaidResultType) {
30
+ BlockaidResultType["Benign"] = "Benign";
31
+ BlockaidResultType["Spam"] = "Spam";
32
+ BlockaidResultType["Warning"] = "Warning";
33
+ BlockaidResultType["Malicious"] = "Malicious";
34
+ })(BlockaidResultType || (BlockaidResultType = {}));
21
35
  /**
22
36
  * Constructs the default {@link MultichainAssetsController} state. This allows
23
37
  * consumers to provide a partial state object when initializing the controller
@@ -188,9 +202,11 @@ async function _MultichainAssetsController_handleAccountAssetListUpdated(event)
188
202
  const existing = this.state.accountsAssets[accountId] || [];
189
203
  // In case accountsAndAssetsToUpdate event is fired with "added" assets that already exist, we don't want to add them again
190
204
  // Also filter out ignored assets
191
- const filteredToBeAddedAssets = added.filter((asset) => !existing.includes(asset) &&
205
+ const preFilteredToBeAddedAssets = added.filter((asset) => !existing.includes(asset) &&
192
206
  isCaipAssetType(asset) &&
193
207
  !__classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_isAssetIgnored).call(this, asset, accountId));
208
+ // Filter out tokens flagged by Blockaid as non-benign
209
+ const filteredToBeAddedAssets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_filterBlockaidSpamTokens).call(this, preFilteredToBeAddedAssets);
194
210
  // In case accountsAndAssetsToUpdate event is fired with "removed" assets that don't exist, we don't want to remove them
195
211
  const filteredToBeRemovedAssets = removed.filter((asset) => existing.includes(asset) && isCaipAssetType(asset));
196
212
  if (filteredToBeAddedAssets.length > 0 ||
@@ -246,7 +262,8 @@ async function _MultichainAssetsController_handleOnAccountAdded(account) {
246
262
  __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_assertControllerMutexIsLocked).call(this);
247
263
  // Get assets list
248
264
  if (account.metadata.snap) {
249
- const assets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_getAssetsList).call(this, account.id, account.metadata.snap.id);
265
+ const allAssets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_getAssetsList).call(this, account.id, account.metadata.snap.id);
266
+ const assets = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_filterBlockaidSpamTokens).call(this, allAssets);
250
267
  await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_refreshAssetsMetadata).call(this, assets);
251
268
  this.update((state) => {
252
269
  state.accountsAssets[account.id] = assets;
@@ -391,6 +408,77 @@ async function _MultichainAssetsController_getAssetsMetadataFrom(assets, snapId)
391
408
  console.error(error);
392
409
  return undefined;
393
410
  }
411
+ }, _MultichainAssetsController_scanBlockaidTokens =
412
+ /**
413
+ * Calls the Blockaid token bulk scanning endpoint for a given chain and
414
+ * set of token addresses.
415
+ *
416
+ * @param chain - The chain name for the Blockaid API (e.g. "solana").
417
+ * @param tokens - The token addresses to scan.
418
+ * @returns The scan response, or null if the request fails or times out.
419
+ */
420
+ async function _MultichainAssetsController_scanBlockaidTokens(chain, tokens) {
421
+ try {
422
+ return await handleFetch(`${SECURITY_ALERTS_BASE_URL}${TOKEN_BULK_SCANNING_ENDPOINT}`, {
423
+ method: 'POST',
424
+ headers: {
425
+ Accept: 'application/json',
426
+ 'Content-Type': 'application/json',
427
+ },
428
+ body: JSON.stringify({ chain, tokens }),
429
+ });
430
+ }
431
+ catch (error) {
432
+ console.error('Blockaid token scan failed:', error);
433
+ return null;
434
+ }
435
+ }, _MultichainAssetsController_filterBlockaidSpamTokens =
436
+ /**
437
+ * Filters out tokens flagged as non-benign by Blockaid. Only tokens with
438
+ * an `assetNamespace` of "token" are scanned (native assets like slip44 are
439
+ * passed through unfiltered). If the API call fails, all tokens are kept.
440
+ *
441
+ * @param assets - The CAIP asset type list to filter.
442
+ * @returns The filtered list with malicious/spam/warning tokens removed.
443
+ */
444
+ async function _MultichainAssetsController_filterBlockaidSpamTokens(assets) {
445
+ // Group scannable token assets by chain namespace
446
+ const tokensByChain = {};
447
+ for (const asset of assets) {
448
+ const { assetNamespace, assetReference, chain } = parseCaipAssetType(asset);
449
+ // Only scan fungible token assets (e.g. SPL tokens), skip native (slip44)
450
+ if (assetNamespace === 'token') {
451
+ const chainName = chain.namespace;
452
+ if (!tokensByChain[chainName]) {
453
+ tokensByChain[chainName] = [];
454
+ }
455
+ tokensByChain[chainName].push({ asset, address: assetReference });
456
+ }
457
+ }
458
+ // If there are no token assets to scan, return as-is
459
+ if (Object.keys(tokensByChain).length === 0) {
460
+ return assets;
461
+ }
462
+ // Build a set of assets to reject (non-benign tokens)
463
+ const rejectedAssets = new Set();
464
+ for (const [chainName, tokenEntries] of Object.entries(tokensByChain)) {
465
+ const addresses = tokenEntries.map((entry) => entry.address);
466
+ const scanResponse = await __classPrivateFieldGet(this, _MultichainAssetsController_instances, "m", _MultichainAssetsController_scanBlockaidTokens).call(this, chainName, addresses);
467
+ if (!scanResponse?.results) {
468
+ // If the scan fails, keep all tokens (fail open)
469
+ continue;
470
+ }
471
+ for (const entry of tokenEntries) {
472
+ const result = scanResponse.results[entry.address];
473
+ // Reject the token only if we have a definitive non-benign result
474
+ if (result?.result_type &&
475
+ result.result_type !== BlockaidResultType.Benign) {
476
+ rejectedAssets.add(entry.asset);
477
+ }
478
+ }
479
+ }
480
+ // Filter while preserving original order
481
+ return assets.filter((asset) => !rejectedAssets.has(asset));
394
482
  }, _MultichainAssetsController_getAssetsList =
395
483
  /**
396
484
  * Get assets list for an account
@@ -1 +1 @@
1
- {"version":3,"file":"MultichainAssetsController.mjs","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AccountsControllerAccountAddedEvent,\n AccountsControllerAccountAssetListUpdatedEvent,\n AccountsControllerAccountRemovedEvent,\n AccountsControllerListMultichainAccountsAction,\n} from '@metamask/accounts-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { isEvmAccountType } from '@metamask/keyring-api';\nimport type {\n AccountAssetListUpdatedEventPayload,\n CaipAssetType,\n CaipAssetTypeOrId,\n} from '@metamask/keyring-api';\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\nimport { KeyringClient } from '@metamask/keyring-snap-client';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n GetPermissions,\n PermissionConstraint,\n SubjectPermissions,\n} from '@metamask/permission-controller';\nimport type {\n GetAllSnaps,\n HandleSnapRequest,\n} from '@metamask/snaps-controllers';\nimport type { FungibleAssetMetadata, Snap, SnapId } from '@metamask/snaps-sdk';\nimport { HandlerType } from '@metamask/snaps-utils';\nimport { isCaipAssetType, parseCaipAssetType } from '@metamask/utils';\nimport type { CaipChainId } from '@metamask/utils';\nimport type { Json, JsonRpcRequest } from '@metamask/utils';\nimport type { MutexInterface } from 'async-mutex';\nimport { Mutex } from 'async-mutex';\n\nimport { getChainIdsCaveat } from './utils';\n\nconst controllerName = 'MultichainAssetsController';\n\nexport type MultichainAssetsControllerState = {\n assetsMetadata: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n accountsAssets: { [account: string]: CaipAssetType[] };\n allIgnoredAssets: { [account: string]: CaipAssetType[] };\n};\n\n// Represents the response of the asset snap's onAssetLookup handler\nexport type AssetMetadataResponse = {\n assets: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n};\n\nexport type MultichainAssetsControllerAccountAssetListUpdatedEvent = {\n type: `${typeof controllerName}:accountAssetListUpdated`;\n payload: AccountsControllerAccountAssetListUpdatedEvent['payload'];\n};\n\n/**\n * Constructs the default {@link MultichainAssetsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link MultichainAssetsController} state.\n */\nexport function getDefaultMultichainAssetsControllerState(): MultichainAssetsControllerState {\n return { accountsAssets: {}, assetsMetadata: {}, allIgnoredAssets: {} };\n}\n\nexport type MultichainAssetsControllerGetAssetMetadataAction = {\n type: `${typeof controllerName}:getAssetMetadata`;\n handler: MultichainAssetsController['getAssetMetadata'];\n};\n\nexport type MultichainAssetsControllerIgnoreAssetsAction = {\n type: `${typeof controllerName}:ignoreAssets`;\n handler: MultichainAssetsController['ignoreAssets'];\n};\n\nexport type MultichainAssetsControllerAddAssetsAction = {\n type: `${typeof controllerName}:addAssets`;\n handler: MultichainAssetsController['addAssets'];\n};\n\n/**\n * Returns the state of the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n MultichainAssetsControllerState\n>;\n\n/**\n * Event emitted when the state of the {@link MultichainAssetsController} changes.\n */\nexport type MultichainAssetsControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n MultichainAssetsControllerState\n >;\n\n/**\n * Actions exposed by the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerActions =\n | MultichainAssetsControllerGetStateAction\n | MultichainAssetsControllerGetAssetMetadataAction\n | MultichainAssetsControllerIgnoreAssetsAction\n | MultichainAssetsControllerAddAssetsAction;\n\n/**\n * Events emitted by {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerEvents =\n | MultichainAssetsControllerStateChangeEvent\n | MultichainAssetsControllerAccountAssetListUpdatedEvent;\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback<Result> = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise<Result>;\n\n/**\n * Actions that this controller is allowed to call.\n */\ntype AllowedActions =\n | HandleSnapRequest\n | GetAllSnaps\n | GetPermissions\n | AccountsControllerListMultichainAccountsAction;\n\n/**\n * Events that this controller is allowed to subscribe.\n */\ntype AllowedEvents =\n | AccountsControllerAccountAddedEvent\n | AccountsControllerAccountRemovedEvent\n | AccountsControllerAccountAssetListUpdatedEvent;\n\n/**\n * Messenger type for the MultichainAssetsController.\n */\nexport type MultichainAssetsControllerMessenger = Messenger<\n typeof controllerName,\n MultichainAssetsControllerActions | AllowedActions,\n MultichainAssetsControllerEvents | AllowedEvents\n>;\n\n/**\n * {@link MultichainAssetsController}'s metadata.\n *\n * This allows us to choose if fields of the state should be persisted or not\n * using the `persist` flag; and if they can be sent to Sentry or not, using\n * the `anonymous` flag.\n */\nconst assetsControllerMetadata: StateMetadata<MultichainAssetsControllerState> =\n {\n assetsMetadata: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n accountsAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n allIgnoredAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n };\n\n// TODO: make this controller extends StaticIntervalPollingController and update all assetsMetadata once a day.\n\nexport class MultichainAssetsController extends BaseController<\n typeof controllerName,\n MultichainAssetsControllerState,\n MultichainAssetsControllerMessenger\n> {\n // Mapping of CAIP-2 Chain ID to Asset Snaps.\n #snaps: Record<CaipChainId, Snap[]>;\n\n readonly #controllerOperationMutex = new Mutex();\n\n constructor({\n messenger,\n state = {},\n }: {\n messenger: MultichainAssetsControllerMessenger;\n state?: Partial<MultichainAssetsControllerState>;\n }) {\n super({\n messenger,\n name: controllerName,\n metadata: assetsControllerMetadata,\n state: {\n ...getDefaultMultichainAssetsControllerState(),\n ...state,\n },\n });\n\n this.#snaps = {};\n\n this.messenger.subscribe(\n 'AccountsController:accountAdded',\n async (account) => await this.#handleOnAccountAddedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountRemoved',\n async (account) => await this.#handleOnAccountRemovedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountAssetListUpdated',\n async (event) => await this.#handleAccountAssetListUpdatedEvent(event),\n );\n\n this.#registerMessageHandlers();\n }\n\n async #handleAccountAssetListUpdatedEvent(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n return this.#withControllerLock(async () =>\n this.#handleAccountAssetListUpdated(event),\n );\n }\n\n async #handleOnAccountAddedEvent(account: InternalAccount) {\n return this.#withControllerLock(async () =>\n this.#handleOnAccountAdded(account),\n );\n }\n\n /**\n * Constructor helper for registering the controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:getAssetMetadata',\n this.getAssetMetadata.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:ignoreAssets',\n this.ignoreAssets.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:addAssets',\n this.addAssets.bind(this),\n );\n }\n\n /**\n * Returns the metadata for the given asset\n *\n * @param asset - The asset to get metadata for\n * @returns The metadata for the asset or undefined if not found.\n */\n getAssetMetadata(asset: CaipAssetType): FungibleAssetMetadata | undefined {\n return this.state.assetsMetadata[asset];\n }\n\n /**\n * Ignores a batch of assets for a specific account.\n *\n * @param assetsToIgnore - Array of asset IDs to ignore.\n * @param accountId - The account ID to ignore assets for.\n */\n ignoreAssets(assetsToIgnore: CaipAssetType[], accountId: string): void {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = state.accountsAssets[\n accountId\n ].filter((asset) => !assetsToIgnore.includes(asset));\n }\n\n if (!state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = [];\n }\n\n const newIgnoredAssets = assetsToIgnore.filter(\n (asset) => !state.allIgnoredAssets[accountId].includes(asset),\n );\n state.allIgnoredAssets[accountId].push(...newIgnoredAssets);\n });\n }\n\n /**\n * Adds multiple assets to the stored asset list for a specific account.\n * All assets must belong to the same chain.\n *\n * @param assetIds - Array of CAIP asset IDs to add (must be from same chain).\n * @param accountId - The account ID to add the assets to.\n * @returns The updated asset list for the account.\n * @throws Error if assets are from different chains.\n */\n async addAssets(\n assetIds: CaipAssetType[],\n accountId: string,\n ): Promise<CaipAssetType[]> {\n if (assetIds.length === 0) {\n return this.state.accountsAssets[accountId] || [];\n }\n\n // Validate that all assets are from the same chain\n const chainIds = new Set(\n assetIds.map((assetId) => parseCaipAssetType(assetId).chainId),\n );\n if (chainIds.size > 1) {\n throw new Error(\n `All assets must belong to the same chain. Found assets from chains: ${Array.from(chainIds).join(', ')}`,\n );\n }\n\n return this.#withControllerLock(async () => {\n // Refresh metadata for all assets\n await this.#refreshAssetsMetadata(assetIds);\n\n const addedAssets: CaipAssetType[] = [];\n\n this.update((state) => {\n // Initialize account assets if it doesn't exist\n if (!state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = [];\n }\n\n // Add assets if they don't already exist\n for (const assetId of assetIds) {\n if (!state.accountsAssets[accountId].includes(assetId)) {\n state.accountsAssets[accountId].push(assetId);\n addedAssets.push(assetId);\n }\n }\n\n // Remove from ignored list if they exist there (inline logic like EVM)\n if (state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = state.allIgnoredAssets[\n accountId\n ].filter((asset) => !assetIds.includes(asset));\n\n // Clean up empty arrays\n if (state.allIgnoredAssets[accountId].length === 0) {\n delete state.allIgnoredAssets[accountId];\n }\n }\n });\n\n // Publish event to notify other controllers (balances, rates) about the new assets\n if (addedAssets.length > 0) {\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [accountId]: {\n added: addedAssets,\n removed: [],\n },\n },\n });\n }\n\n return this.state.accountsAssets[accountId] || [];\n });\n }\n\n /**\n * Checks if an asset is ignored for a specific account.\n *\n * @param asset - The asset ID to check.\n * @param accountId - The account ID to check for.\n * @returns True if the asset is ignored, false otherwise.\n */\n #isAssetIgnored(asset: CaipAssetType, accountId: string): boolean {\n return this.state.allIgnoredAssets[accountId]?.includes(asset) ?? false;\n }\n\n /**\n * Function to update the assets list for an account\n *\n * @param event - The list of assets to update\n */\n async #handleAccountAssetListUpdated(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n this.#assertControllerMutexIsLocked();\n\n const assetsForMetadataRefresh = new Set<CaipAssetType>([]);\n const accountsAndAssetsToUpdate: AccountAssetListUpdatedEventPayload['assets'] =\n {};\n for (const [accountId, { added, removed }] of Object.entries(\n event.assets,\n )) {\n if (added.length > 0 || removed.length > 0) {\n const existing = this.state.accountsAssets[accountId] || [];\n\n // In case accountsAndAssetsToUpdate event is fired with \"added\" assets that already exist, we don't want to add them again\n // Also filter out ignored assets\n const filteredToBeAddedAssets = added.filter(\n (asset) =>\n !existing.includes(asset) &&\n isCaipAssetType(asset) &&\n !this.#isAssetIgnored(asset, accountId),\n );\n\n // In case accountsAndAssetsToUpdate event is fired with \"removed\" assets that don't exist, we don't want to remove them\n const filteredToBeRemovedAssets = removed.filter(\n (asset) => existing.includes(asset) && isCaipAssetType(asset),\n );\n\n if (\n filteredToBeAddedAssets.length > 0 ||\n filteredToBeRemovedAssets.length > 0\n ) {\n accountsAndAssetsToUpdate[accountId] = {\n added: filteredToBeAddedAssets,\n removed: filteredToBeRemovedAssets,\n };\n }\n\n for (const asset of existing) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeAddedAssets) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeRemovedAssets) {\n assetsForMetadataRefresh.delete(asset);\n }\n }\n }\n\n this.update((state) => {\n for (const [accountId, { added, removed }] of Object.entries(\n accountsAndAssetsToUpdate,\n )) {\n const assets = new Set([\n ...(state.accountsAssets[accountId] || []),\n ...added,\n ]);\n for (const asset of removed) {\n assets.delete(asset);\n }\n\n state.accountsAssets[accountId] = Array.from(assets);\n }\n });\n\n // Trigger fetching metadata for new assets\n await this.#refreshAssetsMetadata(Array.from(assetsForMetadataRefresh));\n\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: accountsAndAssetsToUpdate,\n });\n }\n\n /**\n * Checks for non-EVM accounts.\n *\n * @param account - The new account to be checked.\n * @returns True if the account is a non-EVM account, false otherwise.\n */\n #isNonEvmAccount(account: InternalAccount): boolean {\n return (\n !isEvmAccountType(account.type) &&\n // Non-EVM accounts are backed by a Snap for now\n account.metadata.snap !== undefined\n );\n }\n\n /**\n * Handles changes when a new account has been added.\n *\n * @param account - The new account being added.\n */\n async #handleOnAccountAdded(account: InternalAccount): Promise<void> {\n if (!this.#isNonEvmAccount(account)) {\n // Nothing to do here for EVM accounts\n return;\n }\n this.#assertControllerMutexIsLocked();\n\n // Get assets list\n if (account.metadata.snap) {\n const assets = await this.#getAssetsList(\n account.id,\n account.metadata.snap.id,\n );\n await this.#refreshAssetsMetadata(assets);\n this.update((state) => {\n state.accountsAssets[account.id] = assets;\n });\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [account.id]: {\n added: assets,\n removed: [],\n },\n },\n });\n }\n }\n\n /**\n * Handles changes when a new account has been removed.\n *\n * @param accountId - The new account id being removed.\n */\n async #handleOnAccountRemovedEvent(accountId: string): Promise<void> {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n delete state.accountsAssets[accountId];\n }\n if (state.allIgnoredAssets[accountId]) {\n delete state.allIgnoredAssets[accountId];\n }\n // TODO: We are not deleting the assetsMetadata because we will soon make this controller extends StaticIntervalPollingController\n // and update all assetsMetadata once a day.\n });\n }\n\n /**\n * Refreshes the assets snaps and metadata for the given list of assets\n *\n * @param assets - The assets to refresh\n */\n async #refreshAssetsMetadata(assets: CaipAssetType[]) {\n this.#assertControllerMutexIsLocked();\n\n const assetsWithoutMetadata: CaipAssetType[] = assets.filter(\n (asset) => !this.state.assetsMetadata[asset],\n );\n\n // Call the snap to get the metadata\n if (assetsWithoutMetadata.length > 0) {\n // Check if for every asset in assetsWithoutMetadata there is a snap in snaps by chainId else call getAssetSnaps\n if (\n !assetsWithoutMetadata.every((asset: CaipAssetType) => {\n const { chainId } = parseCaipAssetType(asset);\n return Boolean(this.#getAssetSnapFor(chainId));\n })\n ) {\n this.#snaps = this.#getAssetSnaps();\n }\n await this.#updateAssetsMetadata(assetsWithoutMetadata);\n }\n }\n\n /**\n * Updates the assets metadata for the given list of assets\n *\n * @param assets - The assets to update\n */\n async #updateAssetsMetadata(assets: CaipAssetType[]) {\n // Creates a mapping of scope to their respective assets list.\n const assetsByScope: Record<CaipChainId, CaipAssetType[]> = {};\n for (const asset of assets) {\n const { chainId } = parseCaipAssetType(asset);\n if (!assetsByScope[chainId]) {\n assetsByScope[chainId] = [];\n }\n assetsByScope[chainId].push(asset);\n }\n\n let newMetadata: Record<CaipAssetType, FungibleAssetMetadata> = {};\n for (const chainId of Object.keys(assetsByScope) as CaipChainId[]) {\n const assetsForChain = assetsByScope[chainId];\n // Now fetch metadata from the associated asset Snaps:\n const snap = this.#getAssetSnapFor(chainId);\n if (snap) {\n const metadata = await this.#getAssetsMetadataFrom(\n assetsForChain,\n snap.id,\n );\n newMetadata = {\n ...newMetadata,\n ...(metadata?.assets ?? {}),\n };\n }\n }\n this.update((state) => {\n state.assetsMetadata = {\n ...this.state.assetsMetadata,\n ...newMetadata,\n };\n });\n }\n\n /**\n * Creates a mapping of CAIP-2 Chain ID to Asset Snaps.\n *\n * @returns A mapping of CAIP-2 Chain ID to Asset Snaps.\n */\n #getAssetSnaps(): Record<CaipChainId, Snap[]> {\n const snaps: Record<CaipChainId, Snap[]> = {};\n const allSnaps = this.#getAllSnaps();\n const allPermissions = allSnaps.map((snap) =>\n this.#getSnapsPermissions(snap.id),\n );\n\n for (const [index, permission] of allPermissions.entries()) {\n let scopes;\n for (const singlePermissionConstraint of Object.values(permission)) {\n scopes = getChainIdsCaveat(singlePermissionConstraint);\n if (!scopes) {\n continue;\n }\n for (const scope of scopes as CaipChainId[]) {\n if (!snaps[scope]) {\n snaps[scope] = [];\n }\n snaps[scope].push(allSnaps[index]);\n }\n }\n }\n return snaps;\n }\n\n /**\n * Returns the first asset snap for the given scope\n *\n * @param scope - The scope to get the asset snap for\n * @returns The asset snap for the given scope\n */\n #getAssetSnapFor(scope: CaipChainId): Snap | undefined {\n const allSnaps = this.#snaps[scope];\n // Pick only the first one, we ignore the other Snaps if there are multiple candidates for now.\n return allSnaps?.[0]; // Will be undefined if there's no Snaps candidate for this scope.\n }\n\n /**\n * Returns all the asset snaps\n *\n * @returns All the asset snaps\n */\n #getAllSnaps(): Snap[] {\n // TODO: Use dedicated SnapController's action once available for this:\n return this.messenger\n .call('SnapController:getAll')\n .filter((snap) => snap.enabled && !snap.blocked);\n }\n\n /**\n * Returns the permissions for the given origin\n *\n * @param origin - The origin to get the permissions for\n * @returns The permissions for the given origin\n */\n #getSnapsPermissions(\n origin: string,\n ): SubjectPermissions<PermissionConstraint> {\n return this.messenger.call(\n 'PermissionController:getPermissions',\n origin,\n ) as SubjectPermissions<PermissionConstraint>;\n }\n\n /**\n * Returns the metadata for the given assets\n *\n * @param assets - The assets to get metadata for\n * @param snapId - The snap ID to get metadata from\n * @returns The metadata for the assets\n */\n async #getAssetsMetadataFrom(\n assets: CaipAssetType[],\n snapId: string,\n ): Promise<AssetMetadataResponse | undefined> {\n try {\n return (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnAssetsLookup,\n request: {\n jsonrpc: '2.0',\n method: 'onAssetLookup',\n params: {\n assets,\n },\n },\n })) as Promise<AssetMetadataResponse>;\n } catch (error) {\n // Ignore\n console.error(error);\n return undefined;\n }\n }\n\n /**\n * Get assets list for an account\n *\n * @param accountId - AccountId to get assets for\n * @param snapId - Snap ID for the account\n * @returns list of assets\n */\n async #getAssetsList(\n accountId: string,\n snapId: string,\n ): Promise<CaipAssetTypeOrId[]> {\n return await this.#getClient(snapId).listAccountAssets(accountId);\n }\n\n /**\n * Gets a `KeyringClient` for a Snap.\n *\n * @param snapId - ID of the Snap to get the client for.\n * @returns A `KeyringClient` for the Snap.\n */\n #getClient(snapId: string): KeyringClient {\n return new KeyringClient({\n send: async (request: JsonRpcRequest) =>\n (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnKeyringRequest,\n request,\n })) as Promise<Json>,\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(\n 'MultichainAssetsControllerError - Attempt to update state',\n );\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param callback - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock<Result>(\n callback: MutuallyExclusiveCallback<Result>,\n ): Promise<Result> {\n return withLock(this.#controllerOperationMutex, callback);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param callback - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock<Result>(\n mutex: Mutex,\n callback: MutuallyExclusiveCallback<Result>,\n): Promise<Result> {\n const releaseLock = await mutex.acquire();\n\n try {\n return await callback({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n"]}
1
+ {"version":3,"file":"MultichainAssetsController.mjs","sourceRoot":"","sources":["../../src/MultichainAssetsController/MultichainAssetsController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AccountsControllerAccountAddedEvent,\n AccountsControllerAccountAssetListUpdatedEvent,\n AccountsControllerAccountRemovedEvent,\n AccountsControllerListMultichainAccountsAction,\n} from '@metamask/accounts-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { handleFetch } from '@metamask/controller-utils';\nimport { isEvmAccountType } from '@metamask/keyring-api';\nimport type {\n AccountAssetListUpdatedEventPayload,\n CaipAssetType,\n CaipAssetTypeOrId,\n} from '@metamask/keyring-api';\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\nimport { KeyringClient } from '@metamask/keyring-snap-client';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n GetPermissions,\n PermissionConstraint,\n SubjectPermissions,\n} from '@metamask/permission-controller';\nimport type {\n GetAllSnaps,\n HandleSnapRequest,\n} from '@metamask/snaps-controllers';\nimport type { FungibleAssetMetadata, Snap, SnapId } from '@metamask/snaps-sdk';\nimport { HandlerType } from '@metamask/snaps-utils';\nimport { isCaipAssetType, parseCaipAssetType } from '@metamask/utils';\nimport type { CaipChainId } from '@metamask/utils';\nimport type { Json, JsonRpcRequest } from '@metamask/utils';\nimport type { MutexInterface } from 'async-mutex';\nimport { Mutex } from 'async-mutex';\n\nimport { getChainIdsCaveat } from './utils';\n\nconst controllerName = 'MultichainAssetsController';\n\n// Blockaid token security scanning constants\nconst SECURITY_ALERTS_BASE_URL = 'https://security-alerts.api.cx.metamask.io';\nconst TOKEN_BULK_SCANNING_ENDPOINT = '/token/scan-bulk';\n\n/**\n * Result type from Blockaid token security scan.\n */\nexport enum BlockaidResultType {\n Benign = 'Benign',\n Spam = 'Spam',\n Warning = 'Warning',\n Malicious = 'Malicious',\n}\n\n/**\n * Response shape from the Blockaid token bulk scanning endpoint.\n */\ntype BlockaidTokenScanResponse = {\n results: Record<\n string,\n {\n /* eslint-disable @typescript-eslint/naming-convention */\n result_type: BlockaidResultType;\n malicious_score: string;\n attack_types: Record<string, unknown>;\n /* eslint-enable @typescript-eslint/naming-convention */\n chain: string;\n address: string;\n }\n >;\n};\n\nexport type MultichainAssetsControllerState = {\n assetsMetadata: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n accountsAssets: { [account: string]: CaipAssetType[] };\n allIgnoredAssets: { [account: string]: CaipAssetType[] };\n};\n\n// Represents the response of the asset snap's onAssetLookup handler\nexport type AssetMetadataResponse = {\n assets: {\n [asset: CaipAssetType]: FungibleAssetMetadata;\n };\n};\n\nexport type MultichainAssetsControllerAccountAssetListUpdatedEvent = {\n type: `${typeof controllerName}:accountAssetListUpdated`;\n payload: AccountsControllerAccountAssetListUpdatedEvent['payload'];\n};\n\n/**\n * Constructs the default {@link MultichainAssetsController} state. This allows\n * consumers to provide a partial state object when initializing the controller\n * and also helps in constructing complete state objects for this controller in\n * tests.\n *\n * @returns The default {@link MultichainAssetsController} state.\n */\nexport function getDefaultMultichainAssetsControllerState(): MultichainAssetsControllerState {\n return { accountsAssets: {}, assetsMetadata: {}, allIgnoredAssets: {} };\n}\n\nexport type MultichainAssetsControllerGetAssetMetadataAction = {\n type: `${typeof controllerName}:getAssetMetadata`;\n handler: MultichainAssetsController['getAssetMetadata'];\n};\n\nexport type MultichainAssetsControllerIgnoreAssetsAction = {\n type: `${typeof controllerName}:ignoreAssets`;\n handler: MultichainAssetsController['ignoreAssets'];\n};\n\nexport type MultichainAssetsControllerAddAssetsAction = {\n type: `${typeof controllerName}:addAssets`;\n handler: MultichainAssetsController['addAssets'];\n};\n\n/**\n * Returns the state of the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n MultichainAssetsControllerState\n>;\n\n/**\n * Event emitted when the state of the {@link MultichainAssetsController} changes.\n */\nexport type MultichainAssetsControllerStateChangeEvent =\n ControllerStateChangeEvent<\n typeof controllerName,\n MultichainAssetsControllerState\n >;\n\n/**\n * Actions exposed by the {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerActions =\n | MultichainAssetsControllerGetStateAction\n | MultichainAssetsControllerGetAssetMetadataAction\n | MultichainAssetsControllerIgnoreAssetsAction\n | MultichainAssetsControllerAddAssetsAction;\n\n/**\n * Events emitted by {@link MultichainAssetsController}.\n */\nexport type MultichainAssetsControllerEvents =\n | MultichainAssetsControllerStateChangeEvent\n | MultichainAssetsControllerAccountAssetListUpdatedEvent;\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback<Result> = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise<Result>;\n\n/**\n * Actions that this controller is allowed to call.\n */\ntype AllowedActions =\n | HandleSnapRequest\n | GetAllSnaps\n | GetPermissions\n | AccountsControllerListMultichainAccountsAction;\n\n/**\n * Events that this controller is allowed to subscribe.\n */\ntype AllowedEvents =\n | AccountsControllerAccountAddedEvent\n | AccountsControllerAccountRemovedEvent\n | AccountsControllerAccountAssetListUpdatedEvent;\n\n/**\n * Messenger type for the MultichainAssetsController.\n */\nexport type MultichainAssetsControllerMessenger = Messenger<\n typeof controllerName,\n MultichainAssetsControllerActions | AllowedActions,\n MultichainAssetsControllerEvents | AllowedEvents\n>;\n\n/**\n * {@link MultichainAssetsController}'s metadata.\n *\n * This allows us to choose if fields of the state should be persisted or not\n * using the `persist` flag; and if they can be sent to Sentry or not, using\n * the `anonymous` flag.\n */\nconst assetsControllerMetadata: StateMetadata<MultichainAssetsControllerState> =\n {\n assetsMetadata: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n accountsAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n allIgnoredAssets: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n };\n\n// TODO: make this controller extends StaticIntervalPollingController and update all assetsMetadata once a day.\n\nexport class MultichainAssetsController extends BaseController<\n typeof controllerName,\n MultichainAssetsControllerState,\n MultichainAssetsControllerMessenger\n> {\n // Mapping of CAIP-2 Chain ID to Asset Snaps.\n #snaps: Record<CaipChainId, Snap[]>;\n\n readonly #controllerOperationMutex = new Mutex();\n\n constructor({\n messenger,\n state = {},\n }: {\n messenger: MultichainAssetsControllerMessenger;\n state?: Partial<MultichainAssetsControllerState>;\n }) {\n super({\n messenger,\n name: controllerName,\n metadata: assetsControllerMetadata,\n state: {\n ...getDefaultMultichainAssetsControllerState(),\n ...state,\n },\n });\n\n this.#snaps = {};\n\n this.messenger.subscribe(\n 'AccountsController:accountAdded',\n async (account) => await this.#handleOnAccountAddedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountRemoved',\n async (account) => await this.#handleOnAccountRemovedEvent(account),\n );\n this.messenger.subscribe(\n 'AccountsController:accountAssetListUpdated',\n async (event) => await this.#handleAccountAssetListUpdatedEvent(event),\n );\n\n this.#registerMessageHandlers();\n }\n\n async #handleAccountAssetListUpdatedEvent(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n return this.#withControllerLock(async () =>\n this.#handleAccountAssetListUpdated(event),\n );\n }\n\n async #handleOnAccountAddedEvent(account: InternalAccount) {\n return this.#withControllerLock(async () =>\n this.#handleOnAccountAdded(account),\n );\n }\n\n /**\n * Constructor helper for registering the controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:getAssetMetadata',\n this.getAssetMetadata.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:ignoreAssets',\n this.ignoreAssets.bind(this),\n );\n\n this.messenger.registerActionHandler(\n 'MultichainAssetsController:addAssets',\n this.addAssets.bind(this),\n );\n }\n\n /**\n * Returns the metadata for the given asset\n *\n * @param asset - The asset to get metadata for\n * @returns The metadata for the asset or undefined if not found.\n */\n getAssetMetadata(asset: CaipAssetType): FungibleAssetMetadata | undefined {\n return this.state.assetsMetadata[asset];\n }\n\n /**\n * Ignores a batch of assets for a specific account.\n *\n * @param assetsToIgnore - Array of asset IDs to ignore.\n * @param accountId - The account ID to ignore assets for.\n */\n ignoreAssets(assetsToIgnore: CaipAssetType[], accountId: string): void {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = state.accountsAssets[\n accountId\n ].filter((asset) => !assetsToIgnore.includes(asset));\n }\n\n if (!state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = [];\n }\n\n const newIgnoredAssets = assetsToIgnore.filter(\n (asset) => !state.allIgnoredAssets[accountId].includes(asset),\n );\n state.allIgnoredAssets[accountId].push(...newIgnoredAssets);\n });\n }\n\n /**\n * Adds multiple assets to the stored asset list for a specific account.\n * All assets must belong to the same chain.\n *\n * @param assetIds - Array of CAIP asset IDs to add (must be from same chain).\n * @param accountId - The account ID to add the assets to.\n * @returns The updated asset list for the account.\n * @throws Error if assets are from different chains.\n */\n async addAssets(\n assetIds: CaipAssetType[],\n accountId: string,\n ): Promise<CaipAssetType[]> {\n if (assetIds.length === 0) {\n return this.state.accountsAssets[accountId] || [];\n }\n\n // Validate that all assets are from the same chain\n const chainIds = new Set(\n assetIds.map((assetId) => parseCaipAssetType(assetId).chainId),\n );\n if (chainIds.size > 1) {\n throw new Error(\n `All assets must belong to the same chain. Found assets from chains: ${Array.from(chainIds).join(', ')}`,\n );\n }\n\n return this.#withControllerLock(async () => {\n // Refresh metadata for all assets\n await this.#refreshAssetsMetadata(assetIds);\n\n const addedAssets: CaipAssetType[] = [];\n\n this.update((state) => {\n // Initialize account assets if it doesn't exist\n if (!state.accountsAssets[accountId]) {\n state.accountsAssets[accountId] = [];\n }\n\n // Add assets if they don't already exist\n for (const assetId of assetIds) {\n if (!state.accountsAssets[accountId].includes(assetId)) {\n state.accountsAssets[accountId].push(assetId);\n addedAssets.push(assetId);\n }\n }\n\n // Remove from ignored list if they exist there (inline logic like EVM)\n if (state.allIgnoredAssets[accountId]) {\n state.allIgnoredAssets[accountId] = state.allIgnoredAssets[\n accountId\n ].filter((asset) => !assetIds.includes(asset));\n\n // Clean up empty arrays\n if (state.allIgnoredAssets[accountId].length === 0) {\n delete state.allIgnoredAssets[accountId];\n }\n }\n });\n\n // Publish event to notify other controllers (balances, rates) about the new assets\n if (addedAssets.length > 0) {\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [accountId]: {\n added: addedAssets,\n removed: [],\n },\n },\n });\n }\n\n return this.state.accountsAssets[accountId] || [];\n });\n }\n\n /**\n * Checks if an asset is ignored for a specific account.\n *\n * @param asset - The asset ID to check.\n * @param accountId - The account ID to check for.\n * @returns True if the asset is ignored, false otherwise.\n */\n #isAssetIgnored(asset: CaipAssetType, accountId: string): boolean {\n return this.state.allIgnoredAssets[accountId]?.includes(asset) ?? false;\n }\n\n /**\n * Function to update the assets list for an account\n *\n * @param event - The list of assets to update\n */\n async #handleAccountAssetListUpdated(\n event: AccountAssetListUpdatedEventPayload,\n ) {\n this.#assertControllerMutexIsLocked();\n\n const assetsForMetadataRefresh = new Set<CaipAssetType>([]);\n const accountsAndAssetsToUpdate: AccountAssetListUpdatedEventPayload['assets'] =\n {};\n for (const [accountId, { added, removed }] of Object.entries(\n event.assets,\n )) {\n if (added.length > 0 || removed.length > 0) {\n const existing = this.state.accountsAssets[accountId] || [];\n\n // In case accountsAndAssetsToUpdate event is fired with \"added\" assets that already exist, we don't want to add them again\n // Also filter out ignored assets\n const preFilteredToBeAddedAssets = added.filter(\n (asset) =>\n !existing.includes(asset) &&\n isCaipAssetType(asset) &&\n !this.#isAssetIgnored(asset, accountId),\n );\n\n // Filter out tokens flagged by Blockaid as non-benign\n const filteredToBeAddedAssets = await this.#filterBlockaidSpamTokens(\n preFilteredToBeAddedAssets,\n );\n\n // In case accountsAndAssetsToUpdate event is fired with \"removed\" assets that don't exist, we don't want to remove them\n const filteredToBeRemovedAssets = removed.filter(\n (asset) => existing.includes(asset) && isCaipAssetType(asset),\n );\n\n if (\n filteredToBeAddedAssets.length > 0 ||\n filteredToBeRemovedAssets.length > 0\n ) {\n accountsAndAssetsToUpdate[accountId] = {\n added: filteredToBeAddedAssets,\n removed: filteredToBeRemovedAssets,\n };\n }\n\n for (const asset of existing) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeAddedAssets) {\n assetsForMetadataRefresh.add(asset);\n }\n for (const asset of filteredToBeRemovedAssets) {\n assetsForMetadataRefresh.delete(asset);\n }\n }\n }\n\n this.update((state) => {\n for (const [accountId, { added, removed }] of Object.entries(\n accountsAndAssetsToUpdate,\n )) {\n const assets = new Set([\n ...(state.accountsAssets[accountId] || []),\n ...added,\n ]);\n for (const asset of removed) {\n assets.delete(asset);\n }\n\n state.accountsAssets[accountId] = Array.from(assets);\n }\n });\n\n // Trigger fetching metadata for new assets\n await this.#refreshAssetsMetadata(Array.from(assetsForMetadataRefresh));\n\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: accountsAndAssetsToUpdate,\n });\n }\n\n /**\n * Checks for non-EVM accounts.\n *\n * @param account - The new account to be checked.\n * @returns True if the account is a non-EVM account, false otherwise.\n */\n #isNonEvmAccount(account: InternalAccount): boolean {\n return (\n !isEvmAccountType(account.type) &&\n // Non-EVM accounts are backed by a Snap for now\n account.metadata.snap !== undefined\n );\n }\n\n /**\n * Handles changes when a new account has been added.\n *\n * @param account - The new account being added.\n */\n async #handleOnAccountAdded(account: InternalAccount): Promise<void> {\n if (!this.#isNonEvmAccount(account)) {\n // Nothing to do here for EVM accounts\n return;\n }\n this.#assertControllerMutexIsLocked();\n\n // Get assets list\n if (account.metadata.snap) {\n const allAssets = await this.#getAssetsList(\n account.id,\n account.metadata.snap.id,\n );\n const assets = await this.#filterBlockaidSpamTokens(allAssets);\n await this.#refreshAssetsMetadata(assets);\n this.update((state) => {\n state.accountsAssets[account.id] = assets;\n });\n this.messenger.publish(`${controllerName}:accountAssetListUpdated`, {\n assets: {\n [account.id]: {\n added: assets,\n removed: [],\n },\n },\n });\n }\n }\n\n /**\n * Handles changes when a new account has been removed.\n *\n * @param accountId - The new account id being removed.\n */\n async #handleOnAccountRemovedEvent(accountId: string): Promise<void> {\n this.update((state) => {\n if (state.accountsAssets[accountId]) {\n delete state.accountsAssets[accountId];\n }\n if (state.allIgnoredAssets[accountId]) {\n delete state.allIgnoredAssets[accountId];\n }\n // TODO: We are not deleting the assetsMetadata because we will soon make this controller extends StaticIntervalPollingController\n // and update all assetsMetadata once a day.\n });\n }\n\n /**\n * Refreshes the assets snaps and metadata for the given list of assets\n *\n * @param assets - The assets to refresh\n */\n async #refreshAssetsMetadata(assets: CaipAssetType[]) {\n this.#assertControllerMutexIsLocked();\n\n const assetsWithoutMetadata: CaipAssetType[] = assets.filter(\n (asset) => !this.state.assetsMetadata[asset],\n );\n\n // Call the snap to get the metadata\n if (assetsWithoutMetadata.length > 0) {\n // Check if for every asset in assetsWithoutMetadata there is a snap in snaps by chainId else call getAssetSnaps\n if (\n !assetsWithoutMetadata.every((asset: CaipAssetType) => {\n const { chainId } = parseCaipAssetType(asset);\n return Boolean(this.#getAssetSnapFor(chainId));\n })\n ) {\n this.#snaps = this.#getAssetSnaps();\n }\n await this.#updateAssetsMetadata(assetsWithoutMetadata);\n }\n }\n\n /**\n * Updates the assets metadata for the given list of assets\n *\n * @param assets - The assets to update\n */\n async #updateAssetsMetadata(assets: CaipAssetType[]) {\n // Creates a mapping of scope to their respective assets list.\n const assetsByScope: Record<CaipChainId, CaipAssetType[]> = {};\n for (const asset of assets) {\n const { chainId } = parseCaipAssetType(asset);\n if (!assetsByScope[chainId]) {\n assetsByScope[chainId] = [];\n }\n assetsByScope[chainId].push(asset);\n }\n\n let newMetadata: Record<CaipAssetType, FungibleAssetMetadata> = {};\n for (const chainId of Object.keys(assetsByScope) as CaipChainId[]) {\n const assetsForChain = assetsByScope[chainId];\n // Now fetch metadata from the associated asset Snaps:\n const snap = this.#getAssetSnapFor(chainId);\n if (snap) {\n const metadata = await this.#getAssetsMetadataFrom(\n assetsForChain,\n snap.id,\n );\n newMetadata = {\n ...newMetadata,\n ...(metadata?.assets ?? {}),\n };\n }\n }\n this.update((state) => {\n state.assetsMetadata = {\n ...this.state.assetsMetadata,\n ...newMetadata,\n };\n });\n }\n\n /**\n * Creates a mapping of CAIP-2 Chain ID to Asset Snaps.\n *\n * @returns A mapping of CAIP-2 Chain ID to Asset Snaps.\n */\n #getAssetSnaps(): Record<CaipChainId, Snap[]> {\n const snaps: Record<CaipChainId, Snap[]> = {};\n const allSnaps = this.#getAllSnaps();\n const allPermissions = allSnaps.map((snap) =>\n this.#getSnapsPermissions(snap.id),\n );\n\n for (const [index, permission] of allPermissions.entries()) {\n let scopes;\n for (const singlePermissionConstraint of Object.values(permission)) {\n scopes = getChainIdsCaveat(singlePermissionConstraint);\n if (!scopes) {\n continue;\n }\n for (const scope of scopes as CaipChainId[]) {\n if (!snaps[scope]) {\n snaps[scope] = [];\n }\n snaps[scope].push(allSnaps[index]);\n }\n }\n }\n return snaps;\n }\n\n /**\n * Returns the first asset snap for the given scope\n *\n * @param scope - The scope to get the asset snap for\n * @returns The asset snap for the given scope\n */\n #getAssetSnapFor(scope: CaipChainId): Snap | undefined {\n const allSnaps = this.#snaps[scope];\n // Pick only the first one, we ignore the other Snaps if there are multiple candidates for now.\n return allSnaps?.[0]; // Will be undefined if there's no Snaps candidate for this scope.\n }\n\n /**\n * Returns all the asset snaps\n *\n * @returns All the asset snaps\n */\n #getAllSnaps(): Snap[] {\n // TODO: Use dedicated SnapController's action once available for this:\n return this.messenger\n .call('SnapController:getAll')\n .filter((snap) => snap.enabled && !snap.blocked);\n }\n\n /**\n * Returns the permissions for the given origin\n *\n * @param origin - The origin to get the permissions for\n * @returns The permissions for the given origin\n */\n #getSnapsPermissions(\n origin: string,\n ): SubjectPermissions<PermissionConstraint> {\n return this.messenger.call(\n 'PermissionController:getPermissions',\n origin,\n ) as SubjectPermissions<PermissionConstraint>;\n }\n\n /**\n * Returns the metadata for the given assets\n *\n * @param assets - The assets to get metadata for\n * @param snapId - The snap ID to get metadata from\n * @returns The metadata for the assets\n */\n async #getAssetsMetadataFrom(\n assets: CaipAssetType[],\n snapId: string,\n ): Promise<AssetMetadataResponse | undefined> {\n try {\n return (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnAssetsLookup,\n request: {\n jsonrpc: '2.0',\n method: 'onAssetLookup',\n params: {\n assets,\n },\n },\n })) as Promise<AssetMetadataResponse>;\n } catch (error) {\n // Ignore\n console.error(error);\n return undefined;\n }\n }\n\n /**\n * Calls the Blockaid token bulk scanning endpoint for a given chain and\n * set of token addresses.\n *\n * @param chain - The chain name for the Blockaid API (e.g. \"solana\").\n * @param tokens - The token addresses to scan.\n * @returns The scan response, or null if the request fails or times out.\n */\n async #scanBlockaidTokens(\n chain: string,\n tokens: string[],\n ): Promise<BlockaidTokenScanResponse | null> {\n try {\n return await handleFetch(\n `${SECURITY_ALERTS_BASE_URL}${TOKEN_BULK_SCANNING_ENDPOINT}`,\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ chain, tokens }),\n },\n );\n } catch (error) {\n console.error('Blockaid token scan failed:', error);\n return null;\n }\n }\n\n /**\n * Filters out tokens flagged as non-benign by Blockaid. Only tokens with\n * an `assetNamespace` of \"token\" are scanned (native assets like slip44 are\n * passed through unfiltered). If the API call fails, all tokens are kept.\n *\n * @param assets - The CAIP asset type list to filter.\n * @returns The filtered list with malicious/spam/warning tokens removed.\n */\n async #filterBlockaidSpamTokens(\n assets: CaipAssetType[],\n ): Promise<CaipAssetType[]> {\n // Group scannable token assets by chain namespace\n const tokensByChain: Record<\n string,\n { asset: CaipAssetType; address: string }[]\n > = {};\n\n for (const asset of assets) {\n const { assetNamespace, assetReference, chain } =\n parseCaipAssetType(asset);\n\n // Only scan fungible token assets (e.g. SPL tokens), skip native (slip44)\n if (assetNamespace === 'token') {\n const chainName = chain.namespace;\n if (!tokensByChain[chainName]) {\n tokensByChain[chainName] = [];\n }\n tokensByChain[chainName].push({ asset, address: assetReference });\n }\n }\n\n // If there are no token assets to scan, return as-is\n if (Object.keys(tokensByChain).length === 0) {\n return assets;\n }\n\n // Build a set of assets to reject (non-benign tokens)\n const rejectedAssets = new Set<CaipAssetType>();\n\n for (const [chainName, tokenEntries] of Object.entries(tokensByChain)) {\n const addresses = tokenEntries.map((entry) => entry.address);\n const scanResponse = await this.#scanBlockaidTokens(chainName, addresses);\n\n if (!scanResponse?.results) {\n // If the scan fails, keep all tokens (fail open)\n continue;\n }\n\n for (const entry of tokenEntries) {\n const result = scanResponse.results[entry.address];\n // Reject the token only if we have a definitive non-benign result\n if (\n result?.result_type &&\n result.result_type !== BlockaidResultType.Benign\n ) {\n rejectedAssets.add(entry.asset);\n }\n }\n }\n\n // Filter while preserving original order\n return assets.filter((asset) => !rejectedAssets.has(asset));\n }\n\n /**\n * Get assets list for an account\n *\n * @param accountId - AccountId to get assets for\n * @param snapId - Snap ID for the account\n * @returns list of assets\n */\n async #getAssetsList(\n accountId: string,\n snapId: string,\n ): Promise<CaipAssetTypeOrId[]> {\n return await this.#getClient(snapId).listAccountAssets(accountId);\n }\n\n /**\n * Gets a `KeyringClient` for a Snap.\n *\n * @param snapId - ID of the Snap to get the client for.\n * @returns A `KeyringClient` for the Snap.\n */\n #getClient(snapId: string): KeyringClient {\n return new KeyringClient({\n send: async (request: JsonRpcRequest) =>\n (await this.messenger.call('SnapController:handleRequest', {\n snapId: snapId as SnapId,\n origin: 'metamask',\n handler: HandlerType.OnKeyringRequest,\n request,\n })) as Promise<Json>,\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(\n 'MultichainAssetsControllerError - Attempt to update state',\n );\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param callback - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock<Result>(\n callback: MutuallyExclusiveCallback<Result>,\n ): Promise<Result> {\n return withLock(this.#controllerOperationMutex, callback);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param callback - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock<Result>(\n mutex: Mutex,\n callback: MutuallyExclusiveCallback<Result>,\n): Promise<Result> {\n const releaseLock = await mutex.acquire();\n\n try {\n return await callback({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/assets-controllers",
3
- "version": "99.3.2-preview-b271ae9",
3
+ "version": "99.3.2-preview-1a74d12",
4
4
  "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)",
5
5
  "keywords": [
6
6
  "MetaMask",