Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `assetsInfo` or `assetsBalance` payloads, even when prior stored metadata already includes an image ([#9099](https://github.com/MetaMask/core/pull/9099))

## [9.0.1]

### Changed
Expand Down
161 changes: 161 additions & 0 deletions packages/assets-controller/src/AssetsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,167 @@ describe('AssetsController', () => {
});
});

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;
const initialState: Partial<AssetsControllerState> = {
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 }) => {
// Reconciliation occurs when assetsInfo includes the asset.
await controller.handleAssetsUpdate(
{
assetsInfo: {
[imxAssetId]: {
type: 'erc20',
symbol: 'IMX',
name: 'Immutable X',
decimals: 18,
image: 'https://example.com/imx.png',
},
},
},
'TestSource',
);

expect(controller.state.assetsInfo[imxAssetId]).toStrictEqual({
type: 'native',
symbol: 'IMX',
name: 'Immutable X',
decimals: 18,
image: 'https://example.com/imx.png',
});
},
);
});

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<AssetsControllerState> = {
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<AssetsControllerState> = {
assetsInfo: {
[MOCK_ASSET_ID]: {
type: 'erc20',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
},
};

await withController(
{ state: initialState, isBasicFunctionality: () => false },
async ({ controller }) => {
await controller.handleAssetsUpdate(
{
assetsInfo: {
[MOCK_ASSET_ID]: {
type: 'erc20',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
},
},
'TestSource',
);

expect(controller.state.assetsInfo[MOCK_ASSET_ID]?.type).toBe(
'erc20',
);
},
);
});

it('reconciles type when assetsInfo response uses a non-checksummed asset ID', async () => {
const initialState: Partial<AssetsControllerState> = {
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],
).toBeUndefined();
},
);
});

it('updates state with metadata', async () => {
await withController(async ({ controller }) => {
await controller.handleAssetsUpdate(
Expand Down
28 changes: 28 additions & 0 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,34 @@ 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<Caip19AssetId>(
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) {
continue;
}
const correctType = this.#getAssetType(assetId);
if (entry.type !== correctType) {
metadata[assetId] = { ...entry, type: correctType };
if (!changedMetadata.includes(assetId)) {
changedMetadata.push(assetId);
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

if (normalizedResponse.assetsBalance) {
for (const [accountId, accountBalances] of Object.entries(
normalizedResponse.assetsBalance,
Expand Down
Loading