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
3 changes: 1 addition & 2 deletions src/constants/tokenLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
* @dev Here you can add the list of tokens you want to use in the app
* The list follow the standard from: https://tokenlists.org/
*
* Token list must complain with the Schema defined in /src/token.ts
* Token list must comply with the Schema defined in /src/token.ts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤣

*/
export const tokenLists = {
'1INCH': 'https://ipfs.io/ipns/tokens.1inch.eth',
COINGECKO: 'https://tokens.coingecko.com/uniswap/all.json',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we know about some other alternatives we could add here? Just in case this one dies too.

} as const
86 changes: 85 additions & 1 deletion src/hooks/useTokenLists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ vi.mock('@tanstack/react-query', async (importActual) => {
})

import * as tanstackQuery from '@tanstack/react-query'
import { useTokenLists } from './useTokenLists'
import { fetchTokenList, useTokenLists } from './useTokenLists'

const mockFetch = vi.fn()
vi.stubGlobal('fetch', mockFetch)

const mockToken1: Token = {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
Expand Down Expand Up @@ -73,6 +76,87 @@ beforeEach(() => {
})
})

describe('fetchTokenList', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('returns empty token list on HTTP error', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 504,
statusText: 'Gateway Timeout',
})

const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = await fetchTokenList('https://example.com/tokens.json')

expect(result.tokens).toEqual([])
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Token list fetch failed'))
warnSpy.mockRestore()
})

it('returns empty token list on network error', async () => {
mockFetch.mockRejectedValue(new Error('Network error'))

const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = await fetchTokenList('https://example.com/tokens.json')

expect(result.tokens).toEqual([])
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Token list fetch failed'),
'Network error',
)
warnSpy.mockRestore()
})

it('returns empty token list on invalid JSON schema', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ error: 'not a token list' }),
})

const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = await fetchTokenList('https://example.com/tokens.json')

expect(result.tokens).toEqual([])
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid schema'))
warnSpy.mockRestore()
})

it('returns token list on valid response', async () => {
const validTokenList = {
name: 'Test',
timestamp: '2026-01-01',
version: { major: 1, minor: 0, patch: 0 },
tokens: [{ symbol: 'ETH', name: 'Ether', address: '0x0', chainId: 1, decimals: 18 }],
}

mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve(validTokenList),
})

const result = await fetchTokenList('https://example.com/tokens.json')

expect(result.tokens).toHaveLength(1)
expect(result.tokens[0].symbol).toBe('ETH')
})

it('returns empty token list when tokens field is not an array', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ tokens: 'not an array' }),
})

const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = await fetchTokenList('https://example.com/tokens.json')

expect(result.tokens).toEqual([])
warnSpy.mockRestore()
})
})

describe('useTokenLists', () => {
it('returns tokens and tokensByChainId', () => {
vi.mocked(tanstackQuery.useSuspenseQueries).mockReturnValueOnce(
Expand Down
46 changes: 34 additions & 12 deletions src/hooks/useTokenLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export const useTokenLists = (): TokensMap => {
queries: tokenListUrls.map<UseSuspenseQueryOptions<TokenList>>((url) => ({
queryKey: ['tokens-list', url],
queryFn: () => fetchTokenList(url),
staleTime: Number.POSITIVE_INFINITY,
gcTime: Number.POSITIVE_INFINITY,
staleTime: 60 * 60 * 1000,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Super minor, but maybe put these in a const

gcTime: 60 * 60 * 1000,
})),
combine: combineTokenLists,
})
Expand Down Expand Up @@ -145,26 +145,48 @@ function combineTokenLists(results: Array<UseSuspenseQueryResult<TokenList>>): T
return tokensMap
}

const emptyTokenList: TokenList = {
name: '',
timestamp: '',
version: { major: 0, minor: 0, patch: 0 },
tokens: [],
}

/**
* A wrapper around fetch, to return the parsed JSON or throw an error if something goes wrong
* Fetches a token list from a URL. Returns an empty token list on failure
* instead of throwing, so one broken source doesn't block the entire app.
*
* @param url - a link to a list of tokens or 'default' to use the list added as a dependency to the project
* @returns {Promise<TokenList>} a token list
* @param url - a link to a list of tokens or 'default' to use the bundled list
* @returns a token list (empty on failure)
*/
async function fetchTokenList(url: string): Promise<TokenList> {
export async function fetchTokenList(url: string): Promise<TokenList> {
if (url === 'default') {
return defaultTokens as TokenList
}

const result = await fetch(url)
try {
const result = await fetch(url)

if (!result.ok) {
console.warn(`Token list fetch failed for ${url}: HTTP ${result.status} ${result.statusText}`)
return emptyTokenList
}

if (!result.ok) {
throw new Error(
`Something went wrong. HTTP status code: ${result.status}. Status Message: ${result.statusText}`,
const data = await result.json()

if (!data || typeof data !== 'object' || !Array.isArray(data.tokens)) {
console.warn(`Token list fetch for ${url} returned invalid schema; expected a tokens array.`)
return emptyTokenList
}

return data as TokenList
} catch (error) {
console.warn(
`Token list fetch failed for ${url}:`,
error instanceof Error ? error.message : error,
)
return emptyTokenList
}

return result.json()
}

/**
Expand Down
Loading