feat: token data cache (#4534)
* initial cache construction * switched to relay cache, updated hooks * fixed comments
This commit is contained in:
parent
fb389137e7
commit
84070835df
@ -1,4 +1,3 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { AxisBottom, TickFormatter } from '@visx/axis'
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
@ -6,11 +5,12 @@ import { GlyphCircle } from '@visx/glyph'
|
||||
import { Line } from '@visx/shape'
|
||||
import { filterTimeAtom } from 'components/Tokens/state'
|
||||
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
|
||||
import { useTokenPriceQuery } from 'graphql/data/TokenPriceQuery'
|
||||
import { TimePeriod } from 'graphql/data/TopTokenQuery'
|
||||
import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql'
|
||||
import { useTokenPricesCached } from 'graphql/data/Token'
|
||||
import { PricePoint, TimePeriod } from 'graphql/data/Token'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { OPACITY_HOVER } from 'theme'
|
||||
@ -29,8 +29,6 @@ import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
|
||||
|
||||
// TODO: This should be combined with the logic in TimeSelector.
|
||||
|
||||
export type PricePoint = { value: number; timestamp: number }
|
||||
|
||||
export const DATA_EMPTY = { value: 0, timestamp: 0 }
|
||||
|
||||
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
|
||||
@ -72,7 +70,6 @@ export function formatDelta(delta: number) {
|
||||
export const ChartHeader = styled.div`
|
||||
position: absolute;
|
||||
`
|
||||
|
||||
export const TokenPrice = styled.span`
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
@ -124,37 +121,41 @@ const crosshairDateOverhang = 80
|
||||
interface PriceChartProps {
|
||||
width: number
|
||||
height: number
|
||||
token: Token
|
||||
tokenAddress: string
|
||||
priceData?: TokenPrices$key | null
|
||||
}
|
||||
|
||||
export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
export function PriceChart({ width, height, tokenAddress, priceData }: PriceChartProps) {
|
||||
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
|
||||
const locale = useActiveLocale()
|
||||
const theme = useTheme()
|
||||
|
||||
// TODO: Add network selector input, consider using backend type instead of current front end selector type
|
||||
const pricePoints: PricePoint[] = useTokenPriceQuery(token.address, timePeriod, 'ETHEREUM').filter(
|
||||
(p): p is PricePoint => Boolean(p && p.value)
|
||||
)
|
||||
const { priceMap } = useTokenPricesCached(priceData, tokenAddress, 'ETHEREUM', timePeriod)
|
||||
const prices = priceMap.get(timePeriod)
|
||||
|
||||
const hasData = pricePoints.length !== 0
|
||||
|
||||
/* TODO: Implement API calls & cache to use here */
|
||||
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
|
||||
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
|
||||
const startingPrice = prices?.[0] ?? DATA_EMPTY
|
||||
const endingPrice = prices?.[prices.length - 1] ?? DATA_EMPTY
|
||||
const [displayPrice, setDisplayPrice] = useState(startingPrice)
|
||||
const [crosshair, setCrosshair] = useState<number | null>(null)
|
||||
|
||||
const graphWidth = width + crosshairDateOverhang
|
||||
// TODO: remove this logic after suspense is properly added
|
||||
const graphHeight = height - timeOptionsHeight > 0 ? height - timeOptionsHeight : 0
|
||||
const graphInnerHeight = graphHeight - margin.top - margin.bottom > 0 ? graphHeight - margin.top - margin.bottom : 0
|
||||
|
||||
// Defining scales
|
||||
// x scale
|
||||
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width]).nice()
|
||||
const timeScale = useMemo(
|
||||
() => scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width]).nice(),
|
||||
[startingPrice, endingPrice, width]
|
||||
)
|
||||
// y scale
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([graphInnerHeight, 0])
|
||||
const rdScale = useMemo(
|
||||
() =>
|
||||
scaleLinear()
|
||||
.domain(getPriceBounds(prices ?? []))
|
||||
.range([graphInnerHeight, 0]),
|
||||
[prices, graphInnerHeight]
|
||||
)
|
||||
|
||||
function tickFormat(
|
||||
startTimestamp: number,
|
||||
@ -206,16 +207,18 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
|
||||
const handleHover = useCallback(
|
||||
(event: Element | EventType) => {
|
||||
if (!prices) return
|
||||
|
||||
const { x } = localPoint(event) || { x: 0 }
|
||||
const x0 = timeScale.invert(x) // get timestamp from the scalexw
|
||||
const index = bisect(
|
||||
pricePoints.map((x) => x.timestamp),
|
||||
prices.map((x) => x.timestamp),
|
||||
x0,
|
||||
1
|
||||
)
|
||||
|
||||
const d0 = pricePoints[index - 1]
|
||||
const d1 = pricePoints[index]
|
||||
const d0 = prices[index - 1]
|
||||
const d1 = prices[index]
|
||||
let pricePoint = d0
|
||||
|
||||
const hasPreviousData = d1 && d1.timestamp
|
||||
@ -226,7 +229,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
setCrosshair(timeScale(pricePoint.timestamp))
|
||||
setDisplayPrice(pricePoint)
|
||||
},
|
||||
[timeScale, pricePoints]
|
||||
[timeScale, prices]
|
||||
)
|
||||
|
||||
const resetDisplay = useCallback(() => {
|
||||
@ -234,8 +237,8 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
setDisplayPrice(endingPrice)
|
||||
}, [setCrosshair, setDisplayPrice, endingPrice])
|
||||
|
||||
// TODO: connect to loading state
|
||||
if (!hasData) {
|
||||
// TODO: Display no data available error
|
||||
if (!prices) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -264,7 +267,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<LineChart
|
||||
data={pricePoints}
|
||||
data={prices}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
marginTop={margin.top}
|
||||
@ -335,7 +338,13 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
<TimeOptionsWrapper>
|
||||
<TimeOptionsContainer>
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
<TimeButton key={DISPLAYS[time]} active={timePeriod === time} onClick={() => setTimePeriod(time)}>
|
||||
<TimeButton
|
||||
key={DISPLAYS[time]}
|
||||
active={timePeriod === time}
|
||||
onClick={() => {
|
||||
setTimePeriod(time)
|
||||
}}
|
||||
>
|
||||
{DISPLAYS[time]}
|
||||
</TimeButton>
|
||||
))}
|
||||
|
@ -7,9 +7,8 @@ import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetailQuery'
|
||||
import { TokenQuery$data } from 'graphql/data/__generated__/TokenQuery.graphql'
|
||||
import { useCurrency, useToken } from 'hooks/Tokens'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { darken } from 'polished'
|
||||
import { Suspense } from 'react'
|
||||
import { useState } from 'react'
|
||||
@ -18,7 +17,7 @@ import styled from 'styled-components/macro'
|
||||
import { CopyContractAddress } from 'theme'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
import { filterNetworkAtom, useIsFavorited, useToggleFavorite } from '../state'
|
||||
import { useIsFavorited, useToggleFavorite } from '../state'
|
||||
import { ClickFavorited, FavoriteIcon } from '../TokenTable/TokenRow'
|
||||
import LoadingTokenDetail from './LoadingTokenDetail'
|
||||
import Resource from './Resource'
|
||||
@ -169,7 +168,7 @@ export function AboutSection({ address, tokenDetailData }: { address: string; to
|
||||
)
|
||||
}
|
||||
|
||||
export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
export default function LoadedTokenDetail({ address, query }: { address: string; query: TokenQuery$data }) {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const token = useToken(address)
|
||||
let currency = useCurrency(address)
|
||||
@ -179,13 +178,14 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
const chainInfo = getChainInfo(token?.chainId)
|
||||
const networkLabel = chainInfo?.label
|
||||
const networkBadgebackgroundColor = chainInfo?.backgroundColor
|
||||
const filterNetwork = useAtomValue(filterNetworkAtom)
|
||||
const tokenDetailData = useTokenDetailQuery(address, chainIdToChainName(filterNetwork))
|
||||
const relevantTokenDetailData = (({ description, homepageUrl, twitterName }) => ({
|
||||
description,
|
||||
homepageUrl,
|
||||
twitterName,
|
||||
}))(tokenDetailData)
|
||||
|
||||
const tokenData = query.tokenProjects?.[0]
|
||||
const tokenDetails = tokenData?.markets?.[0]
|
||||
const relevantTokenDetailData = {
|
||||
description: tokenData?.description,
|
||||
homepageUrl: tokenData?.homepageUrl,
|
||||
twitterName: tokenData?.twitterName,
|
||||
}
|
||||
|
||||
if (!token || !token.name || !token.symbol || !connectedChainId) {
|
||||
return <LoadingTokenDetail />
|
||||
@ -198,10 +198,8 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
currency = nativeOnChain(connectedChainId)
|
||||
}
|
||||
|
||||
const tokenName = isWrappedNativeToken && currency ? currency.name : tokenDetailData.name
|
||||
const defaultTokenSymbol = tokenDetailData.tokens?.[0]?.symbol ?? token.symbol
|
||||
const tokenSymbol = isWrappedNativeToken && currency ? currency.symbol : defaultTokenSymbol
|
||||
|
||||
const tokenName = tokenData?.name ?? token.name
|
||||
const tokenSymbol = tokenData?.tokens?.[0]?.symbol ?? token.symbol
|
||||
return (
|
||||
<Suspense fallback={<LoadingTokenDetail />}>
|
||||
<TopArea>
|
||||
@ -231,7 +229,11 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ParentSize>{({ width, height }) => <PriceChart token={token} width={width} height={height} />}</ParentSize>
|
||||
<ParentSize>
|
||||
{({ width, height }) => (
|
||||
<PriceChart tokenAddress={address} width={width} height={height} priceData={tokenData?.prices?.[0]} />
|
||||
)}
|
||||
</ParentSize>
|
||||
</ChartContainer>
|
||||
</ChartHeader>
|
||||
<StatsSection>
|
||||
@ -239,13 +241,13 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
<Stat>
|
||||
<Trans>Market cap</Trans>
|
||||
<StatPrice>
|
||||
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
|
||||
{tokenDetails?.marketCap?.value ? formatDollarAmount(tokenDetails.marketCap?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
24H volume
|
||||
<StatPrice>
|
||||
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
|
||||
{tokenDetails?.volume1D?.value ? formatDollarAmount(tokenDetails.volume1D.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
@ -253,13 +255,13 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
<Stat>
|
||||
52W low
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
|
||||
{tokenDetails?.priceLow52W?.value ? formatDollarAmount(tokenDetails.priceLow52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
52W high
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
|
||||
{tokenDetails?.priceHigh52W?.value ? formatDollarAmount(tokenDetails.priceHigh52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TimePeriod } from 'graphql/data/TopTokenQuery'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useRef } from 'react'
|
||||
|
@ -5,7 +5,8 @@ import { EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
|
||||
import { getDurationDetails, SingleTokenData } from 'graphql/data/Token'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode } from 'react'
|
||||
@ -454,29 +455,29 @@ export function LoadingRow() {
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
export default function LoadedRow({
|
||||
tokenAddress,
|
||||
tokenListIndex,
|
||||
tokenListLength,
|
||||
tokenData,
|
||||
timePeriod,
|
||||
}: {
|
||||
tokenAddress: string
|
||||
tokenListIndex: number
|
||||
tokenListLength: number
|
||||
tokenData: TokenData
|
||||
tokenData: SingleTokenData
|
||||
timePeriod: TimePeriod
|
||||
}) {
|
||||
const tokenAddress = tokenData?.tokens?.[0].address
|
||||
const currency = useCurrency(tokenAddress)
|
||||
const tokenName = tokenData.name
|
||||
const tokenSymbol = tokenData.symbol
|
||||
const tokenName = tokenData?.name
|
||||
const tokenSymbol = tokenData?.tokens?.[0].symbol
|
||||
const isFavorited = useIsFavorited(tokenAddress)
|
||||
const toggleFavorite = useToggleFavorite(tokenAddress)
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const filterNetwork = useAtomValue(filterNetworkAtom)
|
||||
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
|
||||
const delta = tokenData.percentChange?.[timePeriod]?.value
|
||||
const arrow = delta ? getDeltaArrow(delta) : null
|
||||
const formattedDelta = delta ? formatDelta(delta) : null
|
||||
const tokenDetails = tokenData?.markets?.[0]
|
||||
const { volume, pricePercentChange } = getDurationDetails(tokenData, timePeriod)
|
||||
const arrow = pricePercentChange ? getDeltaArrow(pricePercentChange) : null
|
||||
const formattedDelta = pricePercentChange ? formatDelta(pricePercentChange) : null
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: filterNetwork,
|
||||
@ -522,7 +523,7 @@ export default function LoadedRow({
|
||||
price={
|
||||
<ClickableContent>
|
||||
<PriceInfoCell>
|
||||
{tokenData.price?.value ? formatDollarAmount(tokenData.price?.value) : '-'}
|
||||
{tokenDetails?.price?.value ? formatDollarAmount(tokenDetails?.price?.value) : '-'}
|
||||
<PercentChangeInfoCell>
|
||||
{formattedDelta}
|
||||
{arrow}
|
||||
@ -538,16 +539,10 @@ export default function LoadedRow({
|
||||
}
|
||||
marketCap={
|
||||
<ClickableContent>
|
||||
{tokenData.marketCap?.value ? formatDollarAmount(tokenData.marketCap?.value) : '-'}
|
||||
</ClickableContent>
|
||||
}
|
||||
volume={
|
||||
<ClickableContent>
|
||||
{tokenData.volume?.[timePeriod]?.value
|
||||
? formatDollarAmount(tokenData.volume?.[timePeriod]?.value ?? undefined)
|
||||
: '-'}
|
||||
{tokenDetails?.marketCap?.value ? formatDollarAmount(tokenDetails?.marketCap?.value) : '-'}
|
||||
</ClickableContent>
|
||||
}
|
||||
volume={<ClickableContent>{volume ? formatDollarAmount(volume ?? undefined) : '-'}</ClickableContent>}
|
||||
sparkLine={
|
||||
<SparkLine>
|
||||
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
|
||||
|
@ -7,7 +7,9 @@ import {
|
||||
sortCategoryAtom,
|
||||
sortDirectionAtom,
|
||||
} from 'components/Tokens/state'
|
||||
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
|
||||
import { TokenTopQuery$data } from 'graphql/data/__generated__/TokenTopQuery.graphql'
|
||||
import { getDurationDetails, SingleTokenData, useTopTokenQuery } from 'graphql/data/Token'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
@ -47,33 +49,37 @@ const TokenRowsContainer = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
function useFilteredTokens(tokens: TokenData[] | undefined) {
|
||||
function useFilteredTokens(data: TokenTopQuery$data): SingleTokenData[] | undefined {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const favoriteTokenAddresses = useAtomValue(favoritesAtom)
|
||||
const favorites = useAtomValue(favoritesAtom)
|
||||
const showFavorites = useAtomValue(showFavoritesAtom)
|
||||
const shownTokens =
|
||||
showFavorites && tokens ? tokens.filter((token) => favoriteTokenAddresses.includes(token.address)) : tokens
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
(shownTokens ?? []).filter((token) => {
|
||||
if (!token.address) {
|
||||
data.topTokenProjects
|
||||
?.filter(
|
||||
(token) => !showFavorites || (token?.tokens?.[0].address && favorites.includes(token?.tokens?.[0].address))
|
||||
)
|
||||
.filter((token) => {
|
||||
const tokenInfo = token?.tokens?.[0]
|
||||
const address = tokenInfo?.address
|
||||
if (!address) {
|
||||
return false
|
||||
}
|
||||
if (!filterString) {
|
||||
} else if (!filterString) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
const lowercaseFilterString = filterString.toLowerCase()
|
||||
const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString)
|
||||
const addressIncludesFilterString = address?.toLowerCase().includes(lowercaseFilterString)
|
||||
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
|
||||
const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString)
|
||||
const symbolIncludesFilterString = tokenInfo?.symbol?.toLowerCase().includes(lowercaseFilterString)
|
||||
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
|
||||
}
|
||||
}),
|
||||
[shownTokens, filterString]
|
||||
[data.topTokenProjects, favorites, filterString, showFavorites]
|
||||
)
|
||||
}
|
||||
|
||||
function useSortedTokens(tokenData: TokenData[] | null) {
|
||||
function useSortedTokens(tokenData: SingleTokenData[] | undefined) {
|
||||
const sortCategory = useAtomValue(sortCategoryAtom)
|
||||
const sortDirection = useAtomValue(sortDirectionAtom)
|
||||
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
|
||||
@ -103,22 +109,25 @@ function useSortedTokens(tokenData: TokenData[] | null) {
|
||||
}
|
||||
let a: number | null | undefined
|
||||
let b: number | null | undefined
|
||||
|
||||
const { volume: aVolume, pricePercentChange: aChange } = getDurationDetails(token1, timePeriod)
|
||||
const { volume: bVolume, pricePercentChange: bChange } = getDurationDetails(token2, timePeriod)
|
||||
switch (sortCategory) {
|
||||
case Category.marketCap:
|
||||
a = token1.marketCap?.value
|
||||
b = token2.marketCap?.value
|
||||
a = token1.markets?.[0]?.marketCap?.value
|
||||
b = token2.markets?.[0]?.marketCap?.value
|
||||
break
|
||||
case Category.price:
|
||||
a = token1.price?.value
|
||||
b = token2.price?.value
|
||||
a = token1.markets?.[0]?.price?.value
|
||||
b = token2.markets?.[0]?.price?.value
|
||||
break
|
||||
case Category.volume:
|
||||
a = token1.volume?.[timePeriod]?.value
|
||||
b = token2.volume?.[timePeriod]?.value
|
||||
a = aVolume
|
||||
b = bVolume
|
||||
break
|
||||
case Category.percentChange:
|
||||
a = token1.percentChange?.[timePeriod]?.value
|
||||
b = token2.percentChange?.[timePeriod]?.value
|
||||
a = aChange
|
||||
b = bChange
|
||||
break
|
||||
}
|
||||
return sortFn(a, b)
|
||||
@ -149,14 +158,15 @@ export function LoadingTokenTable() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenTable({ data }: { data: TokenData[] | undefined }) {
|
||||
export default function TokenTable() {
|
||||
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
|
||||
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
|
||||
const filteredTokens = useFilteredTokens(data)
|
||||
const topTokens = useTopTokenQuery(1, timePeriod)
|
||||
const filteredTokens = useFilteredTokens(topTokens)
|
||||
const sortedFilteredTokens = useSortedTokens(filteredTokens)
|
||||
|
||||
/* loading and error state */
|
||||
if (data === null) {
|
||||
if (topTokens === null) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
@ -184,8 +194,7 @@ export default function TokenTable({ data }: { data: TokenData[] | undefined })
|
||||
<TokenRowsContainer>
|
||||
{sortedFilteredTokens?.map((token, index) => (
|
||||
<LoadedRow
|
||||
key={token.address}
|
||||
tokenAddress={token.address}
|
||||
key={token?.name}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={sortedFilteredTokens.length}
|
||||
tokenData={token}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TimePeriod } from 'graphql/data/TopTokenQuery'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
@ -15,10 +15,11 @@ export const sortCategoryAtom = atom<Category>(Category.marketCap)
|
||||
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing)
|
||||
|
||||
/* for favoriting tokens */
|
||||
export function useToggleFavorite(tokenAddress: string) {
|
||||
export function useToggleFavorite(tokenAddress: string | undefined | null) {
|
||||
const [favoriteTokens, updateFavoriteTokens] = useAtom(favoritesAtom)
|
||||
|
||||
return useCallback(() => {
|
||||
if (!tokenAddress) return
|
||||
let updatedFavoriteTokens
|
||||
if (favoriteTokens.includes(tokenAddress.toLocaleLowerCase())) {
|
||||
updatedFavoriteTokens = favoriteTokens.filter((address: string) => {
|
||||
@ -48,8 +49,11 @@ export function useSetSortCategory(category: Category) {
|
||||
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
|
||||
}
|
||||
|
||||
export function useIsFavorited(tokenAddress: string) {
|
||||
export function useIsFavorited(tokenAddress: string | null | undefined) {
|
||||
const favoritedTokens = useAtomValue<string[]>(favoritesAtom)
|
||||
|
||||
return useMemo(() => favoritedTokens.includes(tokenAddress.toLocaleLowerCase()), [favoritedTokens, tokenAddress])
|
||||
return useMemo(
|
||||
() => (tokenAddress ? favoritedTokens.includes(tokenAddress.toLocaleLowerCase()) : false),
|
||||
[favoritedTokens, tokenAddress]
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,41 @@
|
||||
import { Environment, Network, RecordSource, Store } from 'relay-runtime'
|
||||
import ms from 'ms.macro'
|
||||
import { Variables } from 'react-relay'
|
||||
import { Environment, Network, RecordSource, RequestParameters, Store } from 'relay-runtime'
|
||||
import RelayQueryResponseCache from 'relay-runtime/lib/network/RelayQueryResponseCache'
|
||||
|
||||
import fetchGraphQL from './fetchGraphQL'
|
||||
|
||||
// max number of request in cache, least-recently updated entries purged first
|
||||
const size = 250
|
||||
// number in milliseconds, how long records stay valid in cache
|
||||
const ttl = ms`5m`
|
||||
export const cache = new RelayQueryResponseCache({ size, ttl })
|
||||
|
||||
const fetchQuery = async function wrappedFetchQuery(params: RequestParameters, variables: Variables) {
|
||||
const queryID = params.name
|
||||
const cachedData = cache.get(queryID, variables)
|
||||
|
||||
if (cachedData !== null) return cachedData
|
||||
|
||||
return fetchGraphQL(params, variables).then((data) => {
|
||||
if (params.operationKind !== 'mutation') {
|
||||
cache.set(queryID, variables, data)
|
||||
}
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
// This property tells Relay to not immediately clear its cache when the user
|
||||
// navigates around the app. Relay will hold onto the specified number of
|
||||
// query results, allowing the user to return to recently visited pages
|
||||
// and reusing cached data if its available/fresh.
|
||||
const gcReleaseBufferSize = 10
|
||||
|
||||
const store = new Store(new RecordSource(), { gcReleaseBufferSize })
|
||||
const network = Network.create(fetchQuery)
|
||||
|
||||
// Export a singleton instance of Relay Environment configured with our network function:
|
||||
export default new Environment({
|
||||
network: Network.create(fetchGraphQL),
|
||||
store: new Store(new RecordSource()),
|
||||
network,
|
||||
store,
|
||||
})
|
||||
|
347
src/graphql/data/Token.ts
Normal file
347
src/graphql/data/Token.ts
Normal file
@ -0,0 +1,347 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
|
||||
|
||||
import { TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
|
||||
import { TokenPrices$data, TokenPrices$key } from './__generated__/TokenPrices.graphql'
|
||||
import { Chain, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
|
||||
import { TokenTopQuery, TokenTopQuery$data } from './__generated__/TokenTopQuery.graphql'
|
||||
|
||||
export enum TimePeriod {
|
||||
HOUR,
|
||||
DAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
YEAR,
|
||||
ALL,
|
||||
}
|
||||
|
||||
function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
return 'HOUR'
|
||||
case TimePeriod.DAY:
|
||||
return 'DAY'
|
||||
case TimePeriod.WEEK:
|
||||
return 'WEEK'
|
||||
case TimePeriod.MONTH:
|
||||
return 'MONTH'
|
||||
case TimePeriod.YEAR:
|
||||
return 'YEAR'
|
||||
case TimePeriod.ALL:
|
||||
return 'MAX'
|
||||
}
|
||||
}
|
||||
|
||||
export type PricePoint = { value: number; timestamp: number }
|
||||
|
||||
const topTokensQuery = graphql`
|
||||
query TokenTopQuery($page: Int!, $duration: HistoryDuration!) {
|
||||
topTokenProjects(orderBy: MARKET_CAP, pageSize: 20, currency: USD, page: $page) {
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
name
|
||||
tokens {
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
}
|
||||
prices: markets(currencies: [USD]) {
|
||||
...TokenPrices
|
||||
}
|
||||
markets(currencies: [USD]) {
|
||||
price {
|
||||
value
|
||||
currency
|
||||
}
|
||||
marketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
fullyDilutedMarketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1D: volume(duration: DAY) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1W: volume(duration: WEEK) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1M: volume(duration: MONTH) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1Y: volume(duration: YEAR) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange24h {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1W: pricePercentChange(duration: WEEK) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1M: pricePercentChange(duration: MONTH) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1Y: pricePercentChange(duration: YEAR) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const tokenPricesFragment = graphql`
|
||||
fragment TokenPrices on TokenProjectMarket {
|
||||
priceHistory(duration: $duration) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
type CachedTopToken = NonNullable<NonNullable<TokenTopQuery$data>['topTokenProjects']>[number]
|
||||
|
||||
let cachedTopTokens: Record<string, CachedTopToken> = {}
|
||||
export function useTopTokenQuery(page: number, timePeriod: TimePeriod) {
|
||||
const topTokens = useLazyLoadQuery<TokenTopQuery>(topTokensQuery, { page, duration: toHistoryDuration(timePeriod) })
|
||||
|
||||
cachedTopTokens =
|
||||
topTokens.topTokenProjects?.reduce((acc, current) => {
|
||||
const address = current?.tokens?.[0].address
|
||||
if (address) acc[address] = current
|
||||
return acc
|
||||
}, {} as Record<string, CachedTopToken>) ?? {}
|
||||
console.log(cachedTopTokens)
|
||||
|
||||
return topTokens
|
||||
}
|
||||
|
||||
const tokenQuery = graphql`
|
||||
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!, $skip: Boolean = false) {
|
||||
tokenProjects(contracts: [$contract]) @skip(if: $skip) {
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
name
|
||||
tokens {
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
}
|
||||
prices: markets(currencies: [USD]) {
|
||||
...TokenPrices
|
||||
}
|
||||
markets(currencies: [USD]) {
|
||||
price {
|
||||
value
|
||||
currency
|
||||
}
|
||||
marketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
fullyDilutedMarketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1D: volume(duration: DAY) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1W: volume(duration: WEEK) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1M: volume(duration: MONTH) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1Y: volume(duration: YEAR) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange24h {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1W: pricePercentChange(duration: WEEK) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1M: pricePercentChange(duration: MONTH) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1Y: pricePercentChange(duration: YEAR) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) {
|
||||
const cachedTopToken = cachedTopTokens[address]
|
||||
const data = useLazyLoadQuery<TokenQuery>(tokenQuery, {
|
||||
contract: { address, chain },
|
||||
duration: toHistoryDuration(timePeriod),
|
||||
skip: !!cachedTopToken,
|
||||
})
|
||||
|
||||
return !cachedTopToken ? data : { tokenProjects: [{ ...cachedTopToken }] }
|
||||
}
|
||||
|
||||
const tokenPriceQuery = graphql`
|
||||
query TokenPriceQuery(
|
||||
$contract: ContractInput!
|
||||
$skip1H: Boolean!
|
||||
$skip1D: Boolean!
|
||||
$skip1W: Boolean!
|
||||
$skip1M: Boolean!
|
||||
$skip1Y: Boolean!
|
||||
$skipMax: Boolean!
|
||||
) {
|
||||
tokenProjects(contracts: [$contract]) {
|
||||
markets(currencies: [USD]) {
|
||||
priceHistory1H: priceHistory(duration: HOUR) @skip(if: $skip1H) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1D: priceHistory(duration: DAY) @skip(if: $skip1D) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1W: priceHistory(duration: WEEK) @skip(if: $skip1W) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1M: priceHistory(duration: MONTH) @skip(if: $skip1M) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1Y: priceHistory(duration: YEAR) @skip(if: $skip1Y) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistoryMAX: priceHistory(duration: MAX) @skip(if: $skipMax) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
function filterPrices(prices: TokenPrices$data['priceHistory'] | undefined) {
|
||||
return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
|
||||
}
|
||||
|
||||
export function useTokenPricesCached(
|
||||
key: TokenPrices$key | null | undefined,
|
||||
address: string,
|
||||
chain: Chain,
|
||||
timePeriod: TimePeriod
|
||||
) {
|
||||
// Attempt to use token prices already provided by TokenDetails / TopToken queries
|
||||
const environment = useRelayEnvironment()
|
||||
const fetchedTokenPrices = useFragment(tokenPricesFragment, key ?? null)?.priceHistory
|
||||
|
||||
const [priceMap, setPriceMap] = useState(
|
||||
new Map<TimePeriod, PricePoint[] | undefined>([[timePeriod, filterPrices(fetchedTokenPrices)]])
|
||||
)
|
||||
|
||||
function updatePrices(key: TimePeriod, data?: PricePoint[]) {
|
||||
setPriceMap(new Map(priceMap.set(key, data)))
|
||||
}
|
||||
|
||||
// Fetch the other timePeriods after first render
|
||||
useEffect(() => {
|
||||
// Fetch all time periods except the one already populated
|
||||
fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, {
|
||||
contract: { address, chain },
|
||||
skip1H: timePeriod === TimePeriod.HOUR && !!fetchedTokenPrices,
|
||||
skip1D: timePeriod === TimePeriod.DAY && !!fetchedTokenPrices,
|
||||
skip1W: timePeriod === TimePeriod.WEEK && !!fetchedTokenPrices,
|
||||
skip1M: timePeriod === TimePeriod.MONTH && !!fetchedTokenPrices,
|
||||
skip1Y: timePeriod === TimePeriod.YEAR && !!fetchedTokenPrices,
|
||||
skipMax: timePeriod === TimePeriod.ALL && !!fetchedTokenPrices,
|
||||
}).subscribe({
|
||||
next: (data) => {
|
||||
const markets = data.tokenProjects?.[0]?.markets?.[0]
|
||||
if (markets) {
|
||||
markets.priceHistory1H && updatePrices(TimePeriod.HOUR, filterPrices(markets.priceHistory1H))
|
||||
markets.priceHistory1D && updatePrices(TimePeriod.DAY, filterPrices(markets.priceHistory1D))
|
||||
markets.priceHistory1W && updatePrices(TimePeriod.WEEK, filterPrices(markets.priceHistory1W))
|
||||
markets.priceHistory1M && updatePrices(TimePeriod.MONTH, filterPrices(markets.priceHistory1M))
|
||||
markets.priceHistory1Y && updatePrices(TimePeriod.YEAR, filterPrices(markets.priceHistory1Y))
|
||||
markets.priceHistoryMAX && updatePrices(TimePeriod.ALL, filterPrices(markets.priceHistoryMAX))
|
||||
}
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return { priceMap }
|
||||
}
|
||||
|
||||
export type SingleTokenData = NonNullable<TokenQuery$data['tokenProjects']>[number]
|
||||
export function getDurationDetails(data: SingleTokenData, timePeriod: TimePeriod) {
|
||||
let volume = null
|
||||
let pricePercentChange = null
|
||||
|
||||
const markets = data?.markets?.[0]
|
||||
if (markets) {
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
pricePercentChange = null
|
||||
break
|
||||
case TimePeriod.DAY:
|
||||
volume = markets.volume1D?.value
|
||||
pricePercentChange = markets.pricePercentChange24h?.value
|
||||
break
|
||||
case TimePeriod.WEEK:
|
||||
volume = markets.volume1W?.value
|
||||
pricePercentChange = markets.pricePercentChange1W?.value
|
||||
break
|
||||
case TimePeriod.MONTH:
|
||||
volume = markets.volume1M?.value
|
||||
pricePercentChange = markets.pricePercentChange1M?.value
|
||||
break
|
||||
case TimePeriod.YEAR:
|
||||
volume = markets.volume1Y?.value
|
||||
pricePercentChange = markets.pricePercentChange1Y?.value
|
||||
break
|
||||
case TimePeriod.ALL:
|
||||
volume = null
|
||||
pricePercentChange = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { volume, pricePercentChange }
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useLazyLoadQuery } from 'react-relay'
|
||||
|
||||
import type { Chain, TokenDetailQuery as TokenDetailQueryType } from './__generated__/TokenDetailQuery.graphql'
|
||||
|
||||
export function chainIdToChainName(networkId: SupportedChainId): Chain {
|
||||
switch (networkId) {
|
||||
case SupportedChainId.MAINNET:
|
||||
return 'ETHEREUM'
|
||||
case SupportedChainId.ARBITRUM_ONE:
|
||||
return 'ARBITRUM'
|
||||
case SupportedChainId.OPTIMISM:
|
||||
return 'OPTIMISM'
|
||||
case SupportedChainId.POLYGON:
|
||||
return 'POLYGON'
|
||||
default:
|
||||
return 'ETHEREUM'
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenDetailQuery(address: string, chain: Chain) {
|
||||
const tokenDetail = useLazyLoadQuery<TokenDetailQueryType>(
|
||||
graphql`
|
||||
query TokenDetailQuery($contract: ContractInput!) {
|
||||
tokenProjects(contracts: [$contract]) {
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
name
|
||||
markets(currencies: [USD]) {
|
||||
price {
|
||||
value
|
||||
currency
|
||||
}
|
||||
marketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
fullyDilutedMarketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume24h: volume(duration: DAY) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
tokens {
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
contract: {
|
||||
address,
|
||||
chain,
|
||||
},
|
||||
}
|
||||
)
|
||||
const { description, homepageUrl, twitterName, name, markets, tokens } = tokenDetail?.tokenProjects?.[0] ?? {}
|
||||
const { price, marketCap, fullyDilutedMarketCap, volume24h, priceHigh52W, priceLow52W } = markets?.[0] ?? {}
|
||||
return {
|
||||
description,
|
||||
homepageUrl,
|
||||
twitterName,
|
||||
name,
|
||||
markets,
|
||||
tokens,
|
||||
price,
|
||||
marketCap,
|
||||
fullyDilutedMarketCap,
|
||||
volume24h,
|
||||
priceHigh52W,
|
||||
priceLow52W,
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { useLazyLoadQuery } from 'react-relay'
|
||||
|
||||
import type { Chain, TokenPriceQuery as TokenPriceQueryType } from './__generated__/TokenPriceQuery.graphql'
|
||||
import { TimePeriod } from './TopTokenQuery'
|
||||
|
||||
export function useTokenPriceQuery(address: string, timePeriod: TimePeriod, chain: Chain) {
|
||||
const tokenPrices = useLazyLoadQuery<TokenPriceQueryType>(
|
||||
graphql`
|
||||
query TokenPriceQuery($contract: ContractInput!) {
|
||||
tokenProjects(contracts: [$contract]) {
|
||||
name
|
||||
markets(currencies: [USD]) {
|
||||
priceHistory1H: priceHistory(duration: HOUR) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1D: priceHistory(duration: DAY) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1W: priceHistory(duration: WEEK) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1M: priceHistory(duration: MONTH) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
priceHistory1Y: priceHistory(duration: YEAR) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
tokens {
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
contract: {
|
||||
address,
|
||||
chain,
|
||||
},
|
||||
}
|
||||
)
|
||||
const { priceHistory1H, priceHistory1D, priceHistory1W, priceHistory1M, priceHistory1Y } =
|
||||
tokenPrices.tokenProjects?.[0]?.markets?.[0] ?? {}
|
||||
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
return priceHistory1H ?? []
|
||||
case TimePeriod.DAY:
|
||||
return priceHistory1D ?? []
|
||||
case TimePeriod.WEEK:
|
||||
return priceHistory1W ?? []
|
||||
case TimePeriod.MONTH:
|
||||
return priceHistory1M ?? []
|
||||
case TimePeriod.YEAR:
|
||||
return priceHistory1Y ?? []
|
||||
case TimePeriod.ALL:
|
||||
//TODO: Add functionality for ALL, without requesting it at same time as rest of data for performance reasons
|
||||
return priceHistory1Y ?? []
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { useLazyLoadQuery } from 'react-relay'
|
||||
|
||||
import type { Chain, Currency, TopTokenQuery as TopTokenQueryType } from './__generated__/TopTokenQuery.graphql'
|
||||
|
||||
export enum TimePeriod {
|
||||
HOUR,
|
||||
DAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
YEAR,
|
||||
ALL,
|
||||
}
|
||||
|
||||
interface IAmount {
|
||||
currency: Currency | null
|
||||
value: number | null
|
||||
}
|
||||
|
||||
export type TokenData = {
|
||||
name: string | null
|
||||
address: string
|
||||
chain: Chain | null
|
||||
symbol: string | null
|
||||
price: IAmount | null | undefined
|
||||
marketCap: IAmount | null | undefined
|
||||
volume: Record<TimePeriod, IAmount | null | undefined>
|
||||
percentChange: Record<TimePeriod, IAmount | null | undefined>
|
||||
}
|
||||
|
||||
export interface UseTopTokensResult {
|
||||
data: TokenData[] | undefined
|
||||
error: string | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export function useTopTokenQuery(page: number) {
|
||||
const topTokenData = useLazyLoadQuery<TopTokenQueryType>(
|
||||
graphql`
|
||||
query TopTokenQuery($page: Int!) {
|
||||
topTokenProjects(orderBy: MARKET_CAP, pageSize: 100, currency: USD, page: $page) {
|
||||
name
|
||||
tokens {
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
}
|
||||
markets(currencies: [USD]) {
|
||||
price {
|
||||
value
|
||||
currency
|
||||
}
|
||||
marketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
fullyDilutedMarketCap {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1H: volume(duration: HOUR) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1D: volume(duration: DAY) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1W: volume(duration: WEEK) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1M: volume(duration: MONTH) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume1Y: volume(duration: YEAR) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volumeAll: volume(duration: MAX) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange1H: pricePercentChange(duration: HOUR) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange24h {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1W: pricePercentChange(duration: WEEK) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1M: pricePercentChange(duration: MONTH) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1Y: pricePercentChange(duration: YEAR) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChangeAll: pricePercentChange(duration: MAX) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
page,
|
||||
}
|
||||
)
|
||||
|
||||
const topTokens: TokenData[] | undefined = topTokenData.topTokenProjects?.map((token) =>
|
||||
token?.tokens?.[0].address
|
||||
? {
|
||||
name: token?.name,
|
||||
address: token?.tokens?.[0].address,
|
||||
chain: token?.tokens?.[0].chain,
|
||||
symbol: token?.tokens?.[0].symbol,
|
||||
price: token?.markets?.[0]?.price,
|
||||
marketCap: token?.markets?.[0]?.marketCap,
|
||||
volume: {
|
||||
[TimePeriod.HOUR]: token?.markets?.[0]?.volume1H,
|
||||
[TimePeriod.DAY]: token?.markets?.[0]?.volume1D,
|
||||
[TimePeriod.WEEK]: token?.markets?.[0]?.volume1W,
|
||||
[TimePeriod.MONTH]: token?.markets?.[0]?.volume1M,
|
||||
[TimePeriod.YEAR]: token?.markets?.[0]?.volume1Y,
|
||||
[TimePeriod.ALL]: token?.markets?.[0]?.volumeAll,
|
||||
},
|
||||
percentChange: {
|
||||
[TimePeriod.HOUR]: token?.markets?.[0]?.pricePercentChange1H,
|
||||
[TimePeriod.DAY]: token?.markets?.[0]?.pricePercentChange24h,
|
||||
[TimePeriod.WEEK]: token?.markets?.[0]?.pricePercentChange1W,
|
||||
[TimePeriod.MONTH]: token?.markets?.[0]?.pricePercentChange1M,
|
||||
[TimePeriod.YEAR]: token?.markets?.[0]?.pricePercentChange1Y,
|
||||
[TimePeriod.ALL]: token?.markets?.[0]?.pricePercentChangeAll,
|
||||
},
|
||||
}
|
||||
: ({} as TokenData)
|
||||
)
|
||||
return topTokens
|
||||
}
|
@ -43,10 +43,8 @@ export function useNetworkTokenBalances({ address }: useNetworkTokenBalancesArgs
|
||||
const waitRandom = (min: number, max: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, min + Math.round(Math.random() * Math.max(0, max - min))))
|
||||
try {
|
||||
console.log('useNetworkTokenBalances.fetchNetworkTokenBalances', query)
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
console.log('useNetworkTokenBalances.fetchNetworkTokenBalances', address)
|
||||
await waitRandom(250, 2000)
|
||||
if (Math.random() < 0.05) {
|
||||
throw new Error('fake error')
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
MOBILE_MEDIA_BREAKPOINT,
|
||||
SMALL_MEDIA_BREAKPOINT,
|
||||
} from 'components/Tokens/constants'
|
||||
import { filterTimeAtom } from 'components/Tokens/state'
|
||||
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
|
||||
import FooterBalanceSummary from 'components/Tokens/TokenDetails/FooterBalanceSummary'
|
||||
import LoadingTokenDetail from 'components/Tokens/TokenDetails/LoadingTokenDetail'
|
||||
@ -16,10 +17,12 @@ import Widget, { WIDGET_WIDTH } from 'components/Widget'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { useTokenQuery } from 'graphql/data/Token'
|
||||
import { useIsUserAddedToken, useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const Footer = styled.div`
|
||||
@ -77,7 +80,6 @@ function NetworkBalances(tokenAddress: string | undefined) {
|
||||
}
|
||||
|
||||
export default function TokenDetails() {
|
||||
const location = useLocation()
|
||||
const { tokenAddress } = useParams<{ tokenAddress?: string }>()
|
||||
const token = useToken(tokenAddress)
|
||||
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
|
||||
@ -115,6 +117,9 @@ export default function TokenDetails() {
|
||||
return chainIds
|
||||
}, [connectedChainId])
|
||||
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
const query = useTokenQuery(tokenAddress ?? '', 'ETHEREUM', timePeriod)
|
||||
|
||||
const balancesByNetwork = networkData
|
||||
? chainsToList.map((chainId) => {
|
||||
const amount = networkData[chainId]
|
||||
@ -137,15 +142,17 @@ export default function TokenDetails() {
|
||||
})
|
||||
: null
|
||||
|
||||
if (token === undefined) {
|
||||
return <Navigate to={{ ...location, pathname: '/tokens' }} replace />
|
||||
}
|
||||
// TODO: Fix this logic to not automatically redirect on refresh, yet still catch invalid addresses
|
||||
//const location = useLocation()
|
||||
//if (token === undefined) {
|
||||
// return <Navigate to={{ ...location, pathname: '/tokens' }} replace />
|
||||
//}
|
||||
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
{token && (
|
||||
<>
|
||||
<TokenDetail address={token.address} />
|
||||
<TokenDetail address={token.address} query={query} />
|
||||
<RightPanel>
|
||||
<Widget defaultToken={token ?? undefined} onReviewSwapClick={onReviewSwap} />
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={token.address} warning={tokenWarning} />}
|
||||
|
@ -9,7 +9,6 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar'
|
||||
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
|
||||
import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable'
|
||||
import { TokensNetworkFilterVariant, useTokensNetworkFilterFlag } from 'featureFlags/flags/tokensNetworkFilter'
|
||||
import { useTopTokenQuery } from 'graphql/data/TopTokenQuery'
|
||||
import { useResetAtom } from 'jotai/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@ -69,7 +68,6 @@ const FiltersWrapper = styled.div`
|
||||
`
|
||||
|
||||
const Tokens = () => {
|
||||
const topTokens = useTopTokenQuery(1)
|
||||
const tokensNetworkFilterFlag = useTokensNetworkFilterFlag()
|
||||
const resetFilterString = useResetAtom(filterStringAtom)
|
||||
const location = useLocation()
|
||||
@ -98,7 +96,7 @@ const Tokens = () => {
|
||||
</FiltersWrapper>
|
||||
|
||||
<TokenTableContainer>
|
||||
<TokenTable data={topTokens} />
|
||||
<TokenTable />
|
||||
</TokenTableContainer>
|
||||
</ExploreContainer>
|
||||
</Trace>
|
||||
|
Loading…
Reference in New Issue
Block a user