Skip to content
Merged
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
24 changes: 24 additions & 0 deletions src/hooks/useTokenLists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,30 @@ describe('useTokenLists', () => {
expect(nativeToken?.symbol).toBe('ETH')
})

it('filters out tokens whose chainId is not present in viem/chains and does not log', () => {
// biome-ignore lint/suspicious/noExplicitAny: mocking internal combine param
vi.mocked(tanstackQuery.useSuspenseQueries).mockImplementation(({ combine }: any) => {
const ropstenToken: Token = {
address: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
chainId: 3,
decimals: 18,
name: 'Wrapped Ether (Ropsten)',
symbol: 'WETH',
}
return combine([mockSuspenseQueryResult([mockToken1, ropstenToken])])
})

const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const { result } = renderHook(() => useTokenLists(), { wrapper })

expect(result.current.tokens.some((t) => t.chainId === 3)).toBe(false)
expect(result.current.tokensByChainId[3]).toBeUndefined()
expect(errorSpy).not.toHaveBeenCalled()
expect(result.current.tokens.some((t) => t.address === mockToken1.address)).toBe(true)

errorSpy.mockRestore()
})

it('filters out tokens that fail schema validation', () => {
// biome-ignore lint/suspicious/noExplicitAny: mocking internal combine param
vi.mocked(tanstackQuery.useSuspenseQueries).mockImplementation(({ combine }: any) => {
Expand Down
36 changes: 14 additions & 22 deletions src/hooks/useTokenLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { type Token, type TokenList, tokenSchema } from '@/src/types/token'
import { logger } from '@/src/utils/logger'
import tokenListsCache, { type TokensMap, updateTokenListsCache } from '@/src/utils/tokenListsCache'

const chainsById: Map<number, (typeof chains)[keyof typeof chains]> = new Map()
for (const chain of Object.values(chains)) {
if (!chainsById.has(chain.id)) chainsById.set(chain.id, chain)
}

/**
* Loads and processes token lists from configured sources
*
Expand Down Expand Up @@ -108,15 +113,11 @@ function combineTokenLists(results: Array<UseSuspenseQueryResult<TokenList>>): T
new Map(
results
.flatMap((result) => result.data.tokens)
// tokenSchema enforces EVM address format (0x + 40 hex chars), so non-EVM entries
// (e.g. Solana tokens from @uniswap/default-token-list v18+) are silently dropped here.
// Supporting non-EVM chains would require changes to the address schema, chain config,
// wallet integration, and contract lookup -- out of scope for this EVM-focused starter kit.
.filter((token) => {
const result = tokenSchema.safeParse(token)

return result.success
})
// tokenSchema enforces EVM address format, so non-EVM entries (e.g. Solana in
// @uniswap/default-token-list v18+) are dropped. Tokens on chainIds absent from
// viem/chains are also dropped, so buildNativeToken cannot throw and
// tokensByChainId never accumulates unreachable buckets.
.filter((token) => tokenSchema.safeParse(token).success && chainsById.has(token.chainId))
.map((token) => [tokenKey(token), token]),
).values(),
)
Expand All @@ -126,18 +127,9 @@ function combineTokenLists(results: Array<UseSuspenseQueryResult<TokenList>>): T
const tokensMap = uniqueTokens.reduce<TokensMap>(
(acc, token) => {
if (!acc.tokensByChainId[token.chainId]) {
try {
// if there's a native token for the chain
const nativeToken = buildNativeToken(token.chainId)

// add it to the list
acc.tokensByChainId[token.chainId] = [nativeToken]
acc.tokens.push(nativeToken)
} catch (err) {
console.error(err)
// if there's no native token for the chain, ignore the error
acc.tokensByChainId[token.chainId] = []
}
const nativeToken = buildNativeToken(token.chainId)
acc.tokensByChainId[token.chainId] = [nativeToken]
acc.tokens.push(nativeToken)
}

acc.tokens.push(token)
Expand Down Expand Up @@ -208,7 +200,7 @@ export async function fetchTokenList(url: string): Promise<TokenList> {
* @returns The native token object.
*/
function buildNativeToken(chainId: Token['chainId']): Token {
const tokenInfo = Object.values(chains).find((chain) => chain.id === chainId)?.nativeCurrency
const tokenInfo = chainsById.get(chainId)?.nativeCurrency

if (!tokenInfo) {
throw new Error(`Native token not found for chain ID: ${chainId}`)
Expand Down
Loading