diff --git a/src/core/resources/search/tokenDiscovery.ts b/src/core/resources/search/tokenDiscovery.ts index 42c1219091..e7a1c92ed3 100644 --- a/src/core/resources/search/tokenDiscovery.ts +++ b/src/core/resources/search/tokenDiscovery.ts @@ -12,11 +12,7 @@ const tokenSearchDiscoveryHttp = createHttpClient({ timeout: 30000, }); -type TokenDiscoveryArgs = { - chainId: ChainId; -}; - -const tokenDiscoveryQueryKey = ({ chainId }: TokenDiscoveryArgs) => +const tokenDiscoveryQueryKey = ({ chainId }: { chainId: ChainId }) => createQueryKey('TokenDiscovery', { chainId }, { persisterVersion: 1 }); async function tokenSearchQueryFunction({ @@ -36,12 +32,18 @@ async function tokenSearchQueryFunction({ } } -export function useTokenDiscovery({ chainId }: TokenDiscoveryArgs) { +export function useTokenDiscovery({ + chainId, + select, +}: { + chainId: ChainId; + select?: (data: SearchAsset[]) => T; +}) { return useQuery({ queryKey: tokenDiscoveryQueryKey({ chainId }), queryFn: tokenSearchQueryFunction, staleTime: 15 * 60 * 1000, // 15 min gcTime: 24 * 60 * 60 * 1000, // 1 day - select: (data) => data.slice(0, 3), + select, }); } diff --git a/src/core/resources/search/tokenSearch.ts b/src/core/resources/search/tokenSearch.ts index 42743502f6..91ca7f2243 100644 --- a/src/core/resources/search/tokenSearch.ts +++ b/src/core/resources/search/tokenSearch.ts @@ -1,4 +1,3 @@ -import { isAddress } from '@ethersproject/address'; import { useQueries, useQuery } from '@tanstack/react-query'; import qs from 'qs'; @@ -27,9 +26,7 @@ import { parseTokenSearch } from './parseTokenSearch'; export type TokenSearchArgs = { chainId: ChainId; fromChainId?: ChainId | ''; - keys: TokenSearchAssetKey[]; list: TokenSearchListId; - threshold: TokenSearchThreshold; query: string; }; @@ -46,14 +43,12 @@ export type TokenSearchAllNetworksArgs = { const tokenSearchQueryKey = ({ chainId, fromChainId, - keys, list, - threshold, query, }: TokenSearchArgs) => createQueryKey( 'TokenSearch', - { chainId, fromChainId, keys, list, threshold, query }, + { chainId, fromChainId, list, query }, { persisterVersion: 2 }, ); @@ -63,26 +58,19 @@ type TokenSearchQueryKey = ReturnType; // Query Function async function tokenSearchQueryFunction({ - queryKey: [{ chainId, fromChainId, keys, list, threshold, query }], + queryKey: [{ chainId, fromChainId, list, query }], }: QueryFunctionArgs) { const queryParams: { - keys: string; list: TokenSearchListId; - threshold: TokenSearchThreshold; query?: string; fromChainId?: number; } = { - keys: keys.join(','), list, - threshold, query, }; if (fromChainId) { queryParams.fromChainId = fromChainId; } - if (isAddress(query)) { - queryParams.keys = `networks.${chainId}.address`; - } const url = `/${chainId}/?${qs.stringify(queryParams)}`; try { const tokenSearch = await tokenSearchHttp.get<{ data: SearchAsset[] }>(url); @@ -100,7 +88,7 @@ type TokenSearchResult = QueryFunctionResult; // Query Fetcher export async function fetchTokenSearch( - { chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs, + { chainId, fromChainId, list, query }: TokenSearchArgs, config: QueryConfig< TokenSearchResult, Error, @@ -112,9 +100,7 @@ export async function fetchTokenSearch( queryKey: tokenSearchQueryKey({ chainId, fromChainId, - keys, list, - threshold, query, }), queryFn: tokenSearchQueryFunction, @@ -126,7 +112,7 @@ export async function fetchTokenSearch( // Query Hook export function useTokenSearch( - { chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs, + { chainId, fromChainId, list, query }: TokenSearchArgs, config: QueryConfig< TokenSearchResult, Error, @@ -138,9 +124,7 @@ export function useTokenSearch( queryKey: tokenSearchQueryKey({ chainId, fromChainId, - keys, list, - threshold, query, }), queryFn: tokenSearchQueryFunction, @@ -152,12 +136,7 @@ export function useTokenSearch( // Query Hook export function useTokenSearchAllNetworks( - { - keys, - list, - threshold, - query, - }: Omit, + { list, query }: Omit, config: QueryConfig< TokenSearchResult, Error, @@ -172,13 +151,7 @@ export function useTokenSearchAllNetworks( const queries = useQueries({ queries: rainbowSupportedChains.map(({ id: chainId }) => { return { - queryKey: tokenSearchQueryKey({ - chainId, - keys, - list, - threshold, - query, - }), + queryKey: tokenSearchQueryKey({ chainId, list, query }), queryFn: tokenSearchQueryFunction, refetchOnWindowFocus: false, ...config, diff --git a/src/entries/popup/components/CommandK/useSearchableTokens.ts b/src/entries/popup/components/CommandK/useSearchableTokens.ts index 0ebe4197cc..2d614aec9f 100644 --- a/src/entries/popup/components/CommandK/useSearchableTokens.ts +++ b/src/entries/popup/components/CommandK/useSearchableTokens.ts @@ -18,7 +18,6 @@ import { useHideSmallBalancesStore } from '~/core/state/currentSettings/hideSmal import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { useHiddenAssetStore } from '~/core/state/hiddenAssets/hiddenAssets'; import { ParsedUserAsset } from '~/core/types/assets'; -import { TokenSearchAssetKey, TokenSearchThreshold } from '~/core/types/search'; import { isENSAddressFormat } from '~/core/utils/ethereum'; import { isLowerCaseMatch } from '~/core/utils/strings'; @@ -70,21 +69,6 @@ export const useSearchableTokens = ({ !isENSAddressFormat(debouncedSearchQuery) && !testnetMode; - const queryIsAddress = useMemo( - () => isAddress(debouncedSearchQuery), - [debouncedSearchQuery], - ); - - const keys: TokenSearchAssetKey[] = useMemo( - () => (queryIsAddress ? ['address'] : ['name', 'symbol']), - [queryIsAddress], - ); - - const threshold: TokenSearchThreshold = useMemo( - () => (queryIsAddress ? 'CASE_SENSITIVE_EQUAL' : 'CONTAINS'), - [queryIsAddress], - ); - const enableSearchChainAssets = isAddress(query) && !testnetMode; // All on chain searched assets from all user chains @@ -111,8 +95,6 @@ export const useSearchableTokens = ({ } = useTokenSearchAllNetworks( { list: 'verifiedAssets', - keys, - threshold, query: debouncedSearchQuery, }, { @@ -131,8 +113,6 @@ export const useSearchableTokens = ({ } = useTokenSearchAllNetworks( { list: 'highLiquidityAssets', - keys, - threshold, query: debouncedSearchQuery, }, { diff --git a/src/entries/popup/hooks/useFavoriteAssets.ts b/src/entries/popup/hooks/useFavoriteAssets.ts index e2414ecd5f..afd3ea5b0a 100644 --- a/src/entries/popup/hooks/useFavoriteAssets.ts +++ b/src/entries/popup/hooks/useFavoriteAssets.ts @@ -1,91 +1,42 @@ -import { isAddress } from 'ethers/lib/utils'; -import { useCallback, useEffect, useState } from 'react'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { createQueryKey } from '~/core/react-query'; import { fetchTokenSearch } from '~/core/resources/search/tokenSearch'; import { useFavoritesStore } from '~/core/state/favorites'; -import { ParsedAsset } from '~/core/types/assets'; +import { AddressOrEth } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; -import { SearchAsset } from '~/core/types/search'; -type FavoriteAssets = Record; -const FAVORITES_EMPTY_STATE = { - [ChainId.mainnet]: [], - [ChainId.optimism]: [], - [ChainId.bsc]: [], - [ChainId.polygon]: [], - [ChainId.arbitrum]: [], - [ChainId.base]: [], - [ChainId.zora]: [], - [ChainId.avalanche]: [], - [ChainId.hardhat]: [], - [ChainId.hardhatOptimism]: [], -}; +async function fetchFavoriteToken(address: AddressOrEth, chain: ChainId) { + const results = await fetchTokenSearch({ + chainId: chain, + list: 'verifiedAssets', + query: address.toLowerCase(), + }); + if (results?.[0]) return results[0]; -// expensive hook, only use in top level parent components -export function useFavoriteAssets() { - const { favorites } = useFavoritesStore(); - const [favoritesData, setFavoritesData] = useState( - FAVORITES_EMPTY_STATE, - ); - - const setFavoriteAssetsData = useCallback(async () => { - const chainIds = Object.keys(favorites) - .filter((k) => favorites?.[parseInt(k)]) - .map((c) => +c); - const searches: Promise[] = []; - const newSearchData = {} as Record; - for (const chain of chainIds) { - const addressesByChain = favorites[chain]; - addressesByChain?.forEach((address) => { - const searchAddress = async (add: string) => { - const query = add.toLocaleLowerCase(); - const queryIsAddress = isAddress(query); - const keys = ( - queryIsAddress ? ['address'] : ['name', 'symbol'] - ) as (keyof ParsedAsset)[]; - const threshold = queryIsAddress - ? 'CASE_SENSITIVE_EQUAL' - : 'CONTAINS'; - const results = await fetchTokenSearch({ - chainId: chain, - keys, - list: 'verifiedAssets', - threshold, - query, - }); + const unverifiedSearchResults = await fetchTokenSearch({ + chainId: chain, + list: 'highLiquidityAssets', + query: address.toLowerCase(), + }); + if (!unverifiedSearchResults?.[0]) return unverifiedSearchResults[0]; +} - const currentFavoritesData = newSearchData[chain] || []; - if (results?.[0]) { - newSearchData[chain] = [...currentFavoritesData, results[0]]; - } else { - const unverifiedSearchResults = await fetchTokenSearch({ - chainId: chain, - keys, - list: 'highLiquidityAssets', - threshold, - query, - }); - if (unverifiedSearchResults?.[0]) { - // eslint-disable-next-line require-atomic-updates - newSearchData[chain] = [ - ...currentFavoritesData, - unverifiedSearchResults[0], - ]; - } - } - }; - searches.push(searchAddress(address)); - }); - } - await Promise.all(searches); - setFavoritesData(newSearchData); - }, [favorites]); +export function useFavoriteAssets(chainId: ChainId) { + const favorites = useFavoritesStore((s) => s.favorites[chainId]); - useEffect(() => { - setFavoriteAssetsData(); - }, [setFavoriteAssetsData]); + const { data = [] } = useQuery({ + queryKey: createQueryKey('favorites assets', { chainId, favorites }), + queryFn: async () => { + if (!favorites) throw new Error('No chain favorites'); + return ( + await Promise.all( + favorites.map((address) => fetchFavoriteToken(address, chainId)), + ) + ).filter(Boolean); + }, + placeholderData: keepPreviousData, + }); - return { - favorites: favoritesData, - }; + return { favorites: data }; } diff --git a/src/entries/popup/hooks/useSearchCurrencyLists.ts b/src/entries/popup/hooks/useSearchCurrencyLists.ts index 23765e61e6..6a79c986dd 100644 --- a/src/entries/popup/hooks/useSearchCurrencyLists.ts +++ b/src/entries/popup/hooks/useSearchCurrencyLists.ts @@ -1,6 +1,5 @@ import { isAddress } from '@ethersproject/address'; import { uniqBy } from 'lodash'; -import { rankings } from 'match-sorter'; import { useCallback, useMemo } from 'react'; import { Address } from 'viem'; @@ -12,30 +11,18 @@ import { useTokenSearchAllNetworks } from '~/core/resources/search/tokenSearch'; import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode'; import { ParsedSearchAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; -import { - SearchAsset, - TokenSearchAssetKey, - TokenSearchListId, - TokenSearchThreshold, -} from '~/core/types/search'; +import { SearchAsset, TokenSearchListId } from '~/core/types/search'; import { isSameAsset } from '~/core/utils/assets'; import { getChain, isNativeAsset } from '~/core/utils/chains'; -import { addHexPrefix } from '~/core/utils/hex'; import { isLowerCaseMatch } from '~/core/utils/strings'; -import { filterList } from '../utils/search'; - import { useFavoriteAssets } from './useFavoriteAssets'; const VERIFIED_ASSETS_PAYLOAD: { - keys: TokenSearchAssetKey[]; list: TokenSearchListId; - threshold: TokenSearchThreshold; query: string; } = { - keys: ['symbol', 'name'], list: 'verifiedAssets', - threshold: 'CONTAINS', query: '', }; @@ -83,6 +70,18 @@ function difference( }); } +function queryMatchesAsset( + query: string, + { symbol, name, address, mainnetAddress }: SearchAsset, +) { + return ( + symbol.toLowerCase().includes(query) || + name.toLowerCase().includes(query) || + isLowerCaseMatch(address, query) || + isLowerCaseMatch(mainnetAddress, query) + ); +} + export function useSearchCurrencyLists({ assetToSell, inputChainId, @@ -102,30 +101,15 @@ export function useSearchCurrencyLists({ const query = searchQuery?.toLowerCase() || ''; const enableUnverifiedSearch = query.trim().length > 2; - const isCrosschainSearch = useMemo(() => { - return inputChainId && inputChainId !== outputChainId; - }, [inputChainId, outputChainId]); + const isCrosschainSearch = inputChainId && inputChainId !== outputChainId; // provided during swap to filter token search by available routes - const fromChainId = useMemo(() => { - return isCrosschainSearch ? inputChainId : undefined; - }, [inputChainId, isCrosschainSearch]); - - const queryIsAddress = useMemo(() => isAddress(query), [query]); - - const keys: TokenSearchAssetKey[] = useMemo( - () => (queryIsAddress ? ['address'] : ['name', 'symbol']), - [queryIsAddress], - ); - - const threshold: TokenSearchThreshold = useMemo( - () => (queryIsAddress ? 'CASE_SENSITIVE_EQUAL' : 'CONTAINS'), - [queryIsAddress], - ); + const fromChainId = isCrosschainSearch ? inputChainId : undefined; const { testnetMode } = useTestnetModeStore(); - const enableAllNetworkTokenSearch = queryIsAddress && !testnetMode && !bridge; + const enableAllNetworkTokenSearch = + isAddress(query) && !testnetMode && !bridge; const networkSearchStatus = enableAllNetworkTokenSearch ? AssetToBuyNetworkSearchStatus.all @@ -226,9 +210,7 @@ export function useSearchCurrencyLists({ useTokenSearch( { chainId: outputChainId, - keys, list: 'verifiedAssets', - threshold, query, fromChainId, }, @@ -242,9 +224,7 @@ export function useSearchCurrencyLists({ } = useTokenSearch( { chainId: outputChainId, - keys, list: 'highLiquidityAssets', - threshold, query, fromChainId, }, @@ -255,12 +235,7 @@ export function useSearchCurrencyLists({ // All verified assets from all user chains const { data: targetAllNetworksUnverifiedAssets } = useTokenSearchAllNetworks( - { - keys, - list: 'highLiquidityAssets', - threshold, - query, - }, + { list: 'highLiquidityAssets', query }, { select: (data: SearchAsset[]) => { if (!enableAllNetworkTokenSearch) return []; @@ -273,12 +248,7 @@ export function useSearchCurrencyLists({ // All verified assets from all user chains const { data: targetAllNetworksVerifiedAssets } = useTokenSearchAllNetworks( - { - keys, - list: 'verifiedAssets', - threshold, - query, - }, + { list: 'verifiedAssets', query }, { select: (data: SearchAsset[]) => { if (!enableAllNetworkTokenSearch) return []; @@ -307,30 +277,22 @@ export function useSearchCurrencyLists({ const { data: popularAssets = [] } = useTokenDiscovery({ chainId: outputChainId, + select(popularAssets) { + if (!query) return popularAssets.slice(0, 3); + const a = popularAssets.filter((asset) => + queryMatchesAsset(query, asset), + ); + console.log('popularAssets', { a }); + return a; + }, }); - const { favorites } = useFavoriteAssets(); + const { favorites } = useFavoriteAssets(outputChainId); const favoritesList = useMemo(() => { - const favoritesByChain = favorites[outputChainId] || []; - if (query === '') { - return favoritesByChain; - } else { - const formattedQuery = queryIsAddress - ? addHexPrefix(query).toLowerCase() - : query; - return filterList( - favoritesByChain || [], - formattedQuery, - keys, - { - threshold: queryIsAddress - ? rankings.CASE_SENSITIVE_EQUAL - : rankings.CONTAINS, - }, - ); - } - }, [favorites, keys, outputChainId, query, queryIsAddress]); + if (!query) return favorites; + return favorites.filter((asset) => queryMatchesAsset(query, asset)); + }, [favorites, query]); // static verified asset lists prefetched to display curated lists // we only display crosschain exact matches if located here @@ -609,7 +571,7 @@ export function useSearchCurrencyLists({ if (!sections.length && crosschainExactMatches?.length) { sections.push({ - data: difference(crosschainExactMatches, otherSectionsAssets), + data: crosschainExactMatches, id: 'other_networks', }); }