From 1e20d2d6d9915df10552dce88426ddeed4c06235 Mon Sep 17 00:00:00 2001 From: WC <677680+welps@users.noreply.github.com> Date: Thu, 30 Nov 2023 18:21:04 -0500 Subject: [PATCH 1/4] chore: supply metadata graphql api key (#5211) --- globals.d.ts | 1 + src/graphql/utils/getFetchRequester.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/globals.d.ts b/globals.d.ts index d3d8fcd2ea6..ba2a17c2f89 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -88,6 +88,7 @@ declare module 'react-native-dotenv' { export const LOG_DEBUG: string; export const QUIET_OLD_LOGGER: string; export const ARC_GRAPHQL_API_KEY: string; + export const METADATA_GRAPHQL_API_KEY: string; export const RESERVOIR_API_KEY_PROD: string; export const RESERVOIR_API_KEY_DEV: string; export const RPC_PROXY_BASE_URL_PROD: string; diff --git a/src/graphql/utils/getFetchRequester.ts b/src/graphql/utils/getFetchRequester.ts index 590617dc5e2..6d168018404 100644 --- a/src/graphql/utils/getFetchRequester.ts +++ b/src/graphql/utils/getFetchRequester.ts @@ -2,7 +2,10 @@ import { rainbowFetch, RainbowFetchRequestOpts } from '@/rainbow-fetch'; import { DocumentNode } from 'graphql'; import { resolveRequestDocument } from 'graphql-request'; import { buildGetQueryParams } from '@/graphql/utils/buildGetQueryParams'; -import { ARC_GRAPHQL_API_KEY } from 'react-native-dotenv'; +import { + ARC_GRAPHQL_API_KEY, + METADATA_GRAPHQL_API_KEY, +} from 'react-native-dotenv'; const allowedOperations = ['mutation', 'query']; @@ -40,6 +43,16 @@ const additionalConfig: { 'x-api-key': ARC_GRAPHQL_API_KEY, }, }, + metadata: { + headers: { + Authorization: `Bearer ${METADATA_GRAPHQL_API_KEY}`, + }, + }, + simulation: { + headers: { + Authorization: `Bearer ${METADATA_GRAPHQL_API_KEY}`, + }, + }, }; export function getFetchRequester(config: Config) { From af272bd8e08dc7153844608556cc2ab381d43b8a Mon Sep 17 00:00:00 2001 From: Skylar Barrera Date: Fri, 1 Dec 2023 12:50:56 -0500 Subject: [PATCH 2/4] handle hex tx types (#5214) --- src/screens/SignTransactionSheet.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/screens/SignTransactionSheet.tsx b/src/screens/SignTransactionSheet.tsx index fb66164b754..038f58db073 100644 --- a/src/screens/SignTransactionSheet.tsx +++ b/src/screens/SignTransactionSheet.tsx @@ -99,6 +99,7 @@ import { estimateGasWithPadding, getFlashbotsProvider, getProviderForNetwork, + isHexString, toHex, } from '@/handlers/web3'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; @@ -140,6 +141,7 @@ import { RPCMethod } from '@/walletConnect/types'; import { isAddress } from '@ethersproject/address'; import { methodRegistryLookupAndParse } from '@/utils/methodRegistry'; import { sanitizeTypedData } from '@/utils/signingUtils'; +import { hexToNumber, isHex } from 'viem'; const COLLAPSED_CARD_HEIGHT = 56; const MAX_CARD_HEIGHT = 176; @@ -262,6 +264,9 @@ export const SignTransactionSheet = () => { const calculateGasLimit = useCallback(async () => { calculatingGasLimit.current = true; const txPayload = req; + if (isHex(txPayload?.type)) { + txPayload.type = hexToNumber(txPayload?.type); + } // use the default let gas = txPayload.gasLimit || txPayload.gas; @@ -281,7 +286,6 @@ export const SignTransactionSheet = () => { { gas }, logger.DebugContext.walletconnect ); - // safety precaution: we want to ensure these properties are not used for gas estimation const cleanTxPayload = omitFlatten(txPayload, [ 'gas', @@ -907,6 +911,9 @@ export const SignTransactionSheet = () => { return; } if (sendInsteadOfSign) { + if (isHex(txPayloadUpdated?.type)) { + txPayloadUpdated.type = hexToNumber(txPayloadUpdated?.type); + } response = await sendTransaction({ existingWallet: existingWallet, provider, From c16745e9c6dd22782e603a8ed3f635f59e22235d Mon Sep 17 00:00:00 2001 From: Skylar Barrera Date: Fri, 1 Dec 2023 12:51:15 -0500 Subject: [PATCH 3/4] tx sim: error handling for unknown urls (#5213) --- src/screens/SignTransactionSheet.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/screens/SignTransactionSheet.tsx b/src/screens/SignTransactionSheet.tsx index 038f58db073..9667fe316a7 100644 --- a/src/screens/SignTransactionSheet.tsx +++ b/src/screens/SignTransactionSheet.tsx @@ -222,9 +222,13 @@ export const SignTransactionSheet = () => { null ); const formattedDappUrl = useMemo(() => { - const { hostname } = new URL(transactionDetails?.dappUrl); - return hostname; - }, [transactionDetails?.dappUrl]); + try { + const { hostname } = new URL(transactionDetails?.dappUrl); + return hostname; + } catch { + return transactionDetails?.dappUrl; + } + }, [transactionDetails]); const { gasLimit, From e85c3fe37a390f35f44b904ec5230481bc5015b1 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 1 Dec 2023 12:15:51 -0700 Subject: [PATCH 4/4] [APP-379]: Update client to use new token search aggregator across networks (#5190) * update rainbow fetch to be typed and update token search endpoint * fix swap currency list * update interfaces and transforms * idkwtf i'm doing * remove unused mapping * revert swap changes and fix icon_url * fix lint --- src/entities/tokens.ts | 24 + src/handlers/tokenSearch.ts | 67 +- src/hooks/index.ts | 1 + src/hooks/useSearchCurrencyList.ts | 660 ++++++++++++++++++ src/hooks/useSwapCurrencyList.ts | 4 +- src/rainbow-fetch/index.ts | 36 +- .../discover/components/DiscoverSearch.js | 4 +- 7 files changed, 775 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useSearchCurrencyList.ts diff --git a/src/entities/tokens.ts b/src/entities/tokens.ts index 136a746667d..43a5806649d 100644 --- a/src/entities/tokens.ts +++ b/src/entities/tokens.ts @@ -2,6 +2,7 @@ import { ChainId } from '@rainbow-me/swaps'; import { AssetType } from './assetTypes'; import { EthereumAddress } from '.'; import { Network } from '@/helpers'; +import { Chain } from '@wagmi/chains'; export interface ZerionAssetPrice { value: number; @@ -88,6 +89,29 @@ export interface SwappableAsset extends ParsedAddressAsset { network?: Network; } +export interface TokenSearchNetwork { + address: string; + decimals: number; +} + +export interface TokenSearchToken { + decimals: number; + highLiquidity: boolean; + name: string; + symbol: string; + uniqueId: string; + colors: { primary: string; fallback: string }; + icon_url: string; + color: string; + shadowColor: string; + rainbowMetadataId: number; + isRainbowCurated: boolean; + isVerified: boolean; + networks: { + [chainId in Chain['id']]: TokenSearchNetwork; + }; +} + export interface RainbowToken extends Asset { color?: string; highLiquidity?: boolean; diff --git a/src/handlers/tokenSearch.ts b/src/handlers/tokenSearch.ts index 7288debc3e6..e0dc3eaa3f1 100644 --- a/src/handlers/tokenSearch.ts +++ b/src/handlers/tokenSearch.ts @@ -8,6 +8,12 @@ import { } from '@/entities'; import { logger, RainbowError } from '@/logger'; import { EthereumAddress } from '@rainbow-me/swaps'; +import { RainbowToken, TokenSearchToken } from '@/entities/tokens'; +import ethereumUtils from '@/utils/ethereumUtils'; + +type TokenSearchApiResponse = { + data: TokenSearchToken[]; +}; const tokenSearchApi = new RainbowFetchClient({ baseURL: 'https://token-search.rainbow.me/v2', @@ -18,7 +24,7 @@ const tokenSearchApi = new RainbowFetchClient({ timeout: 30000, }); -export const tokenSearch = async (searchParams: { +export const swapSearch = async (searchParams: { chainId: number; fromChainId?: number | ''; keys: TokenSearchUniswapAssetKey[]; @@ -60,6 +66,65 @@ export const tokenSearch = async (searchParams: { } }; +export const tokenSearch = async (searchParams: { + chainId: number; + fromChainId?: number | ''; + keys: TokenSearchUniswapAssetKey[]; + list: TokenSearchTokenListId; + threshold: TokenSearchThreshold; + query: string; +}): Promise => { + const queryParams: { + keys: TokenSearchUniswapAssetKey[]; + list: TokenSearchTokenListId; + threshold: TokenSearchThreshold; + query?: string; + fromChainId?: number; + } = { + keys: searchParams.keys, + list: searchParams.list, + threshold: searchParams.threshold, + query: searchParams.query, + }; + + try { + if (isAddress(searchParams.query)) { + // @ts-ignore + params.keys = `networks.${params.chainId}.address`; + } + const url = `/?${qs.stringify(queryParams)}`; + const tokenSearch = await tokenSearchApi.get(url); + if (!tokenSearch.data?.data) { + return []; + } + + return tokenSearch.data.data.map(token => { + const networkKeys = Object.keys(token.networks); + const type = ethereumUtils.getAssetTypeFromNetwork( + ethereumUtils.getNetworkFromChainId(Number(networkKeys[0])) + ); + return { + ...token, + address: + token.networks['1']?.address || + token.networks[Number(networkKeys[0])]?.address, + type, + mainnet_address: token.networks['1']?.address, + }; + }); + } catch (e: any) { + logger.error( + new RainbowError(`An error occurred while searching for query`), + { + query: searchParams.query, + message: e.message, + } + ); + + return []; + } +}; + export const walletFilter = async (params: { addresses: EthereumAddress[]; fromChainId: number; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 74545831cf2..94c48d486e6 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -146,6 +146,7 @@ export { useHardwareBackOnFocus, } from './useHardwareBack'; export { default as useSwapCurrencyList } from './useSwapCurrencyList'; +export { default as useSearchCurrencyList } from './useSearchCurrencyList'; export { default as useWalletENSAvatar } from './useWalletENSAvatar'; export { default as useImagePicker } from './useImagePicker'; export { default as useLatestCallback } from './useLatestCallback'; diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts new file mode 100644 index 00000000000..54f6d31511c --- /dev/null +++ b/src/hooks/useSearchCurrencyList.ts @@ -0,0 +1,660 @@ +import lang from 'i18n-js'; +import { getAddress, isAddress } from '@ethersproject/address'; +import { ChainId, EthereumAddress } from '@rainbow-me/swaps'; +import { Contract } from '@ethersproject/contracts'; +import { rankings } from 'match-sorter'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTheme } from '../theme/ThemeContext'; +import usePrevious from './usePrevious'; +import { + AssetType, + RainbowToken, + RainbowToken as RT, + TokenSearchTokenListId, +} from '@/entities'; +import { tokenSearch } from '@/handlers/tokenSearch'; +import { addHexPrefix, getProviderForNetwork } from '@/handlers/web3'; +import tokenSectionTypes from '@/helpers/tokenSectionTypes'; +import { + DAI_ADDRESS, + erc20ABI, + ETH_ADDRESS, + rainbowTokenList, + USDC_ADDRESS, + WBTC_ADDRESS, + WETH_ADDRESS, +} from '@/references'; +import { ethereumUtils, filterList, isLowerCaseMatch, logger } from '@/utils'; +import useSwapCurrencies from '@/hooks/useSwapCurrencies'; +import { Network } from '@/helpers'; +import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; +import { IS_TEST } from '@/env'; +import { useFavorites } from '@/resources/favorites'; + +const MAINNET_CHAINID = 1; +type swapCurrencyListType = + | 'verifiedAssets' + | 'highLiquidityAssets' + | 'lowLiquidityAssets' + | 'favoriteAssets' + | 'curatedAssets' + | 'importedAssets'; + +type CrosschainVerifiedAssets = { + [Network.mainnet]: RT[]; + [Network.optimism]: RT[]; + [Network.polygon]: RT[]; + [Network.bsc]: RT[]; + [Network.arbitrum]: RT[]; +}; + +const abcSort = (list: any[], key?: string) => { + return list.sort((a, b) => { + return key ? a[key]?.localeCompare(b[key]) : a?.localeCompare(b); + }); +}; + +const searchCurrencyList = async (searchParams: { + chainId: number; + fromChainId?: number | ''; + searchList: RT[] | TokenSearchTokenListId; + query: string; +}) => { + const { searchList, query, chainId } = searchParams; + const isAddress = query.match(/^(0x)?[0-9a-fA-F]{40}$/); + const keys: (keyof RT)[] = isAddress ? ['address'] : ['symbol', 'name']; + const formattedQuery = isAddress ? addHexPrefix(query).toLowerCase() : query; + if (typeof searchList === 'string') { + const threshold = isAddress ? 'CASE_SENSITIVE_EQUAL' : 'CONTAINS'; + if ( + chainId === MAINNET_CHAINID && + !formattedQuery && + searchList !== 'verifiedAssets' + ) { + return []; + } + return tokenSearch({ + chainId, + keys, + list: searchList, + threshold, + query: formattedQuery, + }); + } else { + return ( + filterList(searchList, formattedQuery, keys, { + threshold: isAddress + ? rankings.CASE_SENSITIVE_EQUAL + : rankings.CONTAINS, + }) || [] + ); + } +}; + +const useSearchCurrencyList = ( + searchQuery: string, + searchChainId = MAINNET_CHAINID, + isDiscover = false +) => { + const previousChainId = usePrevious(searchChainId); + + const searching = useMemo( + () => searchQuery !== '' || MAINNET_CHAINID !== searchChainId, + [searchChainId, searchQuery] + ); + + const { + favorites: favoriteAddresses, + favoritesMetadata: favoriteMap, + } = useFavorites(); + + const curatedMap = rainbowTokenList.CURATED_TOKENS; + const unfilteredFavorites = Object.values(favoriteMap); + + 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({ + [Network.mainnet]: [], + [Network.optimism]: [], + [Network.polygon]: [], + [Network.bsc]: [], + [Network.arbitrum]: [], + }); + + const crosschainSwapsEnabled = useExperimentalFlag(CROSSCHAIN_SWAPS); + const { inputCurrency } = useSwapCurrencies(); + const previousInputCurrencyType = usePrevious(inputCurrency?.type); + const inputChainId = useMemo( + () => ethereumUtils.getChainIdFromType(inputCurrency?.type), + [inputCurrency?.type] + ); + const isCrosschainSearch = useMemo(() => { + if ( + inputChainId && + inputChainId !== searchChainId && + crosschainSwapsEnabled && + !isDiscover + ) { + return true; + } + }, [inputChainId, searchChainId, crosschainSwapsEnabled, isDiscover]); + + const isFavorite = useCallback( + (address: EthereumAddress) => + favoriteAddresses + .map(a => a?.toLowerCase()) + .includes(address?.toLowerCase()), + [favoriteAddresses] + ); + const handleSearchResponse = useCallback( + (tokens: RT[]): RT[] => { + // These transformations are necessary for L2 tokens to match our spec + return (tokens || []) + .map(token => { + const t: RT = { + ...token, + type: token?.type || AssetType.token, + address: token?.address || token.uniqueId.toLowerCase(), + } as RT; + + return t; + }) + .filter(({ address }) => !isFavorite(address)); + }, + [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({ + searchList: unfilteredFavorites as RainbowToken[], + query: searchQuery, + chainId: searchChainId, + }) + : unfilteredFavorites; + }, [searchChainId, searchQuery, searching, unfilteredFavorites]); + + const getImportedAsset = useCallback( + async (searchQuery: string, chainId: number): Promise => { + if (searching) { + if (isAddress(searchQuery)) { + const tokenListEntry = + rainbowTokenList.RAINBOW_TOKEN_LIST[searchQuery.toLowerCase()]; + if (tokenListEntry) { + return [tokenListEntry]; + } + const network = ethereumUtils.getNetworkFromChainId(chainId); + const provider = await getProviderForNetwork(network); + 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 = + chainId === ChainId.mainnet ? address : `${address}_${network}`; + const type = + chainId === ChainId.mainnet ? AssetType.token : network; + return [ + { + decimals, + favorite: false, + highLiquidity: false, + isRainbowCurated: false, + isVerified: false, + name, + networks: { + [chainId]: { + address, + decimals, + }, + }, + symbol, + type, + uniqueId, + } as RainbowToken, + ]; + } catch (e) { + logger.log('error getting token data'); + logger.log(e); + return null; + } + } + } + return null; + }, + [searching] + ); + + const getCrosschainVerifiedAssetsForNetwork = useCallback( + async (network: Network) => { + const crosschainId = ethereumUtils.getChainIdFromNetwork(network); + const fromChainId = inputChainId !== crosschainId ? inputChainId : ''; + const results = await searchCurrencyList({ + searchList: 'verifiedAssets', + query: '', + chainId: crosschainId, + fromChainId, + }); + setCrosschainVerifiedAssets(state => ({ + ...state, + [network]: handleSearchResponse(results || []), + })); + }, + [handleSearchResponse, inputChainId] + ); + + const getCrosschainVerifiedAssets = useCallback(async () => { + const crosschainAssetRequests: Promise[] = []; + Object.keys(crosschainVerifiedAssets).forEach(network => { + crosschainAssetRequests.push( + getCrosschainVerifiedAssetsForNetwork(network as Network) + ); + }); + 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, + fromChainId: isCrosschainSearch && inputChainId, + }) + ) + ); + break; + case 'highLiquidityAssets': + setHighLiquidityAssets( + handleSearchResponse( + await searchCurrencyList({ + searchList: assetType, + query: searchQuery, + chainId: searchChainId, + fromChainId: isCrosschainSearch && inputChainId, + }) + ) + ); + break; + case 'lowLiquidityAssets': + setLowLiquidityAssets( + handleSearchResponse( + await searchCurrencyList({ + searchList: assetType, + query: searchQuery, + chainId: searchChainId, + fromChainId: isCrosschainSearch && inputChainId, + }) + ) + ); + break; + case 'favoriteAssets': + setFavoriteAssets((await getFavorites()) || []); + break; + case 'importedAssets': { + const importedAssetResult = await getImportedAsset( + searchQuery, + searchChainId + ); + if (importedAssetResult) { + setImportedAssets(handleSearchResponse(importedAssetResult)); + } + break; + } + } + }, + [ + getFavorites, + getImportedAsset, + handleSearchResponse, + searchQuery, + searchChainId, + inputChainId, + isCrosschainSearch, + ] + ); + + const search = useCallback(async () => { + const categories: swapCurrencyListType[] = + searchChainId === MAINNET_CHAINID + ? [ + 'favoriteAssets', + 'highLiquidityAssets', + 'verifiedAssets', + 'importedAssets', + ] + : ['verifiedAssets', 'importedAssets']; + setLoading(true); + 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([]); + setHighLiquidityAssets([]); + setVerifiedAssets([]); + setImportedAssets([]); + }, [getResultsForAssetType]); + + const wasSearching = usePrevious(searching); + const previousSearchQuery = usePrevious(searchQuery); + + useEffect(() => { + setFetchingCrosschainAssets(false); + }, [inputChainId]); + + useEffect(() => { + if (!fetchingCrosschainAssets && crosschainSwapsEnabled) { + setFetchingCrosschainAssets(true); + getCrosschainVerifiedAssets(); + } + }, [ + getCrosschainVerifiedAssets, + fetchingCrosschainAssets, + crosschainSwapsEnabled, + ]); + + useEffect(() => { + const doSearch = async () => { + if ( + (searching && !wasSearching) || + (searching && previousSearchQuery !== searchQuery) || + searchChainId !== previousChainId || + inputCurrency?.type !== previousInputCurrencyType + ) { + if (searchChainId === MAINNET_CHAINID) { + search(); + slowSearch(); + } else { + await search(); + setLowLiquidityAssets([]); + setHighLiquidityAssets([]); + setLoading(false); + } + } else { + clearSearch(); + } + }; + doSearch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + searching, + searchQuery, + searchChainId, + isCrosschainSearch, + inputCurrency?.type, + ]); + + const { colors } = useTheme(); + + const currencyList = useMemo(() => { + const list = []; + + let bridgeAsset = isCrosschainSearch + ? verifiedAssets.find( + asset => + isLowerCaseMatch(asset?.name, inputCurrency?.name) && + asset?.type !== inputCurrency?.type + ) + : null; + 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 (inputCurrency?.name && verifiedAssets.length) { + if (bridgeAsset) { + list.push({ + color: + colors.networkColors[ + ethereumUtils.getNetworkFromType(bridgeAsset.type) + ], + data: [bridgeAsset], + key: 'bridgeAsset', + title: lang.t( + `exchange.token_sections.${tokenSectionTypes.bridgeTokenSection}` + ), + }); + } + } + if (favoriteAssets?.length && searchChainId === MAINNET_CHAINID) { + list.push({ + color: colors.yellowFavorite, + data: abcSort(favoriteAssets, 'name'), + key: 'favorites', + title: lang.t( + `exchange.token_sections.${tokenSectionTypes.favoriteTokenSection}` + ), + }); + } + if (verifiedAssetsWithImport?.length) { + list.push({ + data: verifiedAssetsWithImport, + key: 'verified', + title: lang.t( + `exchange.token_sections.${tokenSectionTypes.verifiedTokenSection}` + ), + useGradientText: !IS_TEST, + }); + } + if (highLiquidityAssetsWithImport?.length) { + list.push({ + data: highLiquidityAssetsWithImport, + key: 'highLiquidity', + title: lang.t( + `exchange.token_sections.${tokenSectionTypes.unverifiedTokenSection}` + ), + }); + } + if (lowLiquidityAssetsWithoutImport?.length) { + list.push({ + data: lowLiquidityAssetsWithoutImport, + key: 'lowLiquidity', + title: lang.t( + `exchange.token_sections.${tokenSectionTypes.lowLiquidityTokenSection}` + ), + }); + } + } else { + const curatedAssets = searchChainId === MAINNET_CHAINID && getCurated(); + if (inputCurrency?.name && isCrosschainSearch && curatedAssets) { + bridgeAsset = curatedAssets.find( + asset => asset?.name === inputCurrency?.name + ); + if (bridgeAsset) { + list.push({ + color: + colors.networkColors[ + ethereumUtils.getNetworkFromType(bridgeAsset.type) + ], + data: [bridgeAsset], + key: 'bridgeAsset', + title: lang.t( + `exchange.token_sections.${tokenSectionTypes.bridgeTokenSection}` + ), + }); + } + } + if (unfilteredFavorites?.length) { + list.push({ + color: colors.yellowFavorite, + data: abcSort(unfilteredFavorites, 'name'), + key: 'unfilteredFavorites', + 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; + }, [ + searching, + importedAssets, + favoriteAssets, + verifiedAssets, + highLiquidityAssets, + lowLiquidityAssets, + colors.yellowFavorite, + unfilteredFavorites, + searchChainId, + getCurated, + isFavorite, + inputCurrency?.name, + colors.networkColors, + isCrosschainSearch, + inputCurrency?.type, + ]); + + const crosschainExactMatches = useMemo(() => { + if (currencyList.length) return []; + if (!searchQuery) return []; + const exactMatches: RT[] = []; + Object.keys(crosschainVerifiedAssets).forEach(network => { + const currentNetworkChainId = ethereumUtils.getChainIdFromNetwork( + network as Network + ); + if (currentNetworkChainId !== searchChainId) { + // including goerli in our networks type is causing this type issue + // @ts-ignore + const exactMatch = crosschainVerifiedAssets[network as Network].find( + (asset: RT) => { + const symbolMatch = isLowerCaseMatch(asset?.symbol, searchQuery); + const nameMatch = isLowerCaseMatch(asset?.name, searchQuery); + return symbolMatch || nameMatch; + } + ); + if (exactMatch) { + exactMatches.push({ ...exactMatch, network }); + } + } + }); + 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, + ]); + + return { + crosschainExactMatches, + swapCurrencyList: currencyList, + swapCurrencyListLoading: loading, + }; +}; + +export default useSearchCurrencyList; diff --git a/src/hooks/useSwapCurrencyList.ts b/src/hooks/useSwapCurrencyList.ts index 0b24a05a1cb..a958cb60f00 100644 --- a/src/hooks/useSwapCurrencyList.ts +++ b/src/hooks/useSwapCurrencyList.ts @@ -12,7 +12,7 @@ import { RainbowToken as RT, TokenSearchTokenListId, } from '@/entities'; -import { tokenSearch } from '@/handlers/tokenSearch'; +import { swapSearch } from '@/handlers/tokenSearch'; import { addHexPrefix, getProviderForNetwork } from '@/handlers/web3'; import tokenSectionTypes from '@/helpers/tokenSectionTypes'; import { @@ -73,7 +73,7 @@ const searchCurrencyList = async (searchParams: { ) { return []; } - return tokenSearch({ + return swapSearch({ chainId, fromChainId, keys, diff --git a/src/rainbow-fetch/index.ts b/src/rainbow-fetch/index.ts index a67d8046e0e..33dfe2d8b6f 100644 --- a/src/rainbow-fetch/index.ts +++ b/src/rainbow-fetch/index.ts @@ -8,10 +8,10 @@ export interface RainbowFetchRequestOpts extends RequestInit { /** * rainbowFetch fetches data and handles response edge cases and error handling. */ -export async function rainbowFetch( +export async function rainbowFetch( url: RequestInfo, opts: RainbowFetchRequestOpts -) { +): Promise<{ data: T; headers: Headers; status: number }> { opts = { headers: {}, method: 'get', @@ -120,8 +120,8 @@ export class RainbowFetchClient { /** * Perform a GET request with the RainbowFetchClient. */ - get(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { - return rainbowFetch(`${this.baseURL}${url}`, { + get(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { + return rainbowFetch(`${this.baseURL}${url}`, { ...opts, method: 'get', }); @@ -130,8 +130,8 @@ export class RainbowFetchClient { /** * Perform a DELETE request with the RainbowFetchClient. */ - delete(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { - return rainbowFetch(`${this.baseURL}${url}`, { + delete(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { + return rainbowFetch(`${this.baseURL}${url}`, { ...opts, method: 'delete', }); @@ -140,8 +140,8 @@ export class RainbowFetchClient { /** * Perform a HEAD request with the RainbowFetchClient. */ - head(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { - return rainbowFetch(`${this.baseURL}${url}`, { + head(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { + return rainbowFetch(`${this.baseURL}${url}`, { ...opts, method: 'head', }); @@ -150,8 +150,8 @@ export class RainbowFetchClient { /** * Perform a OPTIONS request with the RainbowFetchClient. */ - options(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { - return rainbowFetch(`${this.baseURL}${url}`, { + options(url?: RequestInfo, opts?: RainbowFetchRequestOpts) { + return rainbowFetch(`${this.baseURL}${url}`, { ...opts, method: 'options', }); @@ -160,8 +160,8 @@ export class RainbowFetchClient { /** * Perform a POST request with the RainbowFetchClient. */ - post(url?: RequestInfo, body?: any, opts?: RainbowFetchRequestOpts) { - return rainbowFetch(`${this.baseURL}${url}`, { + post(url?: RequestInfo, body?: any, opts?: RainbowFetchRequestOpts) { + return rainbowFetch(`${this.baseURL}${url}`, { ...opts, body, method: 'post', @@ -171,8 +171,8 @@ export class RainbowFetchClient { /** * Perform a PUT request with the RainbowFetchClient. */ - put(url?: RequestInfo, body?: any, opts?: RainbowFetchRequestOpts) { - return rainbowFetch(`${this.baseURL}${url}`, { + put(url?: RequestInfo, body?: any, opts?: RainbowFetchRequestOpts) { + return rainbowFetch(`${this.baseURL}${url}`, { ...opts, body, method: 'put', @@ -182,8 +182,12 @@ export class RainbowFetchClient { /** * Perform a PATCH request with the RainbowFetchClient. */ - patch(url?: RequestInfo, body?: any, opts?: RainbowFetchRequestOpts) { - return rainbowFetch(`${this.baseURL}${url}`, { + patch( + url?: RequestInfo, + body?: any, + opts?: RainbowFetchRequestOpts + ) { + return rainbowFetch(`${this.baseURL}${url}`, { ...opts, body, method: 'patch', diff --git a/src/screens/discover/components/DiscoverSearch.js b/src/screens/discover/components/DiscoverSearch.js index 9a9e1db97e3..8f9f46d2729 100644 --- a/src/screens/discover/components/DiscoverSearch.js +++ b/src/screens/discover/components/DiscoverSearch.js @@ -25,7 +25,7 @@ import { useAccountSettings, useHardwareBackOnFocus, usePrevious, - useSwapCurrencyList, + useSearchCurrencyList, } from '@/hooks'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; @@ -65,7 +65,7 @@ export default function DiscoverSearch() { const currencySelectionListRef = useRef(); const [searchQueryForSearch] = useDebounce(searchQuery, 350); const [ensResults, setEnsResults] = useState([]); - const { swapCurrencyList, swapCurrencyListLoading } = useSwapCurrencyList( + const { swapCurrencyList, swapCurrencyListLoading } = useSearchCurrencyList( searchQueryForSearch, ethereumUtils.getChainIdFromNetwork(Network.mainnet), true