Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f8b5dc1
feat: support adding non-evm tokens
juanmigdr Oct 31, 2025
a8cb379
Merge branch 'main' into feat/support-importing-non-evm-tokens
juanmigdr Oct 31, 2025
c5510a8
chore: updated comment
juanmigdr Oct 31, 2025
e4a6194
chore: updated changelog
juanmigdr Oct 31, 2025
7dbcd10
chore: updated function to accept multiple assets
juanmigdr Nov 3, 2025
45325ab
Merge branch 'main' into feat/support-importing-non-evm-tokens
juanmigdr Nov 3, 2025
28f9108
chore: update changelog
juanmigdr Nov 3, 2025
50030c9
Merge branch 'main' into feat/support-importing-non-evm-tokens
juanmigdr Nov 3, 2025
7887a12
fix: token is not removed from ignored tokens list when added back du…
juanmigdr Nov 3, 2025
9b1aac1
feat(multichain-account-service): add per-provider throttling for non…
mathieuartu Nov 3, 2025
df0e524
feat: updates Subscription Controller exports (#7037)
lwin-kyaw Nov 3, 2025
defacc6
Release/657.0.0 (#7041)
mathieuartu Nov 3, 2025
0f8363d
change: set defi controller to refresh only one account (#6944)
bergarces Nov 3, 2025
be8c8a0
feat: better detection of native multichain assets (#6983)
bergarces Nov 3, 2025
8427056
Merge branch 'main' into feat/support-importing-non-evm-tokens
juanmigdr Nov 3, 2025
c661e70
chore: added tests for ignore tokens fix
juanmigdr Nov 3, 2025
2f8d385
Merge branch 'main' into feat/support-importing-non-evm-tokens
juanmigdr Nov 3, 2025
4be2264
chore: added some more tests
juanmigdr Nov 3, 2025
51ad704
chore: prettier
juanmigdr Nov 3, 2025
66692cf
chore: increase coverage
juanmigdr Nov 3, 2025
6b72b0d
chore: fixed broken tests
juanmigdr Nov 3, 2025
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
5 changes: 5 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add `AssetsByAccountGroup` to list of exported types ([#6983](https://github.com/MetaMask/core/pull/6983))
- Added `addAssets` to allow adding multiple assets for non-EVM chains ([#7016](https://github.com/MetaMask/core/pull/7016))

### Changed

Expand All @@ -22,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `AccountTreeController:selectedAccountGroupChange` updates DeFi positions for the selected address
- `TransactionController:transactionConfirmed` only updates DeFi positions if the transaction is for the selected address

### Fixed

- Fixed token is not removed from ignored tokens list when added back due to case insensiteivity ([#7016](https://github.com/MetaMask/core/pull/7016))

## [86.0.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { deriveStateFromMetadata } from '@metamask/base-controller';
import type {
AccountAssetListUpdatedEventPayload,
CaipAssetType,
CaipAssetTypeOrId,
} from '@metamask/keyring-api';
import {
Expand Down Expand Up @@ -921,6 +922,317 @@ describe('MultichainAssetsController', () => {
});
});

describe('addAssets', () => {
it('should add a single asset to account assets list', async () => {
const { controller } = setupController({
state: {
accountsAssets: {
[mockSolanaAccount.id]: [
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
],
},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const assetToAdd =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr';

const result = await controller.addAssets(
[assetToAdd],
mockSolanaAccount.id,
);

expect(result).toStrictEqual([
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
assetToAdd,
]);
expect(
controller.state.accountsAssets[mockSolanaAccount.id],
).toStrictEqual([
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
assetToAdd,
]);
});

it('should not add duplicate assets', async () => {
const existingAsset =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
const { controller } = setupController({
state: {
accountsAssets: {
[mockSolanaAccount.id]: [existingAsset],
},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const result = await controller.addAssets(
[existingAsset],
mockSolanaAccount.id,
);

expect(result).toStrictEqual([existingAsset]);
expect(
controller.state.accountsAssets[mockSolanaAccount.id],
).toStrictEqual([existingAsset]);
});

it('should remove asset from ignored list when added', async () => {
const assetToAdd = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
const { controller } = setupController({
state: {
accountsAssets: {},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {
[mockSolanaAccount.id]: [assetToAdd],
},
} as MultichainAssetsControllerState,
});

const result = await controller.addAssets(
[assetToAdd],
mockSolanaAccount.id,
);

expect(result).toStrictEqual([assetToAdd]);
expect(
controller.state.accountsAssets[mockSolanaAccount.id],
).toStrictEqual([assetToAdd]);
expect(
controller.state.allIgnoredAssets[mockSolanaAccount.id],
).toBeUndefined();
});

it('should handle adding asset to account with no existing assets', async () => {
const assetToAdd = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
const { controller } = setupController({
state: {
accountsAssets: {},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const result = await controller.addAssets(
[assetToAdd],
mockSolanaAccount.id,
);

expect(result).toStrictEqual([assetToAdd]);
expect(
controller.state.accountsAssets[mockSolanaAccount.id],
).toStrictEqual([assetToAdd]);
});

it('should publish accountAssetListUpdated event when asset is added', async () => {
const { controller, messenger } = setupController({
state: {
accountsAssets: {},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const assetToAdd = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';

// Set up event listener to capture the published event
const eventListener = jest.fn();
messenger.subscribe(
'MultichainAssetsController:accountAssetListUpdated',
eventListener,
);

await controller.addAssets([assetToAdd], mockSolanaAccount.id);

expect(eventListener).toHaveBeenCalledWith({
assets: {
[mockSolanaAccount.id]: {
added: [assetToAdd],
removed: [],
},
},
});
});

it('should add multiple assets from the same chain', async () => {
const { controller } = setupController({
state: {
accountsAssets: {
[mockSolanaAccount.id]: [
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
],
},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const assetsToAdd: CaipAssetType[] = [
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr',
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:AnotherTokenAddress',
];

const result = await controller.addAssets(
assetsToAdd,
mockSolanaAccount.id,
);

expect(result).toStrictEqual([
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
...assetsToAdd,
]);
expect(
controller.state.accountsAssets[mockSolanaAccount.id],
).toStrictEqual([
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
...assetsToAdd,
]);
});

it('should throw error when assets are from different chains', async () => {
const { controller } = setupController({
state: {
accountsAssets: {},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const assetsFromDifferentChains: CaipAssetType[] = [
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
'eip155:1/slip44:60', // Ethereum asset
];

await expect(
controller.addAssets(assetsFromDifferentChains, mockSolanaAccount.id),
).rejects.toThrow(
'All assets must belong to the same chain. Found assets from chains: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1, eip155:1',
);
});

it('should return existing assets when empty array is provided', async () => {
const existingAsset =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
const { controller } = setupController({
state: {
accountsAssets: {
[mockSolanaAccount.id]: [existingAsset],
},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const result = await controller.addAssets([], mockSolanaAccount.id);

expect(result).toStrictEqual([existingAsset]);
});

it('should only publish event for newly added assets', async () => {
const existingAsset =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
const newAsset = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:NewToken';

const { controller, messenger } = setupController({
state: {
accountsAssets: {
[mockSolanaAccount.id]: [existingAsset],
},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const eventListener = jest.fn();
messenger.subscribe(
'MultichainAssetsController:accountAssetListUpdated',
eventListener,
);

await controller.addAssets(
[existingAsset, newAsset],
mockSolanaAccount.id,
);

expect(eventListener).toHaveBeenCalledWith({
assets: {
[mockSolanaAccount.id]: {
added: [newAsset], // Only the new asset should be in the event
removed: [],
},
},
});
});

it('should not publish event when no new assets are added', async () => {
const existingAsset =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';

const { controller, messenger } = setupController({
state: {
accountsAssets: {
[mockSolanaAccount.id]: [existingAsset],
},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {},
} as MultichainAssetsControllerState,
});

const eventListener = jest.fn();
messenger.subscribe(
'MultichainAssetsController:accountAssetListUpdated',
eventListener,
);

await controller.addAssets([existingAsset], mockSolanaAccount.id);

// Event should not be published since no new assets were added
expect(eventListener).not.toHaveBeenCalled();
});

it('should partially remove assets from ignored list when only some are added', async () => {
const ignoredAsset1 =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
const ignoredAsset2 =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token1';
const ignoredAsset3 =
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token2';

const { controller } = setupController({
state: {
accountsAssets: {},
assetsMetadata: mockGetMetadataReturnValue.assets,
allIgnoredAssets: {
[mockSolanaAccount.id]: [
ignoredAsset1,
ignoredAsset2,
ignoredAsset3,
],
},
} as MultichainAssetsControllerState,
});

// Only add two of the three ignored assets
await controller.addAssets(
[ignoredAsset1, ignoredAsset2],
mockSolanaAccount.id,
);

// Should have added the two assets
expect(
controller.state.accountsAssets[mockSolanaAccount.id],
).toStrictEqual([ignoredAsset1, ignoredAsset2]);

// Should have only the third asset remaining in ignored list
expect(
controller.state.allIgnoredAssets[mockSolanaAccount.id],
).toStrictEqual([ignoredAsset3]);
});
});

describe('asset detection with ignored assets', () => {
it('should filter out ignored assets when account assets are updated', async () => {
const ignoredAsset = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
Expand Down
Loading
Loading