Skip to content
Draft
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
2 changes: 2 additions & 0 deletions packages/assets-controller/src/AssetsController.test.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some tests on the new exposed methods from this controller?

Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ describe('AssetsController', () => {
assetsBalance: {},
assetsPrice: {},
customAssets: {},
hiddenAssets: {},
});
});
});
Expand All @@ -213,6 +214,7 @@ describe('AssetsController', () => {
assetsBalance: {},
assetsPrice: {},
customAssets: {},
hiddenAssets: {},
});
});
});
Expand Down
142 changes: 140 additions & 2 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,22 @@ export type AssetsControllerState = {
assetsPrice: { [assetId: string]: Json };
/** Custom assets added by users per account (CAIP-19 asset IDs) */
customAssets: { [accountId: string]: string[] };
/** Hidden assets per account (CAIP-19 asset IDs) - assets user chose to hide */
hiddenAssets: { [accountId: string]: string[] };
};

/**
* Returns the default state for AssetsController.
*
* @returns The default AssetsController state with empty metadata, balance, price, and customAssets maps.
* @returns The default AssetsController state with empty metadata, balance, price, customAssets, and hiddenAssets maps.
*/
export function getDefaultAssetsControllerState(): AssetsControllerState {
return {
assetsMetadata: {},
assetsBalance: {},
assetsPrice: {},
customAssets: {},
hiddenAssets: {},
};
}

Expand Down Expand Up @@ -160,6 +163,21 @@ export type AssetsControllerGetCustomAssetsAction = {
handler: AssetsController['getCustomAssets'];
};

export type AssetsControllerHideAssetAction = {
type: `${typeof CONTROLLER_NAME}:hideAsset`;
handler: AssetsController['hideAsset'];
};

export type AssetsControllerUnhideAssetAction = {
type: `${typeof CONTROLLER_NAME}:unhideAsset`;
handler: AssetsController['unhideAsset'];
};

export type AssetsControllerGetHiddenAssetsAction = {
type: `${typeof CONTROLLER_NAME}:getHiddenAssets`;
handler: AssetsController['getHiddenAssets'];
};

export type AssetsControllerActions =
| AssetsControllerGetStateAction
| AssetsControllerGetAssetsAction
Expand All @@ -170,7 +188,10 @@ export type AssetsControllerActions =
| AssetsControllerAssetsUpdateAction
| AssetsControllerAddCustomAssetAction
| AssetsControllerRemoveCustomAssetAction
| AssetsControllerGetCustomAssetsAction;
| AssetsControllerGetCustomAssetsAction
| AssetsControllerHideAssetAction
| AssetsControllerUnhideAssetAction
| AssetsControllerGetHiddenAssetsAction;

export type AssetsControllerStateChangeEvent = ControllerStateChangeEvent<
typeof CONTROLLER_NAME,
Expand Down Expand Up @@ -290,6 +311,12 @@ const stateMetadata: StateMetadata<AssetsControllerState> = {
includeInDebugSnapshot: false,
usedInUi: true,
},
hiddenAssets: {
persist: true,
includeInStateLogs: false,
includeInDebugSnapshot: false,
usedInUi: true,
},
};

// ============================================================================
Expand Down Expand Up @@ -615,6 +642,21 @@ export class AssetsController extends BaseController<
'AssetsController:getCustomAssets',
this.getCustomAssets.bind(this),
);

this.messenger.registerActionHandler(
'AssetsController:hideAsset',
this.hideAsset.bind(this),
);

this.messenger.registerActionHandler(
'AssetsController:unhideAsset',
this.unhideAsset.bind(this),
);

this.messenger.registerActionHandler(
'AssetsController:getHiddenAssets',
this.getHiddenAssets.bind(this),
);
}

