From b3c59bb41f724d0bdb9de011389a895dba58f5bd Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Thu, 11 Jun 2026 20:32:08 +0100 Subject: [PATCH 01/10] test(TokenDataSource): add middleware test for refetching stale asset metadata This commit introduces a new test case to ensure that the middleware correctly refetches asset metadata when the stored type is stale, even if an image is present. The test verifies that a previously misclassified native asset is updated to its correct type, enhancing the robustness of the asset handling logic. --- .../src/data-sources/TokenDataSource.test.ts | 38 +++++++++++++++++++ .../src/data-sources/TokenDataSource.ts | 11 +++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts index c4de66e06f..24ef5aa7c5 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts @@ -331,6 +331,44 @@ describe('TokenDataSource', () => { expect(next).toHaveBeenCalledWith(context); }); + it('middleware refetches metadata when stored type is stale even if it has an image', async () => { + // Native asset previously mis-detected and stored as `erc20` with an image. + // getAssetType now classifies it as `native`, so the stale entry must be + // refetched (not skipped) so it self-heals to the correct type. + const { controller, apiClient } = setupController({ + messenger: createTestMessenger(), + supportedNetworks: ['eip155:1'], + assetsResponse: [createMockAssetResponse(MOCK_NATIVE_ASSET)], + }); + + const next = jest.fn().mockResolvedValue(undefined); + const context = createMiddlewareContext({ + response: { + detectedAssets: { + 'mock-account-id': [MOCK_NATIVE_ASSET], + }, + }, + getAssetsState: jest.fn().mockReturnValue({ + assetsInfo: { + [MOCK_NATIVE_ASSET]: { + type: 'erc20', + name: 'Mislabelled Native', + symbol: 'MIS', + decimals: 18, + image: 'https://state.com/icon.png', + }, + }, + }), + }); + + await controller.assetsMiddleware(context, next); + + expect(apiClient.tokens.fetchV3Assets).toHaveBeenCalledWith( + [MOCK_NATIVE_ASSET], + expect.anything(), + ); + }); + it('middleware fetches metadata for assets without image in existing metadata', async () => { const { controller, apiClient } = setupController({ messenger: createTestMessenger(), diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.ts b/packages/assets-controller/src/data-sources/TokenDataSource.ts index 836e26bc3e..9469650370 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.ts @@ -351,9 +351,16 @@ export class TokenDataSource { continue; } - // Skip if state already has metadata with image + // Skip if state already has metadata with image — unless its + // stored `type` no longer matches the authoritative type (e.g. a + // native that was previously mis-detected as erc20). A type + // mismatch forces a refetch so the stale entry self-heals to the + // correct type instead of being skipped forever. const existingMetadata = stateMetadata[assetId]; - if (existingMetadata?.image) { + if ( + existingMetadata?.image && + existingMetadata.type === this.#getAssetType(assetId) + ) { continue; } From a4b8d8cf6a6c87107bc301961e3bf6eff2e02601 Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Thu, 11 Jun 2026 23:22:18 +0100 Subject: [PATCH 02/10] revert: TokenDataSource change - did not work --- .../src/data-sources/TokenDataSource.test.ts | 38 ------------------- .../src/data-sources/TokenDataSource.ts | 11 +----- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts index 24ef5aa7c5..c4de66e06f 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.test.ts @@ -331,44 +331,6 @@ describe('TokenDataSource', () => { expect(next).toHaveBeenCalledWith(context); }); - it('middleware refetches metadata when stored type is stale even if it has an image', async () => { - // Native asset previously mis-detected and stored as `erc20` with an image. - // getAssetType now classifies it as `native`, so the stale entry must be - // refetched (not skipped) so it self-heals to the correct type. - const { controller, apiClient } = setupController({ - messenger: createTestMessenger(), - supportedNetworks: ['eip155:1'], - assetsResponse: [createMockAssetResponse(MOCK_NATIVE_ASSET)], - }); - - const next = jest.fn().mockResolvedValue(undefined); - const context = createMiddlewareContext({ - response: { - detectedAssets: { - 'mock-account-id': [MOCK_NATIVE_ASSET], - }, - }, - getAssetsState: jest.fn().mockReturnValue({ - assetsInfo: { - [MOCK_NATIVE_ASSET]: { - type: 'erc20', - name: 'Mislabelled Native', - symbol: 'MIS', - decimals: 18, - image: 'https://state.com/icon.png', - }, - }, - }), - }); - - await controller.assetsMiddleware(context, next); - - expect(apiClient.tokens.fetchV3Assets).toHaveBeenCalledWith( - [MOCK_NATIVE_ASSET], - expect.anything(), - ); - }); - it('middleware fetches metadata for assets without image in existing metadata', async () => { const { controller, apiClient } = setupController({ messenger: createTestMessenger(), diff --git a/packages/assets-controller/src/data-sources/TokenDataSource.ts b/packages/assets-controller/src/data-sources/TokenDataSource.ts index 9469650370..836e26bc3e 100644 --- a/packages/assets-controller/src/data-sources/TokenDataSource.ts +++ b/packages/assets-controller/src/data-sources/TokenDataSource.ts @@ -351,16 +351,9 @@ export class TokenDataSource { continue; } - // Skip if state already has metadata with image — unless its - // stored `type` no longer matches the authoritative type (e.g. a - // native that was previously mis-detected as erc20). A type - // mismatch forces a refetch so the stale entry self-heals to the - // correct type instead of being skipped forever. + // Skip if state already has metadata with image const existingMetadata = stateMetadata[assetId]; - if ( - existingMetadata?.image && - existingMetadata.type === this.#getAssetType(assetId) - ) { + if (existingMetadata?.image) { continue; } From 7480667c98dcb749c2501bc88907c270a41d859d Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Thu, 11 Jun 2026 23:28:00 +0100 Subject: [PATCH 03/10] fix: add reconciliation/self-healing for asset info during update --- .../src/AssetsController.test.ts | 74 +++++++++++++++++++ .../assets-controller/src/AssetsController.ts | 19 +++++ 2 files changed, 93 insertions(+) diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index 9865f69dbc..f5bdf29406 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -1365,6 +1365,80 @@ describe('AssetsController', () => { }); }); + it('reconciles a stale native type stored as erc20 when a response touches the asset', async () => { + // Native (zero-address ERC-20) mis-stored as erc20 by an older version. + const imxAssetId = + 'eip155:13371/erc20:0x0000000000000000000000000000000000000000' as Caip19AssetId; + const initialState: Partial = { + assetsInfo: { + [imxAssetId]: { + type: 'erc20', + symbol: 'IMX', + name: 'Immutable X', + decimals: 18, + image: 'https://example.com/imx.png', + }, + }, + }; + + await withController( + { state: initialState, isBasicFunctionality: () => false }, + async ({ controller }) => { + // A balance report for the asset is enough to drive reconciliation — + // the metadata itself does not need to be re-emitted. + await controller.handleAssetsUpdate( + { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [imxAssetId]: { amount: '1000000000000000000' }, + }, + }, + }, + 'TestSource', + ); + + expect(controller.state.assetsInfo[imxAssetId]).toStrictEqual({ + type: 'native', + symbol: 'IMX', + name: 'Immutable X', + decimals: 18, + image: 'https://example.com/imx.png', + }); + }, + ); + }); + + it('leaves a genuine erc20 type untouched when a response touches it', async () => { + const initialState: Partial = { + assetsInfo: { + [MOCK_ASSET_ID]: { + type: 'erc20', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }, + }; + + await withController( + { state: initialState, isBasicFunctionality: () => false }, + async ({ controller }) => { + await controller.handleAssetsUpdate( + { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [MOCK_ASSET_ID]: { amount: '1000000' }, + }, + }, + }, + 'TestSource', + ); + + expect(controller.state.assetsInfo[MOCK_ASSET_ID]?.type).toBe('erc20'); + }, + ); + }); + it('updates state with metadata', async () => { await withController(async ({ controller }) => { await controller.handleAssetsUpdate( diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 77f95782f4..d138745f25 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -2188,6 +2188,25 @@ export class AssetsController extends BaseController< } } + // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native) + // Important to dynamically update as we start receiving new chain native token changes + const assetsInfoAssetIdsSet = new Set( + Object.keys(response.assetsInfo ?? {}) as Caip19AssetId[], + ); + for (const assetId of assetsInfoAssetIdsSet) { + const entry = metadata[assetId] as FungibleAssetMetadata | undefined; + if (!entry) { + continue; + } + const correctType = this.#getAssetType(assetId); + if (entry.type !== correctType) { + metadata[assetId] = { ...entry, type: correctType }; + if (!changedMetadata.includes(assetId)) { + changedMetadata.push(assetId); + } + } + } + if (normalizedResponse.assetsBalance) { for (const [accountId, accountBalances] of Object.entries( normalizedResponse.assetsBalance, From 3ac329fc8db429dba2ca311a7ba4e19da0e1dfd6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Jun 2026 22:46:37 +0000 Subject: [PATCH 04/10] fix(assets-controller): heal touched native asset types Co-authored-by: Prithpal Sooriya --- packages/assets-controller/CHANGELOG.md | 4 ++++ .../assets-controller/src/AssetsController.ts | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index b7e630d9f4..f3d6fd14be 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/transaction-controller` from `^67.1.0` to `^68.0.0` ([#9089](https://github.com/MetaMask/core/pull/9089)) +### Fixed + +- `AssetsController` now reclassifies stale native-token metadata entries when the asset appears in incoming balance or price payloads, even if metadata is not re-emitted by the data source ([#9099](https://github.com/MetaMask/core/pull/9099)) + ## [9.0.1] ### Changed diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index d138745f25..4148dca84b 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -2189,11 +2189,19 @@ export class AssetsController extends BaseController< } // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native) - // Important to dynamically update as we start receiving new chain native token changes - const assetsInfoAssetIdsSet = new Set( - Object.keys(response.assetsInfo ?? {}) as Caip19AssetId[], - ); - for (const assetId of assetsInfoAssetIdsSet) { + // for any asset ID touched by this response, not only assetsInfo. + const touchedAssetIds = new Set([ + ...(Object.keys(normalizedResponse.assetsInfo ?? {}) as Caip19AssetId[]), + ...(Object.keys(normalizedResponse.assetsPrice ?? {}) as Caip19AssetId[]), + ]); + for (const accountBalances of Object.values( + normalizedResponse.assetsBalance ?? {}, + )) { + for (const assetId of Object.keys(accountBalances) as Caip19AssetId[]) { + touchedAssetIds.add(assetId); + } + } + for (const assetId of touchedAssetIds) { const entry = metadata[assetId] as FungibleAssetMetadata | undefined; if (!entry) { continue; From b92ac2ebf8f4dc0f54e06309cbe5606d506f9c5c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Jun 2026 22:48:23 +0000 Subject: [PATCH 05/10] style(assets-controller): format touched test and controller Co-authored-by: Prithpal Sooriya --- .../assets-controller/src/AssetsController.test.ts | 4 +++- packages/assets-controller/src/AssetsController.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index f5bdf29406..c6f937b22d 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -1434,7 +1434,9 @@ describe('AssetsController', () => { 'TestSource', ); - expect(controller.state.assetsInfo[MOCK_ASSET_ID]?.type).toBe('erc20'); + expect(controller.state.assetsInfo[MOCK_ASSET_ID]?.type).toBe( + 'erc20', + ); }, ); }); diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 4148dca84b..189ed017fe 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -2191,13 +2191,19 @@ export class AssetsController extends BaseController< // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native) // for any asset ID touched by this response, not only assetsInfo. const touchedAssetIds = new Set([ - ...(Object.keys(normalizedResponse.assetsInfo ?? {}) as Caip19AssetId[]), - ...(Object.keys(normalizedResponse.assetsPrice ?? {}) as Caip19AssetId[]), + ...(Object.keys( + normalizedResponse.assetsInfo ?? {}, + ) as Caip19AssetId[]), + ...(Object.keys( + normalizedResponse.assetsPrice ?? {}, + ) as Caip19AssetId[]), ]); for (const accountBalances of Object.values( normalizedResponse.assetsBalance ?? {}, )) { - for (const assetId of Object.keys(accountBalances) as Caip19AssetId[]) { + for (const assetId of Object.keys( + accountBalances, + ) as Caip19AssetId[]) { touchedAssetIds.add(assetId); } } From 23a5b2cab3e49309b93481e25f6c083a72a42c4e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Jun 2026 23:04:20 +0000 Subject: [PATCH 06/10] test(assets-controller): align reconciliation to assetsInfo updates Co-authored-by: Prithpal Sooriya --- packages/assets-controller/CHANGELOG.md | 2 +- .../src/AssetsController.test.ts | 26 +++++++++++------- .../assets-controller/src/AssetsController.ts | 27 +++++-------------- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index f3d6fd14be..81980ee766 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `AssetsController` now reclassifies stale native-token metadata entries when the asset appears in incoming balance or price payloads, even if metadata is not re-emitted by the data source ([#9099](https://github.com/MetaMask/core/pull/9099)) +- `AssetsController` now reclassifies stale native-token metadata entries during `assetsInfo` updates, even when prior stored metadata already includes an image ([#9099](https://github.com/MetaMask/core/pull/9099)) ## [9.0.1] diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index c6f937b22d..e524067f97 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -1365,7 +1365,7 @@ describe('AssetsController', () => { }); }); - it('reconciles a stale native type stored as erc20 when a response touches the asset', async () => { + it('reconciles a stale native type stored as erc20 when assetsInfo includes the asset', async () => { // Native (zero-address ERC-20) mis-stored as erc20 by an older version. const imxAssetId = 'eip155:13371/erc20:0x0000000000000000000000000000000000000000' as Caip19AssetId; @@ -1384,13 +1384,16 @@ describe('AssetsController', () => { await withController( { state: initialState, isBasicFunctionality: () => false }, async ({ controller }) => { - // A balance report for the asset is enough to drive reconciliation — - // the metadata itself does not need to be re-emitted. + // Reconciliation occurs when assetsInfo includes the asset. await controller.handleAssetsUpdate( { - assetsBalance: { - [MOCK_ACCOUNT_ID]: { - [imxAssetId]: { amount: '1000000000000000000' }, + assetsInfo: { + [imxAssetId]: { + type: 'erc20', + symbol: 'IMX', + name: 'Immutable X', + decimals: 18, + image: 'https://example.com/imx.png', }, }, }, @@ -1408,7 +1411,7 @@ describe('AssetsController', () => { ); }); - it('leaves a genuine erc20 type untouched when a response touches it', async () => { + it('leaves a genuine erc20 type untouched when assetsInfo includes it', async () => { const initialState: Partial = { assetsInfo: { [MOCK_ASSET_ID]: { @@ -1425,9 +1428,12 @@ describe('AssetsController', () => { async ({ controller }) => { await controller.handleAssetsUpdate( { - assetsBalance: { - [MOCK_ACCOUNT_ID]: { - [MOCK_ASSET_ID]: { amount: '1000000' }, + assetsInfo: { + [MOCK_ASSET_ID]: { + type: 'erc20', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, }, }, }, diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 189ed017fe..777b7fd058 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -2188,26 +2188,13 @@ export class AssetsController extends BaseController< } } - // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native) - // for any asset ID touched by this response, not only assetsInfo. - const touchedAssetIds = new Set([ - ...(Object.keys( - normalizedResponse.assetsInfo ?? {}, - ) as Caip19AssetId[]), - ...(Object.keys( - normalizedResponse.assetsPrice ?? {}, - ) as Caip19AssetId[]), - ]); - for (const accountBalances of Object.values( - normalizedResponse.assetsBalance ?? {}, - )) { - for (const assetId of Object.keys( - accountBalances, - ) as Caip19AssetId[]) { - touchedAssetIds.add(assetId); - } - } - for (const assetId of touchedAssetIds) { + // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native). + // This reconciliation is only performed for asset IDs that arrive + // in assetsInfo updates. + const assetsInfoAssetIdsSet = new Set( + Object.keys(normalizedResponse.assetsInfo ?? {}) as Caip19AssetId[], + ); + for (const assetId of assetsInfoAssetIdsSet) { const entry = metadata[assetId] as FungibleAssetMetadata | undefined; if (!entry) { continue; From 4b066296b1fa2cdc60d68effd1ba63da9531c4d6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Jun 2026 23:11:50 +0000 Subject: [PATCH 07/10] test(assets-controller): cover reconciliation with normalized asset IDs Co-authored-by: Prithpal Sooriya --- .../src/AssetsController.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index e524067f97..261123a4a2 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -1447,6 +1447,47 @@ describe('AssetsController', () => { ); }); + it('reconciles type when assetsInfo response uses a non-checksummed asset ID', async () => { + const initialState: Partial = { + assetsInfo: { + [MOCK_ASSET_ID]: { + type: 'native', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }, + }; + + await withController( + { state: initialState, isBasicFunctionality: () => false }, + async ({ controller }) => { + await controller.handleAssetsUpdate( + { + assetsInfo: { + [MOCK_ASSET_ID_LOWERCASE]: { + type: 'native', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }, + }, + 'TestSource', + ); + + expect(controller.state.assetsInfo[MOCK_ASSET_ID]?.type).toBe( + 'erc20', + ); + expect( + controller.state.assetsInfo[ + MOCK_ASSET_ID_LOWERCASE as Caip19AssetId + ], + ).toBeUndefined(); + }, + ); + }); + it('updates state with metadata', async () => { await withController(async ({ controller }) => { await controller.handleAssetsUpdate( From 058d8756b3ebc478fc1c62e94e84afe3b608130a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Jun 2026 23:18:29 +0000 Subject: [PATCH 08/10] test(assets-controller): remove unnecessary asset ID assertion cast Co-authored-by: Prithpal Sooriya --- packages/assets-controller/src/AssetsController.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index 261123a4a2..d79b8f056e 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -1480,9 +1480,7 @@ describe('AssetsController', () => { 'erc20', ); expect( - controller.state.assetsInfo[ - MOCK_ASSET_ID_LOWERCASE as Caip19AssetId - ], + controller.state.assetsInfo[MOCK_ASSET_ID_LOWERCASE], ).toBeUndefined(); }, ); From 0eef15d37e44614eff3f47d20f7ac8836f76cbe3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 11 Jun 2026 23:20:38 +0000 Subject: [PATCH 09/10] fix(assets-controller): reconcile stale types on balance touches Co-authored-by: Prithpal Sooriya --- packages/assets-controller/CHANGELOG.md | 2 +- .../src/AssetsController.test.ts | 40 +++++++++++++++++++ .../assets-controller/src/AssetsController.ts | 16 ++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 81980ee766..88394a86c6 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `AssetsController` now reclassifies stale native-token metadata entries during `assetsInfo` updates, even when prior stored metadata already includes an image ([#9099](https://github.com/MetaMask/core/pull/9099)) +- `AssetsController` now reclassifies stale native-token metadata entries when the asset appears in incoming `assetsInfo` or `assetsBalance` payloads, even when prior stored metadata already includes an image ([#9099](https://github.com/MetaMask/core/pull/9099)) ## [9.0.1] diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index d79b8f056e..98a34fe1c9 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -1411,6 +1411,46 @@ describe('AssetsController', () => { ); }); + it('reconciles a stale native type stored as erc20 when assetsBalance includes the asset', async () => { + const imxAssetId = + 'eip155:13371/erc20:0x0000000000000000000000000000000000000000' as Caip19AssetId; + const initialState: Partial = { + assetsInfo: { + [imxAssetId]: { + type: 'erc20', + symbol: 'IMX', + name: 'Immutable X', + decimals: 18, + image: 'https://example.com/imx.png', + }, + }, + }; + + await withController( + { state: initialState, isBasicFunctionality: () => false }, + async ({ controller }) => { + await controller.handleAssetsUpdate( + { + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + [imxAssetId]: { amount: '1000000000000000000' }, + }, + }, + }, + 'TestSource', + ); + + expect(controller.state.assetsInfo[imxAssetId]).toStrictEqual({ + type: 'native', + symbol: 'IMX', + name: 'Immutable X', + decimals: 18, + image: 'https://example.com/imx.png', + }); + }, + ); + }); + it('leaves a genuine erc20 type untouched when assetsInfo includes it', async () => { const initialState: Partial = { assetsInfo: { diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 777b7fd058..d64025b1be 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -2188,12 +2188,20 @@ export class AssetsController extends BaseController< } } - // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native). - // This reconciliation is only performed for asset IDs that arrive - // in assetsInfo updates. - const assetsInfoAssetIdsSet = new Set( + // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native) + // for asset IDs touched by metadata or balance updates. + const assetsInfoAssetIdsSet = new Set( Object.keys(normalizedResponse.assetsInfo ?? {}) as Caip19AssetId[], ); + for (const accountBalances of Object.values( + normalizedResponse.assetsBalance ?? {}, + )) { + for (const assetId of Object.keys( + accountBalances, + ) as Caip19AssetId[]) { + assetsInfoAssetIdsSet.add(assetId); + } + } for (const assetId of assetsInfoAssetIdsSet) { const entry = metadata[assetId] as FungibleAssetMetadata | undefined; if (!entry) { From 249ad8659459a84bb67b6c9b970e92804ae1f3a2 Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Fri, 12 Jun 2026 09:51:51 +0100 Subject: [PATCH 10/10] refactor: cleanup --- packages/assets-controller/CHANGELOG.md | 2 +- .../assets-controller/src/AssetsController.ts | 20 +++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 88394a86c6..e95d3bd446 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `AssetsController` now reclassifies stale native-token metadata entries when the asset appears in incoming `assetsInfo` or `assetsBalance` payloads, even when prior stored metadata already includes an image ([#9099](https://github.com/MetaMask/core/pull/9099)) +- `AssetsController` reconciliate and self-heals stale assetInfo metadata types ([#9099](https://github.com/MetaMask/core/pull/9099)) ## [9.0.1] diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index d64025b1be..35e716ec4b 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -2189,19 +2189,13 @@ export class AssetsController extends BaseController< } // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native) - // for asset IDs touched by metadata or balance updates. - const assetsInfoAssetIdsSet = new Set( - Object.keys(normalizedResponse.assetsInfo ?? {}) as Caip19AssetId[], - ); - for (const accountBalances of Object.values( - normalizedResponse.assetsBalance ?? {}, - )) { - for (const assetId of Object.keys( - accountBalances, - ) as Caip19AssetId[]) { - assetsInfoAssetIdsSet.add(assetId); - } - } + // using IDs from new response assetInfo and assetBalance + const assetsInfoAssetIdsSet = new Set([ + ...Object.keys(normalizedResponse.assetsInfo ?? {}), + ...Object.values(normalizedResponse.assetsBalance ?? {}).flatMap( + (accountBalances) => Object.keys(accountBalances), + ), + ] as Caip19AssetId[]); for (const assetId of assetsInfoAssetIdsSet) { const entry = metadata[assetId] as FungibleAssetMetadata | undefined; if (!entry) {