From 87b9e341548b860319e71fc94cb87e0de7734d90 Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 16 Dec 2024 16:25:51 -0500 Subject: [PATCH 01/23] Remove slow search --- src/hooks/useSearchCurrencyList.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index f3dfca25865..7207e37b763 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -296,16 +296,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main await Promise.all(categories.map(assetType => getResultsForAssetType(assetType))); }, [searchChainId, getResultsForAssetType]); - const slowSearch = useCallback(async () => { - try { - await getResultsForAssetType('lowLiquidityAssets'); - // eslint-disable-next-line no-empty - } catch (e) { - } finally { - setLoading(false); - } - }, [getResultsForAssetType]); - const clearSearch = useCallback(() => { getResultsForAssetType('curatedAssets'); setLowLiquidityAssets([]); @@ -329,7 +319,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main if ((searching && !wasSearching) || (searching && previousSearchQuery !== searchQuery) || searchChainId !== previousChainId) { if (searchChainId === ChainId.mainnet) { search(); - slowSearch(); } else { await search(); setLowLiquidityAssets([]); From 4b50d28cd0597df06544864aaa723305411052c7 Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 16 Dec 2024 16:29:43 -0500 Subject: [PATCH 02/23] Remove curated and rainbow token list usage from discover search currency list --- src/hooks/useSearchCurrencyList.ts | 49 ++---------------------------- 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 7207e37b763..da6349b09be 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -10,7 +10,7 @@ import { RainbowToken, TokenSearchTokenListId } from '@/entities'; import { tokenSearch } from '@/handlers/tokenSearch'; import { addHexPrefix, getProvider } from '@/handlers/web3'; import tokenSectionTypes from '@/helpers/tokenSectionTypes'; -import { DAI_ADDRESS, erc20ABI, ETH_ADDRESS, rainbowTokenList, USDC_ADDRESS, WBTC_ADDRESS, WETH_ADDRESS } from '@/references'; +import { erc20ABI } from '@/references'; import { filterList, isLowerCaseMatch } from '@/utils'; import { logger } from '@/logger'; import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; @@ -20,13 +20,7 @@ import { getUniqueId } from '@/utils/ethereumUtils'; import { ChainId } from '@/state/backendNetworks/types'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; -type swapCurrencyListType = - | 'verifiedAssets' - | 'highLiquidityAssets' - | 'lowLiquidityAssets' - | 'favoriteAssets' - | 'curatedAssets' - | 'importedAssets'; +type swapCurrencyListType = 'verifiedAssets' | 'highLiquidityAssets' | 'lowLiquidityAssets' | 'favoriteAssets' | 'importedAssets'; type CrosschainVerifiedAssets = Record< ChainId.mainnet | ChainId.optimism | ChainId.polygon | ChainId.bsc | ChainId.arbitrum, @@ -76,7 +70,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main const searching = useMemo(() => searchQuery !== '' || ChainId.mainnet !== searchChainId, [searchChainId, searchQuery]); const { favorites: favoriteAddresses, favoritesMetadata: favoriteMap } = useFavorites(); - const curatedMap = rainbowTokenList.CURATED_TOKENS; const unfilteredFavorites = Object.values(favoriteMap).filter(token => token.networks[searchChainId]); const [loading, setLoading] = useState(true); @@ -130,28 +123,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main [isFavorite] ); - const getCurated = useCallback(() => { - const addresses = favoriteAddresses.map(a => a.toLowerCase()); - return Object.values(curatedMap) - .filter(({ address }) => !addresses.includes(address.toLowerCase())) - .sort((t1, t2) => { - const { address: address1, name: name1 } = t1; - const { address: address2, name: name2 } = t2; - const mainnetPriorityTokens = [ETH_ADDRESS, WETH_ADDRESS, DAI_ADDRESS, USDC_ADDRESS, WBTC_ADDRESS]; - const rankA = mainnetPriorityTokens.findIndex(address => address === address1.toLowerCase()); - const rankB = mainnetPriorityTokens.findIndex(address => address === address2.toLowerCase()); - const aIsRanked = rankA > -1; - const bIsRanked = rankB > -1; - if (aIsRanked) { - if (bIsRanked) { - return rankA > rankB ? -1 : 1; - } - return -1; - } - return bIsRanked ? 1 : name1?.localeCompare(name2); - }); - }, [curatedMap, favoriteAddresses]); - const getFavorites = useCallback(async () => { return searching ? await searchCurrencyList({ @@ -166,10 +137,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main async (searchQuery: string, chainId: number): Promise => { if (searching) { if (isAddress(searchQuery)) { - const tokenListEntry = rainbowTokenList.RAINBOW_TOKEN_LIST[searchQuery.toLowerCase()]; - if (tokenListEntry) { - return [tokenListEntry]; - } const provider = getProvider({ chainId }); const tokenContract = new Contract(searchQuery, erc20ABI, provider); try { @@ -297,7 +264,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main }, [searchChainId, getResultsForAssetType]); const clearSearch = useCallback(() => { - getResultsForAssetType('curatedAssets'); setLowLiquidityAssets([]); setHighLiquidityAssets([]); setVerifiedAssets([]); @@ -390,8 +356,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main }); } } else { - const curatedAssets = searchChainId === ChainId.mainnet && getCurated(); - if (unfilteredFavorites?.length) { list.push({ color: colors.yellowFavorite, @@ -400,14 +364,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main title: lang.t(`exchange.token_sections.${tokenSectionTypes.favoriteTokenSection}`), }); } - if (curatedAssets && curatedAssets.length) { - list.push({ - data: curatedAssets, - key: 'curated', - title: lang.t(`exchange.token_sections.${tokenSectionTypes.verifiedTokenSection}`), - useGradientText: !IS_TEST, - }); - } } return list; }, [ @@ -420,7 +376,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main favoriteAssets, searchChainId, colors.yellowFavorite, - getCurated, unfilteredFavorites, ]); From 64b7d25aa6ae8e87fa06d70db9c7559e7e949a1e Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 16 Dec 2024 17:13:23 -0500 Subject: [PATCH 03/23] Add support for useTokenSearchAllNetworks --- .../screens/Swap/resources/search/search.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index 52ed1d53443..3c13f19f482 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -5,9 +5,10 @@ import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { isAddress } from '@ethersproject/address'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueries } from '@tanstack/react-query'; import qs from 'qs'; import { parseTokenSearch } from './utils'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets'; @@ -144,3 +145,27 @@ export function useTokenSearch( keepPreviousData: true, }); } + +export function useTokenSearchAllNetworks( + { list, query }: Omit, + config: QueryConfigWithSelect = {} +) { + const getSupportedChainIds = useBackendNetworksStore(state => state.getSupportedChainIds); + const supportedChains = getSupportedChainIds(); + + const queries = useQueries({ + queries: supportedChains.map(id => { + return { + queryKey: tokenSearchQueryKey({ chainId: id, list, query }), + queryFn: tokenSearchQueryFunction, + refetchOnWindowFocus: false, + ...config, + }; + }), + }); + + return { + data: queries.map(({ data: assets }) => assets || []).flat(), + isFetching: queries.some(({ isFetching }) => isFetching), + }; +} From 839334513a4c9bb09b01ec761230755d83ae330e Mon Sep 17 00:00:00 2001 From: jinchung Date: Tue, 17 Dec 2024 09:55:14 -0500 Subject: [PATCH 04/23] Interim update to useSearchCurrencyLists --- src/components/Discover/DiscoverSearch.tsx | 4 +- src/hooks/useSearchCurrencyList.ts | 410 ++++----------------- 2 files changed, 76 insertions(+), 338 deletions(-) diff --git a/src/components/Discover/DiscoverSearch.tsx b/src/components/Discover/DiscoverSearch.tsx index 5aa4ac6d73b..59ba7baaaa6 100644 --- a/src/components/Discover/DiscoverSearch.tsx +++ b/src/components/Discover/DiscoverSearch.tsx @@ -72,7 +72,7 @@ export default function DiscoverSearch() { const lastSearchQuery = usePrevious(searchQueryForSearch); const [ensResults, setEnsResults] = useState([]); - const { swapCurrencyList, swapCurrencyListLoading } = useSearchCurrencyList(searchQueryForSearch, ChainId.mainnet); + const { swapCurrencyList, swapCurrencyListLoading } = useSearchCurrencyList(searchQueryForSearch); const profilesEnabled = useExperimentalFlag(PROFILES); const marginBottom = TAB_BAR_HEIGHT + safeAreaInsetValues.bottom + 16; @@ -83,7 +83,7 @@ export default function DiscoverSearch() { // 1. favorites // 2. verified // 3. profiles - // 4. unverified + // 4. unverified high liquidity // 5. low liquidity let list = swapCurrencyList; const listKeys = swapCurrencyList.map(item => item.key); diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index da6349b09be..3972ac6d2d7 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -1,31 +1,17 @@ import lang from 'i18n-js'; -import { getAddress, isAddress } from '@ethersproject/address'; -import { EthereumAddress } from '@rainbow-me/swaps'; -import { Contract } from '@ethersproject/contracts'; import { rankings } from 'match-sorter'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { useTheme } from '../theme/ThemeContext'; -import usePrevious from './usePrevious'; -import { RainbowToken, TokenSearchTokenListId } from '@/entities'; -import { tokenSearch } from '@/handlers/tokenSearch'; -import { addHexPrefix, getProvider } from '@/handlers/web3'; +import { addHexPrefix } from '@/handlers/web3'; import tokenSectionTypes from '@/helpers/tokenSectionTypes'; -import { erc20ABI } from '@/references'; -import { filterList, isLowerCaseMatch } from '@/utils'; -import { logger } from '@/logger'; -import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; +import { filterList } from '@/utils'; import { IS_TEST } from '@/env'; import { useFavorites } from '@/resources/favorites'; -import { getUniqueId } from '@/utils/ethereumUtils'; import { ChainId } from '@/state/backendNetworks/types'; -import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; - -type swapCurrencyListType = 'verifiedAssets' | 'highLiquidityAssets' | 'lowLiquidityAssets' | 'favoriteAssets' | 'importedAssets'; - -type CrosschainVerifiedAssets = Record< - ChainId.mainnet | ChainId.optimism | ChainId.polygon | ChainId.bsc | ChainId.arbitrum, - RainbowToken[] ->; +import { getUniqueId } from '@/utils/ethereumUtils'; +import { useTokenSearchAllNetworks } from '@/__swaps__/screens/Swap/resources/search/search'; +import { SearchAsset, TokenSearchAssetKey, TokenSearchThreshold } from '@/__swaps__/types/search'; +import { isAddress } from '@ethersproject/address'; const abcSort = (list: any[], key?: string) => { return list.sort((a, b) => { @@ -33,299 +19,94 @@ const abcSort = (list: any[], key?: string) => { }); }; -const searchCurrencyList = async (searchParams: { - chainId: number; - searchList: RainbowToken[] | TokenSearchTokenListId; - query: string; -}) => { - const { searchList, query, chainId } = searchParams; - const isAddress = query.match(/^(0x)?[0-9a-fA-F]{40}$/); - const keys: (keyof RainbowToken)[] = isAddress ? ['address'] : ['symbol', 'name']; - const formattedQuery = isAddress ? addHexPrefix(query).toLowerCase() : query; - if (typeof searchList === 'string') { - const threshold = isAddress ? 'CASE_SENSITIVE_EQUAL' : 'CONTAINS'; - if (chainId === ChainId.mainnet && !formattedQuery && searchList !== 'verifiedAssets') { - return []; - } - const ts = await tokenSearch({ - chainId, +const useSearchCurrencyList = (searchQuery: string) => { + const searching = useMemo(() => searchQuery !== '', [searchQuery]); + + const { favoritesMetadata: favoriteMap } = useFavorites(); + const unfilteredFavorites = useMemo(() => { + return Object.values(favoriteMap) + .filter(token => token.networks[ChainId.mainnet]) + .map(favToken => ({ + ...favToken, + favorite: true, + mainnetAddress: favToken.networks?.[ChainId.mainnet]?.address || favToken.mainnet_address, + uniqueId: getUniqueId(favToken.address, ChainId.mainnet), + })) as SearchAsset[]; + }, [favoriteMap]); + + const memoizedData = useMemo(() => { + const queryIsAddress = isAddress(searchQuery); + const keys: TokenSearchAssetKey[] = queryIsAddress ? ['address'] : ['name', 'symbol']; + const threshold: TokenSearchThreshold = queryIsAddress ? 'CASE_SENSITIVE_EQUAL' : 'CONTAINS'; + const enableUnverifiedSearch = searchQuery.length > 2; + + return { + queryIsAddress, keys, - list: searchList, threshold, - query: formattedQuery, - }); - return ts; - } else { - return ( - filterList(searchList, formattedQuery, keys, { - threshold: isAddress ? rankings.CASE_SENSITIVE_EQUAL : rankings.CONTAINS, - }) || [] - ); - } -}; - -const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.mainnet) => { - const previousChainId = usePrevious(searchChainId); - - const searching = useMemo(() => searchQuery !== '' || ChainId.mainnet !== searchChainId, [searchChainId, searchQuery]); - - const { favorites: favoriteAddresses, favoritesMetadata: favoriteMap } = useFavorites(); - const unfilteredFavorites = Object.values(favoriteMap).filter(token => token.networks[searchChainId]); - - const [loading, setLoading] = useState(true); - const [favoriteAssets, setFavoriteAssets] = useState([]); - const [importedAssets, setImportedAssets] = useState([]); - const [highLiquidityAssets, setHighLiquidityAssets] = useState([]); - const [lowLiquidityAssets, setLowLiquidityAssets] = useState([]); - const [verifiedAssets, setVerifiedAssets] = useState([]); - const [fetchingCrosschainAssets, setFetchingCrosschainAssets] = useState(false); - const [crosschainVerifiedAssets, setCrosschainVerifiedAssets] = useState({ - [ChainId.apechain]: [], - [ChainId.arbitrum]: [], - [ChainId.avalanche]: [], - [ChainId.base]: [], - [ChainId.bsc]: [], - [ChainId.blast]: [], - [ChainId.degen]: [], - [ChainId.gnosis]: [], - [ChainId.gravity]: [], - [ChainId.ink]: [], - [ChainId.linea]: [], - [ChainId.mainnet]: [], - [ChainId.optimism]: [], - [ChainId.polygon]: [], - [ChainId.sanko]: [], - [ChainId.scroll]: [], - [ChainId.zksync]: [], - [ChainId.zora]: [], - }); - - const crosschainSwapsEnabled = useExperimentalFlag(CROSSCHAIN_SWAPS); - - const isFavorite = useCallback( - (address: EthereumAddress) => favoriteAddresses.map(a => a?.toLowerCase()).includes(address?.toLowerCase()), - [favoriteAddresses] - ); - const handleSearchResponse = useCallback( - (tokens: RainbowToken[]): RainbowToken[] => { - // These transformations are necessary for L2 tokens to match our spec - return (tokens || []) - .map(token => { - const t: RainbowToken = { - ...token, - address: token?.address || token.uniqueId.toLowerCase(), - } as RainbowToken; - - return t; - }) - .filter(({ address }) => !isFavorite(address)); - }, - [isFavorite] - ); + enableUnverifiedSearch, + }; + }, [searchQuery]); - const getFavorites = useCallback(async () => { - return searching - ? await searchCurrencyList({ - searchList: unfilteredFavorites as RainbowToken[], - query: searchQuery, - chainId: searchChainId, - }) - : unfilteredFavorites; - }, [searchChainId, searchQuery, searching, unfilteredFavorites]); + const favoriteAssets = useMemo(() => { + if (searchQuery === '') { + return unfilteredFavorites; + } else { + return filterList( + unfilteredFavorites || [], + memoizedData.queryIsAddress ? addHexPrefix(searchQuery).toLowerCase() : searchQuery, + memoizedData.keys, + { + threshold: memoizedData.queryIsAddress ? rankings.CASE_SENSITIVE_EQUAL : rankings.CONTAINS, + } + ); + } + }, [memoizedData.keys, memoizedData.queryIsAddress, searchQuery, unfilteredFavorites]); - const getImportedAsset = useCallback( - async (searchQuery: string, chainId: number): Promise => { - if (searching) { - if (isAddress(searchQuery)) { - const provider = getProvider({ chainId }); - const tokenContract = new Contract(searchQuery, erc20ABI, provider); - try { - const [name, symbol, decimals, address] = await Promise.all([ - tokenContract.name(), - tokenContract.symbol(), - tokenContract.decimals(), - getAddress(searchQuery), - ]); - const uniqueId = getUniqueId(address, chainId); + const { colors } = useTheme(); - return [ - { - chainId, - address, - decimals, - favorite: false, - highLiquidity: false, - isRainbowCurated: false, - isVerified: false, - name, - networks: { - [chainId]: { - address, - decimals, - }, - }, - symbol, - network: useBackendNetworksStore.getState().getChainsName()[chainId], - uniqueId, - } as RainbowToken, - ]; - } catch (e) { - logger.warn('[useSearchCurrencyList]: error getting token data', { error: (e as Error).message }); - return null; - } - } - } - return null; + // TODO JIN - consolidate this shit below + // TODO JIN - remove the list param + const { data: verifiedAssets, isFetching: loading } = useTokenSearchAllNetworks( + { + list: 'verifiedAssets', + query: searchQuery, }, - [searching] + { + staleTime: 10 * 60 * 1_000, // 10 min + } ); - const getCrosschainVerifiedAssetsForNetwork = useCallback( - async (chainId: ChainId) => { - const results = await searchCurrencyList({ - searchList: 'verifiedAssets', - query: '', - chainId, - }); - setCrosschainVerifiedAssets(state => ({ - ...state, - [chainId]: handleSearchResponse(results || []), - })); + const { data: highLiquidityAssets } = useTokenSearchAllNetworks( + { + list: 'highLiquidityAssets', + query: searchQuery, }, - [handleSearchResponse] + { + staleTime: 10 * 60 * 1_000, // 10 min + } ); - const getCrosschainVerifiedAssets = useCallback(async () => { - const crosschainAssetRequests: Promise[] = []; - Object.keys(crosschainVerifiedAssets).forEach(chainId => { - crosschainAssetRequests.push(getCrosschainVerifiedAssetsForNetwork(Number(chainId))); - }); - await Promise.all(crosschainAssetRequests); - }, [crosschainVerifiedAssets, getCrosschainVerifiedAssetsForNetwork]); - - const getResultsForAssetType = useCallback( - async (assetType: swapCurrencyListType) => { - switch (assetType) { - case 'verifiedAssets': - setVerifiedAssets( - handleSearchResponse( - await searchCurrencyList({ - searchList: assetType, - query: searchQuery, - chainId: searchChainId, - }) - ) - ); - break; - case 'highLiquidityAssets': - setHighLiquidityAssets( - handleSearchResponse( - await searchCurrencyList({ - searchList: assetType, - query: searchQuery, - chainId: searchChainId, - }) - ) - ); - break; - case 'lowLiquidityAssets': - setLowLiquidityAssets( - handleSearchResponse( - await searchCurrencyList({ - searchList: assetType, - query: searchQuery, - chainId: searchChainId, - }) - ) - ); - break; - case 'favoriteAssets': - setFavoriteAssets((await getFavorites()) || []); - break; - case 'importedAssets': { - const importedAssetResult = await getImportedAsset(searchQuery, searchChainId); - if (importedAssetResult) { - setImportedAssets(handleSearchResponse(importedAssetResult)); - } - break; - } - } + const { data: lowLiquidityAssets } = useTokenSearchAllNetworks( + { + list: 'lowLiquidityAssets', + query: searchQuery, }, - [getFavorites, getImportedAsset, handleSearchResponse, searchQuery, searchChainId] - ); - - const search = useCallback(async () => { - const categories: swapCurrencyListType[] = - searchChainId === ChainId.mainnet - ? ['favoriteAssets', 'highLiquidityAssets', 'verifiedAssets', 'importedAssets'] - : ['verifiedAssets', 'importedAssets']; - setLoading(true); - await Promise.all(categories.map(assetType => getResultsForAssetType(assetType))); - }, [searchChainId, getResultsForAssetType]); - - const clearSearch = useCallback(() => { - setLowLiquidityAssets([]); - setHighLiquidityAssets([]); - setVerifiedAssets([]); - setImportedAssets([]); - }, [getResultsForAssetType]); - - const wasSearching = usePrevious(searching); - const previousSearchQuery = usePrevious(searchQuery); - - useEffect(() => { - if (!fetchingCrosschainAssets && crosschainSwapsEnabled) { - setFetchingCrosschainAssets(true); - getCrosschainVerifiedAssets(); + { + staleTime: 10 * 60 * 1_000, // 10 min } - }, [getCrosschainVerifiedAssets, fetchingCrosschainAssets, crosschainSwapsEnabled]); - - useEffect(() => { - const doSearch = async () => { - if ((searching && !wasSearching) || (searching && previousSearchQuery !== searchQuery) || searchChainId !== previousChainId) { - if (searchChainId === ChainId.mainnet) { - search(); - } else { - await search(); - setLowLiquidityAssets([]); - setHighLiquidityAssets([]); - setLoading(false); - } - } else { - clearSearch(); - } - }; - doSearch(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searching, searchQuery, searchChainId]); - - const { colors } = useTheme(); + ); + // TODO JIN - update this and split up the results above const currencyList = useMemo(() => { const list = []; if (searching) { - const importedAsset = importedAssets?.[0]; let verifiedAssetsWithImport = verifiedAssets; let highLiquidityAssetsWithImport = highLiquidityAssets; let lowLiquidityAssetsWithoutImport = lowLiquidityAssets; - const verifiedAddresses = verifiedAssets.map(({ uniqueId }) => uniqueId.toLowerCase()); - const highLiquidityAddresses = verifiedAssets.map(({ uniqueId }) => uniqueId.toLowerCase()); - // this conditional prevents the imported token from jumping - // sections if verified/highliquidity search responds later - // than the contract checker in getImportedAsset - if (importedAsset && !isFavorite(importedAsset?.address)) { - lowLiquidityAssetsWithoutImport = lowLiquidityAssets.filter(({ address }) => address.toLowerCase() !== importedAsset?.address); - if (importedAsset?.isVerified && !verifiedAddresses.includes(importedAsset?.address.toLowerCase())) { - verifiedAssetsWithImport = [importedAsset, ...verifiedAssets]; - } else { - if (!highLiquidityAddresses.includes(importedAsset?.address.toLowerCase())) { - highLiquidityAssetsWithImport = [importedAsset, ...highLiquidityAssets]; - } - } - } - if (favoriteAssets?.length && searchChainId === ChainId.mainnet) { + if (favoriteAssets?.length) { list.push({ color: colors.yellowFavorite, data: abcSort(favoriteAssets, 'name'), @@ -366,52 +147,9 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main } } return list; - }, [ - verifiedAssets, - searching, - importedAssets, - highLiquidityAssets, - lowLiquidityAssets, - isFavorite, - favoriteAssets, - searchChainId, - colors.yellowFavorite, - unfilteredFavorites, - ]); - - const crosschainExactMatches = useMemo(() => { - if (currencyList.length) return []; - if (!searchQuery) return []; - const exactMatches: RainbowToken[] = []; - Object.keys(crosschainVerifiedAssets).forEach(chainId => { - const currentNetworkChainId = Number(chainId); - if (currentNetworkChainId !== searchChainId) { - // including goerli in our networks type is causing this type issue - const exactMatch = crosschainVerifiedAssets[currentNetworkChainId].find((asset: RainbowToken) => { - const symbolMatch = isLowerCaseMatch(asset?.symbol, searchQuery); - const nameMatch = isLowerCaseMatch(asset?.name, searchQuery); - return symbolMatch || nameMatch; - }); - if (exactMatch) { - exactMatches.push({ ...exactMatch, chainId: currentNetworkChainId }); - } - } - }); - if (exactMatches?.length) { - return [ - { - data: exactMatches, - key: 'verified', - title: lang.t(`exchange.token_sections.${tokenSectionTypes.crosschainMatchSection}`), - useGradientText: !IS_TEST, - }, - ]; - } - return []; - }, [crosschainVerifiedAssets, currencyList.length, searchChainId, searchQuery]); + }, [verifiedAssets, searching, highLiquidityAssets, lowLiquidityAssets, favoriteAssets, colors.yellowFavorite, unfilteredFavorites]); return { - crosschainExactMatches, swapCurrencyList: currencyList, swapCurrencyListLoading: loading, }; From 83d44a69f7c93503b2a754ee8ff68cf668ba048f Mon Sep 17 00:00:00 2001 From: jinchung Date: Tue, 17 Dec 2024 11:07:55 -0500 Subject: [PATCH 05/23] Make list param optional for discover search query to move towards consolidated network requests --- .../screens/Swap/resources/search/search.ts | 10 +++--- src/hooks/useSearchCurrencyList.ts | 36 +++++-------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index 3c13f19f482..7b4710fe7e5 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -28,7 +28,7 @@ export type TokenSearchArgs = { chainId?: ChainId; fromChainId?: ChainId | ''; keys?: TokenSearchAssetKey[]; - list: TokenSearchListId; + list?: TokenSearchListId; threshold?: TokenSearchThreshold; query?: string; shouldPersist?: boolean; @@ -55,7 +55,7 @@ async function tokenSearchQueryFunction({ }: QueryFunctionArgs) { const queryParams: { keys?: string; - list: TokenSearchListId; + list?: TokenSearchListId; threshold?: TokenSearchThreshold; query?: string; fromChainId?: number | string; @@ -74,7 +74,7 @@ async function tokenSearchQueryFunction({ } const url = `${chainId ? `/${chainId}` : ''}/?${qs.stringify(queryParams)}`; - const isSearchingVerifiedAssets = queryParams.list === 'verifiedAssets'; + const isSearchingVerifiedAssets = (queryParams.list && queryParams.list === 'verifiedAssets') || !queryParams.list; try { if (isAddressSearch && isSearchingVerifiedAssets) { @@ -147,7 +147,7 @@ export function useTokenSearch( } export function useTokenSearchAllNetworks( - { list, query }: Omit, + { query }: Omit, config: QueryConfigWithSelect = {} ) { const getSupportedChainIds = useBackendNetworksStore(state => state.getSupportedChainIds); @@ -156,7 +156,7 @@ export function useTokenSearchAllNetworks( const queries = useQueries({ queries: supportedChains.map(id => { return { - queryKey: tokenSearchQueryKey({ chainId: id, list, query }), + queryKey: tokenSearchQueryKey({ chainId: id, query }), queryFn: tokenSearchQueryFunction, refetchOnWindowFocus: false, ...config, diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 3972ac6d2d7..768c625ff3b 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -65,31 +65,8 @@ const useSearchCurrencyList = (searchQuery: string) => { const { colors } = useTheme(); - // TODO JIN - consolidate this shit below - // TODO JIN - remove the list param - const { data: verifiedAssets, isFetching: loading } = useTokenSearchAllNetworks( + const { data: searchResultAssets, isFetching: loading } = useTokenSearchAllNetworks( { - list: 'verifiedAssets', - query: searchQuery, - }, - { - staleTime: 10 * 60 * 1_000, // 10 min - } - ); - - const { data: highLiquidityAssets } = useTokenSearchAllNetworks( - { - list: 'highLiquidityAssets', - query: searchQuery, - }, - { - staleTime: 10 * 60 * 1_000, // 10 min - } - ); - - const { data: lowLiquidityAssets } = useTokenSearchAllNetworks( - { - list: 'lowLiquidityAssets', query: searchQuery, }, { @@ -100,9 +77,12 @@ const useSearchCurrencyList = (searchQuery: string) => { // TODO JIN - update this and split up the results above const currencyList = useMemo(() => { const list = []; + // TODO JIN - placeholders + const highLiquidityAssets = searchResultAssets; + const lowLiquidityAssets = searchResultAssets; if (searching) { - let verifiedAssetsWithImport = verifiedAssets; + let verifiedAssetsWithImport = searchResultAssets; let highLiquidityAssetsWithImport = highLiquidityAssets; let lowLiquidityAssetsWithoutImport = lowLiquidityAssets; @@ -137,17 +117,17 @@ const useSearchCurrencyList = (searchQuery: string) => { }); } } else { - if (unfilteredFavorites?.length) { + if (favoriteAssets?.length) { list.push({ color: colors.yellowFavorite, - data: abcSort(unfilteredFavorites, 'name'), + data: abcSort(favoriteAssets, 'name'), key: 'unfilteredFavorites', title: lang.t(`exchange.token_sections.${tokenSectionTypes.favoriteTokenSection}`), }); } } return list; - }, [verifiedAssets, searching, highLiquidityAssets, lowLiquidityAssets, favoriteAssets, colors.yellowFavorite, unfilteredFavorites]); + }, [searchResultAssets, searching, favoriteAssets, colors.yellowFavorite]); return { swapCurrencyList: currencyList, From a2465a2bf44717ee25a93b823a4509c446e97e6d Mon Sep 17 00:00:00 2001 From: jinchung Date: Tue, 17 Dec 2024 11:20:20 -0500 Subject: [PATCH 06/23] Update parsing of consolidated results from backend for verified, high and low liquidity assets --- src/hooks/useSearchCurrencyList.ts | 38 +++++++++++++++++++----------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 768c625ff3b..1cbcc089b2f 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -1,5 +1,6 @@ import lang from 'i18n-js'; import { rankings } from 'match-sorter'; +import { groupBy } from 'lodash'; import { useMemo } from 'react'; import { useTheme } from '../theme/ThemeContext'; import { addHexPrefix } from '@/handlers/web3'; @@ -74,18 +75,19 @@ const useSearchCurrencyList = (searchQuery: string) => { } ); - // TODO JIN - update this and split up the results above const currencyList = useMemo(() => { const list = []; - // TODO JIN - placeholders - const highLiquidityAssets = searchResultAssets; - const lowLiquidityAssets = searchResultAssets; + const { verifiedAssets, highLiquidityAssets, lowLiquidityAssets } = groupBy(searchResultAssets, searchResult => { + if (searchResult.isVerified) { + return 'verifiedAssets'; + } else if (!searchResult.isVerified && searchResult.highLiquidity) { + return 'highLiquidityAssets'; + } else { + return 'lowLiquidityAssets'; + } + }); if (searching) { - let verifiedAssetsWithImport = searchResultAssets; - let highLiquidityAssetsWithImport = highLiquidityAssets; - let lowLiquidityAssetsWithoutImport = lowLiquidityAssets; - if (favoriteAssets?.length) { list.push({ color: colors.yellowFavorite, @@ -94,24 +96,24 @@ const useSearchCurrencyList = (searchQuery: string) => { title: lang.t(`exchange.token_sections.${tokenSectionTypes.favoriteTokenSection}`), }); } - if (verifiedAssetsWithImport?.length) { + if (verifiedAssets?.length) { list.push({ - data: verifiedAssetsWithImport, + data: verifiedAssets, key: 'verified', title: lang.t(`exchange.token_sections.${tokenSectionTypes.verifiedTokenSection}`), useGradientText: !IS_TEST, }); } - if (highLiquidityAssetsWithImport?.length) { + if (highLiquidityAssets?.length) { list.push({ - data: highLiquidityAssetsWithImport, + data: highLiquidityAssets, key: 'highLiquidity', title: lang.t(`exchange.token_sections.${tokenSectionTypes.unverifiedTokenSection}`), }); } - if (lowLiquidityAssetsWithoutImport?.length) { + if (lowLiquidityAssets?.length) { list.push({ - data: lowLiquidityAssetsWithoutImport, + data: lowLiquidityAssets, key: 'lowLiquidity', title: lang.t(`exchange.token_sections.${tokenSectionTypes.lowLiquidityTokenSection}`), }); @@ -125,6 +127,14 @@ const useSearchCurrencyList = (searchQuery: string) => { title: lang.t(`exchange.token_sections.${tokenSectionTypes.favoriteTokenSection}`), }); } + if (verifiedAssets?.length) { + list.push({ + data: verifiedAssets, + key: 'verified', + title: lang.t(`exchange.token_sections.${tokenSectionTypes.verifiedTokenSection}`), + useGradientText: !IS_TEST, + }); + } } return list; }, [searchResultAssets, searching, favoriteAssets, colors.yellowFavorite]); From 0364a8c6830c56ffad6483cf2d51553ae027e09b Mon Sep 17 00:00:00 2001 From: jinchung Date: Tue, 17 Dec 2024 15:32:32 -0500 Subject: [PATCH 07/23] Remove now unused tokenSearch handler --- src/handlers/tokenSearch.ts | 101 ------------------------------------ 1 file changed, 101 deletions(-) delete mode 100644 src/handlers/tokenSearch.ts diff --git a/src/handlers/tokenSearch.ts b/src/handlers/tokenSearch.ts deleted file mode 100644 index 3ea1c0c0f4a..00000000000 --- a/src/handlers/tokenSearch.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { isAddress } from '@ethersproject/address'; -import { qs } from 'url-parse'; -import { RainbowFetchClient } from '../rainbow-fetch'; -import { TokenSearchThreshold, TokenSearchTokenListId } from '@/entities'; -import { logger, RainbowError } from '@/logger'; -import { RainbowToken, TokenSearchToken } from '@/entities/tokens'; -import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; -import { ChainId } from '@/state/backendNetworks/types'; - -const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets'; - -const tokenSearchHttp = new RainbowFetchClient({ - baseURL: 'https://token-search.rainbow.me/v2', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - timeout: 30000, -}); - -function parseTokenSearch(assets: TokenSearchToken[]): RainbowToken[] { - const chainsName = useBackendNetworksStore.getState().getChainsName(); - return assets.map(token => { - const networkKeys = Object.keys(token.networks); - const chainId = Number(networkKeys[0]); - const network = chainsName[chainId]; - return { - ...token, - chainId, - address: token.networks['1']?.address || token.networks[chainId]?.address, - network, - mainnet_address: token.networks['1']?.address, - }; - }); -} - -export const tokenSearch = async (searchParams: { - chainId: ChainId; - fromChainId?: number | ''; - keys: (keyof RainbowToken)[]; - list: TokenSearchTokenListId; - threshold: TokenSearchThreshold; - query: string; -}): Promise => { - const queryParams: { - keys: string; - list: TokenSearchTokenListId; - threshold: TokenSearchThreshold; - query?: string; - fromChainId?: number; - } = { - keys: searchParams.keys.join(','), - list: searchParams.list, - threshold: searchParams.threshold, - query: searchParams.query, - }; - - const { chainId, query } = searchParams; - - const isAddressSearch = query && isAddress(query); - - if (isAddressSearch) { - queryParams.keys = `networks.${chainId}.address`; - } - - const url = `/?${qs.stringify(queryParams)}`; - const isSearchingVerifiedAssets = queryParams.list === 'verifiedAssets'; - - try { - const tokenSearch = await tokenSearchHttp.get<{ data: TokenSearchToken[] }>(url); - - if (isAddressSearch && isSearchingVerifiedAssets) { - if (tokenSearch && tokenSearch.data.data.length > 0) { - return parseTokenSearch(tokenSearch.data.data); - } - - const allVerifiedTokens = await tokenSearchHttp.get<{ data: TokenSearchToken[] }>(ALL_VERIFIED_TOKENS_PARAM); - - const addressQuery = query.trim().toLowerCase(); - - const addressMatchesOnOtherChains = allVerifiedTokens.data.data.filter(a => - Object.values(a.networks).some(n => n?.address === addressQuery) - ); - - return parseTokenSearch(addressMatchesOnOtherChains); - } - - if (!tokenSearch.data?.data) { - return []; - } - - return parseTokenSearch(tokenSearch.data.data); - } catch (e: any) { - logger.error(new RainbowError(`[tokenSearch]: An error occurred while searching for query`), { - query: searchParams.query, - message: e.message, - }); - - return []; - } -}; From edba22c95e579ad74bb9760f46a3a799c4812aa5 Mon Sep 17 00:00:00 2001 From: jinchung Date: Wed, 18 Dec 2024 21:33:00 -0500 Subject: [PATCH 08/23] Temp: experiment with top search results logic for discover --- src/hooks/useSearchCurrencyList.ts | 48 ++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 1cbcc089b2f..57c5c9c6dfc 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -1,19 +1,33 @@ import lang from 'i18n-js'; import { rankings } from 'match-sorter'; import { groupBy } from 'lodash'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTheme } from '../theme/ThemeContext'; import { addHexPrefix } from '@/handlers/web3'; import tokenSectionTypes from '@/helpers/tokenSectionTypes'; -import { filterList } from '@/utils'; +import { isLowerCaseMatch, filterList } from '@/utils'; import { IS_TEST } from '@/env'; import { useFavorites } from '@/resources/favorites'; import { ChainId } from '@/state/backendNetworks/types'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { useTokenSearchAllNetworks } from '@/__swaps__/screens/Swap/resources/search/search'; +import { TokenSearchResult, useTokenSearchAllNetworks } from '@/__swaps__/screens/Swap/resources/search/search'; import { SearchAsset, TokenSearchAssetKey, TokenSearchThreshold } from '@/__swaps__/types/search'; import { isAddress } from '@ethersproject/address'; +const MAX_VERIFIED_RESULTS = 48; + +const getExactMatches = (data: TokenSearchResult, query: string) => { + const isQueryAddress = isAddress(query.trim()); + return data.filter(asset => { + if (isQueryAddress) { + return !!(asset.address?.toLowerCase() === query.trim().toLowerCase()); + } + const symbolMatch = isLowerCaseMatch(asset.symbol, query); + const nameMatch = isLowerCaseMatch(asset.name, query); + return symbolMatch || nameMatch; + }); +}; + const abcSort = (list: any[], key?: string) => { return list.sort((a, b) => { return key ? a[key]?.localeCompare(b[key]) : a?.localeCompare(b); @@ -66,11 +80,39 @@ const useSearchCurrencyList = (searchQuery: string) => { const { colors } = useTheme(); + const selectTopSearchResults = useCallback( + (data: TokenSearchResult) => { + const results = data.filter(asset => { + const hasIcon = asset.icon_url; + const isMatch = hasIcon || searchQuery.length > 2; + + if (!isMatch) { + const crosschainMatch = getExactMatches([asset], searchQuery); + return crosschainMatch.length > 0; + } + + return isMatch; + }); + + const topResults = results + .sort((a, b) => { + if (a.isNativeAsset !== b.isNativeAsset) return a.isNativeAsset ? -1 : 1; + if (a.highLiquidity !== b.highLiquidity) return a.highLiquidity ? -1 : 1; + return Object.keys(b.networks).length - Object.keys(a.networks).length; + }) + .slice(0, MAX_VERIFIED_RESULTS); + + return [...topResults]; + }, + [searchQuery] + ); + const { data: searchResultAssets, isFetching: loading } = useTokenSearchAllNetworks( { query: searchQuery, }, { + select: selectTopSearchResults, staleTime: 10 * 60 * 1_000, // 10 min } ); From bf4faf966a870a37d089c4e83ebb5ac901281eaa Mon Sep 17 00:00:00 2001 From: jinchung Date: Wed, 18 Dec 2024 22:28:41 -0500 Subject: [PATCH 09/23] Experiment: add support for sorting against market cap --- src/__swaps__/types/search.ts | 9 +++++++++ src/hooks/useSearchCurrencyList.ts | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/__swaps__/types/search.ts b/src/__swaps__/types/search.ts index c11d1a6f920..b51a452b481 100644 --- a/src/__swaps__/types/search.ts +++ b/src/__swaps__/types/search.ts @@ -10,6 +10,14 @@ export type TokenSearchThreshold = 'CONTAINS' | 'CASE_SENSITIVE_EQUAL'; export type TokenSearchListId = 'highLiquidityAssets' | 'lowLiquidityAssets' | 'verifiedAssets'; +interface Market { + market_cap: { + value: number; + }; + volume_24h: number; + circulating_supply: number; +} + export type SearchAsset = { address: AddressOrEth; chainId: ChainId; @@ -22,6 +30,7 @@ export type SearchAsset = { isNativeAsset?: boolean; isVerified: boolean; mainnetAddress: AddressOrEth; + market?: Market; name: string; networks: { [chainId in ChainId]?: { diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 57c5c9c6dfc..f99e8bcdbb1 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -97,6 +97,8 @@ const useSearchCurrencyList = (searchQuery: string) => { const topResults = results .sort((a, b) => { if (a.isNativeAsset !== b.isNativeAsset) return a.isNativeAsset ? -1 : 1; + if (a.market?.market_cap?.value !== b.market?.market_cap?.value) + return (b.market?.market_cap?.value || 0) - (a.market?.market_cap?.value || 0); if (a.highLiquidity !== b.highLiquidity) return a.highLiquidity ? -1 : 1; return Object.keys(b.networks).length - Object.keys(a.networks).length; }) From 78b5c210ef764534ba84fa860dc5ab91953880c2 Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 23 Dec 2024 11:30:23 -0500 Subject: [PATCH 10/23] Consolidate multiple queries for chain ID into one query --- .../screens/Swap/resources/search/search.ts | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index 7b4710fe7e5..c7f506707fb 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -5,10 +5,9 @@ import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { isAddress } from '@ethersproject/address'; -import { useQuery, useQueries } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import qs from 'qs'; import { parseTokenSearch } from './utils'; -import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets'; @@ -150,22 +149,8 @@ export function useTokenSearchAllNetworks( { query }: Omit, config: QueryConfigWithSelect = {} ) { - const getSupportedChainIds = useBackendNetworksStore(state => state.getSupportedChainIds); - const supportedChains = getSupportedChainIds(); - - const queries = useQueries({ - queries: supportedChains.map(id => { - return { - queryKey: tokenSearchQueryKey({ chainId: id, query }), - queryFn: tokenSearchQueryFunction, - refetchOnWindowFocus: false, - ...config, - }; - }), + return useQuery(tokenSearchQueryKey({ query }), tokenSearchQueryFunction, { + ...config, + keepPreviousData: true, }); - - return { - data: queries.map(({ data: assets }) => assets || []).flat(), - isFetching: queries.some(({ isFetching }) => isFetching), - }; } From 8c0cff9f9e9fa714142d3c7b3a045591d52e2300 Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 30 Dec 2024 11:15:36 -0500 Subject: [PATCH 11/23] Add function for getting imported asset --- .../screens/Swap/resources/search/search.ts | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index c7f506707fb..e715bffe0bd 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -1,13 +1,19 @@ /* eslint-disable no-nested-ternary */ import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { SearchAsset, TokenSearchAssetKey, TokenSearchListId, TokenSearchThreshold } from '@/__swaps__/types/search'; import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; -import { isAddress } from '@ethersproject/address'; +import { getAddress, isAddress } from '@ethersproject/address'; +import { getUniqueId } from '@/utils/ethereumUtils'; +import { Contract } from '@ethersproject/contracts'; import { useQuery } from '@tanstack/react-query'; import qs from 'qs'; import { parseTokenSearch } from './utils'; +import { RainbowToken } from '@/entities'; +import { getProvider } from '@/handlers/web3'; +import { erc20ABI } from '@/references'; const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets'; @@ -48,6 +54,47 @@ type TokenSearchQueryKey = ReturnType; // /////////////////////////////////////////////// // Query Function +const getImportedAsset = async (searchQuery: string, chainId: number = ChainId.mainnet): Promise => { + if (isAddress(searchQuery)) { + const provider = getProvider({ chainId }); + const tokenContract = new Contract(searchQuery, erc20ABI, provider); + try { + const [name, symbol, decimals, address] = await Promise.all([ + tokenContract.name(), + tokenContract.symbol(), + tokenContract.decimals(), + getAddress(searchQuery), + ]); + const uniqueId = getUniqueId(address, chainId); + + return [ + { + chainId, + address, + decimals, + favorite: false, + highLiquidity: false, + isRainbowCurated: false, + isVerified: false, + name, + networks: { + [chainId]: { + address, + decimals, + }, + }, + symbol, + network: useBackendNetworksStore.getState().getChainsName()[chainId], + uniqueId, + } as RainbowToken, + ]; + } catch (e) { + logger.warn('[useSearchCurrencyList]: error getting token data', { error: (e as Error).message }); + return null; + } + } + return null; +}; async function tokenSearchQueryFunction({ queryKey: [{ chainId, fromChainId, keys, list, threshold, query }], From 74919de6311b6a61eb97cff6efea629a8fd9a15d Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 30 Dec 2024 11:51:31 -0500 Subject: [PATCH 12/23] Add separate query key and function for all networks request to simplify for consolidation later --- .../screens/Swap/resources/search/search.ts | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index e715bffe0bd..e41d96abed2 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -39,6 +39,11 @@ export type TokenSearchArgs = { shouldPersist?: boolean; }; +export type TokenSearchAllNetworksArgs = { + query?: string; + shouldPersist?: boolean; +}; + // /////////////////////////////////////////////// // Query Key @@ -50,8 +55,14 @@ const tokenSearchQueryKey = ({ chainId, fromChainId, keys, list, threshold, quer ); }; +const tokenSearchAllNetworksQueryKey = ({ query, shouldPersist }: TokenSearchAllNetworksArgs) => { + return createQueryKey('TokenSearch', { query }, { persisterVersion: shouldPersist ? 3 : undefined }); +}; + type TokenSearchQueryKey = ReturnType; +type TokenSearchAllNetworksQueryKey = ReturnType; + // /////////////////////////////////////////////// // Query Function const getImportedAsset = async (searchQuery: string, chainId: number = ChainId.mainnet): Promise => { @@ -149,6 +160,36 @@ async function tokenSearchQueryFunction({ } } +async function tokenSearchQueryFunctionAllNetworks({ queryKey: [{ query }] }: QueryFunctionArgs) { + const queryParams: { + query?: string; + } = { + query, + }; + + const isAddressSearch = query && isAddress(query); + + const url = `/?${qs.stringify(queryParams)}`; + + try { + if (isAddressSearch) { + const tokenSearch = await tokenSearchHttp.get<{ data: SearchAsset[] }>(url); + + if (tokenSearch && tokenSearch.data.data.length > 0) { + return parseTokenSearch(tokenSearch.data.data); + } + + return []; + } else { + const tokenSearch = await tokenSearchHttp.get<{ data: SearchAsset[] }>(url); + return parseTokenSearch(tokenSearch.data.data); + } + } catch (e) { + logger.error(new RainbowError('[tokenSearchQueryFunction]: Token search failed'), { url }); + return []; + } +} + export type TokenSearchResult = QueryFunctionResult; // /////////////////////////////////////////////// @@ -193,10 +234,10 @@ export function useTokenSearch( } export function useTokenSearchAllNetworks( - { query }: Omit, - config: QueryConfigWithSelect = {} + { query }: TokenSearchAllNetworksArgs, + config: QueryConfigWithSelect = {} ) { - return useQuery(tokenSearchQueryKey({ query }), tokenSearchQueryFunction, { + return useQuery(tokenSearchAllNetworksQueryKey({ query }), tokenSearchQueryFunctionAllNetworks, { ...config, keepPreviousData: true, }); From ff98d9ef942edcff62d2f18be3fb567372c3a857 Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 30 Dec 2024 12:15:00 -0500 Subject: [PATCH 13/23] Update RainbowToken search result to SearchAsset for imported asset result --- .../screens/Swap/resources/search/search.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index e41d96abed2..aa248ac81a9 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -1,6 +1,5 @@ /* eslint-disable no-nested-ternary */ import { ChainId } from '@/state/backendNetworks/types'; -import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { SearchAsset, TokenSearchAssetKey, TokenSearchListId, TokenSearchThreshold } from '@/__swaps__/types/search'; import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; @@ -11,7 +10,6 @@ import { Contract } from '@ethersproject/contracts'; import { useQuery } from '@tanstack/react-query'; import qs from 'qs'; import { parseTokenSearch } from './utils'; -import { RainbowToken } from '@/entities'; import { getProvider } from '@/handlers/web3'; import { erc20ABI } from '@/references'; @@ -40,8 +38,7 @@ export type TokenSearchArgs = { }; export type TokenSearchAllNetworksArgs = { - query?: string; - shouldPersist?: boolean; + query: string; }; // /////////////////////////////////////////////// @@ -55,8 +52,9 @@ const tokenSearchQueryKey = ({ chainId, fromChainId, keys, list, threshold, quer ); }; -const tokenSearchAllNetworksQueryKey = ({ query, shouldPersist }: TokenSearchAllNetworksArgs) => { - return createQueryKey('TokenSearch', { query }, { persisterVersion: shouldPersist ? 3 : undefined }); +const tokenSearchAllNetworksQueryKey = ({ query }: TokenSearchAllNetworksArgs) => { + const shouldPersist = query === ''; + return createQueryKey('TokenSearchAllNetworks', { query }, { persisterVersion: shouldPersist ? 1 : undefined }); }; type TokenSearchQueryKey = ReturnType; @@ -65,7 +63,7 @@ type TokenSearchAllNetworksQueryKey = ReturnType => { +const getImportedAsset = async (searchQuery: string, chainId: number = ChainId.mainnet): Promise => { if (isAddress(searchQuery)) { const provider = getProvider({ chainId }); const tokenContract = new Contract(searchQuery, erc20ABI, provider); @@ -83,10 +81,10 @@ const getImportedAsset = async (searchQuery: string, chainId: number = ChainId.m chainId, address, decimals, - favorite: false, highLiquidity: false, isRainbowCurated: false, isVerified: false, + mainnetAddress: address, name, networks: { [chainId]: { @@ -95,16 +93,15 @@ const getImportedAsset = async (searchQuery: string, chainId: number = ChainId.m }, }, symbol, - network: useBackendNetworksStore.getState().getChainsName()[chainId], uniqueId, - } as RainbowToken, + } as SearchAsset, ]; } catch (e) { - logger.warn('[useSearchCurrencyList]: error getting token data', { error: (e as Error).message }); - return null; + logger.warn('[getImportedAsset]: error getting imported token data', { error: (e as Error).message }); + return []; } } - return null; + return []; }; async function tokenSearchQueryFunction({ @@ -179,7 +176,8 @@ async function tokenSearchQueryFunctionAllNetworks({ queryKey: [{ query }] }: Qu return parseTokenSearch(tokenSearch.data.data); } - return []; + const result = await getImportedAsset(query); + return result; } else { const tokenSearch = await tokenSearchHttp.get<{ data: SearchAsset[] }>(url); return parseTokenSearch(tokenSearch.data.data); From 733cc00dad4022be0ab5406657bc7a0fbc650dd8 Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 30 Dec 2024 12:53:13 -0500 Subject: [PATCH 14/23] Filter out favorites from all networks results --- src/hooks/useSearchCurrencyList.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index f99e8bcdbb1..05bbd0650b1 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -37,7 +37,7 @@ const abcSort = (list: any[], key?: string) => { const useSearchCurrencyList = (searchQuery: string) => { const searching = useMemo(() => searchQuery !== '', [searchQuery]); - const { favoritesMetadata: favoriteMap } = useFavorites(); + const { favorites: favoriteAddresses, favoritesMetadata: favoriteMap } = useFavorites(); const unfilteredFavorites = useMemo(() => { return Object.values(favoriteMap) .filter(token => token.networks[ChainId.mainnet]) @@ -104,9 +104,13 @@ const useSearchCurrencyList = (searchQuery: string) => { }) .slice(0, MAX_VERIFIED_RESULTS); - return [...topResults]; + const topResultsWithoutFavorites = topResults.filter(asset => { + return !favoriteAddresses.map(a => a?.toLowerCase()).includes(asset.address?.toLowerCase()); + }); + + return [...topResultsWithoutFavorites]; }, - [searchQuery] + [searchQuery, favoriteAddresses] ); const { data: searchResultAssets, isFetching: loading } = useTokenSearchAllNetworks( From 7dc37a96f8995a26f9fa14efb45b8f017a75bca8 Mon Sep 17 00:00:00 2001 From: jinchung Date: Mon, 30 Dec 2024 14:44:35 -0500 Subject: [PATCH 15/23] Update relevance sorting logic --- src/hooks/useSearchCurrencyList.ts | 95 +++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 16 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 05bbd0650b1..38324b909c2 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -34,6 +34,80 @@ const abcSort = (list: any[], key?: string) => { }); }; +type SearchItemWithRelevance = SearchAsset & { + relevance: number; +}; + +const sortTokensByRelevance = (tokens: SearchAsset[], query: string): SearchItemWithRelevance[] => { + const normalizedQuery = query.toLowerCase().trim(); + const tokenWithRelevance: SearchItemWithRelevance[] = []; + + for (const token of tokens) { + const normalizedTokenName = token.name.toLowerCase(); + const normalizedTokenSymbol = token.symbol.toLowerCase(); + const tokenNameWords = normalizedTokenName.split(' '); + const relevance = getTokenRelevance({ + token, + normalizedTokenName, + normalizedQuery, + normalizedTokenSymbol, + tokenNameWords, + }); + + if (relevance > 0) { + tokenWithRelevance.push({ ...token, relevance }); + } + } + + return tokenWithRelevance.sort((a, b) => b.relevance - a.relevance); +}; + +const getStackedRelevanceScore = (token: SearchAsset, topScore: number) => { + if (token.isVerified) { + return topScore; + } else if (token.highLiquidity) { + return topScore - 0.1; + } else if (token.icon_url) { + return topScore - 0.2; + } else if (Object.keys(token.networks).length > 1) { + return topScore - 0.3; + } else { + return topScore - 0.4; + } +}; + +// higher number indicates higher relevance +const getTokenRelevance = ({ + token, + normalizedTokenName, + normalizedQuery, + normalizedTokenSymbol, + tokenNameWords, +}: { + token: SearchAsset; + normalizedTokenName: string; + normalizedQuery: string; + normalizedTokenSymbol?: string; + tokenNameWords: string[]; +}) => { + // High relevance: Leading word in token name starts with query or exact match on symbol + if (normalizedTokenName.startsWith(normalizedQuery) || (normalizedTokenSymbol && normalizedTokenSymbol === normalizedQuery)) { + return getStackedRelevanceScore(token, 5); + } + + // Medium relevance: Non-leading word in token name starts with query + if (tokenNameWords.some((word, index) => index !== 0 && word.startsWith(normalizedQuery))) { + return getStackedRelevanceScore(token, 4); + } + + // Low relevance: Token name contains query + if (tokenNameWords.some(word => word.includes(normalizedQuery))) { + return getStackedRelevanceScore(token, 3); + } + + return 0; +}; + const useSearchCurrencyList = (searchQuery: string) => { const searching = useMemo(() => searchQuery !== '', [searchQuery]); @@ -83,6 +157,9 @@ const useSearchCurrencyList = (searchQuery: string) => { const selectTopSearchResults = useCallback( (data: TokenSearchResult) => { const results = data.filter(asset => { + const isFavorite = favoriteAddresses.map(a => a?.toLowerCase()).includes(asset.address?.toLowerCase()); + if (isFavorite) return false; + const hasIcon = asset.icon_url; const isMatch = hasIcon || searchQuery.length > 2; @@ -93,22 +170,8 @@ const useSearchCurrencyList = (searchQuery: string) => { return isMatch; }); - - const topResults = results - .sort((a, b) => { - if (a.isNativeAsset !== b.isNativeAsset) return a.isNativeAsset ? -1 : 1; - if (a.market?.market_cap?.value !== b.market?.market_cap?.value) - return (b.market?.market_cap?.value || 0) - (a.market?.market_cap?.value || 0); - if (a.highLiquidity !== b.highLiquidity) return a.highLiquidity ? -1 : 1; - return Object.keys(b.networks).length - Object.keys(a.networks).length; - }) - .slice(0, MAX_VERIFIED_RESULTS); - - const topResultsWithoutFavorites = topResults.filter(asset => { - return !favoriteAddresses.map(a => a?.toLowerCase()).includes(asset.address?.toLowerCase()); - }); - - return [...topResultsWithoutFavorites]; + const topResults = sortTokensByRelevance(results, searchQuery); + return topResults.slice(0, MAX_VERIFIED_RESULTS); }, [searchQuery, favoriteAddresses] ); From a05fae2496da609308049be5ee511f4e20dfa3bb Mon Sep 17 00:00:00 2001 From: jinchung Date: Thu, 2 Jan 2025 10:56:41 -0500 Subject: [PATCH 16/23] Add separate parse token method for deduping across networks without passing in a chain id --- .../screens/Swap/resources/search/search.ts | 4 +-- .../screens/Swap/resources/search/utils.ts | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index aa248ac81a9..a53d10d4a4b 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -9,7 +9,7 @@ import { getUniqueId } from '@/utils/ethereumUtils'; import { Contract } from '@ethersproject/contracts'; import { useQuery } from '@tanstack/react-query'; import qs from 'qs'; -import { parseTokenSearch } from './utils'; +import { parseTokenSearch, parseTokenSearchAcrossNetworks } from './utils'; import { getProvider } from '@/handlers/web3'; import { erc20ABI } from '@/references'; @@ -180,7 +180,7 @@ async function tokenSearchQueryFunctionAllNetworks({ queryKey: [{ query }] }: Qu return result; } else { const tokenSearch = await tokenSearchHttp.get<{ data: SearchAsset[] }>(url); - return parseTokenSearch(tokenSearch.data.data); + return parseTokenSearchAcrossNetworks(tokenSearch.data.data); } } catch (e) { logger.error(new RainbowError('[tokenSearchQueryFunction]: Token search failed'), { url }); diff --git a/src/__swaps__/screens/Swap/resources/search/utils.ts b/src/__swaps__/screens/Swap/resources/search/utils.ts index 975c0c3e8f4..7c40ef0e973 100644 --- a/src/__swaps__/screens/Swap/resources/search/utils.ts +++ b/src/__swaps__/screens/Swap/resources/search/utils.ts @@ -1,3 +1,4 @@ +import { uniqBy } from 'lodash'; import { ChainId } from '@/state/backendNetworks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { Address } from 'viem'; @@ -49,3 +50,27 @@ export function parseTokenSearch(assets: SearchAsset[], chainId?: ChainId): Sear return results; } + +export function parseTokenSearchAcrossNetworks(assets: SearchAsset[]): SearchAsset[] { + const results = assets.map(asset => { + const assetNetworks = asset.networks; + const networkKeys = Object.keys(assetNetworks); + const firstNetworkChainId = Number(networkKeys[0] || asset.chainId); + + const mainnetInfo = assetNetworks[ChainId.mainnet]; + const chainId = mainnetInfo ? ChainId.mainnet : firstNetworkChainId; + const address = mainnetInfo ? mainnetInfo.address : assetNetworks[firstNetworkChainId]?.address || asset.address; + const uniqueId = `${address}_${chainId}`; + + return { + ...asset, + chainId, + address, + isNativeAsset: isNativeAsset(address, chainId), + mainnetAddress: mainnetInfo ? mainnetInfo.address : chainId === ChainId.mainnet ? address : ('' as Address), + uniqueId, + }; + }); + const uniqRes = uniqBy(results, 'address'); + return uniqRes; +} From 774f6a9df327e304d13ad514ec29dafc6419bc82 Mon Sep 17 00:00:00 2001 From: jinchung Date: Sun, 5 Jan 2025 16:53:03 -0500 Subject: [PATCH 17/23] Default search list for Discover should be mainnet verified assets --- src/__swaps__/screens/Swap/resources/search/search.ts | 8 +++++++- src/hooks/useSearchCurrencyList.ts | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index a53d10d4a4b..8faa86a9196 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -159,6 +159,7 @@ async function tokenSearchQueryFunction({ async function tokenSearchQueryFunctionAllNetworks({ queryKey: [{ query }] }: QueryFunctionArgs) { const queryParams: { + list?: string; query?: string; } = { query, @@ -166,7 +167,12 @@ async function tokenSearchQueryFunctionAllNetworks({ queryKey: [{ query }] }: Qu const isAddressSearch = query && isAddress(query); - const url = `/?${qs.stringify(queryParams)}`; + const searchDefaultMainnetVerifiedList = query === ''; + if (searchDefaultMainnetVerifiedList) { + queryParams.list = 'verifiedAssets'; + } + + const url = `${searchDefaultMainnetVerifiedList ? `/${ChainId.mainnet}` : ''}/?${qs.stringify(queryParams)}`; try { if (isAddressSearch) { diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 38324b909c2..9c0e9f9a7ad 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -38,12 +38,18 @@ type SearchItemWithRelevance = SearchAsset & { relevance: number; }; +const sortForDefaultList = (tokens: SearchAsset[]) => { + const curated = tokens.filter(asset => asset.highLiquidity && asset.isRainbowCurated); + return curated.sort((a, b) => (b.market?.market_cap?.value || 0) - (a.market?.market_cap?.value || 0)); +}; + const sortTokensByRelevance = (tokens: SearchAsset[], query: string): SearchItemWithRelevance[] => { const normalizedQuery = query.toLowerCase().trim(); const tokenWithRelevance: SearchItemWithRelevance[] = []; for (const token of tokens) { const normalizedTokenName = token.name.toLowerCase(); + const normalizedTokenSymbol = token.symbol.toLowerCase(); const tokenNameWords = normalizedTokenName.split(' '); const relevance = getTokenRelevance({ @@ -170,7 +176,7 @@ const useSearchCurrencyList = (searchQuery: string) => { return isMatch; }); - const topResults = sortTokensByRelevance(results, searchQuery); + const topResults = searchQuery === '' ? sortForDefaultList(results) : sortTokensByRelevance(results, searchQuery); return topResults.slice(0, MAX_VERIFIED_RESULTS); }, [searchQuery, favoriteAddresses] From 920b9e073b8ec5dd401d4852c602fcd5e5612a03 Mon Sep 17 00:00:00 2001 From: jinchung Date: Sun, 5 Jan 2025 17:43:40 -0500 Subject: [PATCH 18/23] Fix filtering out of favorite addresses --- src/hooks/useSearchCurrencyList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 9c0e9f9a7ad..c0b37677361 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -163,7 +163,7 @@ const useSearchCurrencyList = (searchQuery: string) => { const selectTopSearchResults = useCallback( (data: TokenSearchResult) => { const results = data.filter(asset => { - const isFavorite = favoriteAddresses.map(a => a?.toLowerCase()).includes(asset.address?.toLowerCase()); + const isFavorite = favoriteAddresses.map(a => a?.toLowerCase()).includes(asset.uniqueId?.toLowerCase()); if (isFavorite) return false; const hasIcon = asset.icon_url; From 49aedf3a567a8382e2b8892b4c20a6e05cf4a31d Mon Sep 17 00:00:00 2001 From: jinchung Date: Tue, 7 Jan 2025 23:15:51 -0500 Subject: [PATCH 19/23] Require icon url for default discover search list --- src/hooks/useSearchCurrencyList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index c0b37677361..3d234d09651 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -39,7 +39,7 @@ type SearchItemWithRelevance = SearchAsset & { }; const sortForDefaultList = (tokens: SearchAsset[]) => { - const curated = tokens.filter(asset => asset.highLiquidity && asset.isRainbowCurated); + const curated = tokens.filter(asset => asset.highLiquidity && asset.isRainbowCurated && asset.icon_url); return curated.sort((a, b) => (b.market?.market_cap?.value || 0) - (a.market?.market_cap?.value || 0)); }; From 388f8256e78c77a811f60329630a9f70ba018889 Mon Sep 17 00:00:00 2001 From: jinchung Date: Wed, 8 Jan 2025 09:19:53 -0500 Subject: [PATCH 20/23] Add decimals based on network when consolidating token across networks --- src/__swaps__/screens/Swap/resources/search/utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/__swaps__/screens/Swap/resources/search/utils.ts b/src/__swaps__/screens/Swap/resources/search/utils.ts index 7c40ef0e973..f2f669e6799 100644 --- a/src/__swaps__/screens/Swap/resources/search/utils.ts +++ b/src/__swaps__/screens/Swap/resources/search/utils.ts @@ -58,14 +58,17 @@ export function parseTokenSearchAcrossNetworks(assets: SearchAsset[]): SearchAss const firstNetworkChainId = Number(networkKeys[0] || asset.chainId); const mainnetInfo = assetNetworks[ChainId.mainnet]; + const firstNetworkInfo = assetNetworks[firstNetworkChainId]; const chainId = mainnetInfo ? ChainId.mainnet : firstNetworkChainId; - const address = mainnetInfo ? mainnetInfo.address : assetNetworks[firstNetworkChainId]?.address || asset.address; + const address = mainnetInfo ? mainnetInfo.address : firstNetworkInfo?.address || asset.address; + const decimals = mainnetInfo ? mainnetInfo.decimals : firstNetworkInfo?.decimals || asset.decimals; const uniqueId = `${address}_${chainId}`; return { ...asset, - chainId, address, + chainId, + decimals, isNativeAsset: isNativeAsset(address, chainId), mainnetAddress: mainnetInfo ? mainnetInfo.address : chainId === ChainId.mainnet ? address : ('' as Address), uniqueId, From a45a21f4ccdb7d799bc1ca17e583159ebc214984 Mon Sep 17 00:00:00 2001 From: jinchung Date: Wed, 8 Jan 2025 09:53:42 -0500 Subject: [PATCH 21/23] Sort tokens by relevance from for loop to map --- src/hooks/useSearchCurrencyList.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 3d234d09651..8d3cde01527 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -45,9 +45,7 @@ const sortForDefaultList = (tokens: SearchAsset[]) => { const sortTokensByRelevance = (tokens: SearchAsset[], query: string): SearchItemWithRelevance[] => { const normalizedQuery = query.toLowerCase().trim(); - const tokenWithRelevance: SearchItemWithRelevance[] = []; - - for (const token of tokens) { + const tokenWithRelevance: SearchItemWithRelevance[] = tokens.map(token => { const normalizedTokenName = token.name.toLowerCase(); const normalizedTokenSymbol = token.symbol.toLowerCase(); @@ -59,11 +57,8 @@ const sortTokensByRelevance = (tokens: SearchAsset[], query: string): SearchItem normalizedTokenSymbol, tokenNameWords, }); - - if (relevance > 0) { - tokenWithRelevance.push({ ...token, relevance }); - } - } + return { ...token, relevance }; + }); return tokenWithRelevance.sort((a, b) => b.relevance - a.relevance); }; From fb0c111c8a8ebefed18b6a4394e7de6630a578f9 Mon Sep 17 00:00:00 2001 From: jinchung Date: Wed, 8 Jan 2025 09:59:51 -0500 Subject: [PATCH 22/23] Remove stacking of sorts as it is no longer necessary --- src/hooks/useSearchCurrencyList.ts | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 8d3cde01527..0c9c52f6028 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -63,20 +63,6 @@ const sortTokensByRelevance = (tokens: SearchAsset[], query: string): SearchItem return tokenWithRelevance.sort((a, b) => b.relevance - a.relevance); }; -const getStackedRelevanceScore = (token: SearchAsset, topScore: number) => { - if (token.isVerified) { - return topScore; - } else if (token.highLiquidity) { - return topScore - 0.1; - } else if (token.icon_url) { - return topScore - 0.2; - } else if (Object.keys(token.networks).length > 1) { - return topScore - 0.3; - } else { - return topScore - 0.4; - } -}; - // higher number indicates higher relevance const getTokenRelevance = ({ token, @@ -93,17 +79,17 @@ const getTokenRelevance = ({ }) => { // High relevance: Leading word in token name starts with query or exact match on symbol if (normalizedTokenName.startsWith(normalizedQuery) || (normalizedTokenSymbol && normalizedTokenSymbol === normalizedQuery)) { - return getStackedRelevanceScore(token, 5); + return 5; } // Medium relevance: Non-leading word in token name starts with query if (tokenNameWords.some((word, index) => index !== 0 && word.startsWith(normalizedQuery))) { - return getStackedRelevanceScore(token, 4); + return 4; } // Low relevance: Token name contains query if (tokenNameWords.some(word => word.includes(normalizedQuery))) { - return getStackedRelevanceScore(token, 3); + return 3; } return 0; From 219aed8e1423bb28435494434e11d4d9591fab92 Mon Sep 17 00:00:00 2001 From: jinchung Date: Wed, 8 Jan 2025 10:36:06 -0500 Subject: [PATCH 23/23] Fix chart expanded state using chain name instead of ID to index into asset networks object --- src/components/expanded-state/asset/ChartExpandedState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/expanded-state/asset/ChartExpandedState.js b/src/components/expanded-state/asset/ChartExpandedState.js index 4704316db9b..f42f50899e7 100644 --- a/src/components/expanded-state/asset/ChartExpandedState.js +++ b/src/components/expanded-state/asset/ChartExpandedState.js @@ -168,7 +168,7 @@ export default function ChartExpandedState({ asset }) { chainId: asset.chainId, network: asset.network, address: asset.address, - mainnetAddress: asset?.networks?.[useBackendNetworksStore.getState().getChainsName()[ChainId.mainnet]]?.address, + mainnetAddress: asset?.networks?.[ChainId.mainnet]?.address, } : asset; }, [asset, genericAsset, hasBalance]);