// ============================================================================
Expand Down Expand Up @@ -860,6 +902,7 @@ export class AssetsController extends BaseController<
/**
* Add a custom asset for an account.
* Custom assets are included in subscription and fetch operations.
* Adding a custom asset also unhides it if it was previously hidden.
*
* @param accountId - The account ID to add the custom asset for.
* @param assetId - The CAIP-19 asset ID to add.
Expand All @@ -882,6 +925,19 @@ export class AssetsController extends BaseController<
if (!customAssets[accountId].includes(normalizedAssetId)) {
customAssets[accountId].push(normalizedAssetId);
}

// Remove from hidden assets if it was hidden (unhide the asset)
const hiddenAssets = state.hiddenAssets as Record<string, string[]>;
if (hiddenAssets[accountId]) {
hiddenAssets[accountId] = hiddenAssets[accountId].filter(
(id) => id !== normalizedAssetId,
);

// Clean up empty arrays
if (hiddenAssets[accountId].length === 0) {
delete hiddenAssets[accountId];
}
}
});

// Fetch data for the newly added custom asset
Expand Down Expand Up @@ -931,6 +987,85 @@ export class AssetsController extends BaseController<
return (this.state.customAssets[accountId] ?? []) as Caip19AssetId[];
}

// ============================================================================
// HIDDEN ASSETS MANAGEMENT
// ============================================================================

/**
* Hide an asset for an account.
* Hidden assets are excluded from the asset list returned by getAssets.
*
* @param accountId - The account ID to hide the asset for.
* @param assetId - The CAIP-19 asset ID to hide.
*/
hideAsset(accountId: AccountId, assetId: Caip19AssetId): void {
const normalizedAssetId = normalizeAssetId(assetId);

log('Hiding asset', { accountId, assetId: normalizedAssetId });

this.update((state) => {
const hiddenAssets = state.hiddenAssets as Record<string, string[]>;
if (!hiddenAssets[accountId]) {
hiddenAssets[accountId] = [];
}

// Only add if not already present
if (!hiddenAssets[accountId].includes(normalizedAssetId)) {
hiddenAssets[accountId].push(normalizedAssetId);
}
});
}

/**
* Unhide an asset for an account.
*
* @param accountId - The account ID to unhide the asset for.
* @param assetId - The CAIP-19 asset ID to unhide.
*/
unhideAsset(accountId: AccountId, assetId: Caip19AssetId): void {
const normalizedAssetId = normalizeAssetId(assetId);

log('Unhiding asset', { accountId, assetId: normalizedAssetId });

this.update((state) => {
const hiddenAssets = state.hiddenAssets as Record<string, string[]>;
if (hiddenAssets[accountId]) {
hiddenAssets[accountId] = hiddenAssets[accountId].filter(
(id) => id !== normalizedAssetId,
);

// Clean up empty arrays
if (hiddenAssets[accountId].length === 0) {
delete hiddenAssets[accountId];
}
}
});
}

/**
* Get all hidden assets for an account.
*
* @param accountId - The account ID to get hidden assets for.
* @returns Array of CAIP-19 asset IDs for the account's hidden assets.
*/
getHiddenAssets(accountId: AccountId): Caip19AssetId[] {
return (this.state.hiddenAssets[accountId] ?? []) as Caip19AssetId[];
}

/**
* Check if an asset is hidden for an account.
*
* @param accountId - The account ID to check.
* @param assetId - The CAIP-19 asset ID to check.
* @returns True if the asset is hidden, false otherwise.
*/
isAssetHidden(accountId: AccountId, assetId: Caip19AssetId): boolean {
const normalizedAssetId = normalizeAssetId(assetId);
return (
this.state.hiddenAssets[accountId]?.includes(normalizedAssetId) ?? false
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method isAssetHidden is defined but never used

Low Severity

The isAssetHidden() method is defined but never used anywhere in the codebase. Unlike the other hidden asset methods (hideAsset, unhideAsset, getHiddenAssets), this method has no corresponding action type defined and is not registered as a messenger action handler. It cannot be called via the messenger pattern and is never called internally. This appears to be dead code that was added but never integrated.

Fix in Cursor Fix in Web

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a huge deal, we can add some tests to cover this.


Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAssets does not filter out hidden assets

High Severity

The hideAsset method's JSDoc states "Hidden assets are excluded from the asset list returned by getAssets", but #getAssetsFromState doesn't actually filter hidden assets. The method iterates through accountBalances and filters by chain, metadata, and asset type, but never checks this.state.hiddenAssets[account.id] to skip hidden assets. Users who hide assets will still see them returned by getAssets(), completely defeating the feature's purpose.

Additional Locations (1)

Fix in Cursor Fix in Web

// ============================================================================
// SUBSCRIPTIONS
// ============================================================================
Expand Down Expand Up @@ -1771,5 +1906,8 @@ export class AssetsController extends BaseController<
'AssetsController:removeCustomAsset',
);
this.messenger.unregisterActionHandler('AssetsController:getCustomAssets');
this.messenger.unregisterActionHandler('AssetsController:hideAsset');
this.messenger.unregisterActionHandler('AssetsController:unhideAsset');
this.messenger.unregisterActionHandler('AssetsController:getHiddenAssets');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ async function withController<ReturnValue>(
allIgnoredTokens: {},
assetsMetadata: {},
assetsBalance: {},
hiddenAssets: {},
}));

