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
2 changes: 1 addition & 1 deletion eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@
},
"packages/assets-controllers/src/selectors/token-selectors.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 25
"count": 24
},
"@typescript-eslint/prefer-nullish-coalescing": {
"count": 1
Expand Down
4 changes: 4 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Add `useExternalServices` option to `selectAssetsBySelectedAccountGroup` and `selectAllAssets` selectors ([#7545](https://github.com/MetaMask/core/pull/7545))
- When `useExternalServices` is `false`, non-EVM (multichain) assets are excluded from the result
- This allows callers to respect the basic functionality / external services toggle
- Defaults to `true` to maintain backward compatibility
- **BREAKING:** `AccountTrackerController` now requires `KeyringController:getState` action and `KeyringController:lock` event in addition to existing allowed actions and events ([#7492](https://github.com/MetaMask/core/pull/7492))
- Added `#isLocked` property to track keyring lock state, initialized from `KeyringController:getState`
- Added `isActive` getter that returns `true` when keyring is unlocked and user is onboarded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,54 @@ describe('token-selectors', () => {
expect(result).toStrictEqual(expectedMockResult);
});

describe('useExternalServices option', () => {
it('excludes non-EVM (multichain) assets when useExternalServices is false', () => {
const result = selectAssetsBySelectedAccountGroup(mockedMergedState, {
filterTronStakedTokens: true,
useExternalServices: false,
});

// Should only include EVM chains (0x1, 0xa) - no Solana
expect(Object.keys(result)).toStrictEqual(['0x1', '0xa']);

// Verify EVM assets are still present
expect(result['0x1']).toHaveLength(3); // GHO, SUSHI, ETH native
expect(result['0xa']).toHaveLength(2); // USDC, ETH native

// Verify Solana assets are excluded
expect(
result['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
).toBeUndefined();
});

it('includes non-EVM (multichain) assets when useExternalServices is true', () => {
const result = selectAssetsBySelectedAccountGroup(mockedMergedState, {
filterTronStakedTokens: true,
useExternalServices: true,
});

// Should include all chains including Solana
expect(Object.keys(result)).toContain(
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
);

// Verify Solana assets are present
expect(result['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']).toHaveLength(
2,
); // SOL native, JUP token
});

it('uses default useExternalServices: true when not specified', () => {
// Call without opts - should use defaults (useExternalServices: true)
const result = selectAssetsBySelectedAccountGroup(mockedMergedState);

// Should include Solana assets (multichain)
expect(Object.keys(result)).toContain(
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
);
});
});

const arrangeTronState = () => {
const state = cloneDeep(mockedMergedState);

Expand Down Expand Up @@ -1018,6 +1066,7 @@ describe('token-selectors', () => {

const result = selectAssetsBySelectedAccountGroup(state, {
filterTronStakedTokens: false,
useExternalServices: true,
});

expect(result[TrxScope.Mainnet].length > 1).toBe(true);
Expand Down
41 changes: 38 additions & 3 deletions packages/assets-controllers/src/selectors/token-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,18 @@ const selectAllEvmAssets = createAssetListSelector(
},
);

export type SelectAllAssetsOpts = {
/**
* When false, non-EVM (multichain) assets are excluded.
* This should be set to false when basic functionality / external services are disabled.
*/
useExternalServices: boolean;
};

const defaultSelectAllAssetsOpts: SelectAllAssetsOpts = {
useExternalServices: true,
};

const selectAllMultichainAssets = createAssetListSelector(
[
selectAccountsToGroupIdMap,
Expand All @@ -343,6 +355,10 @@ const selectAllMultichainAssets = createAssetListSelector(
(state) => state.balances,
(state) => state.conversionRates,
(state) => state.currentCurrency,
(
_state,
opts: SelectAllAssetsOpts = defaultSelectAllAssetsOpts,
): SelectAllAssetsOpts => opts,
],
(
accountsMap,
Expand All @@ -352,7 +368,13 @@ const selectAllMultichainAssets = createAssetListSelector(
multichainBalances,
multichainConversionRates,
currentCurrency,
opts,
) => {
// Short-circuit when external services are disabled - no multichain assets needed
if (!opts.useExternalServices) {
return {};
}

const groupAssets: AssetsByAccountGroup = {};

for (const [accountId, accountAssets] of Object.entries(multichainTokens)) {
Expand Down Expand Up @@ -444,7 +466,10 @@ const selectAllMultichainAssets = createAssetListSelector(
export const selectAllAssets = createAssetListSelector(
[
selectAllEvmAssets,
selectAllMultichainAssets,
(
state,
opts: SelectAllAssetsOpts = defaultSelectAllAssetsOpts,
): AssetsByAccountGroup => selectAllMultichainAssets(state, opts),
selectAllEvmAccountNativeBalances,
],
(evmAssets, multichainAssets, evmAccountNativeBalances) => {
Expand All @@ -462,10 +487,16 @@ export const selectAllAssets = createAssetListSelector(

export type SelectAccountGroupAssetOpts = {
filterTronStakedTokens: boolean;
/**
* When false, non-EVM chains are filtered out.
* This should be set to false when basic functionality / external services are disabled.
*/
useExternalServices: boolean;
};

const defaultSelectAccountGroupAssetOpts: SelectAccountGroupAssetOpts = {
filterTronStakedTokens: true,
useExternalServices: true,
};

const filterTronStakedTokens = (assetsByAccountGroup: AccountGroupAssets) => {
Expand Down Expand Up @@ -496,12 +527,16 @@ const filterTronStakedTokens = (assetsByAccountGroup: AccountGroupAssets) => {

export const selectAssetsBySelectedAccountGroup = createAssetListSelector(
[
selectAllAssets,
(
state,
opts: SelectAccountGroupAssetOpts = defaultSelectAccountGroupAssetOpts,
): AssetsByAccountGroup =>
selectAllAssets(state, { useExternalServices: opts.useExternalServices }),
Copy link

Choose a reason for hiding this comment

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

Bug: New opts object breaks selector memoization chain

The inline object { useExternalServices: opts.useExternalServices } is created fresh on every call to the first input selector. Since selectAllAssets and selectAllMultichainAssets use default reselect memoization (strict reference equality), this new object reference invalidates their cache on every call. The expensive computation inside selectAllMultichainAssets — including parsing CAIP assets, building balance maps, and fiat conversions — will rerun even when the state hasn't changed. Before this change, selectAllAssets was called directly as an input selector without opts, so its memoization worked correctly.

Fix in Cursor Fix in Web

(state) => state.accountTree,
(
_state,
opts: SelectAccountGroupAssetOpts = defaultSelectAccountGroupAssetOpts,
) => opts,
): SelectAccountGroupAssetOpts => opts,
],
(groupAssets, accountTree, opts) => {
const { selectedAccountGroup } = accountTree;
Expand Down
Loading