From 45dba1ffda76ead1066ae459ce50ca8c15f9ca12 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 15 May 2026 12:10:45 +0100 Subject: [PATCH 1/6] chore: remove allDetectedTokens Earn references --- .../Earn/hooks/useEarnNetworkPolling.test.ts | 178 ------------------ .../UI/Earn/hooks/useEarnNetworkPolling.ts | 56 ------ .../UI/Earn/hooks/useEarnToken.test.ts | 1 - .../UI/Earn/hooks/useEarnTokens.test.ts | 1 - 4 files changed, 236 deletions(-) diff --git a/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts b/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts index 3b1e6a6e71da..d3e206b4beda 100644 --- a/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts +++ b/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts @@ -23,9 +23,6 @@ jest.mock('../../../../core/Engine', () => ({ TokenDetectionController: { detectTokens: jest.fn(), }, - TokensController: { - addTokens: jest.fn(), - }, }, })); @@ -58,10 +55,6 @@ describe('useEarnNetworkPolling', () => { const mockFindNetworkClientIdByChainId = jest.mocked( Engine.context.NetworkController.findNetworkClientIdByChainId, ); - const mockDetectTokens = jest.mocked( - Engine.context.TokenDetectionController.detectTokens, - ); - const mockAddTokens = jest.mocked(Engine.context.TokensController.addTokens); const mockSelectedAccount = MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts[ @@ -91,32 +84,6 @@ describe('useEarnNetworkPolling', () => { PreferencesController: { useTokenDetection: true, }, - TokensController: { - allDetectedTokens: { - '0x1': { - [mockSelectedAccount.address]: { - '0x123': { - address: '0x123', - symbol: 'TEST', - decimals: 18, - image: 'test-image.png', - name: 'Test Token', - }, - }, - }, - '0x89': { - [mockSelectedAccount.address]: { - '0x456': { - address: '0x456', - symbol: 'TEST2', - decimals: 6, - image: 'test2-image.png', - name: 'Test Token 2', - }, - }, - }, - }, - }, }, }, } as unknown as RootState; @@ -128,8 +95,6 @@ describe('useEarnNetworkPolling', () => { if (chainId === '0x89') return 'polygon'; throw new Error(`Network client not found for chain ${chainId}`); }); - mockDetectTokens.mockResolvedValue(undefined); - mockAddTokens.mockResolvedValue(undefined); }); it('should call all polling hooks when mounted', () => { @@ -173,123 +138,6 @@ describe('useEarnNetworkPolling', () => { }); }); - it('should call TokenDetectionController.detectTokens when component mounts', () => { - renderHookWithProvider(() => useEarnNetworkPolling(), { - state: mockState, - }); - - expect(mockDetectTokens).toHaveBeenCalledWith({ - chainIds: expect.any(Array), - selectedAddress: mockSelectedAccount.address, - }); - }); - - it('should not call detectTokens when useTokenDetection is false', () => { - const stateWithoutTokenDetection = { - ...mockState, - engine: { - ...mockState.engine, - backgroundState: { - ...mockState.engine.backgroundState, - PreferencesController: { - useTokenDetection: false, - }, - }, - }, - } as unknown as RootState; - - renderHookWithProvider(() => useEarnNetworkPolling(), { - state: stateWithoutTokenDetection, - }); - - expect(mockDetectTokens).toHaveBeenCalledWith({ - chainIds: expect.any(Array), - selectedAddress: mockSelectedAccount.address, - }); - }); - - it('should not call detectTokens when no selected account', () => { - const stateWithoutAccount = { - ...mockState, - engine: { - ...mockState.engine, - backgroundState: { - ...mockState.engine.backgroundState, - AccountsController: { - ...MOCK_ACCOUNTS_CONTROLLER_STATE, - internalAccounts: { - ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts, - selectedAccount: '', - }, - }, - AccountTreeController: { - accountTree: { - wallets: {}, - }, - selectedAccountGroup: '', - }, - }, - }, - } as unknown as RootState; - - renderHookWithProvider(() => useEarnNetworkPolling(), { - state: stateWithoutAccount, - }); - - expect(mockDetectTokens).toHaveBeenCalledWith({ - chainIds: expect.any(Array), - selectedAddress: undefined, - }); - }); - - it('should call TokensController.addTokens for detected tokens', async () => { - renderHookWithProvider(() => useEarnNetworkPolling(), { - state: mockState, - }); - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)); - - // Verify tokens are added (order may vary based on LENDING_CHAIN_IDS) - expect(mockAddTokens).toHaveBeenCalledWith( - [ - { - address: '0x123', - symbol: 'TEST', - decimals: 18, - image: 'test-image.png', - name: 'Test Token', - isERC721: false, - }, - ], - 'mainnet', - ); - }); - - it('should not call addTokens when no detected tokens', async () => { - const stateWithoutDetectedTokens = { - ...mockState, - engine: { - ...mockState.engine, - backgroundState: { - ...mockState.engine.backgroundState, - TokensController: { - allDetectedTokens: {}, - }, - }, - }, - } as unknown as RootState; - - renderHookWithProvider(() => useEarnNetworkPolling(), { - state: stateWithoutDetectedTokens, - }); - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(mockAddTokens).not.toHaveBeenCalled(); - }); - it('should pass empty chainIds to useTokenDetectionPolling when useTokenDetection is false', () => { const stateWithoutTokenDetection = { ...mockState, @@ -314,32 +162,6 @@ describe('useEarnNetworkPolling', () => { }); }); - it('should handle addTokens errors gracefully', async () => { - mockAddTokens.mockRejectedValue(new Error('Failed to add tokens')); - - expect(() => { - renderHookWithProvider(() => useEarnNetworkPolling(), { - state: mockState, - }); - }).not.toThrow(); - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - - it('should handle detectTokens errors gracefully', async () => { - mockDetectTokens.mockRejectedValue(new Error('Failed to detect tokens')); - - expect(() => { - renderHookWithProvider(() => useEarnNetworkPolling(), { - state: mockState, - }); - }).not.toThrow(); - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - it('should return null', () => { const { result } = renderHookWithProvider(() => useEarnNetworkPolling(), { state: mockState, diff --git a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts index 711b1cae4662..a893b06d472d 100644 --- a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts +++ b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts @@ -1,17 +1,14 @@ -import { Token } from '@metamask/assets-controllers'; import { toHex } from '@metamask/controller-utils'; import { CHAIN_ID_TO_AAVE_POOL_CONTRACT } from '@metamask/stake-sdk'; import { Hex } from '@metamask/utils'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import Engine from '../../../../core/Engine'; import { selectSelectedInternalAccountByScope } from '../../../../selectors/multichainAccounts/accounts'; import { selectUseTokenDetection } from '../../../../selectors/preferencesController'; import useCurrencyRatePolling from '../../../hooks/AssetPolling/useCurrencyRatePolling'; import useTokenBalancesPolling from '../../../hooks/AssetPolling/useTokenBalancesPolling'; import useTokenDetectionPolling from '../../../hooks/AssetPolling/useTokenDetectionPolling'; import useTokenRatesPolling from '../../../hooks/AssetPolling/useTokenRatesPolling'; -import { RootState } from '../../BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test'; import { EVM_SCOPE } from '../constants/networks'; /** @@ -55,9 +52,6 @@ export const useEarnNetworkPolling = () => { EVM_SCOPE, ); const useTokenDetection = useSelector(selectUseTokenDetection); - const tokensState = useSelector( - (state: RootState) => state.engine?.backgroundState?.TokensController, - ); const [lendingChainIds, setLendingChainIds] = useState([]); useTokenBalancesPolling({ chainIds: lendingChainIds }); @@ -78,56 +72,6 @@ export const useEarnNetworkPolling = () => { setLendingChainIds(validChainIds); }, [setLendingChainIds]); - // Import tokens from all lending chains - useEffect(() => { - const importLendingTokens = async () => { - if (!selectedAccount?.address || !useTokenDetection) return; - - const { TokensController } = Engine.context; - const allDetectedTokens = tokensState?.allDetectedTokens || {}; - - for (const chainId of LENDING_CHAIN_IDS) { - const chainDetectedTokens = - allDetectedTokens[chainId]?.[selectedAccount.address]; - if ( - chainDetectedTokens && - Object.keys(chainDetectedTokens).length > 0 - ) { - const tokensToImport = Object.values(chainDetectedTokens).map( - (token: Token) => ({ - address: token.address, - symbol: token.symbol, - decimals: token.decimals, - image: token.image, - name: token.name, - isERC721: false, - }), - ); - - const networkClientId = - Engine.context.NetworkController.findNetworkClientIdByChainId( - chainId, - ); - - if (networkClientId && tokensToImport.length > 0) { - await TokensController.addTokens(tokensToImport, networkClientId); - } - } - } - }; - - Engine.context.TokenDetectionController.detectTokens({ - chainIds: LENDING_CHAIN_IDS, - selectedAddress: selectedAccount?.address as Hex, - }) - .then(importLendingTokens) - .catch(console.error); - }, [ - tokensState?.allDetectedTokens, - selectedAccount?.address, - useTokenDetection, - ]); - return null; }; diff --git a/app/components/UI/Earn/hooks/useEarnToken.test.ts b/app/components/UI/Earn/hooks/useEarnToken.test.ts index dc02431452e6..a659312ebdb1 100644 --- a/app/components/UI/Earn/hooks/useEarnToken.test.ts +++ b/app/components/UI/Earn/hooks/useEarnToken.test.ts @@ -163,7 +163,6 @@ const mockState = { }, }, allIgnoredTokens: {}, - allDetectedTokens: {}, } as TokensControllerState, TokenBalancesController: { tokenBalances: { diff --git a/app/components/UI/Earn/hooks/useEarnTokens.test.ts b/app/components/UI/Earn/hooks/useEarnTokens.test.ts index 5e45f9929021..f4fe9e88cbad 100644 --- a/app/components/UI/Earn/hooks/useEarnTokens.test.ts +++ b/app/components/UI/Earn/hooks/useEarnTokens.test.ts @@ -174,7 +174,6 @@ const mockState = { }, }, allIgnoredTokens: {}, - allDetectedTokens: {}, } as TokensControllerState, TokenBalancesController: { tokenBalances: { From 62598a3abff528de7213fc17a70ae92abe44fe92 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 15 May 2026 12:31:23 +0100 Subject: [PATCH 2/6] remove useEffect --- .../UI/Earn/hooks/useEarnNetworkPolling.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts index a893b06d472d..ef9bafc920b8 100644 --- a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts +++ b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts @@ -52,26 +52,15 @@ export const useEarnNetworkPolling = () => { EVM_SCOPE, ); const useTokenDetection = useSelector(selectUseTokenDetection); - const [lendingChainIds, setLendingChainIds] = useState([]); - useTokenBalancesPolling({ chainIds: lendingChainIds }); - useCurrencyRatePolling({ chainIds: lendingChainIds }); - useTokenRatesPolling({ chainIds: lendingChainIds }); + useTokenBalancesPolling({ chainIds: LENDING_CHAIN_IDS }); + useCurrencyRatePolling({ chainIds: LENDING_CHAIN_IDS }); + useTokenRatesPolling({ chainIds: LENDING_CHAIN_IDS }); useTokenDetectionPolling({ - chainIds: useTokenDetection ? lendingChainIds : [], + chainIds: useTokenDetection ? LENDING_CHAIN_IDS : [], address: selectedAccount?.address as Hex, }); - useEffect(() => { - const validChainIds: Hex[] = []; - - LENDING_CHAIN_IDS.forEach((chainId) => { - validChainIds.push(chainId); - }); - - setLendingChainIds(validChainIds); - }, [setLendingChainIds]); - return null; }; From 9b8e5a705294dae4cfea876797ca93e0a1892244 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 15 May 2026 12:35:31 +0100 Subject: [PATCH 3/6] detectTokens once --- app/components/UI/Earn/hooks/useEarnNetworkPolling.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts index ef9bafc920b8..6d5d5d0e4df0 100644 --- a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts +++ b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts @@ -1,8 +1,9 @@ import { toHex } from '@metamask/controller-utils'; import { CHAIN_ID_TO_AAVE_POOL_CONTRACT } from '@metamask/stake-sdk'; import { Hex } from '@metamask/utils'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useSelector } from 'react-redux'; +import Engine from '../../../../core/Engine'; import { selectSelectedInternalAccountByScope } from '../../../../selectors/multichainAccounts/accounts'; import { selectUseTokenDetection } from '../../../../selectors/preferencesController'; import useCurrencyRatePolling from '../../../hooks/AssetPolling/useCurrencyRatePolling'; @@ -61,6 +62,14 @@ export const useEarnNetworkPolling = () => { address: selectedAccount?.address as Hex, }); + // Import tokens from all lending chains + useEffect(() => { + Engine.context.TokenDetectionController.detectTokens({ + chainIds: LENDING_CHAIN_IDS, + selectedAddress: selectedAccount?.address as Hex, + }).catch(console.error); + }, [selectedAccount?.address, useTokenDetection]); + return null; }; From dedd4885338577a2c6003c3d53a4399293a58f3b Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 15 May 2026 12:39:30 +0100 Subject: [PATCH 4/6] restore tests --- .../Earn/hooks/useEarnNetworkPolling.test.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts b/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts index d3e206b4beda..221b7e306fbc 100644 --- a/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts +++ b/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts @@ -55,6 +55,9 @@ describe('useEarnNetworkPolling', () => { const mockFindNetworkClientIdByChainId = jest.mocked( Engine.context.NetworkController.findNetworkClientIdByChainId, ); + const mockDetectTokens = jest.mocked( + Engine.context.TokenDetectionController.detectTokens, + ); const mockSelectedAccount = MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts[ @@ -95,6 +98,7 @@ describe('useEarnNetworkPolling', () => { if (chainId === '0x89') return 'polygon'; throw new Error(`Network client not found for chain ${chainId}`); }); + mockDetectTokens.mockResolvedValue(undefined); }); it('should call all polling hooks when mounted', () => { @@ -138,6 +142,75 @@ describe('useEarnNetworkPolling', () => { }); }); + it('should call TokenDetectionController.detectTokens when component mounts', () => { + renderHookWithProvider(() => useEarnNetworkPolling(), { + state: mockState, + }); + + expect(mockDetectTokens).toHaveBeenCalledWith({ + chainIds: expect.any(Array), + selectedAddress: mockSelectedAccount.address, + }); + }); + + it('should not call detectTokens when useTokenDetection is false', () => { + const stateWithoutTokenDetection = { + ...mockState, + engine: { + ...mockState.engine, + backgroundState: { + ...mockState.engine.backgroundState, + PreferencesController: { + useTokenDetection: false, + }, + }, + }, + } as unknown as RootState; + + renderHookWithProvider(() => useEarnNetworkPolling(), { + state: stateWithoutTokenDetection, + }); + + expect(mockDetectTokens).toHaveBeenCalledWith({ + chainIds: expect.any(Array), + selectedAddress: mockSelectedAccount.address, + }); + }); + + it('should not call detectTokens when no selected account', () => { + const stateWithoutAccount = { + ...mockState, + engine: { + ...mockState.engine, + backgroundState: { + ...mockState.engine.backgroundState, + AccountsController: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE, + internalAccounts: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts, + selectedAccount: '', + }, + }, + AccountTreeController: { + accountTree: { + wallets: {}, + }, + selectedAccountGroup: '', + }, + }, + }, + } as unknown as RootState; + + renderHookWithProvider(() => useEarnNetworkPolling(), { + state: stateWithoutAccount, + }); + + expect(mockDetectTokens).toHaveBeenCalledWith({ + chainIds: expect.any(Array), + selectedAddress: undefined, + }); + }); + it('should pass empty chainIds to useTokenDetectionPolling when useTokenDetection is false', () => { const stateWithoutTokenDetection = { ...mockState, @@ -162,6 +235,19 @@ describe('useEarnNetworkPolling', () => { }); }); + it('should handle detectTokens errors gracefully', async () => { + mockDetectTokens.mockRejectedValue(new Error('Failed to detect tokens')); + + expect(() => { + renderHookWithProvider(() => useEarnNetworkPolling(), { + state: mockState, + }); + }).not.toThrow(); + + // Wait for async operations to complete + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + it('should return null', () => { const { result } = renderHookWithProvider(() => useEarnNetworkPolling(), { state: mockState, From b3d40becd40d7394e9082a7b12da8445b75d3752 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 15 May 2026 12:47:06 +0100 Subject: [PATCH 5/6] fix test --- .../Earn/hooks/useEarnNetworkPolling.test.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts b/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts index 221b7e306fbc..5428ce99415d 100644 --- a/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts +++ b/app/components/UI/Earn/hooks/useEarnNetworkPolling.test.ts @@ -26,6 +26,8 @@ jest.mock('../../../../core/Engine', () => ({ }, })); +import { toHex } from '@metamask/controller-utils'; +import { CHAIN_ID_TO_AAVE_POOL_CONTRACT } from '@metamask/stake-sdk'; import { renderHookWithProvider } from '../../../../util/test/renderWithProvider'; import useEarnNetworkPolling from './useEarnNetworkPolling'; import { RootState } from '../../../../reducers'; @@ -36,6 +38,10 @@ import useTokenRatesPolling from '../../../hooks/AssetPolling/useTokenRatesPolli import useTokenDetectionPolling from '../../../hooks/AssetPolling/useTokenDetectionPolling'; import Engine from '../../../../core/Engine'; +const LENDING_CHAIN_IDS = Object.keys(CHAIN_ID_TO_AAVE_POOL_CONTRACT).map( + (chainId) => toHex(chainId), +); + // Mock console.warn to avoid noise in tests const originalConsoleWarn = console.warn; beforeAll(() => { @@ -121,23 +127,24 @@ describe('useEarnNetworkPolling', () => { }); }); - it('should initialize with empty chain IDs and network client IDs', () => { + it('should initialize with lending chain IDs', () => { renderHookWithProvider(() => useEarnNetworkPolling(), { state: mockState, }); - // Initially called with empty arrays + const expectedChainIds = expect.arrayContaining(LENDING_CHAIN_IDS); + expect(mockUseTokenBalancesPolling).toHaveBeenCalledWith({ - chainIds: [], + chainIds: expectedChainIds, }); expect(mockUseCurrencyRatePolling).toHaveBeenCalledWith({ - chainIds: [], + chainIds: expectedChainIds, }); expect(mockUseTokenRatesPolling).toHaveBeenCalledWith({ - chainIds: [], + chainIds: expectedChainIds, }); expect(mockUseTokenDetectionPolling).toHaveBeenCalledWith({ - chainIds: [], + chainIds: expectedChainIds, address: mockSelectedAccount.address, }); }); From 8aaf68628f8d30273ba36236b7361c10a8054d3a Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 15 May 2026 12:50:02 +0100 Subject: [PATCH 6/6] remove dependency --- app/components/UI/Earn/hooks/useEarnNetworkPolling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts index 6d5d5d0e4df0..d689e589c0ac 100644 --- a/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts +++ b/app/components/UI/Earn/hooks/useEarnNetworkPolling.ts @@ -68,7 +68,7 @@ export const useEarnNetworkPolling = () => { chainIds: LENDING_CHAIN_IDS, selectedAddress: selectedAccount?.address as Hex, }).catch(console.error); - }, [selectedAccount?.address, useTokenDetection]); + }, [selectedAccount?.address]); return null; };