// Mock TokenListController:getState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ type AssetsControllerGetStateAction = {
allIgnoredTokens: Record<string, Record<string, string[]>>;
assetsMetadata: Record<Caip19AssetId, AssetMetadata>;
assetsBalance: Record<string, Record<string, { amount: string }>>;
hiddenAssets: Record<string, string[]>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (not a blocker) - I wonder if we add the JSDOC like we did for packages/assets-controller/src/data-sources/evm-rpc-services/types/state.ts, it helped readability on what this Record<string, string[]> is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(optional, needs discussion), the Accounts team export a type alias, that could assist with readability

export type AccountId = string;

...Hesitant since it doesn't actually provide any additional type safety... maybe JSDOC is better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I actually much more prefer the approach you used below in packages/assets-controller/src/AssetsController.ts

type Foo = { [accountId: string]: string[] };

I think this is easier to read/understand then Record<string, string[]>

};
};

Expand Down Expand Up @@ -346,13 +347,15 @@ export class RpcDataSource extends BaseController<
_action: 'AssetsController:getState',
): {
assetsBalance: Record<string, Record<string, { amount: string }>>;
hiddenAssets?: Record<string, string[]>;
} => {
const state = this.messenger.call('AssetsController:getState');
return {
assetsBalance: (state.assetsBalance ?? {}) as Record<
string,
Record<string, { amount: string }>
>,
hiddenAssets: state.hiddenAssets ?? {},
};
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,21 @@ export class BalanceFetcher extends StaticIntervalPollingControllerOnly<BalanceP
return [];
}

// Get hidden assets for this account
const hiddenAssets = new Set(state.hiddenAssets?.[accountId] ?? []);

// Convert hex chainId to decimal for CAIP-2 matching
const chainIdDecimal = parseInt(chainId, 16);
const caipChainPrefix = `eip155:${chainIdDecimal}/`;

const tokenMap = new Map<string, TokenFetchInfo>();

for (const assetId of Object.keys(accountBalances)) {
// Skip hidden assets
if (hiddenAssets.has(assetId)) {
continue;
}
Comment on lines +137 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm does this mean we don't fetch and update balances for hidden assets?
Might be problematic for the swaps flow (you could be trying to swap a hidden asset, where the balance is outdated?)

High level, my initial thoughts are that "Hidden Assets" are a UI lever/control, we should still be performing data fetching if there are places that do show this asset.


// Only process ERC20 tokens on the current chain
if (assetId.startsWith(caipChainPrefix) && assetId.includes('/erc20:')) {
// Parse token address from CAIP-19: eip155:1/erc20:0x...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ export type AssetBalanceEntry = {
export type AssetsBalanceState = {
/** Balance data per account: accountId -> assetId -> balance */
assetsBalance: Record<string, Record<string, AssetBalanceEntry>>;
/** Hidden assets per account: accountId -> array of hidden asset IDs */
hiddenAssets?: Record<string, string[]>;
};
3 changes: 3 additions & 0 deletions packages/assets-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type {
AssetsControllerAddCustomAssetAction,
AssetsControllerRemoveCustomAssetAction,
AssetsControllerGetCustomAssetsAction,
AssetsControllerHideAssetAction,
AssetsControllerUnhideAssetAction,
AssetsControllerGetHiddenAssetsAction,
AssetsControllerActions,
AssetsControllerStateChangeEvent,
AssetsControllerBalanceChangedEvent,
Expand Down
4 changes: 4 additions & 0 deletions packages/assets-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ export type DataResponse = {
* - assetsPrice keys: CAIP-19 asset IDs
* - customAssets outer keys: Account IDs (InternalAccount.id UUIDs)
* - customAssets inner values: CAIP-19 asset IDs array
* - hiddenAssets outer keys: Account IDs (InternalAccount.id UUIDs)
* - hiddenAssets inner values: CAIP-19 asset IDs array
*/
export type AssetsControllerStateInternal = {
/** Shared metadata for all assets (stored once per asset) */
Expand All @@ -368,6 +370,8 @@ export type AssetsControllerStateInternal = {
assetsPrice: Record<Caip19AssetId, AssetPrice>;
/** Custom assets added by users per account */
customAssets: Record<AccountId, Caip19AssetId[]>;
/** Hidden assets per account - assets user chose to hide */
hiddenAssets: Record<AccountId, Caip19AssetId[]>;
};

/**
Expand Down
Loading