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', () => {
})
})
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', () => {
// Search for uni token by name.
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
......
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 { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
......@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
export default function QueryTokenLogo(
props: AssetLogoBaseProps & {
token?: TopToken | TokenQueryData
token?: TopToken | TokenQueryData | SearchToken
}
) {
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
return (
<AssetLogo
isNative={props.token?.address === NATIVE_CHAIN_ID}
isNative={props.token?.standard === TokenStandard.Native || props.token?.address === NATIVE_CHAIN_ID}
chainId={chainId}
address={props.token?.address}
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 @@
import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import { useSearchTokens } from 'graphql/data/SearchTokens'
import useDebounce from 'hooks/useDebounce'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
......@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex'
import { magicalGradientOnHover } from 'nft/css/common.css'
import { useIsMobile, useIsTablet } from 'nft/hooks'
import { fetchSearchCollections } from 'nft/queries'
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
......@@ -64,16 +65,8 @@ export const SearchBar = () => {
}
)
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
['searchTokens', debouncedSearchValue],
() => fetchSearchTokens(debouncedSearchValue),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!debouncedSearchValue.length,
}
)
const { chainId } = useWeb3React()
const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
const isNFTPage = useIsNftPage()
......
import { Trans } from '@lingui/macro'
import { useTrace } from '@uniswap/analytics'
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 { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { subheadSmall } from 'nft/css/common.css'
import { useSearchHistory } from 'nft/hooks'
import { fetchTrendingCollections } from 'nft/queries'
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency'
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
return (suggestion as FungibleToken).decimals === undefined
function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) {
return (suggestion as SearchToken).decimals === undefined
}
interface SearchBarDropdownSectionProps {
toggleOpen: () => void
suggestions: (GenieCollection | FungibleToken)[]
suggestions: (GenieCollection | SearchToken)[]
header: JSX.Element
headerIcon?: JSX.Element
hoveredIndex: number | undefined
......@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({
) : (
<TokenRow
key={suggestion.address}
token={suggestion as FungibleToken}
token={suggestion as SearchToken}
isHovered={hoveredIndex === index + startingIndex}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
......@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({
)
}
function isKnownToken(token: SearchToken) {
return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning
}
interface SearchBarDropdownProps {
toggleOpen: () => void
tokens: FungibleToken[]
tokens: SearchToken[]
collections: GenieCollection[]
queryText: string
hasInput: boolean
......@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({
isLoading,
}: SearchBarDropdownProps) => {
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 isNFTPage = useIsNftPage()
const isTokenPage = pathname.includes('/tokens')
......@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({
[isNFTPage, trendingCollectionResults]
)
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
useEffect(() => {
trendingTokenResults?.forEach(updateSearchHistory)
}, [trendingTokenResults, updateSearchHistory])
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
const trendingTokensLength = isTokenPage ? 3 : 2
const trendingTokens = useMemo(
() =>
trendingTokenResults
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
() => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array<SearchToken>(trendingTokensLength)],
[trendingTokenData, trendingTokensLength]
)
const totalSuggestions = hasInput
......@@ -197,10 +192,9 @@ export const SearchBarDropdown = ({
}, [toggleOpen, hoveredIndex, totalSuggestions])
const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
const hasVerifiedToken = tokens.some((token) => token.onDefaultList)
const hasKnownToken = tokens.some(isKnownToken)
const showCollectionsFirst =
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) ||
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
(isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection)
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
......@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({
}}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
isLoading={!searchHistory}
/>
)}
{!isNFTPage && (
......@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({
}}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading}
isLoading={!trendingTokenData}
/>
)}
{!isTokenPage && (
......@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({
trendingCollections,
trendingCollectionsAreLoading,
trendingTokens,
trendingTokensAreLoading,
trendingTokenData,
hoveredIndex,
toggleOpen,
shortenedHistory,
......@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({
queryText,
totalSuggestions,
trace,
searchHistory,
])
return (
......
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName } from '@uniswap/analytics-events'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import AssetLogo from 'components/Logo/AssetLogo'
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { checkSearchTokenWarning } from 'constants/tokenSafety'
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { getTokenDetailsURL } from 'graphql/data/util'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
import { vars } from 'nft/css/sprinkles.css'
import { useSearchHistory } from 'nft/hooks'
import { FungibleToken, GenieCollection } from 'nft/types'
import { GenieCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useState } from 'react'
......@@ -22,6 +20,7 @@ import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
const PriceChangeContainer = styled.div`
......@@ -59,16 +58,15 @@ export const CollectionRow = ({
}: CollectionRowProps) => {
const [brokenImage, setBrokenImage] = 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 handleClick = useCallback(() => {
addToSearchHistory(collection)
addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum })
toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, collection, toggleOpen, eventProperties])
}, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties])
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
......@@ -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 {
token: FungibleToken
token: SearchToken
isHovered: boolean
setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void
......@@ -145,19 +134,18 @@ interface TokenRowProps {
}
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(token)
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
address && addRecentlySearchedAsset({ address, chain: token.chain })
toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, toggleOpen, token, eventProperties])
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
const [bridgedAddress, bridgedChain] = useBridgedAddress(token)
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
const tokenDetailsPath = getTokenDetailsURL(token)
// Close the modal on escape
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
......@@ -173,7 +161,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
}
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
const arrow = getDeltaArrow(token.price24hChange, 18)
const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18)
return (
<Link
......@@ -186,35 +174,33 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
>
<Row style={{ width: '65%' }}>
<AssetLogo
isNative={token.address === NATIVE_CHAIN_ID}
address={token.address}
chainId={token.chainId}
<QueryTokenLogo
token={token}
symbol={token.symbol}
size="36px"
backupImg={token.logoURI}
style={{ margin: '8px 8px 8px 0' }}
backupImg={token.project?.logoUrl}
style={{ paddingRight: '8px' }}
/>
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
<TokenSafetyIcon warning={checkWarning(token.address)} />
<TokenSafetyIcon warning={checkSearchTokenWarning(token)} />
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>
</Row>
<Column className={styles.suggestionSecondaryContainer}>
{token.priceUsd && (
{token.market?.price?.value && (
<Row gap="4">
<Box className={styles.primaryText}>{formatUSDPrice(token.priceUsd)}</Box>
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
</Row>
)}
{token.price24hChange && (
{token.market?.pricePercentChange?.value && (
<PriceChangeContainer>
<ArrowCell>{arrow}</ArrowCell>
<PriceChangeText isNegative={token.price24hChange < 0}>
{Math.abs(token.price24hChange).toFixed(2)}%
<PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
{Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
</PriceChangeText>
</PriceChangeContainer>
)}
......
......@@ -133,19 +133,19 @@ export default function TokenDetails({
if (!address) return
const bridgedAddress = crossChainMap[update]
if (bridgedAddress) {
startTokenTransition(() => navigate(getTokenDetailsURL(bridgedAddress, update)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain })))
} 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)
const navigateToWidgetSelectedToken = useCallback(
(token: Currency) => {
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
startTokenTransition(() => navigate(getTokenDetailsURL(address, chain)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
},
[chain, navigate]
)
......
......@@ -456,7 +456,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
return (
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
<StyledLink
to={getTokenDetailsURL(token.address ?? '', token.chain)}
to={getTokenDetailsURL(token)}
onClick={() =>
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
}
......
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 { NATIVE_CHAIN_ID } from './tokens'
......@@ -94,3 +96,11 @@ export function checkWarning(tokenAddress: string) {
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`
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
......
......@@ -33,6 +33,7 @@ gql`
chain
address
symbol
standard
market(currency: USD) {
id
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]
)
}
......@@ -95,6 +95,11 @@ export enum Currency {
Usd = 'USD'
}
export enum DatasourceProvider {
Alternate = 'ALTERNATE',
Legacy = 'LEGACY'
}
export type Dimensions = {
__typename?: 'Dimensions';
height?: Maybe<Scalars['Float']>;
......@@ -544,6 +549,20 @@ export type PairInput = {
tokenAmountB: TokenAmountInput;
};
export type PermitDetailsInput = {
amount: Scalars['String'];
expiration: Scalars['String'];
nonce: Scalars['String'];
token: Scalars['String'];
};
export type PermitInput = {
details: PermitDetailsInput;
sigDeadline: Scalars['String'];
signature: Scalars['String'];
spender: Scalars['String'];
};
/** v3 pool parameters as defined by https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts */
export type PoolInput = {
fee: Scalars['Int'];
......@@ -859,6 +878,7 @@ export enum TokenStandard {
}
export type TokenTradeInput = {
permit?: InputMaybe<PermitInput>;
routes?: InputMaybe<TokenTradeRoutesInput>;
slippageToleranceBasisPoints?: InputMaybe<Scalars['Int']>;
tokenAmount: TokenAmountInput;
......@@ -924,13 +944,28 @@ export enum TransactionStatus {
Pending = 'PENDING'
}
export type RecentlySearchedAssetsQueryVariables = Exact<{
collectionAddresses: Array<Scalars['String']> | Scalars['String'];
contracts: Array<ContractInput> | ContractInput;
}>;
export type RecentlySearchedAssetsQuery = { __typename?: 'Query', nftCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', node: { __typename?: 'NftCollection', collectionId: string, isVerified?: boolean, name?: string, numAssets?: number, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, value: number } }> } }> }, tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type SearchTokensQueryVariables = Exact<{
searchQuery: Scalars['String'];
}>;
export type SearchTokensQuery = { __typename?: 'Query', searchTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type TokenQueryVariables = Exact<{
chain: Chain;
address?: InputMaybe<Scalars['String']>;
}>;
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
export type TokenPriceQueryVariables = Exact<{
chain: Chain;
......@@ -947,7 +982,7 @@ export type TopTokens100QueryVariables = Exact<{
}>;
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
export type TopTokensSparklineQueryVariables = Exact<{
duration: HistoryDuration;
......@@ -957,6 +992,13 @@ export type TopTokensSparklineQueryVariables = Exact<{
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } }> };
export type TrendingTokensQueryVariables = Exact<{
chain: Chain;
}>;
export type TrendingTokensQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type AssetQueryVariables = Exact<{
address: Scalars['String'];
orderBy?: InputMaybe<NftAssetSortableField>;
......@@ -1009,6 +1051,155 @@ export type NftRouteQueryVariables = Exact<{
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', currency: Currency, value: string } } };
export const RecentlySearchedAssetsDocument = 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
}
}
}
`;
/**
* __useRecentlySearchedAssetsQuery__
*
* To run a query within a React component, call `useRecentlySearchedAssetsQuery` and pass it any options that fit your needs.
* When your component renders, `useRecentlySearchedAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useRecentlySearchedAssetsQuery({
* variables: {
* collectionAddresses: // value for 'collectionAddresses'
* contracts: // value for 'contracts'
* },
* });
*/
export function useRecentlySearchedAssetsQuery(baseOptions: Apollo.QueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
}
export function useRecentlySearchedAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
}
export type RecentlySearchedAssetsQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsQuery>;
export type RecentlySearchedAssetsLazyQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsLazyQuery>;
export type RecentlySearchedAssetsQueryResult = Apollo.QueryResult<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>;
export const SearchTokensDocument = 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
}
}
}
`;
/**
* __useSearchTokensQuery__
*
* To run a query within a React component, call `useSearchTokensQuery` and pass it any options that fit your needs.
* When your component renders, `useSearchTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useSearchTokensQuery({
* variables: {
* searchQuery: // value for 'searchQuery'
* },
* });
*/
export function useSearchTokensQuery(baseOptions: Apollo.QueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
}
export function useSearchTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
}
export type SearchTokensQueryHookResult = ReturnType<typeof useSearchTokensQuery>;
export type SearchTokensLazyQueryHookResult = ReturnType<typeof useSearchTokensLazyQuery>;
export type SearchTokensQueryResult = Apollo.QueryResult<SearchTokensQuery, SearchTokensQueryVariables>;
export const TokenDocument = gql`
query Token($chain: Chain!, $address: String = null) {
token(chain: $chain, address: $address) {
......@@ -1018,6 +1209,7 @@ export const TokenDocument = gql`
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
......@@ -1147,6 +1339,7 @@ export const TopTokens100Document = gql`
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
......@@ -1252,6 +1445,69 @@ export function useTopTokensSparklineLazyQuery(baseOptions?: Apollo.LazyQueryHoo
export type TopTokensSparklineQueryHookResult = ReturnType<typeof useTopTokensSparklineQuery>;
export type TopTokensSparklineLazyQueryHookResult = ReturnType<typeof useTopTokensSparklineLazyQuery>;
export type TopTokensSparklineQueryResult = Apollo.QueryResult<TopTokensSparklineQuery, TopTokensSparklineQueryVariables>;
export const TrendingTokensDocument = 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
}
}
}
`;
/**
* __useTrendingTokensQuery__
*
* To run a query within a React component, call `useTrendingTokensQuery` and pass it any options that fit your needs.
* When your component renders, `useTrendingTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTrendingTokensQuery({
* variables: {
* chain: // value for 'chain'
* },
* });
*/
export function useTrendingTokensQuery(baseOptions: Apollo.QueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
}
export function useTrendingTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
}
export type TrendingTokensQueryHookResult = ReturnType<typeof useTrendingTokensQuery>;
export type TrendingTokensLazyQueryHookResult = ReturnType<typeof useTrendingTokensLazyQuery>;
export type TrendingTokensQueryResult = Apollo.QueryResult<TrendingTokensQuery, TrendingTokensQueryVariables>;
export const AssetDocument = gql`
query Asset($address: String!, $orderBy: NftAssetSortableField, $asc: Boolean, $filter: NftAssetsFilterInput, $first: Int, $after: String, $last: Int, $before: String) {
nftAssets(
......
import { QueryResult } from '@apollo/client'
import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import ms from 'ms.macro'
import { useEffect } from 'react'
......@@ -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 function getTokenDetailsURL(address: string, chainName?: Chain, chainId?: number) {
if (address === ZERO_ADDRESS && chainId && chainId === SupportedChainId.MAINNET) {
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 getTokenDetailsURL({ address, chain }: { address?: string | null; chain: Chain }) {
return `/tokens/${chain.toLowerCase()}/${address ?? NATIVE_CHAIN_ID}`
}
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
......@@ -10,9 +11,9 @@ import { FungibleToken, GenieCollection } from 'nft/types'
*/
export function organizeSearchResults(
isNFTPage: boolean,
tokenResults: FungibleToken[],
tokenResults: SearchToken[],
collectionResults: GenieCollection[]
): [FungibleToken[], GenieCollection[]] {
): [SearchToken[], GenieCollection[]] {
const reducedTokens =
tokenResults?.slice(0, isNFTPage ? 3 : collectionResults.length < 3 ? 8 - collectionResults.length : 5) ?? []
const reducedCollections = collectionResults.slice(0, 8 - reducedTokens.length)
......
......@@ -8,7 +8,6 @@ export * from './useMarketplaceSelect'
export * from './useNFTList'
export * from './useNFTSelect'
export * from './useProfilePageState'
export * from './useSearchHistory'
export * from './useSelectAsset'
export * from './useSellAsset'
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 {
cumulativeLooksAmount: 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