From a910cfd598999981f2605e543862e8b3fb1f9ab3 Mon Sep 17 00:00:00 2001 From: salimtb Date: Mon, 10 Aug 2026 17:31:02 +0200 Subject: [PATCH 1/5] feat!: respect useTokenDetection preference in AssetsController When autodetection is off, strip new-to-state fungible tokens from the pipeline while keeping natives, staking, custom, and already-tracked holdings. Requires PreferencesController:getState on the messenger. --- packages/assets-controller/CHANGELOG.md | 6 + .../src/AssetsController.test.ts | 86 ++++++++ .../assets-controller/src/AssetsController.ts | 35 ++- packages/assets-controller/src/index.ts | 1 + .../middlewares/DetectionMiddleware.test.ts | 205 ++++++++++++++++++ .../src/middlewares/DetectionMiddleware.ts | 116 +++++++++- .../src/middlewares/index.ts | 1 + 7 files changed, 443 insertions(+), 7 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index aefa1d0c02d..4d55ed559b8 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `DetectionMiddlewareOptions` with an `isTokenDetectionEnabled` callback to `DetectionMiddleware`. `AssetsController` wires it from the `useTokenDetection` preference read via `PreferencesController:getState` (defaulting to enabled when `PreferencesController` is not registered), so when the user's token-autodetection preference is off, new-to-state fungible tokens (`erc20` and `token` namespaces, e.g. ERC-20 and SPL) are neither detected nor persisted from any pipeline, including websocket (account-activity) updates — their balances and stub metadata are stripped from the response. Native assets, staking-contract assets, custom (user-imported) assets, and holdings already tracked in state are unaffected + ### Changed +- **BREAKING:** `AssetsControllerMessenger` now requires the `PreferencesController:getState` action to be allowed + - `AssetsController` calls it to read the user's `useTokenDetection` preference; clients must add the action to the allowed actions when constructing the restricted messenger - Bump `@metamask/transaction-controller` from `^69.5.1` to `^69.5.2` ([#9823](https://github.com/MetaMask/core/pull/9823)) ## [13.1.2] diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index 075b220955f..635c4ca1474 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -824,6 +824,92 @@ describe('AssetsController', () => { }); }); + describe('token detection preference', () => { + const registerUseTokenDetection = ( + messenger: RootMessenger, + useTokenDetection: boolean, + ): void => { + ( + messenger as { + registerActionHandler: (a: string, h: () => unknown) => void; + } + ).registerActionHandler('PreferencesController:getState', () => ({ + useTokenDetection, + })); + }; + + // Built per test: the pipeline mutates the response in place when it + // strips gated tokens, so a shared object would leak across tests. + const newAssetsUpdate = (): DataResponse => ({ + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_ID]: { amount: '1000000' }, + [MOCK_NATIVE_ASSET_ID]: { amount: '2000000000000000000' }, + }, + }, + }); + + // isBasicFunctionality is disabled so the occurrence filter (which needs + // a real token API response) stays out of the way and the tests observe + // DetectionMiddleware's preference gating in isolation. + it('strips new fungible tokens but keeps natives when useTokenDetection is false', async () => { + await withController( + { isBasicFunctionality: () => false }, + async ({ controller, messenger }) => { + registerUseTokenDetection(messenger, false); + + await controller.handleAssetsUpdate( + newAssetsUpdate(), + 'AccountActivityDataSource', + ); + + expect( + controller.state.assetsBalance[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_ID], + ).toBeUndefined(); + expect( + controller.state.assetsBalance[MOCK_ACCOUNT_ID]?.[ + MOCK_NATIVE_ASSET_ID + ], + ).toBeDefined(); + }, + ); + }); + + it('keeps new fungible tokens when useTokenDetection is true', async () => { + await withController( + { isBasicFunctionality: () => false }, + async ({ controller, messenger }) => { + registerUseTokenDetection(messenger, true); + + await controller.handleAssetsUpdate( + newAssetsUpdate(), + 'AccountActivityDataSource', + ); + + expect( + controller.state.assetsBalance[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_ID], + ).toBeDefined(); + }, + ); + }); + + it('keeps new fungible tokens when PreferencesController is not registered (fail open)', async () => { + await withController( + { isBasicFunctionality: () => false }, + async ({ controller }) => { + await controller.handleAssetsUpdate( + newAssetsUpdate(), + 'AccountActivityDataSource', + ); + + expect( + controller.state.assetsBalance[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_ID], + ).toBeDefined(); + }, + ); + }); + }); + describe('getCustomAssets', () => { it('returns empty array for account with no custom assets', async () => { await withController(({ controller }) => { diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index ed4cd54dc99..43f139b3085 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -45,7 +45,10 @@ import type { PermissionControllerStateChange, } from '@metamask/permission-controller'; import { PhishingControllerBulkScanTokensAction } from '@metamask/phishing-controller'; -import type { PreferencesControllerStateChangeEvent } from '@metamask/preferences-controller'; +import type { + PreferencesControllerGetStateAction, + PreferencesControllerStateChangeEvent, +} from '@metamask/preferences-controller'; import type { RemoteFeatureFlagControllerGetStateAction, RemoteFeatureFlagControllerStateChangeEvent, @@ -339,7 +342,9 @@ type AllowedActions = // PhishingController | PhishingControllerBulkScanTokensAction // AccountsApiDataSource (Accounts API v6 balances feature flag) - | RemoteFeatureFlagControllerGetStateAction; + | RemoteFeatureFlagControllerGetStateAction + // DetectionMiddleware ("Autodetect tokens" preference) + | PreferencesControllerGetStateAction; type AllowedEvents = // AssetsController @@ -826,6 +831,23 @@ export class AssetsController extends BaseController< readonly #detectionMiddleware: DetectionMiddleware; + /** + * "Autodetect tokens" preference; fails open (enabled) on clients that + * don't register PreferencesController (e.g. mobile). + * + * @returns Whether token autodetection is enabled. + */ + readonly #tokenDetectionEnabled = (): boolean => { + try { + const preferencesState = this.messenger.call( + 'PreferencesController:getState', + ); + return preferencesState?.useTokenDetection ?? true; + } catch { + return true; + } + }; + readonly #customAssetGraduationMiddleware: CustomAssetGraduationMiddleware; readonly #rpcFallbackMiddleware: RpcFallbackMiddleware; @@ -964,7 +986,9 @@ export class AssetsController extends BaseController< getSelectedCurrency: (): SupportedCurrency => this.state.selectedCurrency, ...priceDataSourceConfig, }); - this.#detectionMiddleware = new DetectionMiddleware(); + this.#detectionMiddleware = new DetectionMiddleware({ + isTokenDetectionEnabled: this.#tokenDetectionEnabled, + }); this.#customAssetGraduationMiddleware = new CustomAssetGraduationMiddleware( { getSelectedAccountId: (): AccountId | undefined => { @@ -3877,9 +3901,12 @@ export class AssetsController extends BaseController< // Websocket updates can carry brand-new spam airdrops: enrich them // with Token API occurrences and drop below-floor tokens BEFORE // detection, so spam is never detected, enriched, priced or persisted. + // Skipped when token detection is off — DetectionMiddleware strips + // every new token anyway, so the occurrence lookups would be wasted. const shouldFilterOccurrences = sourceId === 'AccountActivityDataSource' && - this.#isBasicFunctionality(); + this.#isBasicFunctionality() && + this.#tokenDetectionEnabled(); const enrichmentSources: AssetsDataSource[] = [ ...(shouldGraduateCustomAssets diff --git a/packages/assets-controller/src/index.ts b/packages/assets-controller/src/index.ts index fca7ca96476..98f4c8a3a10 100644 --- a/packages/assets-controller/src/index.ts +++ b/packages/assets-controller/src/index.ts @@ -172,6 +172,7 @@ export { } from './middlewares/index.js'; export type { CustomAssetGraduationMiddlewareOptions, + DetectionMiddlewareOptions, RpcFallbackMiddlewareOptions, } from './middlewares/index.js'; diff --git a/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts b/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts index e876e47f946..4de6e8d80fa 100644 --- a/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts +++ b/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts @@ -17,6 +17,13 @@ const MOCK_ASSET_1 = const MOCK_ASSET_2 = 'eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7' as Caip19AssetId; const MOCK_NATIVE_ASSET = 'eip155:1/slip44:60' as Caip19AssetId; +const MOCK_SOL_NATIVE_ASSET = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501' as Caip19AssetId; +const MOCK_SPL_ASSET = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' as Caip19AssetId; +// Mainnet pooled-staking contract — exempt from detection gating. +const MOCK_STAKING_ASSET = + 'eip155:1/erc20:0x4fef9d741011476750a243ac70b9789a63dd47df' as Caip19AssetId; function createMockAccount( overrides?: Partial, @@ -435,6 +442,204 @@ describe('DetectionMiddleware', () => { expect(next).toHaveBeenCalledWith(context); }); + describe('when token detection is disabled', () => { + const setupDisabled = (): DetectionMiddleware => + new DetectionMiddleware({ isTokenDetectionEnabled: () => false }); + + it('does not detect new tokens and strips them from the response (erc20 and SPL), keeping natives', async () => { + const middleware = setupDisabled(); + const context = createMiddlewareContext({ + response: { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_1]: { amount: '1000' }, + [MOCK_SPL_ASSET]: { amount: '5' }, + [MOCK_NATIVE_ASSET]: { amount: '2' }, + [MOCK_SOL_NATIVE_ASSET]: { amount: '3' }, + }, + }, + assetsInfo: { + [MOCK_ASSET_1]: { + type: 'erc20', + name: 'Stub Token', + symbol: 'STUB', + decimals: 18, + }, + }, + }, + }); + const next = jest.fn().mockImplementation((ctx) => Promise.resolve(ctx)); + + await middleware.assetsMiddleware(context, next); + + // Tokens are gone from balances and stub metadata; natives survive and + // are still detected so their metadata/prices get fetched. + expect(context.response.assetsBalance?.[MOCK_ACCOUNT_ID]).toStrictEqual({ + [MOCK_NATIVE_ASSET]: { amount: '2' }, + [MOCK_SOL_NATIVE_ASSET]: { amount: '3' }, + }); + expect(context.response.assetsInfo?.[MOCK_ASSET_1]).toBeUndefined(); + expect(context.response.detectedAssets).toStrictEqual({ + [MOCK_ACCOUNT_ID]: [MOCK_NATIVE_ASSET, MOCK_SOL_NATIVE_ASSET], + }); + expect(next).toHaveBeenCalledWith(context); + }); + + it('keeps balance updates for tokens already tracked in state balances', async () => { + const middleware = setupDisabled(); + const context = createMiddlewareContext({ + response: { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_1]: { amount: '1000' }, + }, + }, + }, + getAssetsState: jest.fn().mockReturnValue({ + assetsInfo: {}, + assetsBalance: { + [MOCK_ACCOUNT_ID]: { [MOCK_ASSET_1]: { amount: '5' } }, + }, + customAssets: {}, + assetsPrice: {}, + }), + }); + const next = jest.fn().mockImplementation((ctx) => Promise.resolve(ctx)); + + await middleware.assetsMiddleware(context, next); + + expect( + context.response.assetsBalance?.[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_1], + ).toStrictEqual({ amount: '1000' }); + expect(context.response.detectedAssets).toBeUndefined(); + }); + + it('keeps balance updates for tokens known through state metadata', async () => { + const middleware = setupDisabled(); + const context = createMiddlewareContext( + { + response: { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_1]: { amount: '1000' }, + }, + }, + }, + }, + [MOCK_ASSET_1], + ); + const next = jest.fn().mockImplementation((ctx) => Promise.resolve(ctx)); + + await middleware.assetsMiddleware(context, next); + + expect( + context.response.assetsBalance?.[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_1], + ).toStrictEqual({ amount: '1000' }); + }); + + it('keeps and still detects custom assets (user-imported)', async () => { + const middleware = setupDisabled(); + const context = createMiddlewareContext({ + response: { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_1]: { amount: '1000' }, + [MOCK_ASSET_2]: { amount: '2000' }, + }, + }, + }, + getAssetsState: jest.fn().mockReturnValue({ + assetsInfo: {}, + assetsBalance: {}, + customAssets: { [MOCK_ACCOUNT_ID]: [MOCK_ASSET_1] }, + assetsPrice: {}, + }), + }); + const next = jest.fn().mockImplementation((ctx) => Promise.resolve(ctx)); + + await middleware.assetsMiddleware(context, next); + + // Custom asset survives and is detected; the other token is stripped. + expect(context.response.assetsBalance?.[MOCK_ACCOUNT_ID]).toStrictEqual({ + [MOCK_ASSET_1]: { amount: '1000' }, + }); + expect(context.response.detectedAssets).toStrictEqual({ + [MOCK_ACCOUNT_ID]: [MOCK_ASSET_1], + }); + }); + + it('exempts custom assets stored in a different address case', async () => { + const middleware = setupDisabled(); + const checksummedAsset = + 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Caip19AssetId; + const context = createMiddlewareContext({ + response: { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_1]: { amount: '1000' }, + }, + }, + }, + getAssetsState: jest.fn().mockReturnValue({ + assetsInfo: {}, + assetsBalance: {}, + customAssets: { [MOCK_ACCOUNT_ID]: [checksummedAsset] }, + assetsPrice: {}, + }), + }); + const next = jest.fn().mockImplementation((ctx) => Promise.resolve(ctx)); + + await middleware.assetsMiddleware(context, next); + + expect( + context.response.assetsBalance?.[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_1], + ).toStrictEqual({ amount: '1000' }); + }); + + it('keeps staking contract balances', async () => { + const middleware = setupDisabled(); + const context = createMiddlewareContext({ + response: { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_STAKING_ASSET]: { amount: '1.5' }, + }, + }, + }, + }); + const next = jest.fn().mockImplementation((ctx) => Promise.resolve(ctx)); + + await middleware.assetsMiddleware(context, next); + + expect( + context.response.assetsBalance?.[MOCK_ACCOUNT_ID]?.[ + MOCK_STAKING_ASSET + ], + ).toStrictEqual({ amount: '1.5' }); + }); + + it('does not queue prices for stripped tokens but still queues natives', async () => { + const middleware = setupDisabled(); + const context = createMiddlewareContext({ + response: { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_1]: { amount: '1000' }, + [MOCK_NATIVE_ASSET]: { amount: '2' }, + }, + }, + }, + }); + const next = jest.fn().mockImplementation((ctx) => Promise.resolve(ctx)); + + await middleware.assetsMiddleware(context, next); + + expect(context.request.assetsForPriceUpdate).toStrictEqual([ + normalizeAssetId(MOCK_NATIVE_ASSET), + ]); + }); + }); + it('retrieves middleware from instance', async () => { const { middleware } = setupController(); const middlewareFn = middleware.assetsMiddleware; diff --git a/packages/assets-controller/src/middlewares/DetectionMiddleware.ts b/packages/assets-controller/src/middlewares/DetectionMiddleware.ts index 805c59143e1..5ecf608da37 100644 --- a/packages/assets-controller/src/middlewares/DetectionMiddleware.ts +++ b/packages/assets-controller/src/middlewares/DetectionMiddleware.ts @@ -1,3 +1,7 @@ +import { parseCaipAssetType } from '@metamask/utils'; + +import { isStakingContractAssetId } from '../data-sources/evm-rpc-services/index.js'; +import { CaipAssetNamespace } from '../data-sources/TokenDataSource.js'; import { projectLogger, createModuleLogger } from '../logger.js'; import { forDataTypes } from '../types.js'; import type { AccountId, Caip19AssetId, Middleware } from '../types.js'; @@ -9,8 +13,28 @@ import { normalizeAssetId } from '../utils/index.js'; const CONTROLLER_NAME = 'DetectionMiddleware'; -// Logger for debugging -createModuleLogger(projectLogger, CONTROLLER_NAME); +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + +/** + * Whether an asset is subject to the token-detection toggle: fungible tokens + * (EVM `erc20`, non-EVM `token` — SPL, TRC, etc.). Natives (`slip44`), + * staking-contract assets, and anything unparseable are never gated. + * + * @param assetId - The CAIP-19 asset ID to classify. + * @returns `true` when the asset should be blocked while detection is off. + */ +function isDetectionGatedAsset(assetId: string): boolean { + try { + const { assetNamespace } = parseCaipAssetType(assetId as Caip19AssetId); + return ( + (assetNamespace === (CaipAssetNamespace.Erc20 as string) || + assetNamespace === (CaipAssetNamespace.Token as string)) && + !isStakingContractAssetId(assetId) + ); + } catch { + return false; + } +} // ============================================================================ // DETECTION MIDDLEWARE @@ -29,15 +53,38 @@ createModuleLogger(projectLogger, CONTROLLER_NAME); * - Each account's custom assets from state are always included because they * may have no balance yet and are explicitly managed by the user. * + * When token detection is disabled (`isTokenDetectionEnabled` returns false), + * new-to-state fungible tokens (`erc20` / `token` namespaces) are neither + * detected nor persisted: their balances and stub metadata are stripped from + * the response. Natives, staking-contract assets, custom (user-imported) + * assets, and assets already tracked in state are unaffected. + * * Usage: * ```typescript * const detectionMiddleware = new DetectionMiddleware(); * const middleware = detectionMiddleware.assetsMiddleware; * ``` */ + +/** Constructor options for {@link DetectionMiddleware}. */ +export type DetectionMiddlewareOptions = { + /** + * Whether automatic token detection is enabled (e.g. the user's + * "Autodetect tokens" preference). Defaults to `() => true`. + */ + isTokenDetectionEnabled?: () => boolean; +}; + export class DetectionMiddleware { readonly name = CONTROLLER_NAME; + readonly #isTokenDetectionEnabled: () => boolean; + + constructor(options: DetectionMiddlewareOptions = {}) { + this.#isTokenDetectionEnabled = + options.isTokenDetectionEnabled ?? ((): boolean => true); + } + getName(): string { return this.name; } @@ -71,6 +118,21 @@ export class DetectionMiddleware { const detectedAssets: Record = {}; + const detectionEnabled = this.#isTokenDetectionEnabled(); + // Lower-cased asset IDs stripped from the response because token + // detection is off; used to purge their stub metadata afterwards. + const strippedAssetIds = new Set(); + // Lower-cased state metadata keys, built lazily (only when detection is + // off) so the destructive strip below cannot misfire on a known holding + // reported by a source in a different address case. + let knownMetadataLower: Set | null = null; + const getKnownMetadataLower = (): Set => { + knownMetadataLower ??= new Set( + Object.keys(stateAssetsInfo).map((id) => id.toLowerCase()), + ); + return knownMetadataLower; + }; + // 1. From balance response: only include assets that are genuinely new — // not already present in state.assetsBalance or state.assetsInfo. if (response.assetsBalance) { @@ -80,6 +142,17 @@ export class DetectionMiddleware { const detected: Caip19AssetId[] = []; const stateAccountBalances = stateAssetsBalance[accountId] ?? {}; + const customForAccount = stateCustomAssets?.[accountId] ?? []; + const customLowerIds = detectionEnabled + ? null + : new Set(customForAccount.map((id) => id.toLowerCase())); + const knownBalanceLower = detectionEnabled + ? null + : new Set( + Object.keys(stateAccountBalances).map((id) => + id.toLowerCase(), + ), + ); for (const assetId of Object.keys( accountBalances as Record, @@ -92,12 +165,32 @@ export class DetectionMiddleware { ) { continue; } + + // Token detection off: new-to-state fungible tokens (erc20 / SPL + // and similar `token` namespaces) are neither detected nor + // persisted. Natives, staking contracts, custom assets, and + // holdings already known to state (any address case) still flow. + if ( + !detectionEnabled && + isDetectionGatedAsset(assetId) && + !customLowerIds?.has(assetId.toLowerCase()) + ) { + const lowerId = assetId.toLowerCase(); + if ( + !knownBalanceLower?.has(lowerId) && + !getKnownMetadataLower().has(lowerId) + ) { + delete (accountBalances as Record)[assetId]; + strippedAssetIds.add(lowerId); + } + continue; + } + detected.push(caipAssetId); } // Merge custom assets for this account, applying the same filter: // skip if already in state balance or already has metadata. - const customForAccount = stateCustomAssets?.[accountId] ?? []; for (const assetId of customForAccount) { if (detected.includes(assetId)) { continue; @@ -117,6 +210,23 @@ export class DetectionMiddleware { } } + // Drop stub metadata (e.g. websocket-seeded name/symbol) of stripped + // tokens so it never persists to state — a persisted stub would make + // the token look "known" on the next update and let it bypass the + // detection gate. + if (strippedAssetIds.size > 0) { + if (response.assetsInfo) { + for (const assetId of Object.keys(response.assetsInfo)) { + if (strippedAssetIds.has(assetId.toLowerCase())) { + delete response.assetsInfo[assetId as Caip19AssetId]; + } + } + } + log('Token detection disabled - stripped new tokens from response', { + assetIds: [...strippedAssetIds], + }); + } + // 2. Accounts in request that weren't in balance response: include their // custom assets that are not yet in state. for (const { account } of request.accountsWithSupportedChains) { diff --git a/packages/assets-controller/src/middlewares/index.ts b/packages/assets-controller/src/middlewares/index.ts index d6796382aa3..aa509e625ff 100644 --- a/packages/assets-controller/src/middlewares/index.ts +++ b/packages/assets-controller/src/middlewares/index.ts @@ -1,6 +1,7 @@ export { CustomAssetGraduationMiddleware } from './CustomAssetGraduationMiddleware.js'; export type { CustomAssetGraduationMiddlewareOptions } from './CustomAssetGraduationMiddleware.js'; export { DetectionMiddleware } from './DetectionMiddleware.js'; +export type { DetectionMiddlewareOptions } from './DetectionMiddleware.js'; export { RpcFallbackMiddleware } from './RpcFallbackMiddleware.js'; export type { RpcFallbackMiddlewareOptions } from './RpcFallbackMiddleware.js'; export { From e25f46dc6716a93dc9a5c46c5110c661ed14f846 Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 12 Aug 2026 10:26:09 +0200 Subject: [PATCH 2/5] feat: refresh assets when token detection is re-enabled Subscribe to PreferencesController:stateChange and force-run getAssets when useTokenDetection flips back on so tokens skipped while the preference was off are detected without waiting for the next poll. --- eslint-suppressions.json | 2 +- packages/assets-controller/CHANGELOG.md | 1 + .../src/AssetsController.test.ts | 44 +++++++++++++++++++ .../assets-controller/src/AssetsController.ts | 33 ++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index c32201a64ed..c7e99f9bf40 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -157,7 +157,7 @@ }, "packages/assets-controller/src/AssetsController.ts": { "no-restricted-syntax": { - "count": 3 + "count": 4 } }, "packages/assets-controller/src/__fixtures__/MockAssetControllerMessenger.ts": { diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 4d55ed559b8..8ef207ce7ad 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `DetectionMiddlewareOptions` with an `isTokenDetectionEnabled` callback to `DetectionMiddleware`. `AssetsController` wires it from the `useTokenDetection` preference read via `PreferencesController:getState` (defaulting to enabled when `PreferencesController` is not registered), so when the user's token-autodetection preference is off, new-to-state fungible tokens (`erc20` and `token` namespaces, e.g. ERC-20 and SPL) are neither detected nor persisted from any pipeline, including websocket (account-activity) updates — their balances and stub metadata are stripped from the response. Native assets, staking-contract assets, custom (user-imported) assets, and holdings already tracked in state are unaffected + - `AssetsController` also subscribes to `PreferencesController:stateChange` and force-refreshes balances, metadata, and prices when the preference is turned back on, so tokens skipped while it was off are detected without waiting for the next poll ### Changed diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index 635c4ca1474..a2fe3ea5490 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -2930,6 +2930,50 @@ describe('AssetsController', () => { }); }); + it('force refreshes assets when the token detection preference is turned on', async () => { + await withController(async ({ controller, messenger }) => { + const getAssetsSpy = jest + .spyOn(controller, 'getAssets') + .mockResolvedValue({}); + + (messenger.publish as CallableFunction)( + 'PreferencesController:stateChange', + { useTokenDetection: true }, + [], + ); + await flushPromises(); + + expect(getAssetsSpy).toHaveBeenCalledWith( + [expect.objectContaining({ id: MOCK_ACCOUNT_ID })], + { + forceUpdate: true, + dataTypes: ['balance', 'metadata', 'price'], + }, + ); + + getAssetsSpy.mockRestore(); + }); + }); + + it('does not refresh assets when the token detection preference is turned off', async () => { + await withController(async ({ controller, messenger }) => { + const getAssetsSpy = jest + .spyOn(controller, 'getAssets') + .mockResolvedValue({}); + + (messenger.publish as CallableFunction)( + 'PreferencesController:stateChange', + { useTokenDetection: false }, + [], + ); + await flushPromises(); + + expect(getAssetsSpy).not.toHaveBeenCalled(); + + getAssetsSpy.mockRestore(); + }); + }); + it('publishes balanceChanged event when balance updates', async () => { await withController(async ({ controller, messenger }) => { const balanceChangedHandler = jest.fn(); diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 43f139b3085..d2ec0bf6931 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -1193,6 +1193,20 @@ export class AssetsController extends BaseController< }, clientControllerSelectors.selectIsUiOpen, ); + // "Autodetect tokens" preference. Turning it on re-runs the pipeline so + // tokens skipped while it was off are detected without waiting for the + // next poll. Turning it off needs no refresh: already-tracked assets stay + // in state, and the next update strips new ones anyway. + this.messenger.subscribe( + 'PreferencesController:stateChange', + (useTokenDetection: boolean) => { + if (useTokenDetection) { + this.#refreshAssetsAfterTokenDetectionEnabled(); + } + }, + (state) => state.useTokenDetection, + ); + this.messenger.subscribe('KeyringController:unlock', () => { this.#keyringUnlocked = true; this.#updateActive(); @@ -3806,6 +3820,25 @@ export class AssetsController extends BaseController< }); } + /** + * Re-run the assets pipeline after the user turns "Autodetect tokens" back + * on, so tokens that were filtered out while it was off are detected, + * enriched, and priced right away. + */ + #refreshAssetsAfterTokenDetectionEnabled(): void { + const accounts = this.#getSelectedAccounts(); + if (accounts.length === 0) { + return; + } + + this.getAssets(accounts, { + forceUpdate: true, + dataTypes: ['balance', 'metadata', 'price'], + }).catch((error) => { + log('Failed to refresh assets after token detection enabled', { error }); + }); + } + /** * Refresh balances and fetch missing prices after a network is added. * From 174861ee7a08301de48eabadcc066b8c3185a9ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 08:45:40 +0000 Subject: [PATCH 3/5] chore: add PR number to changelog and fix formatting Co-authored-by: Salim TOUBAL --- packages/assets-controller/CHANGELOG.md | 4 ++-- .../src/middlewares/DetectionMiddleware.test.ts | 4 +--- .../assets-controller/src/middlewares/DetectionMiddleware.ts | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 8ef207ce7ad..2944c7bca33 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -9,12 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `DetectionMiddlewareOptions` with an `isTokenDetectionEnabled` callback to `DetectionMiddleware`. `AssetsController` wires it from the `useTokenDetection` preference read via `PreferencesController:getState` (defaulting to enabled when `PreferencesController` is not registered), so when the user's token-autodetection preference is off, new-to-state fungible tokens (`erc20` and `token` namespaces, e.g. ERC-20 and SPL) are neither detected nor persisted from any pipeline, including websocket (account-activity) updates — their balances and stub metadata are stripped from the response. Native assets, staking-contract assets, custom (user-imported) assets, and holdings already tracked in state are unaffected +- Add `DetectionMiddlewareOptions` with an `isTokenDetectionEnabled` callback to `DetectionMiddleware`. `AssetsController` wires it from the `useTokenDetection` preference read via `PreferencesController:getState` (defaulting to enabled when `PreferencesController` is not registered), so when the user's token-autodetection preference is off, new-to-state fungible tokens (`erc20` and `token` namespaces, e.g. ERC-20 and SPL) are neither detected nor persisted from any pipeline, including websocket (account-activity) updates — their balances and stub metadata are stripped from the response. Native assets, staking-contract assets, custom (user-imported) assets, and holdings already tracked in state are unaffected ([#9835](https://github.com/MetaMask/core/pull/9835)) - `AssetsController` also subscribes to `PreferencesController:stateChange` and force-refreshes balances, metadata, and prices when the preference is turned back on, so tokens skipped while it was off are detected without waiting for the next poll ### Changed -- **BREAKING:** `AssetsControllerMessenger` now requires the `PreferencesController:getState` action to be allowed +- **BREAKING:** `AssetsControllerMessenger` now requires the `PreferencesController:getState` action to be allowed ([#9835](https://github.com/MetaMask/core/pull/9835)) - `AssetsController` calls it to read the user's `useTokenDetection` preference; clients must add the action to the allowed actions when constructing the restricted messenger - Bump `@metamask/transaction-controller` from `^69.5.1` to `^69.5.2` ([#9823](https://github.com/MetaMask/core/pull/9823)) diff --git a/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts b/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts index 4de6e8d80fa..45fee81489c 100644 --- a/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts +++ b/packages/assets-controller/src/middlewares/DetectionMiddleware.test.ts @@ -612,9 +612,7 @@ describe('DetectionMiddleware', () => { await middleware.assetsMiddleware(context, next); expect( - context.response.assetsBalance?.[MOCK_ACCOUNT_ID]?.[ - MOCK_STAKING_ASSET - ], + context.response.assetsBalance?.[MOCK_ACCOUNT_ID]?.[MOCK_STAKING_ASSET], ).toStrictEqual({ amount: '1.5' }); }); diff --git a/packages/assets-controller/src/middlewares/DetectionMiddleware.ts b/packages/assets-controller/src/middlewares/DetectionMiddleware.ts index 5ecf608da37..e66bc8a9c33 100644 --- a/packages/assets-controller/src/middlewares/DetectionMiddleware.ts +++ b/packages/assets-controller/src/middlewares/DetectionMiddleware.ts @@ -149,9 +149,7 @@ export class DetectionMiddleware { const knownBalanceLower = detectionEnabled ? null : new Set( - Object.keys(stateAccountBalances).map((id) => - id.toLowerCase(), - ), + Object.keys(stateAccountBalances).map((id) => id.toLowerCase()), ); for (const assetId of Object.keys( From 343be1ad63e7728fd2b1fa998852b65a23af8864 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 09:38:22 +0000 Subject: [PATCH 4/5] refactor: remove createModuleLogger from assets-controller package Co-authored-by: Salim TOUBAL --- .../assets-controller/src/AssetsController.ts | 155 ++---------------- .../data-sources/AccountActivityDataSource.ts | 23 +-- .../src/data-sources/AccountsApiDataSource.ts | 32 +--- .../src/data-sources/PriceDataSource.ts | 24 +-- .../src/data-sources/RpcDataSource.ts | 132 +-------------- .../src/data-sources/SnapDataSource.ts | 23 +-- .../data-sources/StakedBalanceDataSource.ts | 81 ++------- .../src/data-sources/TokenDataSource.ts | 14 +- .../services/TokenDetector.ts | 14 +- packages/assets-controller/src/logger.ts | 4 +- .../CustomAssetGraduationMiddleware.ts | 7 - .../src/middlewares/RpcFallbackMiddleware.ts | 7 - .../src/migrations/healAssetsInfoMetadata.ts | 9 - 13 files changed, 50 insertions(+), 475 deletions(-) diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index d2ec0bf6931..b11a40c2679 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -108,7 +108,6 @@ import { getDefaultAssetMetadata, } from './defaults.js'; import { AssetsDataSourceError } from './errors.js'; -import { projectLogger, createModuleLogger } from './logger.js'; import { CustomAssetGraduationMiddleware } from './middlewares/CustomAssetGraduationMiddleware.js'; import { DetectionMiddleware } from './middlewares/DetectionMiddleware.js'; import { @@ -227,8 +226,6 @@ const TRACE_UPDATE_PARENT = 'AssetsUpdateEnrichment'; const TRACE_SUBSCRIPTION_ERROR = 'AssetsSubscriptionError'; const TRACE_STATE_SIZE = 'AssetsStateSize'; -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - // ============================================================================ // STATE TYPES // ============================================================================ @@ -920,12 +917,7 @@ export class AssetsController extends BaseController< ): void => { try { this.#handleActiveChainsUpdate(dataSourceName, chains, previousChains); - } catch (error) { - log('Failed to handle active chains update', { - dataSourceName, - error, - }); - } + } catch (error) {} }; this.#accountActivityDataSource = new AccountActivityDataSource({ @@ -1006,10 +998,6 @@ export class AssetsController extends BaseController< rpcDataSource: this.#rpcDataSource, }); - log('Initializing AssetsController', { - defaultUpdateInterval: this.#defaultUpdateInterval, - }); - this.#initializeState(); this.#subscribeToEvents(); this.#registerActionHandlers(); @@ -1042,10 +1030,6 @@ export class AssetsController extends BaseController< .catch((error) => { // Failure to populate native asset cache is non-fatal; // #isNativeAsset falls back to the seed data from buildNativeAssetsFromConstant. - log( - 'Failed to populate native asset cache, falling back to seed data', - error, - ); }); // Seed the cache synchronously so that synchronous consumers (e.g. @@ -1064,11 +1048,6 @@ export class AssetsController extends BaseController< 'NetworkEnablementController:getState', ); this.#enabledChains = this.#extractEnabledChains(enabledNetworkMap); - - log('Initialized state', { - enabledNetworkMap, - enabledChains: this.#enabledChains, - }); } /** @@ -1165,9 +1144,7 @@ export class AssetsController extends BaseController< this.#handleNetworkAdded(networkConfiguration.chainId); this.#refreshAssetsAfterNetworkAdded( networkConfiguration.chainId, - ).catch((error) => { - log('Failed to refresh assets after network added', { error }); - }); + ).catch((error) => {}); }, ); @@ -1258,11 +1235,7 @@ export class AssetsController extends BaseController< this.getAssets([matchedAccount], { chainIds: [caipChainId], forceUpdate: true, - }).catch((error) => { - log('Failed to refresh assets after unapproved transaction added', { - error, - }); - }); + }).catch((error) => {}); } #onTransactionConfirmed(transactionMeta: TransactionMeta): void { @@ -1287,9 +1260,7 @@ export class AssetsController extends BaseController< this.getAssets([matchedAccount], { chainIds: [caipChainId], forceUpdate: true, - }).catch((error) => { - log('Failed to refresh assets after transaction confirmed', { error }); - }); + }).catch((error) => {}); } /** @@ -1338,11 +1309,6 @@ export class AssetsController extends BaseController< return; } - log('Account tree changed with new accounts, re-subscribing', { - previousCount: this.#lastKnownAccountIds.size, - currentCount: currentIds.size, - }); - const newAccounts = accounts.filter( (account) => !this.#lastKnownAccountIds.has(account.id), ); @@ -1350,9 +1316,7 @@ export class AssetsController extends BaseController< this.#lastKnownAccountIds = currentIds; this.#ensureNativeBalancesDefaultZero(); this.#ensureDefaultTrackedAssetsSeeded(); - this.#runAccountTreeRefresh(accounts, newAccounts).catch((error) => { - log('Failed to refresh assets after tree change', error); - }); + this.#runAccountTreeRefresh(accounts, newAccounts).catch((error) => {}); } else { this.#start(); } @@ -1377,7 +1341,6 @@ export class AssetsController extends BaseController< } this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]); } catch (error) { - log('Failed to fetch assets after tree change', error); this.#subscribeAssets(); this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]); } finally { @@ -1404,7 +1367,6 @@ export class AssetsController extends BaseController< this.#subscribeAssets(); this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]); } catch (error) { - log('Failed to fetch assets on startup', error); this.#ensureNativeBalancesDefaultZero(); this.#ensureDefaultTrackedAssetsSeeded(); this.#subscribeAssets(); @@ -1446,11 +1408,6 @@ export class AssetsController extends BaseController< if (!this.#uiOpen || !this.#keyringUnlocked || !this.#isEnabled()) { return; } - log('Data source active chains changed', { - dataSourceId, - chainCount: activeChains.length, - chains: activeChains, - }); const previous: ChainId[] = previousChains; @@ -2119,8 +2076,6 @@ export class AssetsController extends BaseController< ): Promise { const normalizedAssetId = normalizeAssetId(assetId); - log('Adding custom asset', { accountId, assetId: normalizedAssetId }); - this.update((state) => { const customAssets = state.customAssets as Record; if (!customAssets[accountId]) { @@ -2205,8 +2160,6 @@ export class AssetsController extends BaseController< removeCustomAsset(accountId: AccountId, assetId: Caip19AssetId): void { const normalizedAssetId = normalizeAssetId(assetId); - log('Removing custom asset', { accountId, assetId: normalizedAssetId }); - this.update((state) => { if (state.customAssets[accountId]) { state.customAssets[accountId] = state.customAssets[accountId].filter( @@ -2249,8 +2202,6 @@ export class AssetsController extends BaseController< hideAsset(assetId: Caip19AssetId): void { const normalizedAssetId = normalizeAssetId(assetId); - log('Hiding asset', { assetId: normalizedAssetId }); - this.update((state) => { if (!state.assetPreferences[normalizedAssetId]) { state.assetPreferences[normalizedAssetId] = {}; @@ -2267,8 +2218,6 @@ export class AssetsController extends BaseController< unhideAsset(assetId: Caip19AssetId): void { const normalizedAssetId = normalizeAssetId(assetId); - log('Unhiding asset', { assetId: normalizedAssetId }); - this.update((state) => { const prefs = state.assetPreferences[normalizedAssetId]; if (prefs) { @@ -2300,11 +2249,6 @@ export class AssetsController extends BaseController< state.selectedCurrency = selectedCurrency; }); - log('Current currency changed', { - previousCurrency, - selectedCurrency, - }); - if (!this.#isBasicFunctionality()) { return; } @@ -2318,9 +2262,7 @@ export class AssetsController extends BaseController< assetsForPriceUpdate: Object.values(this.state.assetsBalance).flatMap( (balances) => Object.keys(balances) as Caip19AssetId[], ), - }).catch((error) => { - log('Failed to fetch asset prices after current currency change', error); - }); + }).catch((error) => {}); } /** @@ -2375,9 +2317,7 @@ export class AssetsController extends BaseController< dataTypes: ['price'], chainIds, assetsForPriceUpdate, - }).catch((error) => { - log('Failed to fetch missing prices', { error }); - }); + }).catch((error) => {}); } // ============================================================================ @@ -2887,23 +2827,6 @@ export class AssetsController extends BaseController< changedMetadata.length > 0 || changedPriceAssets.length > 0 ) { - log('State updated', { - changedBalances: - changedBalances.length > 0 ? changedBalances : undefined, - changedMetadataCount: - changedMetadata.length > 0 ? changedMetadata.length : undefined, - changedPricesCount: - changedPriceAssets.length > 0 - ? changedPriceAssets.length - : undefined, - newAssets: - Object.keys(detectedAssets).length > 0 - ? Object.entries(detectedAssets).map(([accountId, assets]) => ({ - accountId, - assets, - })) - : undefined, - }); } // Publish balance changed events @@ -3123,15 +3046,8 @@ export class AssetsController extends BaseController< return; } - log('Starting asset tracking', { - selectedAccountCount: accounts.length, - enabledChainCount: chainIds.length, - }); - this.#lastKnownAccountIds = new Set(accounts.map((a) => a.id)); - this.#runStartupRefresh(accounts).catch((error) => { - log('Failed to start asset tracking', error); - }); + this.#runStartupRefresh(accounts).catch((error) => {}); } /** @@ -3139,11 +3055,6 @@ export class AssetsController extends BaseController< * Called when app closes or keyring locks. */ #stop(): void { - log('Stopping asset tracking', { - activeSubscriptionCount: this.#activeSubscriptions.size, - hasPriceSubscription: this.#activeSubscriptions.has('ds:PriceDataSource'), - }); - this.#firstInitFetchReported = false; this.#stateSizeReported = false; this.#lastKnownAccountIds = new Set(); @@ -3438,15 +3349,6 @@ export class AssetsController extends BaseController< const existingSubscription = this.#activeSubscriptions.get(subscriptionKey); const isUpdate = existingSubscription !== undefined; - log('Subscribe to data source', { - sourceId, - subscriptionKey, - isUpdate, - accountCount: accounts.length, - chainCount: chains.length, - customAssetsOnly: options.customAssetsOnly === true, - }); - const subscribeReq: SubscriptionRequest = { request: this.#buildDataRequest(accounts, chains, { assetTypes: ['fungible'], @@ -3609,11 +3511,6 @@ export class AssetsController extends BaseController< async #handleAccountGroupChanged(): Promise { const accounts = this.#getSelectedAccounts(); - log('Account group changed', { - accountCount: accounts.length, - accountIds: accounts.map((a) => a.id), - }); - this.#lastKnownAccountIds = new Set(accounts.map((a) => a.id)); const releaseLock = await this.#accountRefreshMutex.acquire(); @@ -3657,13 +3554,6 @@ export class AssetsController extends BaseController< } } - log('Enabled networks changed', { - previousCount: previousChains.size, - newCount: this.#enabledChains.size, - addedChains, - removedChains, - }); - // Note: We intentionally do NOT delete balance data for disabled chains. // Users may want to see historical balances even if the network is currently disabled. // The data will simply not be updated until the network is re-enabled. @@ -3714,11 +3604,6 @@ export class AssetsController extends BaseController< return; } - log('Network added — seeding default tracked assets', { - hexChainId, - caipChainId, - }); - this.#ensureDefaultTrackedAssetsSeeded([caipChainId]); const accounts = this.#getSelectedAccounts(); this.#fetchMissingPricesWithoutCache(accounts, [caipChainId]); @@ -3779,11 +3664,6 @@ export class AssetsController extends BaseController< return; } - log('Selected EVM network switched', { - selectedNetworkClientId: networkState.selectedNetworkClientId, - selectedChainId, - }); - const releaseLock = await this.#accountRefreshMutex.acquire(); try { await this.#refreshActiveChainsOnNetworkSwitch(); @@ -3815,9 +3695,7 @@ export class AssetsController extends BaseController< this.getAssets(accounts, { forceUpdate: true, dataTypes: ['balance', 'metadata'], - }).catch((error) => { - log('Failed to refresh assets after network change', { error }); - }); + }).catch((error) => {}); } /** @@ -3834,9 +3712,7 @@ export class AssetsController extends BaseController< this.getAssets(accounts, { forceUpdate: true, dataTypes: ['balance', 'metadata', 'price'], - }).catch((error) => { - log('Failed to refresh assets after token detection enabled', { error }); - }); + }).catch((error) => {}); } /** @@ -3881,12 +3757,6 @@ export class AssetsController extends BaseController< sourceId: string, request?: DataRequest, ): Promise { - log('Assets updated from data source', { - sourceId, - hasBalance: Boolean(response.assetsBalance), - hasPrice: Boolean(response.assetsPrice), - }); - // Enrichment spans only before unlock/first-init fetch completes. const pipelineTrace = this.#firstInitFetchReported ? undefined @@ -4003,11 +3873,6 @@ export class AssetsController extends BaseController< // ============================================================================ destroy(): void { - log('Destroying AssetsController', { - dataSourceCount: this.#allBalanceDataSources.length, - subscriptionCount: this.#activeSubscriptions.size, - }); - // Destroy instantiated data sources this.#accountActivityDataSource?.destroy?.(); this.#accountsApiDataSource?.destroy?.(); diff --git a/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts b/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts index 196af953ac0..e6c185b4554 100644 --- a/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts +++ b/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts @@ -8,7 +8,6 @@ import { isCaipChainId } from '@metamask/utils'; import BigNumberJS from 'bignumber.js'; import type { AssetsControllerMessenger } from '../AssetsController.js'; -import { projectLogger, createModuleLogger } from '../logger.js'; import type { AssetBalance, AssetMetadata, @@ -27,8 +26,6 @@ import type { DataSourceState } from './AbstractDataSource.js'; const CONTROLLER_NAME = 'AccountActivityDataSource'; -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - // ============================================================================ // BALANCE UPDATE PROCESSING // ============================================================================ @@ -304,13 +301,9 @@ export class AccountActivityDataSource extends AbstractDataSource< }; Promise.resolve(this.#onAssetsUpdate(response, request)).catch( - (error) => { - log('Failed to report balance update', { error }); - }, + (error) => {}, ); - } catch (error) { - log('Error handling balance update', error); - } + } catch (error) {} } /** @@ -405,9 +398,7 @@ export class AccountActivityDataSource extends AbstractDataSource< this.updateActiveChains(Array.from(next), (updatedChains) => this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); - } catch (error) { - log('Error handling status change', error); - } + } catch (error) {} }; // ============================================================================ @@ -420,18 +411,14 @@ export class AccountActivityDataSource extends AbstractDataSource< 'AccountActivityService:balanceUpdated', this.#onBalanceUpdatedBound, ); - } catch (error) { - log('Failed to unsubscribe from balanceUpdated', { error }); - } + } catch (error) {} try { this.#messenger.unsubscribe( 'AccountActivityService:statusChanged', this.#onAccountActivityStatusChanged, ); - } catch (error) { - log('Failed to unsubscribe from statusChanged', { error }); - } + } catch (error) {} super.destroy(); } diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts index 3de2633e958..ebbb348902a 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts @@ -14,7 +14,6 @@ import { } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; -import { projectLogger, createModuleLogger } from '../logger.js'; import type { ChainId, Caip19AssetId, @@ -44,8 +43,6 @@ const CONTROLLER_NAME = 'AccountsApiDataSource'; const DEFAULT_POLL_INTERVAL = 30_000; const DEFAULT_FETCH_TIMEOUT_MS = 15_000; -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - // ============================================================================ // MESSENGER TYPES // ============================================================================ @@ -274,11 +271,7 @@ export class AccountsApiDataSource extends AbstractDataSource< async #handleMigrationFeatureFlagsChanged(): Promise { try { await this.#refreshActiveChains(); - } catch (error) { - log('Failed to refresh active chains after feature flag change', { - error, - }); - } + } catch (error) {} } /** @@ -329,9 +322,7 @@ export class AccountsApiDataSource extends AbstractDataSource< }, 20 * 60 * 1000, ); - } catch (error) { - log('Failed to fetch active chains', error); - } + } catch (error) {} } async #refreshActiveChains(): Promise { @@ -352,9 +343,7 @@ export class AccountsApiDataSource extends AbstractDataSource< this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); } - } catch (error) { - log('Failed to refresh active chains', error); - } + } catch (error) {} } /** @@ -452,8 +441,6 @@ export class AccountsApiDataSource extends AbstractDataSource< response.assetsBalance = assetsBalance; response.updateMode = 'merge'; } catch (error) { - log('Fetch FAILED', { error, chains: chainsToFetch }); - // On error, mark all chains as errors so they can be handled by next middleware response.errors = response.errors ?? {}; for (const chainId of chainsToFetch) { @@ -761,7 +748,6 @@ export class AccountsApiDataSource extends AbstractDataSource< successfullyHandledChains = []; } } catch (error) { - log('Middleware fetch failed', { error }); successfullyHandledChains = []; } @@ -826,13 +812,7 @@ export class AccountsApiDataSource extends AbstractDataSource< forceUpdate: true, }); await existing.onAssetsUpdate(fetchResponse); - } catch (error) { - log('Initial fetch for added chains failed', { - subscriptionId, - addedChains, - error, - }); - } + } catch (error) {} } return; } @@ -859,9 +839,7 @@ export class AccountsApiDataSource extends AbstractDataSource< // Report update to AssetsController via callback await subscription.onAssetsUpdate(fetchResponse); - } catch (error) { - log('Subscription poll failed', { subscriptionId, error }); - } + } catch (error) {} }; // Set up polling diff --git a/packages/assets-controller/src/data-sources/PriceDataSource.ts b/packages/assets-controller/src/data-sources/PriceDataSource.ts index 56a263b4294..b343e57ca8e 100644 --- a/packages/assets-controller/src/data-sources/PriceDataSource.ts +++ b/packages/assets-controller/src/data-sources/PriceDataSource.ts @@ -5,7 +5,6 @@ import type { import { ApiPlatformClient } from '@metamask/core-backend'; import { parseCaipAssetType } from '@metamask/utils'; -import { projectLogger, createModuleLogger } from '../logger.js'; import { forDataTypes } from '../types.js'; import type { Caip19AssetId, @@ -39,8 +38,6 @@ const FRESHNESS_TTL_POLL_RATIO = 0.9; /** Maximum number of asset IDs per Price API request. */ const PRICE_API_BATCH_SIZE = 50; -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - // ============================================================================ // OPTIONS // ============================================================================ @@ -263,9 +260,7 @@ export class PriceDataSource { ...(response.assetsPrice ?? {}), ...spotPrices, }; - } catch (error) { - log('Failed to fetch prices via middleware', { error }); - } + } catch (error) {} // Call next() at the end to continue the middleware chain return next(ctx); @@ -447,10 +442,6 @@ export class PriceDataSource { continue; } } catch (error) { - log('Skipping malformed asset ID in balance state', { - assetId, - error, - }); continue; } } @@ -461,7 +452,6 @@ export class PriceDataSource { return [...assetIds]; } catch (error) { - log('Failed to get asset IDs from balance state', { error }); return []; } } @@ -504,9 +494,7 @@ export class PriceDataSource { ...(response.assetsPrice ?? {}), ...spotPrices, }; - } catch (error) { - log('Failed to fetch prices', { error }); - } + } catch (error) {} return response; } @@ -548,9 +536,7 @@ export class PriceDataSource { updateMode: 'merge', }); } - } catch (error) { - log('Subscription update fetch failed', { subscriptionId, error }); - } + } catch (error) {} return; } } @@ -600,9 +586,7 @@ export class PriceDataSource { updateMode: 'merge', }); } - } catch (error) { - log('Subscription poll failed', { subscriptionId, error }); - } + } catch (error) {} }; // Set up polling diff --git a/packages/assets-controller/src/data-sources/RpcDataSource.ts b/packages/assets-controller/src/data-sources/RpcDataSource.ts index 8b3a7795db2..e95b3037b82 100644 --- a/packages/assets-controller/src/data-sources/RpcDataSource.ts +++ b/packages/assets-controller/src/data-sources/RpcDataSource.ts @@ -28,7 +28,6 @@ import type { AssetsControllerGetStateAction, AssetsControllerMessenger, } from '../AssetsController.js'; -import { projectLogger, createModuleLogger } from '../logger.js'; import type { ChainId, Caip19AssetId, @@ -69,8 +68,6 @@ const CONTROLLER_NAME = 'RpcDataSource'; const DEFAULT_BALANCE_INTERVAL = 30_000; // 30 seconds const DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - // Allowed actions that RpcDataSource can call export type RpcDataSourceAllowedActions = | NetworkControllerGetStateAction @@ -266,14 +263,6 @@ export class RpcDataSource extends AbstractDataSource< const detectionInterval = options.detectionInterval ?? DEFAULT_DETECTION_INTERVAL; - log('Initializing RpcDataSource', { - timeout: this.#timeout, - balanceInterval, - detectionInterval, - tokenDetectionEnabled: this.#tokenDetectionEnabled(), - useExternalService: this.#useExternalService(), - }); - // Initialize MulticallClient with a provider getter this.#multicallClient = new MulticallClient({ getProvider: (hexChainId: string): RpcProvider => { @@ -329,9 +318,7 @@ export class RpcDataSource extends AbstractDataSource< this.#balanceFetcher.setOnBalanceUpdate(async (result) => { try { await this.#handleBalanceUpdate(result); - } catch (error) { - log('Balance update handler failed', { error }); - } + } catch (error) {} }); // Initialize TokenDetector with polling interval. The TokensApiClient is @@ -354,9 +341,7 @@ export class RpcDataSource extends AbstractDataSource< this.#tokenDetector.setOnDetectionUpdate((result) => { try { this.#handleDetectionUpdate(result); - } catch (error) { - log('Detection update handler failed', { error }); - } + } catch (error) {} }); this.#subscribeToNetworkController(); @@ -378,16 +363,11 @@ export class RpcDataSource extends AbstractDataSource< */ #convertToHumanReadable(rawBalance: string, decimals: number): string { if (!Number.isFinite(decimals) || decimals < 0) { - log('Invalid decimals — defaulting balance to "0"', { - rawBalance, - decimals, - }); return '0'; } const rawAmount = new BigNumberJS(rawBalance); if (!rawAmount.isFinite()) { - log('Invalid raw balance — defaulting to "0"', { rawBalance, decimals }); return '0'; } @@ -549,15 +529,8 @@ export class RpcDataSource extends AbstractDataSource< dataTypes: ['balance'], }; - log('Balance update response', { - accountId: result.accountId, - newBalanceCount: Object.keys(newBalances).length, - }); - for (const subscription of this.#activeSubscriptions.values()) { - subscription.onAssetsUpdate(response, request)?.catch((error) => { - log('Failed to update assets', { error }); - }); + subscription.onAssetsUpdate(response, request)?.catch((error) => {}); } } @@ -567,10 +540,6 @@ export class RpcDataSource extends AbstractDataSource< * @param result - The token detection result. */ #handleDetectionUpdate(result: TokenDetectionResult): void { - log('Detected new tokens', { - count: result.detectedAssets.length, - }); - // Build new metadata from detected assets const newMetadata: Record = {}; if (result.detectedAssets.length > 0) { @@ -632,9 +601,7 @@ export class RpcDataSource extends AbstractDataSource< }; for (const subscription of this.#activeSubscriptions.values()) { - subscription.onAssetsUpdate(response, request)?.catch((error) => { - log('Failed to update detected assets', { error }); - }); + subscription.onAssetsUpdate(response, request)?.catch((error) => {}); } } @@ -642,7 +609,6 @@ export class RpcDataSource extends AbstractDataSource< this.#messenger.subscribe( 'NetworkController:stateChange', (networkState: NetworkState) => { - log('NetworkController state changed'); this.#clearProviderCache(); this.#updateFromNetworkState(networkState); }, @@ -665,9 +631,7 @@ export class RpcDataSource extends AbstractDataSource< } const caipChainId = `eip155:${parseInt(hexChainId, 16)}` as ChainId; this.#refreshBalanceForChains([caipChainId], 'transactionConfirmed').catch( - (error) => { - log('Failed to refresh balance after transaction confirmed', { error }); - }, + (error) => {}, ); } @@ -731,30 +695,18 @@ export class RpcDataSource extends AbstractDataSource< await subscription.onAssetsUpdate(responseWithMode, request); appliedCount += 1; - } catch (error) { - log('Failed to fetch balance after transaction', { - context, - chains: subscriptionChains, - error, - }); - } + } catch (error) {} } if (appliedCount === 0 && context === 'transactionConfirmed') { - log('No RpcDataSource subscription covers chain after transaction', { - chainsToFetch, - }); } } #initializeFromNetworkController(): void { - log('Initializing from NetworkController'); try { const networkState = this.#messenger.call('NetworkController:getState'); this.#updateFromNetworkState(networkState); - } catch (error) { - log('Failed to initialize from NetworkController', error); - } + } catch (error) {} } /** @@ -802,11 +754,6 @@ export class RpcDataSource extends AbstractDataSource< } } - log('Network state updated', { - configuredChains: Object.keys(chainStatuses), - activeChains, - }); - // Check if chains changed const previousChains = [...this.#activeChains]; const previousSet = new Set(previousChains); @@ -850,7 +797,6 @@ export class RpcDataSource extends AbstractDataSource< return web3Provider; } catch (error) { - log('Failed to get provider for chain', { chainId, error }); return undefined; } } @@ -953,7 +899,6 @@ export class RpcDataSource extends AbstractDataSource< * @param interval - The polling interval in milliseconds. */ setBalancePollingInterval(interval: number): void { - log('Setting balance polling interval', { interval }); this.#balanceFetcher.setIntervalLength(interval); } @@ -972,7 +917,6 @@ export class RpcDataSource extends AbstractDataSource< * @param interval - The polling interval in milliseconds. */ setDetectionPollingInterval(interval: number): void { - log('Setting detection polling interval', { interval }); this.#tokenDetector.setIntervalLength(interval); } @@ -987,7 +931,6 @@ export class RpcDataSource extends AbstractDataSource< async fetch(request: DataRequest): Promise { if (!this.#isOnboarded()) { - log('Skipping fetch - onboarding not complete'); return {}; } @@ -997,14 +940,7 @@ export class RpcDataSource extends AbstractDataSource< this.#activeChains.includes(chainId), ); - log('Fetch requested', { - accounts: request.accountsWithSupportedChains.map((a) => a.account.id), - requestedChains: request.chainIds, - chainsToFetch, - }); - if (chainsToFetch.length === 0) { - log('No active chains to fetch'); return response; } @@ -1130,8 +1066,6 @@ export class RpcDataSource extends AbstractDataSource< }; } } catch (error) { - log('Failed to fetch balance', { address, chainId, error }); - if (!assetsBalance[accountId]) { assetsBalance[accountId] = {}; } @@ -1164,22 +1098,11 @@ export class RpcDataSource extends AbstractDataSource< } if (failedChains.length > 0) { - log('Fetch PARTIAL - some chains failed', { - successChains: chainsToFetch.filter( - (chain) => !failedChains.includes(chain), - ), - failedChains, - }); - response.errors = {}; for (const chainId of failedChains) { response.errors[chainId] = 'RPC fetch failed'; } } else { - log('Fetch SUCCESS', { - chains: chainsToFetch, - accountCount: Object.keys(assetsBalance).length, - }); } response.assetsBalance = assetsBalance; @@ -1212,8 +1135,6 @@ export class RpcDataSource extends AbstractDataSource< const hexChainId = caipChainIdToHex(chainId); const { address, id: accountId } = account; - log('Running token detection', { chainId, accountId }); - try { const result = await this.#tokenDetector.detectTokens( hexChainId, @@ -1226,16 +1147,9 @@ export class RpcDataSource extends AbstractDataSource< ); if (result.detectedAssets.length === 0) { - log('No new tokens detected'); return {}; } - log('Detected new tokens', { - count: result.detectedAssets.length, - chainId, - accountId, - }); - // Convert detected assets to DataResponse format const balances: Record = {}; const assetsInfo: Record = {}; @@ -1287,7 +1201,6 @@ export class RpcDataSource extends AbstractDataSource< return response; } catch (error) { - log('Token detection failed', { chainId, accountId, error }); return {}; } } @@ -1306,11 +1219,6 @@ export class RpcDataSource extends AbstractDataSource< let successfullyHandledChains: ChainId[] = []; - log('Middleware fetching', { - chains: supportedChains, - accounts: request.accountsWithSupportedChains.map((a) => a.account.id), - }); - const response = await this.fetch({ ...request, chainIds: supportedChains, @@ -1368,7 +1276,6 @@ export class RpcDataSource extends AbstractDataSource< */ async subscribe(subscriptionRequest: SubscriptionRequest): Promise { if (!this.#isOnboarded()) { - log('Skipping subscribe - onboarding not complete'); return; } @@ -1383,16 +1290,7 @@ export class RpcDataSource extends AbstractDataSource< ) : request.chainIds; - log('Subscribe requested', { - subscriptionId, - isUpdate, - accounts: request.accountsWithSupportedChains.map((a) => a.account.id), - chainsToSubscribe, - activeChainsFallback: this.#activeChains.length === 0, - }); - if (chainsToSubscribe.length === 0) { - log('No active chains to subscribe'); return; } @@ -1400,11 +1298,6 @@ export class RpcDataSource extends AbstractDataSource< if (isUpdate) { const existing = this.#activeSubscriptions.get(subscriptionId); if (existing) { - log('Updating existing subscription - restarting polling', { - subscriptionId, - existingChains: existing.chains, - newChains: chainsToSubscribe, - }); // Don't return early - continue to unsubscribe and restart polling } } @@ -1473,13 +1366,6 @@ export class RpcDataSource extends AbstractDataSource< accounts, onAssetsUpdate: subscriptionRequest.onAssetsUpdate, }); - - log('Subscription SUCCESS', { - subscriptionId, - chains: chainsToSubscribe, - balancePollingCount: balancePollingTokens.length, - detectionPollingCount: detectionPollingTokens.length, - }); } /** @@ -1501,7 +1387,6 @@ export class RpcDataSource extends AbstractDataSource< } this.#activeSubscriptions.delete(subscriptionId); - log('Unsubscribed and stopped polling', { subscriptionId }); } } @@ -1516,7 +1401,6 @@ export class RpcDataSource extends AbstractDataSource< const state = this.#messenger.call('AssetsController:getState'); return state.assetsInfo ?? {}; } catch (error) { - log('Failed to get existing assets metadata', { error }); return {}; } } @@ -1525,8 +1409,6 @@ export class RpcDataSource extends AbstractDataSource< * Destroy the data source and clean up resources. */ destroy(): void { - log('Destroying RpcDataSource'); - this.#unsubscribeTransactionConfirmed?.(); // Stop all polling diff --git a/packages/assets-controller/src/data-sources/SnapDataSource.ts b/packages/assets-controller/src/data-sources/SnapDataSource.ts index 6e4dd8b6a4b..01748f85152 100644 --- a/packages/assets-controller/src/data-sources/SnapDataSource.ts +++ b/packages/assets-controller/src/data-sources/SnapDataSource.ts @@ -18,7 +18,6 @@ import { parseCaipAssetType } from '@metamask/utils'; import type { Json, JsonRpcRequest } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; -import { projectLogger, createModuleLogger } from '../logger.js'; import type { AssetBalance, ChainId, @@ -61,8 +60,6 @@ export type AccountsControllerAccountBalancesUpdatedEvent = { payload: [AccountBalancesUpdatedEventPayload]; }; -const log = createModuleLogger(projectLogger, 'SnapDataSource'); - // ============================================================================ // CONSTANTS // ============================================================================ @@ -292,10 +289,6 @@ export class SnapDataSource extends AbstractDataSource< try { chainId = extractChainFromAssetId(assetId); } catch (error) { - log('Skipping snap balance for malformed asset ID', { - assetId, - error, - }); continue; } if (this.#isChainSupportedBySnap(chainId)) { @@ -347,7 +340,6 @@ export class SnapDataSource extends AbstractDataSource< 'SnapController:getRunnableSnaps', ) as Snap[]; } catch (error) { - log('Failed to get runnable snaps', error); return []; } } @@ -368,7 +360,6 @@ export class SnapDataSource extends AbstractDataSource< snapId, ) as SubjectPermissions; } catch (error) { - log('Failed to get permissions for snap', { snapId, error }); return undefined; } } @@ -430,7 +421,6 @@ export class SnapDataSource extends AbstractDataSource< // AssetsController not ready yet - expected during initialization } } catch (error) { - log('Keyring snap discovery failed', { error }); this.state.chainToSnap = {}; try { const previous = [...this.state.activeChains]; @@ -588,7 +578,6 @@ export class SnapDataSource extends AbstractDataSource< (chainId) => !failedChains.has(chainId), ); } catch (error) { - log('Middleware fetch failed', { error }); successfullyHandledChains = []; } @@ -642,9 +631,7 @@ export class SnapDataSource extends AbstractDataSource< if (Object.keys(fetchResponse.assetsBalance ?? {}).length > 0) { await this.#onAssetsUpdate(fetchResponse); } - } catch (error) { - log('Initial fetch failed', { subscriptionId, error }); - } + } catch (error) {} } // ============================================================================ @@ -696,9 +683,7 @@ export class SnapDataSource extends AbstractDataSource< 'AccountsController:accountBalancesUpdated', this.#handleSnapBalancesUpdatedBound, ); - } catch (error) { - log('Failed to unsubscribe from snap keyring events', { error }); - } + } catch (error) {} // Unsubscribe from permission changes try { @@ -706,9 +691,7 @@ export class SnapDataSource extends AbstractDataSource< 'PermissionController:stateChange', this.#handlePermissionStateChangeBound, ); - } catch (error) { - log('Failed to unsubscribe from permission changes', { error }); - } + } catch (error) {} // Clear keyring client cache this.#keyringClientCache.clear(); diff --git a/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts b/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts index a3eccda40f0..ecb745ccaf4 100644 --- a/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts +++ b/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts @@ -11,7 +11,6 @@ import { import type { Hex } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; -import { projectLogger, createModuleLogger } from '../logger.js'; import type { AccountId, ChainId, @@ -48,8 +47,6 @@ const STAKED_ETH_METADATA: AssetMetadata = { decimals: 18, }; -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - /** Optional configuration for StakedBalanceDataSource. */ export type StakedBalanceDataSourceConfig = { /** Whether staked balance fetching is enabled (default: true). */ @@ -180,11 +177,6 @@ export class StakedBalanceDataSource extends AbstractDataSource< this.#enabled = options.enabled !== false; this.#supportedChainIds = getSupportedStakingChainIds() as ChainId[]; - log('Initializing StakedBalanceDataSource', { - enabled: this.#enabled, - pollInterval: this.#pollInterval, - }); - // Create StakedBalanceFetcher with provider getter this.#stakedBalanceFetcher = new StakedBalanceFetcher({ pollingInterval: this.#pollInterval, @@ -196,9 +188,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< this.#stakedBalanceFetcher.setOnStakedBalanceUpdate((result) => { try { this.#handleStakedBalanceUpdate(result); - } catch (error) { - log('Staked balance update handler failed', { error }); - } + } catch (error) {} }); this.#messenger.subscribe( @@ -226,7 +216,6 @@ export class StakedBalanceDataSource extends AbstractDataSource< */ #onNetworkStateChange(): void { this.#providerCache.clear(); - log('Provider cache cleared after network state change'); } /** @@ -301,9 +290,9 @@ export class StakedBalanceDataSource extends AbstractDataSource< const caipChainId = `eip155:${parseInt(hexChainId, 16)}` as ChainId; const toRefresh = this.#getToRefreshForChains([caipChainId]); if (toRefresh.length > 0) { - this.#refreshStakedBalanceAfterTransaction(toRefresh).catch((error) => { - log('Failed to refresh staked balance after transaction', { error }); - }); + this.#refreshStakedBalanceAfterTransaction(toRefresh).catch( + (error) => {}, + ); } } @@ -406,13 +395,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< ...existing, [assetId]: { amount: result.amount }, }; - } catch (error) { - log('Failed to fetch staked balance in transaction refresh', { - chainId, - accountId: account.id, - error, - }); - } + } catch (error) {} } const chainIds = [...new Set(toRefresh.map(({ chainId }) => chainId))]; @@ -450,11 +433,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< for (const subscription of this.#activeSubscriptions.values()) { subscription .onAssetsUpdate(response, request) - ?.catch((error: unknown) => { - log('Failed to report staked balance update after transaction', { - error, - }); - }); + ?.catch((error: unknown) => {}); } } } @@ -473,7 +452,6 @@ export class StakedBalanceDataSource extends AbstractDataSource< state?.enabledNetworkMap ?? {}, ); } catch (error) { - log('Failed to get NetworkEnablementController state', { error }); this.#initializeActiveChainsFromEnabledMap({}); } } @@ -576,7 +554,6 @@ export class StakedBalanceDataSource extends AbstractDataSource< this.#providerCache.set(hexChainId, provider); return provider; } catch (error) { - log('Failed to get provider for chain', { hexChainId, error }); return undefined; } } @@ -616,18 +593,10 @@ export class StakedBalanceDataSource extends AbstractDataSource< dataTypes: ['balance'], }; - log('Staked balance update', { - accountId: result.accountId, - chainId: caipChainId, - amount: result.balance.amount, - }); - for (const subscription of this.#activeSubscriptions.values()) { subscription .onAssetsUpdate(response, request) - ?.catch((error: unknown) => { - log('Failed to report staked balance update', { error }); - }); + ?.catch((error: unknown) => {}); } } @@ -683,13 +652,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< balances[account.id] ??= {}; const assetId = stakedAssetId(chainId, contractAddress); balances[account.id][assetId] = { amount: result.amount }; - } catch (error) { - log('Failed to fetch staked balance', { - chainId, - accountId: account.id, - error, - }); - } + } catch (error) {} } } @@ -751,9 +714,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< }; } } - } catch (error) { - log('Middleware fetch failed', { error }); - } + } catch (error) {} // Pass all chains through (staked balance doesn't claim chains) return next(context); @@ -774,14 +735,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< activeChainsSet.has(chainId), ); - log('Subscribe requested', { - subscriptionId, - isUpdate, - chainsToSubscribe, - }); - if (chainsToSubscribe.length === 0) { - log('No staking chains to subscribe'); return; } @@ -789,9 +743,6 @@ export class StakedBalanceDataSource extends AbstractDataSource< if (isUpdate) { const existing = this.#activeSubscriptions.get(subscriptionId); if (existing) { - log('Updating existing subscription - restarting polling', { - subscriptionId, - }); } } @@ -870,19 +821,9 @@ export class StakedBalanceDataSource extends AbstractDataSource< ) { subscriptionRequest .onAssetsUpdate?.(initialResponse) - ?.catch((error) => { - log('Initial staked balance update failed', { error }); - }); + ?.catch((error) => {}); } - } catch (error) { - log('Initial staked balance fetch failed', { error }); - } - - log('Subscription SUCCESS', { - subscriptionId, - chains: chainsToSubscribe, - pollingCount: pollingTokens.length, - }); + } catch (error) {} } /** diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.ts b/packages/assets-controller/src/data-sources/TokenDataSource.ts index fffed2b9461..dfce600c66d 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.ts @@ -9,7 +9,6 @@ import { KnownCaipNamespace, parseCaipAssetType } from '@metamask/utils'; import type { CaipAssetType } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; -import { projectLogger, createModuleLogger } from '../logger.js'; import { forDataTypes } from '../types.js'; import type { Caip19AssetId, @@ -30,8 +29,6 @@ import { const CONTROLLER_NAME = 'TokenDataSource'; const DEFAULT_FETCH_TIMEOUT_MS = 15_000; -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - /** Max asset IDs per tokens API request. */ const TOKENS_API_BATCH_SIZE = 50; @@ -218,7 +215,6 @@ export class TokenDataSource { return new Set(allNetworks); } catch (error) { - log('Failed to fetch supported networks', { error }); return new Set(); } } @@ -238,7 +234,6 @@ export class TokenDataSource { this.#fetchTimeoutMs, ); } catch (error) { - log('Failed to fetch suggested occurrence floors', { error }); return {}; } } @@ -341,7 +336,6 @@ export class TokenDataSource { } } } catch (error) { - log('Blockaid bulk token scan failed; keeping all tokens', { error }); return assets; } @@ -481,13 +475,9 @@ export class TokenDataSource { } } } - log('Filtered low-occurrence websocket assets', { - assetIds: [...spamAssetIds], - }); } } catch (error) { // Fail open — keep all assets when occurrences cannot be fetched. - log('Failed to fetch occurrences for websocket update', { error }); } return next(ctx); @@ -768,9 +758,7 @@ export class TokenDataSource { } } } - } catch (error) { - log('Failed to fetch metadata', { error }); - } + } catch (error) {} // Call next() at the end to continue the middleware chain return next(ctx); diff --git a/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts b/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts index 13cf367424d..c3178c7357f 100644 --- a/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts +++ b/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts @@ -1,7 +1,6 @@ import { StaticIntervalPollingControllerOnly } from '@metamask/polling-controller'; import type { CaipAssetType } from '@metamask/utils'; -import { projectLogger, createModuleLogger } from '../../../logger.js'; import type { MulticallClient } from '../clients/index.js'; import type { TokensApiClient } from '../clients/TokensApiClient.js'; import type { @@ -18,8 +17,6 @@ import type { } from '../types/index.js'; import { reduceInBatchesSerially } from '../utils/index.js'; -const log = createModuleLogger(projectLogger, 'TokenDetector'); - const DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes export type TokenDetectorConfig = { @@ -113,8 +110,8 @@ export class TokenDetector extends StaticIntervalPollingControllerOnly 0) { this.#onDetectionUpdate(result); } - } catch (error) { - log('Token detection poll failed', { chainId: input.chainId, error }); + } catch { + // Silently handle errors } } @@ -226,13 +223,8 @@ export class TokenDetector extends StaticIntervalPollingControllerOnly AccountId | undefined; removeCustomAsset: (accountId: AccountId, assetId: Caip19AssetId) => void; @@ -98,10 +95,6 @@ export class CustomAssetGraduationMiddleware { if (!customSet.has(normalizedAssetId)) { continue; } - log('Graduating custom asset', { - accountId, - assetId: normalizedAssetId, - }); this.#removeCustomAsset(accountId, normalizedAssetId); } diff --git a/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts b/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts index fb164a89126..cbcd14addfb 100644 --- a/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts +++ b/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts @@ -1,4 +1,3 @@ -import { projectLogger, createModuleLogger } from '../logger.js'; import { forDataTypes } from '../types.js'; import type { AssetsDataSource, @@ -10,8 +9,6 @@ import { mergeDataResponses } from './ParallelMiddleware.js'; const CONTROLLER_NAME = 'RpcFallbackMiddleware'; -const log = createModuleLogger(projectLogger, CONTROLLER_NAME); - export type RpcFallbackMiddlewareOptions = { /** The RPC data source to use as a fallback. */ rpcDataSource: AssetsDataSource; @@ -49,10 +46,6 @@ export class RpcFallbackMiddleware { return next(ctx); } - log('Retrying failed chains on RPC', { - chains: [...erroredChains], - }); - const filteredRequest = { ...ctx.request, chainIds: ctx.request.chainIds.filter((id) => erroredChains.has(id)), diff --git a/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts b/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts index 5546a7faefe..86968fb39ee 100644 --- a/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts +++ b/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts @@ -9,7 +9,6 @@ import { } from '@metamask/utils'; import { cloneDeep } from 'lodash'; -import { createModuleLogger, projectLogger } from '../logger.js'; import type { AccountId, Caip19AssetId, @@ -93,8 +92,6 @@ export type AssetsInfoHealingPatch = { customAssets: Record; }; -const log = createModuleLogger(projectLogger, 'tempHealAssetsInfoMetadata'); - export type TempHealAssetsInfoMetadataOptions = { /** Current `AssetsController` state the healing patch is computed against. */ state: AssetsControllerStateInternal; @@ -123,7 +120,6 @@ export function tempHealAssetsInfoMetadata({ captureException, }: TempHealAssetsInfoMetadataOptions): AssetsControllerStateInternal { const reportError = (error: unknown): void => { - log('Failed to heal assetsInfo metadata', error); captureException?.( new Error( `AssetsController: temporary assetsInfo metadata healing failed: ${getErrorMessage( @@ -148,11 +144,6 @@ export function tempHealAssetsInfoMetadata({ const nextState = cloneDeep(state); applyHealingPatch(nextState, patch); - log('Healed wiped assetsInfo metadata for niche-chain tokens', { - healedAssetsInfoCount: Object.keys(patch.assetsInfo).length, - healedCustomAssetsAccounts: Object.keys(patch.customAssets).length, - }); - return nextState; } catch (error) { reportError(error); From 127f52153bd38826ab215ff82b3b1637c96d59cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 09:41:18 +0000 Subject: [PATCH 5/5] Revert createModuleLogger removal from entire package Co-authored-by: Salim TOUBAL --- .../assets-controller/src/AssetsController.ts | 155 ++++++++++++++++-- .../data-sources/AccountActivityDataSource.ts | 23 ++- .../src/data-sources/AccountsApiDataSource.ts | 32 +++- .../src/data-sources/PriceDataSource.ts | 24 ++- .../src/data-sources/RpcDataSource.ts | 132 ++++++++++++++- .../src/data-sources/SnapDataSource.ts | 23 ++- .../data-sources/StakedBalanceDataSource.ts | 81 +++++++-- .../src/data-sources/TokenDataSource.ts | 14 +- .../services/TokenDetector.ts | 14 +- packages/assets-controller/src/logger.ts | 4 +- .../CustomAssetGraduationMiddleware.ts | 7 + .../src/middlewares/RpcFallbackMiddleware.ts | 7 + .../src/migrations/healAssetsInfoMetadata.ts | 9 + 13 files changed, 475 insertions(+), 50 deletions(-) diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index b11a40c2679..d2ec0bf6931 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -108,6 +108,7 @@ import { getDefaultAssetMetadata, } from './defaults.js'; import { AssetsDataSourceError } from './errors.js'; +import { projectLogger, createModuleLogger } from './logger.js'; import { CustomAssetGraduationMiddleware } from './middlewares/CustomAssetGraduationMiddleware.js'; import { DetectionMiddleware } from './middlewares/DetectionMiddleware.js'; import { @@ -226,6 +227,8 @@ const TRACE_UPDATE_PARENT = 'AssetsUpdateEnrichment'; const TRACE_SUBSCRIPTION_ERROR = 'AssetsSubscriptionError'; const TRACE_STATE_SIZE = 'AssetsStateSize'; +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + // ============================================================================ // STATE TYPES // ============================================================================ @@ -917,7 +920,12 @@ export class AssetsController extends BaseController< ): void => { try { this.#handleActiveChainsUpdate(dataSourceName, chains, previousChains); - } catch (error) {} + } catch (error) { + log('Failed to handle active chains update', { + dataSourceName, + error, + }); + } }; this.#accountActivityDataSource = new AccountActivityDataSource({ @@ -998,6 +1006,10 @@ export class AssetsController extends BaseController< rpcDataSource: this.#rpcDataSource, }); + log('Initializing AssetsController', { + defaultUpdateInterval: this.#defaultUpdateInterval, + }); + this.#initializeState(); this.#subscribeToEvents(); this.#registerActionHandlers(); @@ -1030,6 +1042,10 @@ export class AssetsController extends BaseController< .catch((error) => { // Failure to populate native asset cache is non-fatal; // #isNativeAsset falls back to the seed data from buildNativeAssetsFromConstant. + log( + 'Failed to populate native asset cache, falling back to seed data', + error, + ); }); // Seed the cache synchronously so that synchronous consumers (e.g. @@ -1048,6 +1064,11 @@ export class AssetsController extends BaseController< 'NetworkEnablementController:getState', ); this.#enabledChains = this.#extractEnabledChains(enabledNetworkMap); + + log('Initialized state', { + enabledNetworkMap, + enabledChains: this.#enabledChains, + }); } /** @@ -1144,7 +1165,9 @@ export class AssetsController extends BaseController< this.#handleNetworkAdded(networkConfiguration.chainId); this.#refreshAssetsAfterNetworkAdded( networkConfiguration.chainId, - ).catch((error) => {}); + ).catch((error) => { + log('Failed to refresh assets after network added', { error }); + }); }, ); @@ -1235,7 +1258,11 @@ export class AssetsController extends BaseController< this.getAssets([matchedAccount], { chainIds: [caipChainId], forceUpdate: true, - }).catch((error) => {}); + }).catch((error) => { + log('Failed to refresh assets after unapproved transaction added', { + error, + }); + }); } #onTransactionConfirmed(transactionMeta: TransactionMeta): void { @@ -1260,7 +1287,9 @@ export class AssetsController extends BaseController< this.getAssets([matchedAccount], { chainIds: [caipChainId], forceUpdate: true, - }).catch((error) => {}); + }).catch((error) => { + log('Failed to refresh assets after transaction confirmed', { error }); + }); } /** @@ -1309,6 +1338,11 @@ export class AssetsController extends BaseController< return; } + log('Account tree changed with new accounts, re-subscribing', { + previousCount: this.#lastKnownAccountIds.size, + currentCount: currentIds.size, + }); + const newAccounts = accounts.filter( (account) => !this.#lastKnownAccountIds.has(account.id), ); @@ -1316,7 +1350,9 @@ export class AssetsController extends BaseController< this.#lastKnownAccountIds = currentIds; this.#ensureNativeBalancesDefaultZero(); this.#ensureDefaultTrackedAssetsSeeded(); - this.#runAccountTreeRefresh(accounts, newAccounts).catch((error) => {}); + this.#runAccountTreeRefresh(accounts, newAccounts).catch((error) => { + log('Failed to refresh assets after tree change', error); + }); } else { this.#start(); } @@ -1341,6 +1377,7 @@ export class AssetsController extends BaseController< } this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]); } catch (error) { + log('Failed to fetch assets after tree change', error); this.#subscribeAssets(); this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]); } finally { @@ -1367,6 +1404,7 @@ export class AssetsController extends BaseController< this.#subscribeAssets(); this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]); } catch (error) { + log('Failed to fetch assets on startup', error); this.#ensureNativeBalancesDefaultZero(); this.#ensureDefaultTrackedAssetsSeeded(); this.#subscribeAssets(); @@ -1408,6 +1446,11 @@ export class AssetsController extends BaseController< if (!this.#uiOpen || !this.#keyringUnlocked || !this.#isEnabled()) { return; } + log('Data source active chains changed', { + dataSourceId, + chainCount: activeChains.length, + chains: activeChains, + }); const previous: ChainId[] = previousChains; @@ -2076,6 +2119,8 @@ export class AssetsController extends BaseController< ): Promise { const normalizedAssetId = normalizeAssetId(assetId); + log('Adding custom asset', { accountId, assetId: normalizedAssetId }); + this.update((state) => { const customAssets = state.customAssets as Record; if (!customAssets[accountId]) { @@ -2160,6 +2205,8 @@ export class AssetsController extends BaseController< removeCustomAsset(accountId: AccountId, assetId: Caip19AssetId): void { const normalizedAssetId = normalizeAssetId(assetId); + log('Removing custom asset', { accountId, assetId: normalizedAssetId }); + this.update((state) => { if (state.customAssets[accountId]) { state.customAssets[accountId] = state.customAssets[accountId].filter( @@ -2202,6 +2249,8 @@ export class AssetsController extends BaseController< hideAsset(assetId: Caip19AssetId): void { const normalizedAssetId = normalizeAssetId(assetId); + log('Hiding asset', { assetId: normalizedAssetId }); + this.update((state) => { if (!state.assetPreferences[normalizedAssetId]) { state.assetPreferences[normalizedAssetId] = {}; @@ -2218,6 +2267,8 @@ export class AssetsController extends BaseController< unhideAsset(assetId: Caip19AssetId): void { const normalizedAssetId = normalizeAssetId(assetId); + log('Unhiding asset', { assetId: normalizedAssetId }); + this.update((state) => { const prefs = state.assetPreferences[normalizedAssetId]; if (prefs) { @@ -2249,6 +2300,11 @@ export class AssetsController extends BaseController< state.selectedCurrency = selectedCurrency; }); + log('Current currency changed', { + previousCurrency, + selectedCurrency, + }); + if (!this.#isBasicFunctionality()) { return; } @@ -2262,7 +2318,9 @@ export class AssetsController extends BaseController< assetsForPriceUpdate: Object.values(this.state.assetsBalance).flatMap( (balances) => Object.keys(balances) as Caip19AssetId[], ), - }).catch((error) => {}); + }).catch((error) => { + log('Failed to fetch asset prices after current currency change', error); + }); } /** @@ -2317,7 +2375,9 @@ export class AssetsController extends BaseController< dataTypes: ['price'], chainIds, assetsForPriceUpdate, - }).catch((error) => {}); + }).catch((error) => { + log('Failed to fetch missing prices', { error }); + }); } // ============================================================================ @@ -2827,6 +2887,23 @@ export class AssetsController extends BaseController< changedMetadata.length > 0 || changedPriceAssets.length > 0 ) { + log('State updated', { + changedBalances: + changedBalances.length > 0 ? changedBalances : undefined, + changedMetadataCount: + changedMetadata.length > 0 ? changedMetadata.length : undefined, + changedPricesCount: + changedPriceAssets.length > 0 + ? changedPriceAssets.length + : undefined, + newAssets: + Object.keys(detectedAssets).length > 0 + ? Object.entries(detectedAssets).map(([accountId, assets]) => ({ + accountId, + assets, + })) + : undefined, + }); } // Publish balance changed events @@ -3046,8 +3123,15 @@ export class AssetsController extends BaseController< return; } + log('Starting asset tracking', { + selectedAccountCount: accounts.length, + enabledChainCount: chainIds.length, + }); + this.#lastKnownAccountIds = new Set(accounts.map((a) => a.id)); - this.#runStartupRefresh(accounts).catch((error) => {}); + this.#runStartupRefresh(accounts).catch((error) => { + log('Failed to start asset tracking', error); + }); } /** @@ -3055,6 +3139,11 @@ export class AssetsController extends BaseController< * Called when app closes or keyring locks. */ #stop(): void { + log('Stopping asset tracking', { + activeSubscriptionCount: this.#activeSubscriptions.size, + hasPriceSubscription: this.#activeSubscriptions.has('ds:PriceDataSource'), + }); + this.#firstInitFetchReported = false; this.#stateSizeReported = false; this.#lastKnownAccountIds = new Set(); @@ -3349,6 +3438,15 @@ export class AssetsController extends BaseController< const existingSubscription = this.#activeSubscriptions.get(subscriptionKey); const isUpdate = existingSubscription !== undefined; + log('Subscribe to data source', { + sourceId, + subscriptionKey, + isUpdate, + accountCount: accounts.length, + chainCount: chains.length, + customAssetsOnly: options.customAssetsOnly === true, + }); + const subscribeReq: SubscriptionRequest = { request: this.#buildDataRequest(accounts, chains, { assetTypes: ['fungible'], @@ -3511,6 +3609,11 @@ export class AssetsController extends BaseController< async #handleAccountGroupChanged(): Promise { const accounts = this.#getSelectedAccounts(); + log('Account group changed', { + accountCount: accounts.length, + accountIds: accounts.map((a) => a.id), + }); + this.#lastKnownAccountIds = new Set(accounts.map((a) => a.id)); const releaseLock = await this.#accountRefreshMutex.acquire(); @@ -3554,6 +3657,13 @@ export class AssetsController extends BaseController< } } + log('Enabled networks changed', { + previousCount: previousChains.size, + newCount: this.#enabledChains.size, + addedChains, + removedChains, + }); + // Note: We intentionally do NOT delete balance data for disabled chains. // Users may want to see historical balances even if the network is currently disabled. // The data will simply not be updated until the network is re-enabled. @@ -3604,6 +3714,11 @@ export class AssetsController extends BaseController< return; } + log('Network added — seeding default tracked assets', { + hexChainId, + caipChainId, + }); + this.#ensureDefaultTrackedAssetsSeeded([caipChainId]); const accounts = this.#getSelectedAccounts(); this.#fetchMissingPricesWithoutCache(accounts, [caipChainId]); @@ -3664,6 +3779,11 @@ export class AssetsController extends BaseController< return; } + log('Selected EVM network switched', { + selectedNetworkClientId: networkState.selectedNetworkClientId, + selectedChainId, + }); + const releaseLock = await this.#accountRefreshMutex.acquire(); try { await this.#refreshActiveChainsOnNetworkSwitch(); @@ -3695,7 +3815,9 @@ export class AssetsController extends BaseController< this.getAssets(accounts, { forceUpdate: true, dataTypes: ['balance', 'metadata'], - }).catch((error) => {}); + }).catch((error) => { + log('Failed to refresh assets after network change', { error }); + }); } /** @@ -3712,7 +3834,9 @@ export class AssetsController extends BaseController< this.getAssets(accounts, { forceUpdate: true, dataTypes: ['balance', 'metadata', 'price'], - }).catch((error) => {}); + }).catch((error) => { + log('Failed to refresh assets after token detection enabled', { error }); + }); } /** @@ -3757,6 +3881,12 @@ export class AssetsController extends BaseController< sourceId: string, request?: DataRequest, ): Promise { + log('Assets updated from data source', { + sourceId, + hasBalance: Boolean(response.assetsBalance), + hasPrice: Boolean(response.assetsPrice), + }); + // Enrichment spans only before unlock/first-init fetch completes. const pipelineTrace = this.#firstInitFetchReported ? undefined @@ -3873,6 +4003,11 @@ export class AssetsController extends BaseController< // ============================================================================ destroy(): void { + log('Destroying AssetsController', { + dataSourceCount: this.#allBalanceDataSources.length, + subscriptionCount: this.#activeSubscriptions.size, + }); + // Destroy instantiated data sources this.#accountActivityDataSource?.destroy?.(); this.#accountsApiDataSource?.destroy?.(); diff --git a/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts b/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts index e6c185b4554..196af953ac0 100644 --- a/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts +++ b/packages/assets-controller/src/data-sources/AccountActivityDataSource.ts @@ -8,6 +8,7 @@ import { isCaipChainId } from '@metamask/utils'; import BigNumberJS from 'bignumber.js'; import type { AssetsControllerMessenger } from '../AssetsController.js'; +import { projectLogger, createModuleLogger } from '../logger.js'; import type { AssetBalance, AssetMetadata, @@ -26,6 +27,8 @@ import type { DataSourceState } from './AbstractDataSource.js'; const CONTROLLER_NAME = 'AccountActivityDataSource'; +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + // ============================================================================ // BALANCE UPDATE PROCESSING // ============================================================================ @@ -301,9 +304,13 @@ export class AccountActivityDataSource extends AbstractDataSource< }; Promise.resolve(this.#onAssetsUpdate(response, request)).catch( - (error) => {}, + (error) => { + log('Failed to report balance update', { error }); + }, ); - } catch (error) {} + } catch (error) { + log('Error handling balance update', error); + } } /** @@ -398,7 +405,9 @@ export class AccountActivityDataSource extends AbstractDataSource< this.updateActiveChains(Array.from(next), (updatedChains) => this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); - } catch (error) {} + } catch (error) { + log('Error handling status change', error); + } }; // ============================================================================ @@ -411,14 +420,18 @@ export class AccountActivityDataSource extends AbstractDataSource< 'AccountActivityService:balanceUpdated', this.#onBalanceUpdatedBound, ); - } catch (error) {} + } catch (error) { + log('Failed to unsubscribe from balanceUpdated', { error }); + } try { this.#messenger.unsubscribe( 'AccountActivityService:statusChanged', this.#onAccountActivityStatusChanged, ); - } catch (error) {} + } catch (error) { + log('Failed to unsubscribe from statusChanged', { error }); + } super.destroy(); } diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts index ebbb348902a..3de2633e958 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts @@ -14,6 +14,7 @@ import { } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; +import { projectLogger, createModuleLogger } from '../logger.js'; import type { ChainId, Caip19AssetId, @@ -43,6 +44,8 @@ const CONTROLLER_NAME = 'AccountsApiDataSource'; const DEFAULT_POLL_INTERVAL = 30_000; const DEFAULT_FETCH_TIMEOUT_MS = 15_000; +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + // ============================================================================ // MESSENGER TYPES // ============================================================================ @@ -271,7 +274,11 @@ export class AccountsApiDataSource extends AbstractDataSource< async #handleMigrationFeatureFlagsChanged(): Promise { try { await this.#refreshActiveChains(); - } catch (error) {} + } catch (error) { + log('Failed to refresh active chains after feature flag change', { + error, + }); + } } /** @@ -322,7 +329,9 @@ export class AccountsApiDataSource extends AbstractDataSource< }, 20 * 60 * 1000, ); - } catch (error) {} + } catch (error) { + log('Failed to fetch active chains', error); + } } async #refreshActiveChains(): Promise { @@ -343,7 +352,9 @@ export class AccountsApiDataSource extends AbstractDataSource< this.#onActiveChainsUpdated(this.getName(), updatedChains, previous), ); } - } catch (error) {} + } catch (error) { + log('Failed to refresh active chains', error); + } } /** @@ -441,6 +452,8 @@ export class AccountsApiDataSource extends AbstractDataSource< response.assetsBalance = assetsBalance; response.updateMode = 'merge'; } catch (error) { + log('Fetch FAILED', { error, chains: chainsToFetch }); + // On error, mark all chains as errors so they can be handled by next middleware response.errors = response.errors ?? {}; for (const chainId of chainsToFetch) { @@ -748,6 +761,7 @@ export class AccountsApiDataSource extends AbstractDataSource< successfullyHandledChains = []; } } catch (error) { + log('Middleware fetch failed', { error }); successfullyHandledChains = []; } @@ -812,7 +826,13 @@ export class AccountsApiDataSource extends AbstractDataSource< forceUpdate: true, }); await existing.onAssetsUpdate(fetchResponse); - } catch (error) {} + } catch (error) { + log('Initial fetch for added chains failed', { + subscriptionId, + addedChains, + error, + }); + } } return; } @@ -839,7 +859,9 @@ export class AccountsApiDataSource extends AbstractDataSource< // Report update to AssetsController via callback await subscription.onAssetsUpdate(fetchResponse); - } catch (error) {} + } catch (error) { + log('Subscription poll failed', { subscriptionId, error }); + } }; // Set up polling diff --git a/packages/assets-controller/src/data-sources/PriceDataSource.ts b/packages/assets-controller/src/data-sources/PriceDataSource.ts index b343e57ca8e..56a263b4294 100644 --- a/packages/assets-controller/src/data-sources/PriceDataSource.ts +++ b/packages/assets-controller/src/data-sources/PriceDataSource.ts @@ -5,6 +5,7 @@ import type { import { ApiPlatformClient } from '@metamask/core-backend'; import { parseCaipAssetType } from '@metamask/utils'; +import { projectLogger, createModuleLogger } from '../logger.js'; import { forDataTypes } from '../types.js'; import type { Caip19AssetId, @@ -38,6 +39,8 @@ const FRESHNESS_TTL_POLL_RATIO = 0.9; /** Maximum number of asset IDs per Price API request. */ const PRICE_API_BATCH_SIZE = 50; +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + // ============================================================================ // OPTIONS // ============================================================================ @@ -260,7 +263,9 @@ export class PriceDataSource { ...(response.assetsPrice ?? {}), ...spotPrices, }; - } catch (error) {} + } catch (error) { + log('Failed to fetch prices via middleware', { error }); + } // Call next() at the end to continue the middleware chain return next(ctx); @@ -442,6 +447,10 @@ export class PriceDataSource { continue; } } catch (error) { + log('Skipping malformed asset ID in balance state', { + assetId, + error, + }); continue; } } @@ -452,6 +461,7 @@ export class PriceDataSource { return [...assetIds]; } catch (error) { + log('Failed to get asset IDs from balance state', { error }); return []; } } @@ -494,7 +504,9 @@ export class PriceDataSource { ...(response.assetsPrice ?? {}), ...spotPrices, }; - } catch (error) {} + } catch (error) { + log('Failed to fetch prices', { error }); + } return response; } @@ -536,7 +548,9 @@ export class PriceDataSource { updateMode: 'merge', }); } - } catch (error) {} + } catch (error) { + log('Subscription update fetch failed', { subscriptionId, error }); + } return; } } @@ -586,7 +600,9 @@ export class PriceDataSource { updateMode: 'merge', }); } - } catch (error) {} + } catch (error) { + log('Subscription poll failed', { subscriptionId, error }); + } }; // Set up polling diff --git a/packages/assets-controller/src/data-sources/RpcDataSource.ts b/packages/assets-controller/src/data-sources/RpcDataSource.ts index e95b3037b82..8b3a7795db2 100644 --- a/packages/assets-controller/src/data-sources/RpcDataSource.ts +++ b/packages/assets-controller/src/data-sources/RpcDataSource.ts @@ -28,6 +28,7 @@ import type { AssetsControllerGetStateAction, AssetsControllerMessenger, } from '../AssetsController.js'; +import { projectLogger, createModuleLogger } from '../logger.js'; import type { ChainId, Caip19AssetId, @@ -68,6 +69,8 @@ const CONTROLLER_NAME = 'RpcDataSource'; const DEFAULT_BALANCE_INTERVAL = 30_000; // 30 seconds const DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + // Allowed actions that RpcDataSource can call export type RpcDataSourceAllowedActions = | NetworkControllerGetStateAction @@ -263,6 +266,14 @@ export class RpcDataSource extends AbstractDataSource< const detectionInterval = options.detectionInterval ?? DEFAULT_DETECTION_INTERVAL; + log('Initializing RpcDataSource', { + timeout: this.#timeout, + balanceInterval, + detectionInterval, + tokenDetectionEnabled: this.#tokenDetectionEnabled(), + useExternalService: this.#useExternalService(), + }); + // Initialize MulticallClient with a provider getter this.#multicallClient = new MulticallClient({ getProvider: (hexChainId: string): RpcProvider => { @@ -318,7 +329,9 @@ export class RpcDataSource extends AbstractDataSource< this.#balanceFetcher.setOnBalanceUpdate(async (result) => { try { await this.#handleBalanceUpdate(result); - } catch (error) {} + } catch (error) { + log('Balance update handler failed', { error }); + } }); // Initialize TokenDetector with polling interval. The TokensApiClient is @@ -341,7 +354,9 @@ export class RpcDataSource extends AbstractDataSource< this.#tokenDetector.setOnDetectionUpdate((result) => { try { this.#handleDetectionUpdate(result); - } catch (error) {} + } catch (error) { + log('Detection update handler failed', { error }); + } }); this.#subscribeToNetworkController(); @@ -363,11 +378,16 @@ export class RpcDataSource extends AbstractDataSource< */ #convertToHumanReadable(rawBalance: string, decimals: number): string { if (!Number.isFinite(decimals) || decimals < 0) { + log('Invalid decimals — defaulting balance to "0"', { + rawBalance, + decimals, + }); return '0'; } const rawAmount = new BigNumberJS(rawBalance); if (!rawAmount.isFinite()) { + log('Invalid raw balance — defaulting to "0"', { rawBalance, decimals }); return '0'; } @@ -529,8 +549,15 @@ export class RpcDataSource extends AbstractDataSource< dataTypes: ['balance'], }; + log('Balance update response', { + accountId: result.accountId, + newBalanceCount: Object.keys(newBalances).length, + }); + for (const subscription of this.#activeSubscriptions.values()) { - subscription.onAssetsUpdate(response, request)?.catch((error) => {}); + subscription.onAssetsUpdate(response, request)?.catch((error) => { + log('Failed to update assets', { error }); + }); } } @@ -540,6 +567,10 @@ export class RpcDataSource extends AbstractDataSource< * @param result - The token detection result. */ #handleDetectionUpdate(result: TokenDetectionResult): void { + log('Detected new tokens', { + count: result.detectedAssets.length, + }); + // Build new metadata from detected assets const newMetadata: Record = {}; if (result.detectedAssets.length > 0) { @@ -601,7 +632,9 @@ export class RpcDataSource extends AbstractDataSource< }; for (const subscription of this.#activeSubscriptions.values()) { - subscription.onAssetsUpdate(response, request)?.catch((error) => {}); + subscription.onAssetsUpdate(response, request)?.catch((error) => { + log('Failed to update detected assets', { error }); + }); } } @@ -609,6 +642,7 @@ export class RpcDataSource extends AbstractDataSource< this.#messenger.subscribe( 'NetworkController:stateChange', (networkState: NetworkState) => { + log('NetworkController state changed'); this.#clearProviderCache(); this.#updateFromNetworkState(networkState); }, @@ -631,7 +665,9 @@ export class RpcDataSource extends AbstractDataSource< } const caipChainId = `eip155:${parseInt(hexChainId, 16)}` as ChainId; this.#refreshBalanceForChains([caipChainId], 'transactionConfirmed').catch( - (error) => {}, + (error) => { + log('Failed to refresh balance after transaction confirmed', { error }); + }, ); } @@ -695,18 +731,30 @@ export class RpcDataSource extends AbstractDataSource< await subscription.onAssetsUpdate(responseWithMode, request); appliedCount += 1; - } catch (error) {} + } catch (error) { + log('Failed to fetch balance after transaction', { + context, + chains: subscriptionChains, + error, + }); + } } if (appliedCount === 0 && context === 'transactionConfirmed') { + log('No RpcDataSource subscription covers chain after transaction', { + chainsToFetch, + }); } } #initializeFromNetworkController(): void { + log('Initializing from NetworkController'); try { const networkState = this.#messenger.call('NetworkController:getState'); this.#updateFromNetworkState(networkState); - } catch (error) {} + } catch (error) { + log('Failed to initialize from NetworkController', error); + } } /** @@ -754,6 +802,11 @@ export class RpcDataSource extends AbstractDataSource< } } + log('Network state updated', { + configuredChains: Object.keys(chainStatuses), + activeChains, + }); + // Check if chains changed const previousChains = [...this.#activeChains]; const previousSet = new Set(previousChains); @@ -797,6 +850,7 @@ export class RpcDataSource extends AbstractDataSource< return web3Provider; } catch (error) { + log('Failed to get provider for chain', { chainId, error }); return undefined; } } @@ -899,6 +953,7 @@ export class RpcDataSource extends AbstractDataSource< * @param interval - The polling interval in milliseconds. */ setBalancePollingInterval(interval: number): void { + log('Setting balance polling interval', { interval }); this.#balanceFetcher.setIntervalLength(interval); } @@ -917,6 +972,7 @@ export class RpcDataSource extends AbstractDataSource< * @param interval - The polling interval in milliseconds. */ setDetectionPollingInterval(interval: number): void { + log('Setting detection polling interval', { interval }); this.#tokenDetector.setIntervalLength(interval); } @@ -931,6 +987,7 @@ export class RpcDataSource extends AbstractDataSource< async fetch(request: DataRequest): Promise { if (!this.#isOnboarded()) { + log('Skipping fetch - onboarding not complete'); return {}; } @@ -940,7 +997,14 @@ export class RpcDataSource extends AbstractDataSource< this.#activeChains.includes(chainId), ); + log('Fetch requested', { + accounts: request.accountsWithSupportedChains.map((a) => a.account.id), + requestedChains: request.chainIds, + chainsToFetch, + }); + if (chainsToFetch.length === 0) { + log('No active chains to fetch'); return response; } @@ -1066,6 +1130,8 @@ export class RpcDataSource extends AbstractDataSource< }; } } catch (error) { + log('Failed to fetch balance', { address, chainId, error }); + if (!assetsBalance[accountId]) { assetsBalance[accountId] = {}; } @@ -1098,11 +1164,22 @@ export class RpcDataSource extends AbstractDataSource< } if (failedChains.length > 0) { + log('Fetch PARTIAL - some chains failed', { + successChains: chainsToFetch.filter( + (chain) => !failedChains.includes(chain), + ), + failedChains, + }); + response.errors = {}; for (const chainId of failedChains) { response.errors[chainId] = 'RPC fetch failed'; } } else { + log('Fetch SUCCESS', { + chains: chainsToFetch, + accountCount: Object.keys(assetsBalance).length, + }); } response.assetsBalance = assetsBalance; @@ -1135,6 +1212,8 @@ export class RpcDataSource extends AbstractDataSource< const hexChainId = caipChainIdToHex(chainId); const { address, id: accountId } = account; + log('Running token detection', { chainId, accountId }); + try { const result = await this.#tokenDetector.detectTokens( hexChainId, @@ -1147,9 +1226,16 @@ export class RpcDataSource extends AbstractDataSource< ); if (result.detectedAssets.length === 0) { + log('No new tokens detected'); return {}; } + log('Detected new tokens', { + count: result.detectedAssets.length, + chainId, + accountId, + }); + // Convert detected assets to DataResponse format const balances: Record = {}; const assetsInfo: Record = {}; @@ -1201,6 +1287,7 @@ export class RpcDataSource extends AbstractDataSource< return response; } catch (error) { + log('Token detection failed', { chainId, accountId, error }); return {}; } } @@ -1219,6 +1306,11 @@ export class RpcDataSource extends AbstractDataSource< let successfullyHandledChains: ChainId[] = []; + log('Middleware fetching', { + chains: supportedChains, + accounts: request.accountsWithSupportedChains.map((a) => a.account.id), + }); + const response = await this.fetch({ ...request, chainIds: supportedChains, @@ -1276,6 +1368,7 @@ export class RpcDataSource extends AbstractDataSource< */ async subscribe(subscriptionRequest: SubscriptionRequest): Promise { if (!this.#isOnboarded()) { + log('Skipping subscribe - onboarding not complete'); return; } @@ -1290,7 +1383,16 @@ export class RpcDataSource extends AbstractDataSource< ) : request.chainIds; + log('Subscribe requested', { + subscriptionId, + isUpdate, + accounts: request.accountsWithSupportedChains.map((a) => a.account.id), + chainsToSubscribe, + activeChainsFallback: this.#activeChains.length === 0, + }); + if (chainsToSubscribe.length === 0) { + log('No active chains to subscribe'); return; } @@ -1298,6 +1400,11 @@ export class RpcDataSource extends AbstractDataSource< if (isUpdate) { const existing = this.#activeSubscriptions.get(subscriptionId); if (existing) { + log('Updating existing subscription - restarting polling', { + subscriptionId, + existingChains: existing.chains, + newChains: chainsToSubscribe, + }); // Don't return early - continue to unsubscribe and restart polling } } @@ -1366,6 +1473,13 @@ export class RpcDataSource extends AbstractDataSource< accounts, onAssetsUpdate: subscriptionRequest.onAssetsUpdate, }); + + log('Subscription SUCCESS', { + subscriptionId, + chains: chainsToSubscribe, + balancePollingCount: balancePollingTokens.length, + detectionPollingCount: detectionPollingTokens.length, + }); } /** @@ -1387,6 +1501,7 @@ export class RpcDataSource extends AbstractDataSource< } this.#activeSubscriptions.delete(subscriptionId); + log('Unsubscribed and stopped polling', { subscriptionId }); } } @@ -1401,6 +1516,7 @@ export class RpcDataSource extends AbstractDataSource< const state = this.#messenger.call('AssetsController:getState'); return state.assetsInfo ?? {}; } catch (error) { + log('Failed to get existing assets metadata', { error }); return {}; } } @@ -1409,6 +1525,8 @@ export class RpcDataSource extends AbstractDataSource< * Destroy the data source and clean up resources. */ destroy(): void { + log('Destroying RpcDataSource'); + this.#unsubscribeTransactionConfirmed?.(); // Stop all polling diff --git a/packages/assets-controller/src/data-sources/SnapDataSource.ts b/packages/assets-controller/src/data-sources/SnapDataSource.ts index 01748f85152..6e4dd8b6a4b 100644 --- a/packages/assets-controller/src/data-sources/SnapDataSource.ts +++ b/packages/assets-controller/src/data-sources/SnapDataSource.ts @@ -18,6 +18,7 @@ import { parseCaipAssetType } from '@metamask/utils'; import type { Json, JsonRpcRequest } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; +import { projectLogger, createModuleLogger } from '../logger.js'; import type { AssetBalance, ChainId, @@ -60,6 +61,8 @@ export type AccountsControllerAccountBalancesUpdatedEvent = { payload: [AccountBalancesUpdatedEventPayload]; }; +const log = createModuleLogger(projectLogger, 'SnapDataSource'); + // ============================================================================ // CONSTANTS // ============================================================================ @@ -289,6 +292,10 @@ export class SnapDataSource extends AbstractDataSource< try { chainId = extractChainFromAssetId(assetId); } catch (error) { + log('Skipping snap balance for malformed asset ID', { + assetId, + error, + }); continue; } if (this.#isChainSupportedBySnap(chainId)) { @@ -340,6 +347,7 @@ export class SnapDataSource extends AbstractDataSource< 'SnapController:getRunnableSnaps', ) as Snap[]; } catch (error) { + log('Failed to get runnable snaps', error); return []; } } @@ -360,6 +368,7 @@ export class SnapDataSource extends AbstractDataSource< snapId, ) as SubjectPermissions; } catch (error) { + log('Failed to get permissions for snap', { snapId, error }); return undefined; } } @@ -421,6 +430,7 @@ export class SnapDataSource extends AbstractDataSource< // AssetsController not ready yet - expected during initialization } } catch (error) { + log('Keyring snap discovery failed', { error }); this.state.chainToSnap = {}; try { const previous = [...this.state.activeChains]; @@ -578,6 +588,7 @@ export class SnapDataSource extends AbstractDataSource< (chainId) => !failedChains.has(chainId), ); } catch (error) { + log('Middleware fetch failed', { error }); successfullyHandledChains = []; } @@ -631,7 +642,9 @@ export class SnapDataSource extends AbstractDataSource< if (Object.keys(fetchResponse.assetsBalance ?? {}).length > 0) { await this.#onAssetsUpdate(fetchResponse); } - } catch (error) {} + } catch (error) { + log('Initial fetch failed', { subscriptionId, error }); + } } // ============================================================================ @@ -683,7 +696,9 @@ export class SnapDataSource extends AbstractDataSource< 'AccountsController:accountBalancesUpdated', this.#handleSnapBalancesUpdatedBound, ); - } catch (error) {} + } catch (error) { + log('Failed to unsubscribe from snap keyring events', { error }); + } // Unsubscribe from permission changes try { @@ -691,7 +706,9 @@ export class SnapDataSource extends AbstractDataSource< 'PermissionController:stateChange', this.#handlePermissionStateChangeBound, ); - } catch (error) {} + } catch (error) { + log('Failed to unsubscribe from permission changes', { error }); + } // Clear keyring client cache this.#keyringClientCache.clear(); diff --git a/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts b/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts index ecb745ccaf4..a3eccda40f0 100644 --- a/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts +++ b/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts @@ -11,6 +11,7 @@ import { import type { Hex } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; +import { projectLogger, createModuleLogger } from '../logger.js'; import type { AccountId, ChainId, @@ -47,6 +48,8 @@ const STAKED_ETH_METADATA: AssetMetadata = { decimals: 18, }; +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + /** Optional configuration for StakedBalanceDataSource. */ export type StakedBalanceDataSourceConfig = { /** Whether staked balance fetching is enabled (default: true). */ @@ -177,6 +180,11 @@ export class StakedBalanceDataSource extends AbstractDataSource< this.#enabled = options.enabled !== false; this.#supportedChainIds = getSupportedStakingChainIds() as ChainId[]; + log('Initializing StakedBalanceDataSource', { + enabled: this.#enabled, + pollInterval: this.#pollInterval, + }); + // Create StakedBalanceFetcher with provider getter this.#stakedBalanceFetcher = new StakedBalanceFetcher({ pollingInterval: this.#pollInterval, @@ -188,7 +196,9 @@ export class StakedBalanceDataSource extends AbstractDataSource< this.#stakedBalanceFetcher.setOnStakedBalanceUpdate((result) => { try { this.#handleStakedBalanceUpdate(result); - } catch (error) {} + } catch (error) { + log('Staked balance update handler failed', { error }); + } }); this.#messenger.subscribe( @@ -216,6 +226,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< */ #onNetworkStateChange(): void { this.#providerCache.clear(); + log('Provider cache cleared after network state change'); } /** @@ -290,9 +301,9 @@ export class StakedBalanceDataSource extends AbstractDataSource< const caipChainId = `eip155:${parseInt(hexChainId, 16)}` as ChainId; const toRefresh = this.#getToRefreshForChains([caipChainId]); if (toRefresh.length > 0) { - this.#refreshStakedBalanceAfterTransaction(toRefresh).catch( - (error) => {}, - ); + this.#refreshStakedBalanceAfterTransaction(toRefresh).catch((error) => { + log('Failed to refresh staked balance after transaction', { error }); + }); } } @@ -395,7 +406,13 @@ export class StakedBalanceDataSource extends AbstractDataSource< ...existing, [assetId]: { amount: result.amount }, }; - } catch (error) {} + } catch (error) { + log('Failed to fetch staked balance in transaction refresh', { + chainId, + accountId: account.id, + error, + }); + } } const chainIds = [...new Set(toRefresh.map(({ chainId }) => chainId))]; @@ -433,7 +450,11 @@ export class StakedBalanceDataSource extends AbstractDataSource< for (const subscription of this.#activeSubscriptions.values()) { subscription .onAssetsUpdate(response, request) - ?.catch((error: unknown) => {}); + ?.catch((error: unknown) => { + log('Failed to report staked balance update after transaction', { + error, + }); + }); } } } @@ -452,6 +473,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< state?.enabledNetworkMap ?? {}, ); } catch (error) { + log('Failed to get NetworkEnablementController state', { error }); this.#initializeActiveChainsFromEnabledMap({}); } } @@ -554,6 +576,7 @@ export class StakedBalanceDataSource extends AbstractDataSource< this.#providerCache.set(hexChainId, provider); return provider; } catch (error) { + log('Failed to get provider for chain', { hexChainId, error }); return undefined; } } @@ -593,10 +616,18 @@ export class StakedBalanceDataSource extends AbstractDataSource< dataTypes: ['balance'], }; + log('Staked balance update', { + accountId: result.accountId, + chainId: caipChainId, + amount: result.balance.amount, + }); + for (const subscription of this.#activeSubscriptions.values()) { subscription .onAssetsUpdate(response, request) - ?.catch((error: unknown) => {}); + ?.catch((error: unknown) => { + log('Failed to report staked balance update', { error }); + }); } } @@ -652,7 +683,13 @@ export class StakedBalanceDataSource extends AbstractDataSource< balances[account.id] ??= {}; const assetId = stakedAssetId(chainId, contractAddress); balances[account.id][assetId] = { amount: result.amount }; - } catch (error) {} + } catch (error) { + log('Failed to fetch staked balance', { + chainId, + accountId: account.id, + error, + }); + } } } @@ -714,7 +751,9 @@ export class StakedBalanceDataSource extends AbstractDataSource< }; } } - } catch (error) {} + } catch (error) { + log('Middleware fetch failed', { error }); + } // Pass all chains through (staked balance doesn't claim chains) return next(context); @@ -735,7 +774,14 @@ export class StakedBalanceDataSource extends AbstractDataSource< activeChainsSet.has(chainId), ); + log('Subscribe requested', { + subscriptionId, + isUpdate, + chainsToSubscribe, + }); + if (chainsToSubscribe.length === 0) { + log('No staking chains to subscribe'); return; } @@ -743,6 +789,9 @@ export class StakedBalanceDataSource extends AbstractDataSource< if (isUpdate) { const existing = this.#activeSubscriptions.get(subscriptionId); if (existing) { + log('Updating existing subscription - restarting polling', { + subscriptionId, + }); } } @@ -821,9 +870,19 @@ export class StakedBalanceDataSource extends AbstractDataSource< ) { subscriptionRequest .onAssetsUpdate?.(initialResponse) - ?.catch((error) => {}); + ?.catch((error) => { + log('Initial staked balance update failed', { error }); + }); } - } catch (error) {} + } catch (error) { + log('Initial staked balance fetch failed', { error }); + } + + log('Subscription SUCCESS', { + subscriptionId, + chains: chainsToSubscribe, + pollingCount: pollingTokens.length, + }); } /** diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.ts b/packages/assets-controller/src/data-sources/TokenDataSource.ts index dfce600c66d..fffed2b9461 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.ts @@ -9,6 +9,7 @@ import { KnownCaipNamespace, parseCaipAssetType } from '@metamask/utils'; import type { CaipAssetType } from '@metamask/utils'; import type { AssetsControllerMessenger } from '../AssetsController.js'; +import { projectLogger, createModuleLogger } from '../logger.js'; import { forDataTypes } from '../types.js'; import type { Caip19AssetId, @@ -29,6 +30,8 @@ import { const CONTROLLER_NAME = 'TokenDataSource'; const DEFAULT_FETCH_TIMEOUT_MS = 15_000; +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + /** Max asset IDs per tokens API request. */ const TOKENS_API_BATCH_SIZE = 50; @@ -215,6 +218,7 @@ export class TokenDataSource { return new Set(allNetworks); } catch (error) { + log('Failed to fetch supported networks', { error }); return new Set(); } } @@ -234,6 +238,7 @@ export class TokenDataSource { this.#fetchTimeoutMs, ); } catch (error) { + log('Failed to fetch suggested occurrence floors', { error }); return {}; } } @@ -336,6 +341,7 @@ export class TokenDataSource { } } } catch (error) { + log('Blockaid bulk token scan failed; keeping all tokens', { error }); return assets; } @@ -475,9 +481,13 @@ export class TokenDataSource { } } } + log('Filtered low-occurrence websocket assets', { + assetIds: [...spamAssetIds], + }); } } catch (error) { // Fail open — keep all assets when occurrences cannot be fetched. + log('Failed to fetch occurrences for websocket update', { error }); } return next(ctx); @@ -758,7 +768,9 @@ export class TokenDataSource { } } } - } catch (error) {} + } catch (error) { + log('Failed to fetch metadata', { error }); + } // Call next() at the end to continue the middleware chain return next(ctx); diff --git a/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts b/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts index c3178c7357f..13cf367424d 100644 --- a/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts +++ b/packages/assets-controller/src/data-sources/evm-rpc-services/services/TokenDetector.ts @@ -1,6 +1,7 @@ import { StaticIntervalPollingControllerOnly } from '@metamask/polling-controller'; import type { CaipAssetType } from '@metamask/utils'; +import { projectLogger, createModuleLogger } from '../../../logger.js'; import type { MulticallClient } from '../clients/index.js'; import type { TokensApiClient } from '../clients/TokensApiClient.js'; import type { @@ -17,6 +18,8 @@ import type { } from '../types/index.js'; import { reduceInBatchesSerially } from '../utils/index.js'; +const log = createModuleLogger(projectLogger, 'TokenDetector'); + const DEFAULT_DETECTION_INTERVAL = 180_000; // 3 minutes export type TokenDetectorConfig = { @@ -110,8 +113,8 @@ export class TokenDetector extends StaticIntervalPollingControllerOnly 0) { this.#onDetectionUpdate(result); } - } catch { - // Silently handle errors + } catch (error) { + log('Token detection poll failed', { chainId: input.chainId, error }); } } @@ -223,8 +226,13 @@ export class TokenDetector extends StaticIntervalPollingControllerOnly AccountId | undefined; removeCustomAsset: (accountId: AccountId, assetId: Caip19AssetId) => void; @@ -95,6 +98,10 @@ export class CustomAssetGraduationMiddleware { if (!customSet.has(normalizedAssetId)) { continue; } + log('Graduating custom asset', { + accountId, + assetId: normalizedAssetId, + }); this.#removeCustomAsset(accountId, normalizedAssetId); } diff --git a/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts b/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts index cbcd14addfb..fb164a89126 100644 --- a/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts +++ b/packages/assets-controller/src/middlewares/RpcFallbackMiddleware.ts @@ -1,3 +1,4 @@ +import { projectLogger, createModuleLogger } from '../logger.js'; import { forDataTypes } from '../types.js'; import type { AssetsDataSource, @@ -9,6 +10,8 @@ import { mergeDataResponses } from './ParallelMiddleware.js'; const CONTROLLER_NAME = 'RpcFallbackMiddleware'; +const log = createModuleLogger(projectLogger, CONTROLLER_NAME); + export type RpcFallbackMiddlewareOptions = { /** The RPC data source to use as a fallback. */ rpcDataSource: AssetsDataSource; @@ -46,6 +49,10 @@ export class RpcFallbackMiddleware { return next(ctx); } + log('Retrying failed chains on RPC', { + chains: [...erroredChains], + }); + const filteredRequest = { ...ctx.request, chainIds: ctx.request.chainIds.filter((id) => erroredChains.has(id)), diff --git a/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts b/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts index 86968fb39ee..5546a7faefe 100644 --- a/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts +++ b/packages/assets-controller/src/migrations/healAssetsInfoMetadata.ts @@ -9,6 +9,7 @@ import { } from '@metamask/utils'; import { cloneDeep } from 'lodash'; +import { createModuleLogger, projectLogger } from '../logger.js'; import type { AccountId, Caip19AssetId, @@ -92,6 +93,8 @@ export type AssetsInfoHealingPatch = { customAssets: Record; }; +const log = createModuleLogger(projectLogger, 'tempHealAssetsInfoMetadata'); + export type TempHealAssetsInfoMetadataOptions = { /** Current `AssetsController` state the healing patch is computed against. */ state: AssetsControllerStateInternal; @@ -120,6 +123,7 @@ export function tempHealAssetsInfoMetadata({ captureException, }: TempHealAssetsInfoMetadataOptions): AssetsControllerStateInternal { const reportError = (error: unknown): void => { + log('Failed to heal assetsInfo metadata', error); captureException?.( new Error( `AssetsController: temporary assetsInfo metadata healing failed: ${getErrorMessage( @@ -144,6 +148,11 @@ export function tempHealAssetsInfoMetadata({ const nextState = cloneDeep(state); applyHealingPatch(nextState, patch); + log('Healed wiped assetsInfo metadata for niche-chain tokens', { + healedAssetsInfoCount: Object.keys(patch.assetsInfo).length, + healedCustomAssetsAccounts: Object.keys(patch.customAssets).length, + }); + return nextState; } catch (error) { reportError(error);