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
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export function useRecentTokenSection(
allTokens: TokenWithLogo[],
favoriteTokens: TokenWithLogo[],
activeChainId?: number,
allowedTokens?: TokenWithLogo[],
): RecentTokenSection {
const { recentTokens, addRecentToken, clearRecentTokens } = useRecentTokens({
allTokens,
favoriteTokens,
allowedTokens,
activeChainId,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import { doesTokenMatchSymbolOrAddress } from '@cowprotocol/common-utils'
import { getAddressKey } from '@cowprotocol/cow-sdk'
import { getTokenSearchFilter, TokenSearchResponse, useSearchToken } from '@cowprotocol/tokens'

import { useInjectedWidgetParams } from 'entities/injectedWidget'

import { Field } from 'legacy/state/types'

import { useAddTokenImportCallback } from '../../hooks/useAddTokenImportCallback'
import { useSelectTokenWidgetState } from '../../hooks/useSelectTokenWidgetState'
import { useTokenListContext } from '../../hooks/useTokenListContext'
Expand All @@ -19,28 +15,16 @@ import { TokenSearchContent } from '../../pure/TokenSearchContent'
export function TokenSearchResults(): ReactNode {
const { searchInput } = useTokenListViewState()

const { selectTokenContext, areTokensFromBridge, allTokens, bridgeSupportedTokensMap } = useTokenListContext()
const { tokenLists, sellTokenLists, buyTokenLists } = useInjectedWidgetParams()
const { selectTokenContext, areTokensFromBridge, allTokens, bridgeSupportedTokensMap, hasScopedListRestriction } =
useTokenListContext()

const { onTokenListItemClick } = selectTokenContext

const { field, onSelectToken } = useSelectTokenWidgetState()
const { onSelectToken } = useSelectTokenWidgetState()

// Search all tokens (used in both modes)
const defaultSearchResults = useSearchToken(searchInput)
const filter = useMemo(() => getTokenSearchFilter(searchInput), [searchInput])
const hasScopedListRestriction = useMemo(() => {
if (field === Field.INPUT) {
return !!(tokenLists?.length || sellTokenLists?.length)
}

if (field === Field.OUTPUT) {
return !!(tokenLists?.length || buyTokenLists?.length)
}

return !!(tokenLists?.length || sellTokenLists?.length || buyTokenLists?.length)
}, [buyTokenLists?.length, field, sellTokenLists?.length, tokenLists?.length])

const searchResults: TokenSearchResponse = useMemo(() => {
if (!hasScopedListRestriction && !areTokensFromBridge) {
return defaultSearchResults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface UseHydratedRecentTokensParams {
storedTokensByChain: StoredRecentTokensByChain
tokensByKey: Map<string, TokenWithLogo>
favoriteKeys: Set<string>
allowedTokenKeys?: Set<string>
activeChainId?: number
maxItems?: number
}
Expand All @@ -21,6 +22,7 @@ export function useHydratedRecentTokens({
storedTokensByChain,
tokensByKey,
favoriteKeys,
allowedTokenKeys,
activeChainId,
maxItems = RECENT_TOKENS_LIMIT,
}: UseHydratedRecentTokensParams): TokenWithLogo[] {
Expand All @@ -36,6 +38,10 @@ export function useHydratedRecentTokens({
continue
}

if (allowedTokenKeys && !allowedTokenKeys.has(key)) {
continue
}

const hydrated = hydrateStoredToken(entry, tokensByKey.get(key))

if (hydrated) {
Expand All @@ -49,5 +55,5 @@ export function useHydratedRecentTokens({
}

return result
}, [activeChainId, favoriteKeys, maxItems, storedTokensByChain, tokensByKey])
}, [activeChainId, allowedTokenKeys, favoriteKeys, maxItems, storedTokensByChain, tokensByKey])
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,82 @@ describe('useRecentTokens', () => {
expect(result.current.recentTokens[0].symbol).toBe('TKN')
})

it('hydrates persisted custom tokens when no token-list restriction is active', () => {
setStoredTokens({
[SupportedChainId.MAINNET]: [createStoredToken(SupportedChainId.MAINNET, ADDRESS_1, 'CUSTOM')],
})
const store = createStoreWithLocalStorage()
const wrapper = createTestWrapper(store)

const { result } = renderHook(
() =>
useRecentTokens({
allTokens: [],
favoriteTokens: [],
activeChainId: SupportedChainId.MAINNET,
}),
{ wrapper },
)

expect(result.current.recentTokens).toHaveLength(1)
expect(result.current.recentTokens[0].symbol).toBe('CUSTOM')
})

it('omits persisted tokens outside the allowed token set', () => {
const allowedToken = createTestToken(SupportedChainId.MAINNET, ADDRESS_1, 'ALLOWED')
const blockedToken = createTestToken(SupportedChainId.MAINNET, ADDRESS_2, 'BLOCKED')

setStoredTokens({
[SupportedChainId.MAINNET]: [
createStoredToken(SupportedChainId.MAINNET, ADDRESS_2, 'BLOCKED'),
createStoredToken(SupportedChainId.MAINNET, ADDRESS_1, 'ALLOWED'),
],
})
const store = createStoreWithLocalStorage()
const wrapper = createTestWrapper(store)

const { result } = renderHook(
() =>
useRecentTokens({
allTokens: [allowedToken, blockedToken],
favoriteTokens: [],
allowedTokens: [allowedToken],
activeChainId: SupportedChainId.MAINNET,
}),
{ wrapper },
)

expect(result.current.recentTokens).toEqual([allowedToken])
})

it('filters blocked tokens before applying the recent-token limit', () => {
const allowedToken = createTestToken(SupportedChainId.MAINNET, ADDRESS_1, 'ALLOWED')
const blockedToken = createTestToken(SupportedChainId.MAINNET, ADDRESS_2, 'BLOCKED')

setStoredTokens({
[SupportedChainId.MAINNET]: [
createStoredToken(SupportedChainId.MAINNET, ADDRESS_2, 'BLOCKED'),
createStoredToken(SupportedChainId.MAINNET, ADDRESS_1, 'ALLOWED'),
],
})
const store = createStoreWithLocalStorage()
const wrapper = createTestWrapper(store)

const { result } = renderHook(
() =>
useRecentTokens({
allTokens: [allowedToken, blockedToken],
favoriteTokens: [],
allowedTokens: [allowedToken],
activeChainId: SupportedChainId.MAINNET,
maxItems: 1,
}),
{ wrapper },
)

expect(result.current.recentTokens).toEqual([allowedToken])
})

it('does not return tokens from other chains', () => {
setStoredTokens({
[SupportedChainId.GNOSIS_CHAIN]: [createStoredToken(SupportedChainId.GNOSIS_CHAIN, ADDRESS_1, 'TKN')],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useHydratedRecentTokens } from './useHydratedRecentTokens'
import { useRecentTokensStorage } from './useRecentTokensStorage'

import {
buildFavoriteTokenKeys,
buildTokenKeySet,
buildTokensByKey,
persistRecentTokenSelection as persistRecentTokenSelectionInternal,
RECENT_TOKENS_LIMIT,
Expand All @@ -21,18 +21,21 @@ export interface RecentTokensState {
interface UseRecentTokensParams {
allTokens: TokenWithLogo[]
favoriteTokens: TokenWithLogo[]
allowedTokens?: TokenWithLogo[]
activeChainId?: number
maxItems?: number
}

export function useRecentTokens({
allTokens,
favoriteTokens,
allowedTokens,
activeChainId,
maxItems = RECENT_TOKENS_LIMIT,
}: UseRecentTokensParams): RecentTokensState {
const tokensByKey = useMemo(() => buildTokensByKey(allTokens), [allTokens])
const favoriteKeys = useMemo(() => buildFavoriteTokenKeys(favoriteTokens), [favoriteTokens])
const favoriteKeys = useMemo(() => buildTokenKeySet(favoriteTokens), [favoriteTokens])
const allowedTokenKeys = useMemo(() => (allowedTokens ? buildTokenKeySet(allowedTokens) : undefined), [allowedTokens])

const {
storedTokensByChain,
Expand All @@ -47,6 +50,7 @@ export function useRecentTokens({
storedTokensByChain,
tokensByKey,
favoriteKeys,
allowedTokenKeys,
activeChainId,
maxItems,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ describe('useTokenListContext', () => {
isLoading: false,
tokens: [mockToken],
favoriteTokens: [mockToken],
allowedRecentTokens: [mockToken],
hasScopedListRestriction: true,
areTokensFromBridge: true,
isRouteAvailable: true,
bridgeSupportedTokensMap: { [mockToken.address.toLowerCase()]: true },
Expand All @@ -120,7 +122,26 @@ describe('useTokenListContext', () => {
it('uses resolved default chain for active chain and exposed target chain', () => {
const { result } = renderHook(() => useTokenListContext())

expect(mockUseRecentTokenSection).toHaveBeenCalledWith([mockToken], [mockToken], SupportedChainId.LINEA)
expect(mockUseRecentTokenSection).toHaveBeenCalledWith([mockToken], [mockToken], SupportedChainId.LINEA, [
mockToken,
])
expect(result.current.selectedTargetChainId).toBe(SupportedChainId.LINEA)
})

it('preserves unrestricted recent-token hydration when no allowed set is provided', () => {
mockUseTokensToSelect.mockReturnValue({
isLoading: false,
tokens: [mockToken],
favoriteTokens: [],
allowedRecentTokens: undefined,
hasScopedListRestriction: false,
areTokensFromBridge: false,
isRouteAvailable: undefined,
bridgeSupportedTokensMap: null,
})

renderHook(() => useTokenListContext())

expect(mockUseRecentTokenSection).toHaveBeenCalledWith([mockToken], [], SupportedChainId.LINEA, undefined)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface TokenListContext {
// Loading state
areTokensLoading: boolean
areTokensFromBridge: boolean
hasScopedListRestriction: boolean

// Bridge support map (null when loading, populated when bridge tokens are fetched)
bridgeSupportedTokensMap: Record<string, boolean> | null
Expand Down Expand Up @@ -59,6 +60,7 @@ export function useTokenListContext(): TokenListContext {
tokensState.tokens,
tokensState.favoriteTokens,
activeChainId,
tokensState.allowedRecentTokens,
)

// Favorite tokens (empty in standalone mode)
Expand All @@ -77,6 +79,7 @@ export function useTokenListContext(): TokenListContext {
recentTokens,
areTokensLoading: tokensState.isLoading,
areTokensFromBridge: tokensState.areTokensFromBridge,
hasScopedListRestriction: tokensState.hasScopedListRestriction,
bridgeSupportedTokensMap: tokensState.bridgeSupportedTokensMap,
hideFavoriteTokensTooltip: isInjectedWidget(),
selectedTargetChainId,
Expand All @@ -88,6 +91,7 @@ export function useTokenListContext(): TokenListContext {
tokensState.tokens,
tokensState.isLoading,
tokensState.areTokensFromBridge,
tokensState.hasScopedListRestriction,
tokensState.bridgeSupportedTokensMap,
favoriteTokens,
recentTokens,
Expand Down
Loading
Loading