Commit 82646b77 authored by cartcrom's avatar cartcrom Committed by GitHub

feat: migrate search tokens to gql (#5802)

* init

* feat: search tokens hook

* feat: search ordering

* feat: separated FungibleToken parsing into sep function

* refactor: memoized search token sorting

* fix: cache waterfall issue

* fix: removed no longer relevant test

* feat: trending tokens from gql (#5805)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* fix: linted

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: unwrap native trending tokens

* feat: refetch recently searched (#5894)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* feat: recently searched tokens query

* fix: linted

* feat: combined query function

* feat: recently searched hooks

* feat: combined query

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: empty history loading state

* fix: revert change

* fix: revert unintended nft query change

* refactor: state functions

* fix: removed unnused query var

* fix: unwrap native trending tokens

* feat: remove fungible token type (#5896)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* feat: recently searched tokens query

* fix: linted

* feat: combined query function

* feat: recently searched hooks

* feat: combined query

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: empty history loading state

* fix: revert change

* fix: revert unintended nft query change

* refactor: state functions

* fix: removed unnused query var

* refactor: remove FungibleToken type

* refactor: use TokenStandard.Native instead of string

* refactor: improve boolean logic readability

* refactor: removed duplicate code

* fix: unwrap native trending tokens

* fix: type error

* fix: duplicate entry bug

* refactor: use undefined instead of null/string cast

* fix: update apollo types

* fix: polygon edge case & polish
parent 1992c5de
...@@ -10,14 +10,6 @@ describe('Universal search bar', () => { ...@@ -10,14 +10,6 @@ describe('Universal search bar', () => {
}) })
}) })
it('should yield no results found when contract address is search term', () => {
// Search for uni token contract address.
cy.get('[data-cy="search-bar-input"]').last().type('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('[data-cy="search-bar"]')
.should('contain.text', 'No tokens found.')
.and('contain.text', 'No NFT collections found.')
})
it('should yield clickable result for regular token or nft collection search term', () => { it('should yield clickable result for regular token or nft collection search term', () => {
// Search for uni token by name. // Search for uni token by name.
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni') cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
......
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { TokenQueryData } from 'graphql/data/Token' import { TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens' import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
...@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo' ...@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
export default function QueryTokenLogo( export default function QueryTokenLogo(
props: AssetLogoBaseProps & { props: AssetLogoBaseProps & {
token?: TopToken | TokenQueryData token?: TopToken | TokenQueryData | SearchToken
} }
) { ) {
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
return ( return (
<AssetLogo <AssetLogo
isNative={props.token?.address === NATIVE_CHAIN_ID} isNative={props.token?.standard === TokenStandard.Native || props.token?.address === NATIVE_CHAIN_ID}
chainId={chainId} chainId={chainId}
address={props.token?.address} address={props.token?.address}
symbol={props.token?.symbol} symbol={props.token?.symbol}
......
import { SupportedChainId } from 'constants/chains'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useAtom } from 'jotai'
import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { GenieCollection } from 'nft/types'
import { useCallback, useMemo } from 'react'
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
type RecentlySearchedAsset = {
isNft?: boolean
address: string
chain: Chain
}
// Temporary measure used until backend supports addressing by "NATIVE"
const NATIVE_QUERY_ADDRESS_INPUT = null as unknown as string
function getQueryAddress(chain: Chain) {
return getNativeTokenDBAddress(chain) ?? NATIVE_QUERY_ADDRESS_INPUT
}
const recentlySearchedAssetsAtom = atomWithStorage<RecentlySearchedAsset[]>('recentlySearchedAssets', [])
export function useAddRecentlySearchedAsset() {
const [searchHistory, updateSearchHistory] = useAtom(recentlySearchedAssetsAtom)
return useCallback(
(asset: RecentlySearchedAsset) => {
// Removes the new asset if it was already in the array
const newHistory = searchHistory.filter(
(oldAsset) => !(oldAsset.address === asset.address && oldAsset.chain === asset.chain)
)
newHistory.unshift(asset)
updateSearchHistory(newHistory)
},
[searchHistory, updateSearchHistory]
)
}
export function useRecentlySearchedAssets() {
const history = useAtomValue(recentlySearchedAssetsAtom)
const shortenedHistory = useMemo(() => history.slice(0, 4), [history])
const { data: queryData, loading } = useRecentlySearchedAssetsQuery({
variables: {
collectionAddresses: shortenedHistory.filter((asset) => asset.isNft).map((asset) => asset.address),
contracts: shortenedHistory
.filter((asset) => !asset.isNft)
.map((token) => ({
address: token.address === NATIVE_CHAIN_ID ? getQueryAddress(token.chain) : token.address,
chain: token.chain,
})),
},
})
const data = useMemo(() => {
if (shortenedHistory.length === 0) return []
else if (!queryData) return undefined
// Collects both tokens and collections in a map, so they can later be returned in original order
const resultsMap: { [key: string]: GenieCollection | SearchToken } = {}
const queryCollections = queryData?.nftCollections?.edges.map((edge) => edge.node as NonNullable<NftCollection>)
const collections = queryCollections?.map(
(queryCollection): GenieCollection => {
return {
address: queryCollection.nftContracts?.[0]?.address ?? '',
isVerified: queryCollection?.isVerified,
name: queryCollection?.name,
stats: {
floor_price: queryCollection?.markets?.[0]?.floorPrice?.value,
total_supply: queryCollection?.numAssets,
},
imageUrl: queryCollection?.image?.url ?? '',
}
},
[queryCollections]
)
collections?.forEach((collection) => (resultsMap[collection.address] = collection))
queryData.tokens?.filter(Boolean).forEach((token) => {
resultsMap[token.address ?? `NATIVE-${token.chain}`] = token
})
const data: (SearchToken | GenieCollection)[] = []
shortenedHistory.forEach((asset) => {
if (asset.address === 'NATIVE') {
// Handles special case where wMATIC data needs to be used for MATIC
const native = nativeOnChain(CHAIN_NAME_TO_CHAIN_ID[asset.chain] ?? SupportedChainId.MAINNET)
const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}`
const result = resultsMap[queryAddress]
if (result) data.push({ ...result, address: 'NATIVE', ...native })
} else {
const result = resultsMap[asset.address]
if (result) data.push(result)
}
})
return data
}, [queryData, shortenedHistory])
return { data, loading }
}
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
import { t, Trans } from '@lingui/macro' import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics' import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events' import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx' import clsx from 'clsx'
import { useSearchTokens } from 'graphql/data/SearchTokens'
import useDebounce from 'hooks/useDebounce' import useDebounce from 'hooks/useDebounce'
import { useIsNftPage } from 'hooks/useIsNftPage' import { useIsNftPage } from 'hooks/useIsNftPage'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
...@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex' ...@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex'
import { magicalGradientOnHover } from 'nft/css/common.css' import { magicalGradientOnHover } from 'nft/css/common.css'
import { useIsMobile, useIsTablet } from 'nft/hooks' import { useIsMobile, useIsTablet } from 'nft/hooks'
import { fetchSearchCollections } from 'nft/queries' import { fetchSearchCollections } from 'nft/queries'
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
...@@ -64,16 +65,8 @@ export const SearchBar = () => { ...@@ -64,16 +65,8 @@ export const SearchBar = () => {
} }
) )
const { data: tokens, isLoading: tokensAreLoading } = useQuery( const { chainId } = useWeb3React()
['searchTokens', debouncedSearchValue], const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
() => fetchSearchTokens(debouncedSearchValue),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!debouncedSearchValue.length,
}
)
const isNFTPage = useIsNftPage() const isNFTPage = useIsNftPage()
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useTrace } from '@uniswap/analytics' import { useTrace } from '@uniswap/analytics'
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events' import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import useTrendingTokens from 'graphql/data/TrendingTokens'
import { useIsNftPage } from 'hooks/useIsNftPage' import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { subheadSmall } from 'nft/css/common.css' import { subheadSmall } from 'nft/css/common.css'
import { useSearchHistory } from 'nft/hooks'
import { fetchTrendingCollections } from 'nft/queries' import { fetchTrendingCollections } from 'nft/queries'
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher' import { GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency' import { formatEthPrice } from 'nft/utils/currency'
import { ReactNode, useEffect, useMemo, useState } from 'react' import { ReactNode, useEffect, useMemo, useState } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { ClockIcon, TrendingArrow } from '../../nft/components/icons' import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css' import * as styles from './SearchBar.css'
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow' import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) { function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) {
return (suggestion as FungibleToken).decimals === undefined return (suggestion as SearchToken).decimals === undefined
} }
interface SearchBarDropdownSectionProps { interface SearchBarDropdownSectionProps {
toggleOpen: () => void toggleOpen: () => void
suggestions: (GenieCollection | FungibleToken)[] suggestions: (GenieCollection | SearchToken)[]
header: JSX.Element header: JSX.Element
headerIcon?: JSX.Element headerIcon?: JSX.Element
hoveredIndex: number | undefined hoveredIndex: number | undefined
...@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({ ...@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({
) : ( ) : (
<TokenRow <TokenRow
key={suggestion.address} key={suggestion.address}
token={suggestion as FungibleToken} token={suggestion as SearchToken}
isHovered={hoveredIndex === index + startingIndex} isHovered={hoveredIndex === index + startingIndex}
setHoveredIndex={setHoveredIndex} setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen} toggleOpen={toggleOpen}
...@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({ ...@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({
) )
} }
function isKnownToken(token: SearchToken) {
return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning
}
interface SearchBarDropdownProps { interface SearchBarDropdownProps {
toggleOpen: () => void toggleOpen: () => void
tokens: FungibleToken[] tokens: SearchToken[]
collections: GenieCollection[] collections: GenieCollection[]
queryText: string queryText: string
hasInput: boolean hasInput: boolean
...@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({ ...@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({
isLoading, isLoading,
}: SearchBarDropdownProps) => { }: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0) const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory]) const { data: searchHistory } = useRecentlySearchedAssets()
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory])
const { pathname } = useLocation() const { pathname } = useLocation()
const isNFTPage = useIsNftPage() const isNFTPage = useIsNftPage()
const isTokenPage = pathname.includes('/tokens') const isTokenPage = pathname.includes('/tokens')
...@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({ ...@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({
[isNFTPage, trendingCollectionResults] [isNFTPage, trendingCollectionResults]
) )
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery( const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
useEffect(() => {
trendingTokenResults?.forEach(updateSearchHistory)
}, [trendingTokenResults, updateSearchHistory])
const trendingTokensLength = isTokenPage ? 3 : 2 const trendingTokensLength = isTokenPage ? 3 : 2
const trendingTokens = useMemo( const trendingTokens = useMemo(
() => () => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array<SearchToken>(trendingTokensLength)],
trendingTokenResults [trendingTokenData, trendingTokensLength]
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
) )
const totalSuggestions = hasInput const totalSuggestions = hasInput
...@@ -197,10 +192,9 @@ export const SearchBarDropdown = ({ ...@@ -197,10 +192,9 @@ export const SearchBarDropdown = ({
}, [toggleOpen, hoveredIndex, totalSuggestions]) }, [toggleOpen, hoveredIndex, totalSuggestions])
const hasVerifiedCollection = collections.some((collection) => collection.isVerified) const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
const hasVerifiedToken = tokens.some((token) => token.onDefaultList) const hasKnownToken = tokens.some(isKnownToken)
const showCollectionsFirst = const showCollectionsFirst =
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) || (isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection)
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH })) const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
...@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({ ...@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({
}} }}
header={<Trans>Recent searches</Trans>} header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />} headerIcon={<ClockIcon />}
isLoading={!searchHistory}
/> />
)} )}
{!isNFTPage && ( {!isNFTPage && (
...@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({ ...@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({
}} }}
header={<Trans>Popular tokens</Trans>} header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />} headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading} isLoading={!trendingTokenData}
/> />
)} )}
{!isTokenPage && ( {!isTokenPage && (
...@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({ ...@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({
trendingCollections, trendingCollections,
trendingCollectionsAreLoading, trendingCollectionsAreLoading,
trendingTokens, trendingTokens,
trendingTokensAreLoading, trendingTokenData,
hoveredIndex, hoveredIndex,
toggleOpen, toggleOpen,
shortenedHistory, shortenedHistory,
...@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({ ...@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({
queryText, queryText,
totalSuggestions, totalSuggestions,
trace, trace,
searchHistory,
]) ])
return ( return (
......
import { sendAnalyticsEvent } from '@uniswap/analytics' import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName } from '@uniswap/analytics-events' import { InterfaceEventName } from '@uniswap/analytics-events'
import { formatUSDPrice } from '@uniswap/conedison/format' import { formatUSDPrice } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx' import clsx from 'clsx'
import AssetLogo from 'components/Logo/AssetLogo' import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon' import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo' import { checkSearchTokenWarning } from 'constants/tokenSafety'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { checkWarning } from 'constants/tokenSafety' import { SearchToken } from 'graphql/data/SearchTokens'
import { getTokenDetailsURL } from 'graphql/data/util' import { getTokenDetailsURL } from 'graphql/data/util'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons' import { VerifiedIcon } from 'nft/components/icons'
import { vars } from 'nft/css/sprinkles.css' import { vars } from 'nft/css/sprinkles.css'
import { useSearchHistory } from 'nft/hooks' import { GenieCollection } from 'nft/types'
import { FungibleToken, GenieCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils/currency' import { ethNumberStandardFormatter } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas' import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
...@@ -22,6 +20,7 @@ import { Link, useNavigate } from 'react-router-dom' ...@@ -22,6 +20,7 @@ import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart' import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css' import * as styles from './SearchBar.css'
const PriceChangeContainer = styled.div` const PriceChangeContainer = styled.div`
...@@ -59,16 +58,15 @@ export const CollectionRow = ({ ...@@ -59,16 +58,15 @@ export const CollectionRow = ({
}: CollectionRowProps) => { }: CollectionRowProps) => {
const [brokenImage, setBrokenImage] = useState(false) const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
)
const navigate = useNavigate() const navigate = useNavigate()
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
addToSearchHistory(collection) addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum })
toggleOpen() toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties }) sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, collection, toggleOpen, eventProperties]) }, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties])
useEffect(() => { useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => { const keyDownHandler = (event: KeyboardEvent) => {
...@@ -126,17 +124,8 @@ export const CollectionRow = ({ ...@@ -126,17 +124,8 @@ export const CollectionRow = ({
) )
} }
function useBridgedAddress(token: FungibleToken): [string | undefined, number | undefined, string | undefined] {
const { chainId: connectedChainId } = useWeb3React()
const bridgedAddress = connectedChainId ? token.extensions?.bridgeInfo?.[connectedChainId]?.tokenAddress : undefined
if (bridgedAddress && connectedChainId) {
return [bridgedAddress, connectedChainId, getChainInfo(connectedChainId)?.circleLogoUrl]
}
return [undefined, undefined, undefined]
}
interface TokenRowProps { interface TokenRowProps {
token: FungibleToken token: SearchToken
isHovered: boolean isHovered: boolean
setHoveredIndex: (index: number | undefined) => void setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void toggleOpen: () => void
...@@ -145,19 +134,18 @@ interface TokenRowProps { ...@@ -145,19 +134,18 @@ interface TokenRowProps {
} }
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => { export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
const addToSearchHistory = useSearchHistory( const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const navigate = useNavigate() const navigate = useNavigate()
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
addToSearchHistory(token) const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
address && addRecentlySearchedAsset({ address, chain: token.chain })
toggleOpen() toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties }) sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, toggleOpen, token, eventProperties]) }, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
const [bridgedAddress, bridgedChain] = useBridgedAddress(token) const tokenDetailsPath = getTokenDetailsURL(token)
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
// Close the modal on escape // Close the modal on escape
useEffect(() => { useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => { const keyDownHandler = (event: KeyboardEvent) => {
...@@ -173,7 +161,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, ...@@ -173,7 +161,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
} }
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath]) }, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
const arrow = getDeltaArrow(token.price24hChange, 18) const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18)
return ( return (
<Link <Link
...@@ -186,35 +174,33 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, ...@@ -186,35 +174,33 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }} style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
> >
<Row style={{ width: '65%' }}> <Row style={{ width: '65%' }}>
<AssetLogo <QueryTokenLogo
isNative={token.address === NATIVE_CHAIN_ID} token={token}
address={token.address}
chainId={token.chainId}
symbol={token.symbol} symbol={token.symbol}
size="36px" size="36px"
backupImg={token.logoURI} backupImg={token.project?.logoUrl}
style={{ margin: '8px 8px 8px 0' }} style={{ paddingRight: '8px' }}
/> />
<Column className={styles.suggestionPrimaryContainer}> <Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full"> <Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box> <Box className={styles.primaryText}>{token.name}</Box>
<TokenSafetyIcon warning={checkWarning(token.address)} /> <TokenSafetyIcon warning={checkSearchTokenWarning(token)} />
</Row> </Row>
<Box className={styles.secondaryText}>{token.symbol}</Box> <Box className={styles.secondaryText}>{token.symbol}</Box>
</Column> </Column>
</Row> </Row>
<Column className={styles.suggestionSecondaryContainer}> <Column className={styles.suggestionSecondaryContainer}>
{token.priceUsd && ( {token.market?.price?.value && (
<Row gap="4"> <Row gap="4">
<Box className={styles.primaryText}>{formatUSDPrice(token.priceUsd)}</Box> <Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
</Row> </Row>
)} )}
{token.price24hChange && ( {token.market?.pricePercentChange?.value && (
<PriceChangeContainer> <PriceChangeContainer>
<ArrowCell>{arrow}</ArrowCell> <ArrowCell>{arrow}</ArrowCell>
<PriceChangeText isNegative={token.price24hChange < 0}> <PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
{Math.abs(token.price24hChange).toFixed(2)}% {Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
</PriceChangeText> </PriceChangeText>
</PriceChangeContainer> </PriceChangeContainer>
)} )}
......
...@@ -133,19 +133,19 @@ export default function TokenDetails({ ...@@ -133,19 +133,19 @@ export default function TokenDetails({
if (!address) return if (!address) return
const bridgedAddress = crossChainMap[update] const bridgedAddress = crossChainMap[update]
if (bridgedAddress) { if (bridgedAddress) {
startTokenTransition(() => navigate(getTokenDetailsURL(bridgedAddress, update))) startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain })))
} else if (didFetchFromChain || token?.isNative) { } else if (didFetchFromChain || token?.isNative) {
startTokenTransition(() => navigate(getTokenDetailsURL(address, update))) startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
} }
}, },
[address, crossChainMap, didFetchFromChain, navigate, token?.isNative] [address, chain, crossChainMap, didFetchFromChain, navigate, token?.isNative]
) )
useOnGlobalChainSwitch(navigateToTokenForChain) useOnGlobalChainSwitch(navigateToTokenForChain)
const navigateToWidgetSelectedToken = useCallback( const navigateToWidgetSelectedToken = useCallback(
(token: Currency) => { (token: Currency) => {
const address = token.isNative ? NATIVE_CHAIN_ID : token.address const address = token.isNative ? NATIVE_CHAIN_ID : token.address
startTokenTransition(() => navigate(getTokenDetailsURL(address, chain))) startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
}, },
[chain, navigate] [chain, navigate]
) )
......
...@@ -456,7 +456,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -456,7 +456,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
return ( return (
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}> <div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
<StyledLink <StyledLink
to={getTokenDetailsURL(token.address ?? '', token.chain)} to={getTokenDetailsURL(token)}
onClick={() => onClick={() =>
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties) sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
} }
......
import { Plural, Trans } from '@lingui/macro' import { Plural, Trans } from '@lingui/macro'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { ZERO_ADDRESS } from './misc' import { ZERO_ADDRESS } from './misc'
import { NATIVE_CHAIN_ID } from './tokens' import { NATIVE_CHAIN_ID } from './tokens'
...@@ -94,3 +96,11 @@ export function checkWarning(tokenAddress: string) { ...@@ -94,3 +96,11 @@ export function checkWarning(tokenAddress: string) {
return BlockedWarning return BlockedWarning
} }
} }
// TODO(cartcrom): Replace all usage of WARNING_LEVEL with SafetyLevel
export function checkSearchTokenWarning(token: SearchToken) {
if (!token.address) {
return token.standard === TokenStandard.Native ? null : StrongWarning
}
return checkWarning(token.address)
}
import gql from 'graphql-tag'
gql`
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
nftCollections(filter: { addresses: $collectionAddresses }) {
edges {
node {
collectionId
image {
url
}
isVerified
name
numAssets
nftContracts {
address
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
}
}
}
}
tokens(contracts: $contracts) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { Chain, SearchTokensQuery, useSearchTokensQuery } from './__generated__/types-and-hooks'
import { chainIdToBackendName } from './util'
gql`
query SearchTokens($searchQuery: String!) {
searchTokens(searchQuery: $searchQuery) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`
export type SearchToken = NonNullable<NonNullable<SearchTokensQuery['searchTokens']>[number]>
function isMoreRevelantToken(current: SearchToken, existing: SearchToken | undefined, searchChain: Chain) {
if (!existing) return true
// Always priotize natives, and if both tokens are native, prefer native on current chain (i.e. Matic on Polygon over Matic on Mainnet )
if (current.standard === 'NATIVE' && (existing.standard !== 'NATIVE' || current.chain === searchChain)) return true
// Prefer tokens on the searched chain, otherwise prefer mainnet tokens
return current.chain === searchChain || (existing.chain !== searchChain && current.chain === Chain.Ethereum)
}
// Places natives first, wrapped native on current chain next, then sorts by volume
function searchTokenSortFunction(
searchChain: Chain,
wrappedNativeAddress: string | undefined,
a: SearchToken,
b: SearchToken
) {
if (a.standard === 'NATIVE') {
if (b.standard === 'NATIVE') {
if (a.chain === searchChain) return -1
else if (b.chain === searchChain) return 1
else return 0
} else return -1
} else if (b.standard === 'NATIVE') return 1
else if (wrappedNativeAddress && a.address === wrappedNativeAddress) return -1
else if (wrappedNativeAddress && b.address === wrappedNativeAddress) return 1
else return (b.market?.volume24H?.value ?? 0) - (a.market?.volume24H?.value ?? 0)
}
export function useSearchTokens(searchQuery: string, chainId: number) {
const { data, loading, error } = useSearchTokensQuery({
variables: {
searchQuery,
},
})
const sortedTokens = useMemo(() => {
const searchChain = chainIdToBackendName(chainId)
// Stores results, allowing overwriting cross-chain tokens w/ more 'relevant token'
const selectionMap: { [projectId: string]: SearchToken } = {}
data?.searchTokens?.forEach((token) => {
if (token.project?.id) {
const existing = selectionMap[token.project.id]
if (isMoreRevelantToken(token, existing, searchChain)) selectionMap[token.project.id] = token
}
})
return Object.values(selectionMap).sort(
searchTokenSortFunction.bind(null, searchChain, WRAPPED_NATIVE_CURRENCY[chainId]?.address)
)
}, [data, chainId])
return {
data: sortedTokens,
loading,
error,
}
}
...@@ -22,6 +22,7 @@ gql` ...@@ -22,6 +22,7 @@ gql`
chain chain
address address
symbol symbol
standard
market(currency: USD) { market(currency: USD) {
id id
totalValueLocked { totalValueLocked {
......
...@@ -33,6 +33,7 @@ gql` ...@@ -33,6 +33,7 @@ gql`
chain chain
address address
symbol symbol
standard
market(currency: USD) { market(currency: USD) {
id id
totalValueLocked { totalValueLocked {
......
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { useTrendingTokensQuery } from './__generated__/types-and-hooks'
import { chainIdToBackendName, unwrapToken } from './util'
gql`
query TrendingTokens($chain: Chain!) {
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`
export default function useTrendingTokens(chainId?: number) {
const chain = chainIdToBackendName(chainId)
const { data, loading } = useTrendingTokensQuery({ variables: { chain } })
return useMemo(
() => ({ data: data?.topTokens?.map((token) => unwrapToken(chainId ?? 1, token)), loading }),
[chainId, data?.topTokens, loading]
)
}
import { QueryResult } from '@apollo/client' import { QueryResult } from '@apollo/client'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useEffect } from 'react' import { useEffect } from 'react'
...@@ -96,17 +95,8 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = { ...@@ -96,17 +95,8 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo] export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
export function getTokenDetailsURL(address: string, chainName?: Chain, chainId?: number) { export function getTokenDetailsURL({ address, chain }: { address?: string | null; chain: Chain }) {
if (address === ZERO_ADDRESS && chainId && chainId === SupportedChainId.MAINNET) { return `/tokens/${chain.toLowerCase()}/${address ?? NATIVE_CHAIN_ID}`
return `/tokens/${CHAIN_ID_TO_BACKEND_NAME[chainId].toLowerCase()}/${NATIVE_CHAIN_ID}`
} else if (chainName) {
return `/tokens/${chainName.toLowerCase()}/${address}`
} else if (chainId) {
const chainName = CHAIN_ID_TO_BACKEND_NAME[chainId]
return chainName ? `/tokens/${chainName.toLowerCase()}/${address}` : ''
} else {
return ''
}
} }
export function unwrapToken< export function unwrapToken<
......
import { FungibleToken, GenieCollection } from 'nft/types' import { SearchToken } from 'graphql/data/SearchTokens'
import { GenieCollection } from 'nft/types'
/** /**
* Organizes the number of Token and NFT results to be shown to a user depending on if they're in the NFT or Token experience * Organizes the number of Token and NFT results to be shown to a user depending on if they're in the NFT or Token experience
...@@ -10,9 +11,9 @@ import { FungibleToken, GenieCollection } from 'nft/types' ...@@ -10,9 +11,9 @@ import { FungibleToken, GenieCollection } from 'nft/types'
*/ */
export function organizeSearchResults( export function organizeSearchResults(
isNFTPage: boolean, isNFTPage: boolean,
tokenResults: FungibleToken[], tokenResults: SearchToken[],
collectionResults: GenieCollection[] collectionResults: GenieCollection[]
): [FungibleToken[], GenieCollection[]] { ): [SearchToken[], GenieCollection[]] {
const reducedTokens = const reducedTokens =
tokenResults?.slice(0, isNFTPage ? 3 : collectionResults.length < 3 ? 8 - collectionResults.length : 5) ?? [] tokenResults?.slice(0, isNFTPage ? 3 : collectionResults.length < 3 ? 8 - collectionResults.length : 5) ?? []
const reducedCollections = collectionResults.slice(0, 8 - reducedTokens.length) const reducedCollections = collectionResults.slice(0, 8 - reducedTokens.length)
......
...@@ -8,7 +8,6 @@ export * from './useMarketplaceSelect' ...@@ -8,7 +8,6 @@ export * from './useMarketplaceSelect'
export * from './useNFTList' export * from './useNFTList'
export * from './useNFTSelect' export * from './useNFTSelect'
export * from './useProfilePageState' export * from './useProfilePageState'
export * from './useSearchHistory'
export * from './useSelectAsset' export * from './useSelectAsset'
export * from './useSellAsset' export * from './useSellAsset'
export * from './useSendTransaction' export * from './useSendTransaction'
......
import { FungibleToken, GenieCollection } from 'nft/types'
import create from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface SearchHistoryProps {
history: (FungibleToken | GenieCollection)[]
addItem: (item: FungibleToken | GenieCollection) => void
updateItem: (update: FungibleToken | GenieCollection) => void
}
export const useSearchHistory = create<SearchHistoryProps>()(
persist(
devtools((set) => ({
history: [],
addItem: (item: FungibleToken | GenieCollection) => {
set(({ history }) => {
const historyCopy = [...history]
if (historyCopy.length === 0 || historyCopy[0].address !== item.address) historyCopy.unshift(item)
return { history: historyCopy }
})
},
updateItem: (update: FungibleToken | GenieCollection) => {
set(({ history }) => {
const index = history.findIndex((item) => item.address === update.address)
if (index === -1) return { history }
const historyCopy = [...history]
historyCopy[index] = update
return { history: historyCopy }
})
},
})),
{ name: 'useSearchHistory' }
)
)
import { FungibleToken } from '../../types'
const TOKEN_API_URL = process.env.REACT_APP_TEMP_API_URL
export const fetchSearchTokens = async (tokenQuery: string): Promise<FungibleToken[]> => {
if (!TOKEN_API_URL) return Promise.resolve([])
const url = `${TOKEN_API_URL}/tokens/search?tokenQuery=${tokenQuery}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await r.json()
// TODO Undo favoritism
return data.data
? data.data.sort((a: FungibleToken, b: FungibleToken) => (b.name === 'Uniswap' ? 1 : b.volume24h - a.volume24h))
: []
}
import { unwrapToken } from 'graphql/data/util'
import { FungibleToken } from '../../types'
const TOKEN_API_URL = process.env.REACT_APP_TEMP_API_URL
export const fetchTrendingTokens = async (numTokens?: number): Promise<FungibleToken[]> => {
if (!TOKEN_API_URL) return Promise.resolve([])
const url = `${TOKEN_API_URL}/tokens/trending${numTokens ? `?numTokens=${numTokens}` : ''}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const { data } = (await r.json()) as { data: FungibleToken[] }
return data ? data.map((token) => unwrapToken(token.chainId, token)) : []
}
...@@ -3,26 +3,3 @@ export interface LooksRareRewardsData { ...@@ -3,26 +3,3 @@ export interface LooksRareRewardsData {
cumulativeLooksAmount: string cumulativeLooksAmount: string
cumulativeLooksProof: string[] cumulativeLooksProof: string[]
} }
interface BridgeInfoEntry {
tokenAddress?: string
}
interface FungibleTokenExtensions {
bridgeInfo?: { [chain: number]: BridgeInfoEntry }
}
export interface FungibleToken {
name: string
address: string
symbol: string
decimals: number
chainId: number
logoURI: string
coinGeckoId: string
priceUsd: number
price24hChange: number
volume24h: number
onDefaultList?: boolean
extensions?: FungibleTokenExtensions
marketCap: number
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment