Skip to content

Commit 740dfec

Browse files
authored
feat: support ignoring non-evm tokens (#6981)
## Explanation The feature to hide tokens is available for EVM assets but not for non-EVM assets. This task involves implementing a similar token hiding feature for non-EVM assets. This is crucial for improving user experience by allowing users to hide unwanted tokens, especially in light of spam and malicious token issues. The implementation should be prioritized to align with upcoming Solana campaigns. NOTE: - Changes are needed on Mobile and extension to support this <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References https://consensyssoftware.atlassian.net/browse/ASSETS-1425 <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces `allIgnoredAssets` with `ignoreAssets` action to hide non‑EVM assets per account and updates detection, selectors, and balance calculations to exclude ignored assets, including cleanup on account removal. > > - **MultichainAssetsController**: > - Add `allIgnoredAssets` to state and expose `ignoreAssets` action; register handler. > - Filter ignored assets in `accountAssetListUpdated` flow; remove ignored and active assets on account removal. > - Keep `assetsMetadata` handling unchanged; add metadata/state exposure for UI/persist. > - **Balances**: > - Update `calculateBalanceForAllWallets`, `calculateBalanceChangeForAllWallets`, and `calculateBalanceChangeForAccountGroup` signatures to accept `MultichainAssetsController` state. > - Exclude ignored non‑EVM assets from totals and change calculations. > - **Selectors** (`token-selectors.ts`): > - Include `allIgnoredAssets` in selector state; exclude ignored multichain assets from returned asset lists. > - **Tests**: > - Add coverage for ignoring/filtering assets, detection behavior, and cleanup on account removal; adjust balance tests to pass new controller state. > - **Changelog**: > - Note addition of `ignoreAssets` for non‑EVM chains. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f79eece. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8a31998 commit 740dfec

File tree

8 files changed

+394
-11
lines changed

8 files changed

+394
-11
lines changed

packages/assets-controllers/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- **BREAKING:** Added constructor argument `tokenPricesService` in `currencyRateController` ([#6863](https://github.com/MetaMask/core/pull/6863))
1313

1414
- Added `fetchExchangeRates` function to fetch exchange rates from price-api ([#6863](https://github.com/MetaMask/core/pull/6863))
15+
- Added `ignoreAssets` to allow ignoring assets for non-EVM chains ([#6981](https://github.com/MetaMask/core/pull/6981))
1516

1617
### Changed
1718

packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.test.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ describe('MultichainAssetsController', () => {
338338
expect(controller.state).toStrictEqual({
339339
accountsAssets: {},
340340
assetsMetadata: {},
341+
allIgnoredAssets: {},
341342
});
342343
});
343344

@@ -354,6 +355,7 @@ describe('MultichainAssetsController', () => {
354355
expect(controller.state).toStrictEqual({
355356
accountsAssets: {},
356357
assetsMetadata: {},
358+
allIgnoredAssets: {},
357359
});
358360
});
359361

@@ -386,6 +388,7 @@ describe('MultichainAssetsController', () => {
386388
[mockSolanaAccount.id]: mockHandleRequestOnAssetsLookupReturnValue,
387389
},
388390
assetsMetadata: mockGetMetadataReturnValue.assets,
391+
allIgnoredAssets: {},
389392
});
390393
});
391394

@@ -454,6 +457,7 @@ describe('MultichainAssetsController', () => {
454457
...mockGetMetadataResponse.assets,
455458
...mockGetMetadataReturnValue.assets,
456459
},
460+
allIgnoredAssets: {},
457461
});
458462
});
459463

@@ -510,6 +514,7 @@ describe('MultichainAssetsController', () => {
510514
assetsMetadata: {
511515
...mockGetMetadataReturnValue.assets,
512516
},
517+
allIgnoredAssets: {},
513518
});
514519
});
515520

@@ -544,6 +549,7 @@ describe('MultichainAssetsController', () => {
544549
},
545550

546551
assetsMetadata: mockGetMetadataReturnValue.assets,
552+
allIgnoredAssets: {},
547553
});
548554
// Remove an EVM account
549555
messenger.publish('AccountsController:accountRemoved', mockEthAccount.id);
@@ -556,6 +562,7 @@ describe('MultichainAssetsController', () => {
556562
},
557563

558564
assetsMetadata: mockGetMetadataReturnValue.assets,
565+
allIgnoredAssets: {},
559566
});
560567
});
561568

@@ -590,6 +597,7 @@ describe('MultichainAssetsController', () => {
590597
},
591598

592599
assetsMetadata: mockGetMetadataReturnValue.assets,
600+
allIgnoredAssets: {},
593601
});
594602
// Remove the added solana account
595603
messenger.publish(
@@ -603,6 +611,7 @@ describe('MultichainAssetsController', () => {
603611
accountsAssets: {},
604612

605613
assetsMetadata: mockGetMetadataReturnValue.assets,
614+
allIgnoredAssets: {},
606615
});
607616
});
608617

@@ -621,6 +630,7 @@ describe('MultichainAssetsController', () => {
621630
[mockSolanaAccountId1]: mockHandleRequestOnAssetsLookupReturnValue,
622631
},
623632
assetsMetadata: mockGetMetadataReturnValue.assets,
633+
allIgnoredAssets: {},
624634
} as MultichainAssetsControllerState,
625635
});
626636

@@ -710,6 +720,7 @@ describe('MultichainAssetsController', () => {
710720
[mockSolanaAccountId1]: mockHandleRequestOnAssetsLookupReturnValue,
711721
},
712722
assetsMetadata: mockGetMetadataReturnValue,
723+
allIgnoredAssets: {},
713724
} as MultichainAssetsControllerState,
714725
});
715726

@@ -756,6 +767,7 @@ describe('MultichainAssetsController', () => {
756767
],
757768
},
758769
assetsMetadata: mockGetMetadataReturnValue,
770+
allIgnoredAssets: {},
759771
} as MultichainAssetsControllerState,
760772
});
761773

@@ -831,6 +843,229 @@ describe('MultichainAssetsController', () => {
831843
});
832844
});
833845

846+
describe('ignoreAssets', () => {
847+
it('should ignore assets and remove them from active assets list', () => {
848+
const { controller } = setupController({
849+
state: {
850+
accountsAssets: {
851+
[mockSolanaAccount.id]: [
852+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
853+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr',
854+
],
855+
},
856+
assetsMetadata: mockGetMetadataReturnValue.assets,
857+
allIgnoredAssets: {},
858+
} as MultichainAssetsControllerState,
859+
});
860+
861+
const assetToIgnore =
862+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
863+
864+
controller.ignoreAssets([assetToIgnore], mockSolanaAccount.id);
865+
866+
expect(
867+
controller.state.accountsAssets[mockSolanaAccount.id],
868+
).toStrictEqual([
869+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr',
870+
]);
871+
expect(
872+
controller.state.allIgnoredAssets[mockSolanaAccount.id],
873+
).toStrictEqual([assetToIgnore]);
874+
});
875+
876+
it('should not add duplicate assets to ignored list', () => {
877+
const assetToIgnore =
878+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
879+
const { controller } = setupController({
880+
state: {
881+
accountsAssets: {
882+
[mockSolanaAccount.id]: [assetToIgnore],
883+
},
884+
assetsMetadata: mockGetMetadataReturnValue.assets,
885+
allIgnoredAssets: {
886+
[mockSolanaAccount.id]: [assetToIgnore],
887+
},
888+
} as MultichainAssetsControllerState,
889+
});
890+
891+
controller.ignoreAssets([assetToIgnore], mockSolanaAccount.id);
892+
893+
expect(
894+
controller.state.allIgnoredAssets[mockSolanaAccount.id],
895+
).toStrictEqual([assetToIgnore]);
896+
});
897+
898+
it('should handle ignoring assets for accounts with no existing ignored assets', () => {
899+
const { controller } = setupController({
900+
state: {
901+
accountsAssets: {
902+
[mockSolanaAccount.id]: [
903+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
904+
],
905+
},
906+
assetsMetadata: mockGetMetadataReturnValue.assets,
907+
allIgnoredAssets: {},
908+
} as MultichainAssetsControllerState,
909+
});
910+
911+
const assetToIgnore =
912+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
913+
controller.ignoreAssets([assetToIgnore], mockSolanaAccount.id);
914+
915+
expect(
916+
controller.state.allIgnoredAssets[mockSolanaAccount.id],
917+
).toStrictEqual([assetToIgnore]);
918+
expect(
919+
controller.state.accountsAssets[mockSolanaAccount.id],
920+
).toStrictEqual([]);
921+
});
922+
});
923+
924+
describe('asset detection with ignored assets', () => {
925+
it('should filter out ignored assets when account assets are updated', async () => {
926+
const ignoredAsset = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
927+
const activeAsset =
928+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr';
929+
930+
const { controller, messenger } = setupController({
931+
state: {
932+
accountsAssets: {},
933+
assetsMetadata: mockGetMetadataReturnValue.assets,
934+
allIgnoredAssets: {
935+
[mockSolanaAccount.id]: [ignoredAsset],
936+
},
937+
} as MultichainAssetsControllerState,
938+
});
939+
940+
// Simulate asset list update that includes both ignored and new assets
941+
messenger.publish('AccountsController:accountAssetListUpdated', {
942+
assets: {
943+
[mockSolanaAccount.id]: {
944+
added: [ignoredAsset, activeAsset],
945+
removed: [],
946+
},
947+
},
948+
});
949+
950+
// Wait for async processing
951+
await advanceTime({ clock: useFakeTimers(), duration: 0 });
952+
953+
// Only the non-ignored asset should be added
954+
expect(
955+
controller.state.accountsAssets[mockSolanaAccount.id],
956+
).toStrictEqual([activeAsset]);
957+
958+
// Ignored asset should remain in ignored list
959+
expect(
960+
controller.state.allIgnoredAssets[mockSolanaAccount.id],
961+
).toStrictEqual([ignoredAsset]);
962+
});
963+
964+
it('should keep ignored assets filtered out during automatic detection', async () => {
965+
const ignoredAsset = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
966+
967+
const { controller, messenger } = setupController({
968+
state: {
969+
accountsAssets: {},
970+
assetsMetadata: mockGetMetadataReturnValue.assets,
971+
allIgnoredAssets: {
972+
[mockSolanaAccount.id]: [ignoredAsset],
973+
},
974+
} as MultichainAssetsControllerState,
975+
});
976+
977+
// Simulate automatic asset detection trying to re-add ignored asset
978+
messenger.publish('AccountsController:accountAssetListUpdated', {
979+
assets: {
980+
[mockSolanaAccount.id]: {
981+
added: [ignoredAsset],
982+
removed: [],
983+
},
984+
},
985+
});
986+
987+
// Wait for async processing
988+
await advanceTime({ clock: useFakeTimers(), duration: 0 });
989+
990+
// Ignored asset should remain filtered out and stay in ignored list
991+
expect(
992+
controller.state.accountsAssets[mockSolanaAccount.id],
993+
).toBeUndefined();
994+
expect(
995+
controller.state.allIgnoredAssets[mockSolanaAccount.id],
996+
).toStrictEqual([ignoredAsset]);
997+
});
998+
999+
it('should add all assets when new account is added (no pre-existing ignored assets)', async () => {
1000+
const asset1 = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501';
1001+
const asset2 =
1002+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr';
1003+
1004+
const { controller, messenger } = setupController({
1005+
state: {
1006+
accountsAssets: {},
1007+
assetsMetadata: mockGetMetadataReturnValue.assets,
1008+
allIgnoredAssets: {},
1009+
} as MultichainAssetsControllerState,
1010+
mocks: {
1011+
handleRequestReturnValue: [asset1, asset2],
1012+
},
1013+
});
1014+
1015+
// Simulate account being added
1016+
messenger.publish('AccountsController:accountAdded', mockSolanaAccount);
1017+
1018+
// Wait for async processing
1019+
await advanceTime({ clock: useFakeTimers(), duration: 0 });
1020+
1021+
// All assets should be added to active list (no ignored assets for new account)
1022+
expect(
1023+
controller.state.accountsAssets[mockSolanaAccount.id],
1024+
).toStrictEqual([asset1, asset2]);
1025+
1026+
// No ignored assets for new account
1027+
expect(
1028+
controller.state.allIgnoredAssets[mockSolanaAccount.id],
1029+
).toBeUndefined();
1030+
});
1031+
});
1032+
1033+
describe('account removal with ignored assets', () => {
1034+
it('should clean up ignored assets when account is removed', async () => {
1035+
const { controller, messenger } = setupController({
1036+
state: {
1037+
accountsAssets: {
1038+
[mockSolanaAccount.id]: [
1039+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501',
1040+
],
1041+
},
1042+
assetsMetadata: mockGetMetadataReturnValue.assets,
1043+
allIgnoredAssets: {
1044+
[mockSolanaAccount.id]: [
1045+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr',
1046+
],
1047+
},
1048+
} as MultichainAssetsControllerState,
1049+
});
1050+
1051+
// Simulate account removal
1052+
messenger.publish(
1053+
'AccountsController:accountRemoved',
1054+
mockSolanaAccount.id,
1055+
);
1056+
1057+
// Wait for async processing
1058+
await advanceTime({ clock: useFakeTimers(), duration: 0 });
1059+
1060+
expect(
1061+
controller.state.accountsAssets[mockSolanaAccount.id],
1062+
).toBeUndefined();
1063+
expect(
1064+
controller.state.allIgnoredAssets[mockSolanaAccount.id],
1065+
).toBeUndefined();
1066+
});
1067+
});
1068+
8341069
describe('metadata', () => {
8351070
it('includes expected state in debug snapshots', () => {
8361071
const { controller } = setupController();
@@ -868,6 +1103,7 @@ describe('MultichainAssetsController', () => {
8681103
).toMatchInlineSnapshot(`
8691104
Object {
8701105
"accountsAssets": Object {},
1106+
"allIgnoredAssets": Object {},
8711107
"assetsMetadata": Object {},
8721108
}
8731109
`);
@@ -885,6 +1121,7 @@ describe('MultichainAssetsController', () => {
8851121
).toMatchInlineSnapshot(`
8861122
Object {
8871123
"accountsAssets": Object {},
1124+
"allIgnoredAssets": Object {},
8881125
"assetsMetadata": Object {},
8891126
}
8901127
`);

0 commit comments

Comments
 (0)