Commit 9e070107 authored by Jordan Frankfurt's avatar Jordan Frankfurt Committed by GitHub

feat: new gql schema (#4654)

* new gql schema

* refactor: switch explore over to new queries (#4657)

* initial commit
* improved performance, added filtering
* addressed pr comments
* fixed typescript issue

* drop leftover comment

* clean up loaded row props
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
parent 2a92b299
import { curveCardinal, scaleLinear } from 'd3' import { curveCardinal, scaleLinear } from 'd3'
import { SingleTokenData, TimePeriod, useTokenPricesFromFragment } from 'graphql/data/Token' import { filterPrices } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import React from 'react' import React from 'react'
import { useTheme } from 'styled-components/macro' import { useTheme } from 'styled-components/macro'
...@@ -11,7 +13,7 @@ type PricePoint = { value: number; timestamp: number } ...@@ -11,7 +13,7 @@ type PricePoint = { value: number; timestamp: number }
interface SparklineChartProps { interface SparklineChartProps {
width: number width: number
height: number height: number
tokenData: SingleTokenData tokenData: TopToken
pricePercentChange: number | undefined | null pricePercentChange: number | undefined | null
timePeriod: TimePeriod timePeriod: TimePeriod
} }
...@@ -19,7 +21,7 @@ interface SparklineChartProps { ...@@ -19,7 +21,7 @@ interface SparklineChartProps {
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) { function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) {
const theme = useTheme() const theme = useTheme()
// for sparkline // for sparkline
const pricePoints = useTokenPricesFromFragment(tokenData?.prices?.[0]) ?? [] const pricePoints = filterPrices(tokenData?.market?.priceHistory) ?? []
const hasData = pricePoints.length !== 0 const hasData = pricePoints.length !== 0
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
......
...@@ -111,7 +111,7 @@ export default function ChartSection({ token, tokenData }: { token: Token; token ...@@ -111,7 +111,7 @@ export default function ChartSection({ token, tokenData }: { token: Token; token
<ChartContainer> <ChartContainer>
<ParentSize> <ParentSize>
{({ width, height }) => ( {({ width, height }) => (
<PriceChart tokenAddress={token.address} width={width} height={height} priceData={tokenData?.prices?.[0]} /> <PriceChart tokenAddress={token.address} width={width} height={height} priceDataFragmentRef={null} />
)} )}
</ParentSize> </ParentSize>
</ChartContainer> </ChartContainer>
......
...@@ -17,7 +17,8 @@ import { ...@@ -17,7 +17,8 @@ import {
} from 'd3' } from 'd3'
import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql' import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql'
import { useTokenPricesCached } from 'graphql/data/Token' import { useTokenPricesCached } from 'graphql/data/Token'
import { PricePoint, TimePeriod } from 'graphql/data/Token' import { PricePoint } from 'graphql/data/Token'
import { TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale' import { useActiveLocale } from 'hooks/useActiveLocale'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
...@@ -131,15 +132,15 @@ interface PriceChartProps { ...@@ -131,15 +132,15 @@ interface PriceChartProps {
width: number width: number
height: number height: number
tokenAddress: string tokenAddress: string
priceData?: TokenPrices$key | null priceDataFragmentRef?: TokenPrices$key | null
} }
export function PriceChart({ width, height, tokenAddress, priceData }: PriceChartProps) { export function PriceChart({ width, height, tokenAddress, priceDataFragmentRef }: PriceChartProps) {
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom) const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
const locale = useActiveLocale() const locale = useActiveLocale()
const theme = useTheme() const theme = useTheme()
const { priceMap } = useTokenPricesCached(priceData, tokenAddress, 'ETHEREUM', timePeriod) const { priceMap } = useTokenPricesCached(priceDataFragmentRef, tokenAddress, 'ETHEREUM', timePeriod)
const prices = priceMap.get(timePeriod) const prices = priceMap.get(timePeriod)
// first price point on the x-axis of the current time period's chart // first price point on the x-axis of the current time period's chart
......
import { TimePeriod } from 'graphql/data/Token' import { TimePeriod } from 'graphql/data/util'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useRef } from 'react' import { useRef } from 'react'
......
...@@ -6,7 +6,8 @@ import SparklineChart from 'components/Charts/SparklineChart' ...@@ -6,7 +6,8 @@ import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { getDurationDetails, SingleTokenData, TimePeriod } from 'graphql/data/Token' import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import { useCurrency } from 'hooks/Tokens' import { useCurrency } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react' import { ReactNode } from 'react'
...@@ -27,14 +28,13 @@ import { ...@@ -27,14 +28,13 @@ import {
filterNetworkAtom, filterNetworkAtom,
filterStringAtom, filterStringAtom,
filterTimeAtom, filterTimeAtom,
sortCategoryAtom, sortAscendingAtom,
sortDirectionAtom, sortMethodAtom,
useIsFavorited, useIsFavorited,
useSetSortCategory, useSetSortMethod,
useToggleFavorite, useToggleFavorite,
} from '../state' } from '../state'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart' import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { Category, SortDirection } from '../types'
import { DISPLAYS } from './TimeSelector' import { DISPLAYS } from './TimeSelector'
const Cell = styled.div` const Cell = styled.div`
...@@ -329,9 +329,10 @@ const LogoContainer = styled.div` ...@@ -329,9 +329,10 @@ const LogoContainer = styled.div`
` `
/* formatting for volume with timeframe header display */ /* formatting for volume with timeframe header display */
function getHeaderDisplay(category: string, timeframe: TimePeriod): string { function getHeaderDisplay(method: string, timeframe: TimePeriod): string {
if (category === Category.volume || category === Category.percentChange) return `${DISPLAYS[timeframe]} ${category}` if (method === TokenSortMethod.VOLUME || method === TokenSortMethod.PERCENT_CHANGE)
return category return `${DISPLAYS[timeframe]} ${method}`
return method
} }
/* Get singular header cell for header row */ /* Get singular header cell for header row */
...@@ -339,20 +340,20 @@ function HeaderCell({ ...@@ -339,20 +340,20 @@ function HeaderCell({
category, category,
sortable, sortable,
}: { }: {
category: Category // TODO: change this to make it work for trans category: TokenSortMethod // TODO: change this to make it work for trans
sortable: boolean sortable: boolean
}) { }) {
const theme = useTheme() const theme = useTheme()
const sortDirection = useAtomValue<SortDirection>(sortDirectionAtom) const sortAscending = useAtomValue(sortAscendingAtom)
const handleSortCategory = useSetSortCategory(category) const handleSortCategory = useSetSortMethod(category)
const sortCategory = useAtomValue<Category>(sortCategoryAtom) const sortMethod = useAtomValue(sortMethodAtom)
const timeframe = useAtomValue<TimePeriod>(filterTimeAtom) const timeframe = useAtomValue(filterTimeAtom)
if (sortCategory === category) { if (sortMethod === category) {
return ( return (
<HeaderCellWrapper onClick={handleSortCategory}> <HeaderCellWrapper onClick={handleSortCategory}>
<SortArrowCell> <SortArrowCell>
{sortDirection === SortDirection.increasing ? ( {sortAscending ? (
<ArrowUp size={14} color={theme.accentActive} /> <ArrowUp size={14} color={theme.accentActive} />
) : ( ) : (
<ArrowDown size={14} color={theme.accentActive} /> <ArrowDown size={14} color={theme.accentActive} />
...@@ -430,10 +431,10 @@ export function HeaderRow() { ...@@ -430,10 +431,10 @@ export function HeaderRow() {
favorited={null} favorited={null}
listNumber="#" listNumber="#"
tokenInfo={<Trans>Token Name</Trans>} tokenInfo={<Trans>Token Name</Trans>}
price={<HeaderCell category={Category.price} sortable />} price={<HeaderCell category={TokenSortMethod.PRICE} sortable />}
percentChange={<HeaderCell category={Category.percentChange} sortable />} percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} sortable />}
marketCap={<HeaderCell category={Category.marketCap} sortable />} marketCap={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} sortable />}
volume={<HeaderCell category={Category.volume} sortable />} volume={<HeaderCell category={TokenSortMethod.VOLUME} sortable />}
sparkLine={null} sparkLine={null}
/> />
) )
...@@ -462,31 +463,28 @@ export function LoadingRow() { ...@@ -462,31 +463,28 @@ export function LoadingRow() {
) )
} }
/* Loaded State: row component with token information */ interface LoadedRowProps {
export default function LoadedRow({
tokenListIndex,
tokenListLength,
tokenData,
timePeriod,
}: {
tokenListIndex: number tokenListIndex: number
tokenListLength: number tokenListLength: number
tokenData: SingleTokenData token: TopToken
timePeriod: TimePeriod }
}) {
const tokenAddress = tokenData?.tokens?.[0].address /* Loaded State: row component with token information */
export default function LoadedRow({ tokenListIndex, tokenListLength, token }: LoadedRowProps) {
const tokenAddress = token?.address
const currency = useCurrency(tokenAddress) const currency = useCurrency(tokenAddress)
const tokenName = tokenData?.name const tokenName = token?.name
const tokenSymbol = tokenData?.tokens?.[0].symbol const tokenSymbol = token?.symbol
const isFavorited = useIsFavorited(tokenAddress) const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress) const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom) const filterNetwork = useAtomValue(filterNetworkAtom)
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const tokenDetails = tokenData?.markets?.[0] const timePeriod = useAtomValue(filterTimeAtom)
const { volume, pricePercentChange } = getDurationDetails(tokenData, timePeriod) const delta = token?.market?.pricePercentChange?.value
const arrow = pricePercentChange ? getDeltaArrow(pricePercentChange) : null const arrow = delta ? getDeltaArrow(delta) : null
const formattedDelta = pricePercentChange ? formatDelta(pricePercentChange) : null const formattedDelta = delta ? formatDelta(delta) : null
const sortAscending = useAtomValue(sortAscendingAtom)
const exploreTokenSelectedEventProperties = { const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork, chain_id: filterNetwork,
...@@ -516,7 +514,7 @@ export default function LoadedRow({ ...@@ -516,7 +514,7 @@ export default function LoadedRow({
<FavoriteIcon isFavorited={isFavorited} /> <FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited> </ClickFavorited>
} }
listNumber={tokenListIndex + 1} listNumber={sortAscending ? 100 - tokenListIndex : tokenListIndex + 1}
tokenInfo={ tokenInfo={
<ClickableName> <ClickableName>
<LogoContainer> <LogoContainer>
...@@ -532,7 +530,7 @@ export default function LoadedRow({ ...@@ -532,7 +530,7 @@ export default function LoadedRow({
price={ price={
<ClickableContent> <ClickableContent>
<PriceInfoCell> <PriceInfoCell>
{tokenDetails?.price?.value ? formatDollarAmount(tokenDetails?.price?.value) : '-'} {token?.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'}
<PercentChangeInfoCell> <PercentChangeInfoCell>
{formattedDelta} {formattedDelta}
{arrow} {arrow}
...@@ -542,16 +540,20 @@ export default function LoadedRow({ ...@@ -542,16 +540,20 @@ export default function LoadedRow({
} }
percentChange={ percentChange={
<ClickableContent> <ClickableContent>
{formattedDelta} {formattedDelta ?? '-'}
{arrow} {arrow}
</ClickableContent> </ClickableContent>
} }
marketCap={ marketCap={
<ClickableContent> <ClickableContent>
{tokenDetails?.marketCap?.value ? formatDollarAmount(tokenDetails?.marketCap?.value) : '-'} {token?.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'}
</ClickableContent>
}
volume={
<ClickableContent>
{token?.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'}
</ClickableContent> </ClickableContent>
} }
volume={<ClickableContent>{volume ? formatDollarAmount(volume ?? undefined) : '-'}</ClickableContent>}
sparkLine={ sparkLine={
<SparkLine> <SparkLine>
<ParentSize> <ParentSize>
...@@ -559,8 +561,8 @@ export default function LoadedRow({ ...@@ -559,8 +561,8 @@ export default function LoadedRow({
<SparklineChart <SparklineChart
width={width} width={width}
height={height} height={height}
tokenData={tokenData} tokenData={token}
pricePercentChange={pricePercentChange} pricePercentChange={token?.market?.pricePercentChange?.value}
timePeriod={timePeriod} timePeriod={timePeriod}
/> />
)} )}
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { import { showFavoritesAtom } from 'components/Tokens/state'
favoritesAtom, import { usePrefetchTopTokens, useTopTokens } from 'graphql/data/TopTokens'
filterStringAtom,
filterTimeAtom,
showFavoritesAtom,
sortCategoryAtom,
sortDirectionAtom,
} from 'components/Tokens/state'
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 { useAtomValue } from 'jotai/utils'
import { ReactNode, Suspense, useCallback, useMemo } from 'react' import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants' import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
import { Category, SortDirection } from '../types'
import LoadedRow, { HeaderRow, LoadingRow } from './TokenRow' import LoadedRow, { HeaderRow, LoadingRow } from './TokenRow'
const GridContainer = styled.div` const GridContainer = styled.div`
...@@ -49,93 +39,6 @@ const TokenRowsContainer = styled.div` ...@@ -49,93 +39,6 @@ const TokenRowsContainer = styled.div`
width: 100%; width: 100%;
` `
function useFilteredTokens(data: TokenTopQuery$data): SingleTokenData[] | undefined {
const filterString = useAtomValue(filterStringAtom)
const favorites = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom)
return useMemo(
() =>
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
} else if (!filterString) {
return true
} else {
const lowercaseFilterString = filterString.toLowerCase()
const addressIncludesFilterString = address?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = tokenInfo?.symbol?.toLowerCase().includes(lowercaseFilterString)
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
}
}),
[data.topTokenProjects, favorites, filterString, showFavorites]
)
}
function useSortedTokens(tokenData: SingleTokenData[] | undefined) {
const sortCategory = useAtomValue(sortCategoryAtom)
const sortDirection = useAtomValue(sortDirectionAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const sortFn = useCallback(
(a: any, b: any) => {
if (a > b) {
return sortDirection === SortDirection.decreasing ? -1 : 1
} else if (a < b) {
return sortDirection === SortDirection.decreasing ? 1 : -1
}
return 0
},
[sortDirection]
)
return useMemo(
() =>
tokenData &&
tokenData.sort((token1, token2) => {
if (!tokenData) {
return 0
}
// fix delta/percent change property
if (!token1 || !token2 || !sortDirection || !sortCategory) {
return 0
}
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.markets?.[0]?.marketCap?.value
b = token2.markets?.[0]?.marketCap?.value
break
case Category.price:
a = token1.markets?.[0]?.price?.value
b = token2.markets?.[0]?.price?.value
break
case Category.volume:
a = aVolume
b = bVolume
break
case Category.percentChange:
a = aChange
b = bChange
break
}
return sortFn(a, b)
}),
[tokenData, sortDirection, sortCategory, sortFn, timePeriod]
)
}
function NoTokensState({ message }: { message: ReactNode }) { function NoTokensState({ message }: { message: ReactNode }) {
return ( return (
<GridContainer> <GridContainer>
...@@ -160,49 +63,46 @@ export function LoadingTokenTable() { ...@@ -160,49 +63,46 @@ export function LoadingTokenTable() {
export default function TokenTable() { export default function TokenTable() {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom) const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const topTokens = useTopTokenQuery(1, timePeriod)
const filteredTokens = useFilteredTokens(topTokens)
const sortedFilteredTokens = useSortedTokens(filteredTokens)
/* loading and error state */ // TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
if (topTokens === null) { const prefetchedTokens = usePrefetchTopTokens()
return ( const { loading, tokens, loadMoreTokens } = useTopTokens(prefetchedTokens)
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occured loading tokens. Please try again.</Trans>
</>
}
/>
)
}
if (showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
}
if (!showFavorites && sortedFilteredTokens?.length === 0) { /* loading and error state */
return <NoTokensState message={<Trans>No tokens found</Trans>} /> if (loading) {
return <LoadingTokenTable />
} else {
if (!tokens) {
return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occured loading tokens. Please try again.</Trans>
</>
}
/>
)
} else if (tokens?.length === 0) {
return showFavorites ? (
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
) : (
<NoTokensState message={<Trans>No tokens found</Trans>} />
)
} else {
return (
<>
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{tokens?.map((token, index) => (
<LoadedRow key={token?.name} tokenListIndex={index} tokenListLength={tokens.length} token={token} />
))}
</TokenRowsContainer>
</GridContainer>
<button onClick={loadMoreTokens}>load more</button>
</>
)
}
} }
return (
<Suspense fallback={<LoadingTokenTable />}>
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{sortedFilteredTokens?.map((token, index) => (
<LoadedRow
key={token?.name}
tokenListIndex={index}
tokenListLength={sortedFilteredTokens.length}
tokenData={token}
timePeriod={timePeriod}
/>
))}
</TokenRowsContainer>
</GridContainer>
</Suspense>
)
} }
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { TimePeriod } from 'graphql/data/Token' import { TokenSortMethod } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils' import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { Category, SortDirection } from './types'
export const favoritesAtom = atomWithStorage<string[]>('favorites', []) export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false) export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
export const filterStringAtom = atomWithReset<string>('') export const filterStringAtom = atomWithReset<string>('')
export const filterNetworkAtom = atom<SupportedChainId>(SupportedChainId.MAINNET) export const filterNetworkAtom = atom<SupportedChainId>(SupportedChainId.MAINNET)
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY) export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY)
export const sortCategoryAtom = atom<Category>(Category.marketCap) export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED)
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing) export const sortAscendingAtom = atom<boolean>(false)
/* for favoriting tokens */ /* for favoriting tokens */
export function useToggleFavorite(tokenAddress: string | undefined | null) { export function useToggleFavorite(tokenAddress: string | undefined | null) {
...@@ -33,20 +32,18 @@ export function useToggleFavorite(tokenAddress: string | undefined | null) { ...@@ -33,20 +32,18 @@ export function useToggleFavorite(tokenAddress: string | undefined | null) {
} }
/* keep track of sort category for token table */ /* keep track of sort category for token table */
export function useSetSortCategory(category: Category) { export function useSetSortMethod(newSortMethod: TokenSortMethod) {
const [sortCategory, setSortCategory] = useAtom(sortCategoryAtom) const [sortMethod, setSortMethod] = useAtom(sortMethodAtom)
const [sortDirection, setDirectionCategory] = useAtom(sortDirectionAtom) const [sortAscending, setSortAscending] = useAtom(sortAscendingAtom)
return useCallback(() => { return useCallback(() => {
if (category === sortCategory) { if (sortMethod === newSortMethod) {
const oppositeDirection = setSortAscending(!sortAscending)
sortDirection === SortDirection.increasing ? SortDirection.decreasing : SortDirection.increasing
setDirectionCategory(oppositeDirection)
} else { } else {
setSortCategory(category) setSortMethod(newSortMethod)
setDirectionCategory(SortDirection.decreasing) setSortAscending(false)
} }
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory]) }, [sortMethod, setSortMethod, setSortAscending, sortAscending, newSortMethod])
} }
export function useIsFavorited(tokenAddress: string | null | undefined) { export function useIsFavorited(tokenAddress: string | null | undefined) {
......
export enum Category {
percentChange = 'Change',
marketCap = 'Market Cap',
price = 'Price',
volume = 'Volume',
}
export enum SortDirection {
increasing = 'Increasing',
decreasing = 'Decreasing',
}
...@@ -31,7 +31,9 @@ const fetchQuery = async function wrappedFetchQuery(params: RequestParameters, v ...@@ -31,7 +31,9 @@ const fetchQuery = async function wrappedFetchQuery(params: RequestParameters, v
// and reusing cached data if its available/fresh. // and reusing cached data if its available/fresh.
const gcReleaseBufferSize = 10 const gcReleaseBufferSize = 10
const store = new Store(new RecordSource(), { gcReleaseBufferSize }) const queryCacheExpirationTime = ms`1m`
const store = new Store(new RecordSource(), { gcReleaseBufferSize, queryCacheExpirationTime })
const network = Network.create(fetchQuery) const network = Network.create(fetchQuery)
// Export a singleton instance of Relay Environment configured with our network function: // Export a singleton instance of Relay Environment configured with our network function:
......
import graphql from 'babel-plugin-relay/macro' import graphql from 'babel-plugin-relay/macro'
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay' import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql' import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { TokenPrices$data, TokenPrices$key } from './__generated__/TokenPrices.graphql' import { TokenPrices$data, TokenPrices$key } from './__generated__/TokenPrices.graphql'
import { Chain, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql' import { TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { TokenTopQuery, TokenTopQuery$data } from './__generated__/TokenTopQuery.graphql' import { TimePeriod, toHistoryDuration } from './util'
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 } export type PricePoint = { value: number; timestamp: number }
const topTokensQuery = graphql` export const projectMetaDataFragment = graphql`
query TokenTopQuery($page: Int!, $duration: HistoryDuration!) { fragment Token_TokenProject_Metadata on TokenProject {
topTokenProjects(orderBy: MARKET_CAP, pageSize: 20, currency: USD, page: $page) { description
description homepageUrl
homepageUrl twitterName
twitterName name
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` const tokenPricesFragment = graphql`
...@@ -115,22 +25,6 @@ const tokenPricesFragment = graphql` ...@@ -115,22 +25,6 @@ const tokenPricesFragment = graphql`
} }
} }
` `
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` const tokenQuery = graphql`
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!, $skip: Boolean = false) { query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!, $skip: Boolean = false) {
...@@ -206,14 +100,14 @@ const tokenQuery = graphql` ...@@ -206,14 +100,14 @@ const tokenQuery = graphql`
` `
export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) { export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) {
const cachedTopToken = cachedTopTokens[address] //const cachedTopToken = cachedTopTokens[address]
const data = useLazyLoadQuery<TokenQuery>(tokenQuery, { const data = useLazyLoadQuery<TokenQuery>(tokenQuery, {
contract: { address, chain }, contract: { address, chain },
duration: toHistoryDuration(timePeriod), duration: toHistoryDuration(timePeriod),
skip: !!cachedTopToken, skip: false,
}) })
return !cachedTopToken ? data : { tokenProjects: [{ ...cachedTopToken }] } return data
} }
const tokenPriceQuery = graphql` const tokenPriceQuery = graphql`
...@@ -267,22 +161,25 @@ export function useTokenPricesFromFragment(key: TokenPrices$key | null | undefin ...@@ -267,22 +161,25 @@ export function useTokenPricesFromFragment(key: TokenPrices$key | null | undefin
} }
export function useTokenPricesCached( export function useTokenPricesCached(
key: TokenPrices$key | null | undefined, priceDataFragmentRef: TokenPrices$key | null | undefined,
address: string, address: string,
chain: Chain, chain: Chain,
timePeriod: TimePeriod timePeriod: TimePeriod
) { ) {
// Attempt to use token prices already provided by TokenDetails / TopToken queries // Attempt to use token prices already provided by TokenDetails / TopToken queries
const environment = useRelayEnvironment() const environment = useRelayEnvironment()
const fetchedTokenPrices = useFragment(tokenPricesFragment, key ?? null)?.priceHistory const fetchedTokenPrices = useFragment(tokenPricesFragment, priceDataFragmentRef ?? null)?.priceHistory
const [priceMap, setPriceMap] = useState( const [priceMap, setPriceMap] = useState<Map<TimePeriod, PricePoint[] | undefined>>(
new Map<TimePeriod, PricePoint[] | undefined>([[timePeriod, filterPrices(fetchedTokenPrices)]]) new Map([[timePeriod, filterPrices(fetchedTokenPrices)]])
) )
function updatePrices(key: TimePeriod, data?: PricePoint[]) { const updatePrices = useCallback(
setPriceMap(new Map(priceMap.set(key, data))) (key: TimePeriod, data?: PricePoint[]) => {
} setPriceMap(new Map(priceMap.set(key, data)))
},
[priceMap]
)
// Fetch the other timePeriods after first render // Fetch the other timePeriods after first render
useEffect(() => { useEffect(() => {
...@@ -315,38 +212,3 @@ export function useTokenPricesCached( ...@@ -315,38 +212,3 @@ export function useTokenPricesCached(
} }
export type SingleTokenData = NonNullable<TokenQuery$data['tokenProjects']>[number] 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 }
}
import graphql from 'babel-plugin-relay/macro'
import {
favoritesAtom,
filterStringAtom,
filterTimeAtom,
showFavoritesAtom,
sortAscendingAtom,
sortMethodAtom,
} from 'components/Tokens/state'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { ContractInput, TopTokens_TokensQuery } from './__generated__/TopTokens_TokensQuery.graphql'
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { toHistoryDuration, useCurrentChainName } from './util'
export function usePrefetchTopTokens() {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const chain = useCurrentChainName()
const top100 = useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain })
return top100
}
const topTokens100Query = graphql`
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
id
name
chain
address
symbol
market(currency: USD) {
totalValueLocked {
value
currency
}
price {
value
currency
}
pricePercentChange(duration: $duration) {
currency
value
}
volume(duration: $duration) {
value
currency
}
}
}
}
`
export enum TokenSortMethod {
PRICE = 'Price',
PERCENT_CHANGE = 'Change',
TOTAL_VALUE_LOCKED = 'TVL',
VOLUME = 'Volume',
}
export type PrefetchedTopToken = NonNullable<TopTokens100Query['response']['topTokens']>[number]
function useSortedTokens(tokens: TopTokens100Query['response']['topTokens']) {
const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
return useMemo(() => {
if (!tokens) return []
let tokenArray = Array.from(tokens)
switch (sortMethod) {
case TokenSortMethod.PRICE:
tokenArray = tokenArray.sort((a, b) => (b?.market?.price?.value ?? 0) - (a?.market?.price?.value ?? 0))
break
case TokenSortMethod.PERCENT_CHANGE:
tokenArray = tokenArray.sort(
(a, b) => (b?.market?.pricePercentChange?.value ?? 0) - (a?.market?.pricePercentChange?.value ?? 0)
)
break
case TokenSortMethod.TOTAL_VALUE_LOCKED:
tokenArray = tokenArray.sort(
(a, b) => (b?.market?.totalValueLocked?.value ?? 0) - (a?.market?.totalValueLocked?.value ?? 0)
)
break
case TokenSortMethod.VOLUME:
tokenArray = tokenArray.sort((a, b) => (b?.market?.volume?.value ?? 0) - (a?.market?.volume?.value ?? 0))
break
}
return sortAscending ? tokenArray.reverse() : tokenArray
}, [tokens, sortMethod, sortAscending])
}
function useFilteredTokens(tokens: PrefetchedTopToken[]) {
const filterString = useAtomValue(filterStringAtom)
const favorites = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
return useMemo(() => {
if (!tokens) {
return []
}
let returnTokens = tokens
if (showFavorites) {
returnTokens = returnTokens?.filter((token) => token?.address && favorites.includes(token.address))
}
if (lowercaseFilterString) {
returnTokens = returnTokens?.filter((token) => {
const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString)
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
})
}
return returnTokens
}, [tokens, showFavorites, lowercaseFilterString, favorites])
}
const PAGE_SIZE = 20
function toContractInput(token: PrefetchedTopToken) {
return {
address: token?.address ?? '',
chain: token?.chain ?? 'ETHEREUM',
}
}
export type TopToken = NonNullable<TopTokens_TokensQuery['response']['tokens']>[number]
export function useTopTokens(prefetchedData: TopTokens100Query['response']) {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const environment = useRelayEnvironment()
const [tokens, setTokens] = useState<TopToken[]>()
const [page, setPage] = useState(1)
const prefetchedSelectedTokens = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
const [loading, setLoading] = useState(true)
// TopTokens should ideally be fetched with usePaginationFragment. The backend does not current support graphql cursors;
// in the meantime, fetchQuery is used, as other relay hooks do not allow the refreshing and lazy loading we need
const loadTokens = useCallback(
(contracts: ContractInput[], onSuccess: (data: TopTokens_TokensQuery['response'] | undefined) => void) => {
fetchQuery<TopTokens_TokensQuery>(
environment,
tokensQuery,
{ contracts, duration },
{ fetchPolicy: 'store-or-network' }
)
.toPromise()
.then(onSuccess)
},
[duration, environment]
)
const loadMoreTokens = useCallback(() => {
const contracts = prefetchedSelectedTokens.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE).map(toContractInput)
loadTokens(contracts, (data) => {
if (data?.tokens) {
setTokens([...(tokens ?? []), ...data.tokens])
setPage(page + 1)
}
})
}, [loadTokens, page, prefetchedSelectedTokens, tokens])
// Reset count when filters are changed
useEffect(() => {
setLoading(true)
setTokens([])
const contracts = prefetchedSelectedTokens.slice(0, PAGE_SIZE).map(toContractInput)
loadTokens(contracts, (data) => {
if (data?.tokens) {
// @ts-ignore prevent typescript from complaining about readonly data
setTokens(data.tokens)
setLoading(false)
setPage(1)
}
})
}, [loadTokens, prefetchedSelectedTokens])
return { loading, tokens, loadMoreTokens }
}
export const tokensQuery = graphql`
query TopTokens_TokensQuery($contracts: [ContractInput!]!, $duration: HistoryDuration!) {
tokens(contracts: $contracts) {
id
name
chain
address
symbol
market(currency: USD) {
totalValueLocked {
value
currency
}
priceHistory(duration: $duration) {
timestamp
value
}
price {
value
currency
}
volume(duration: $duration) {
value
currency
}
pricePercentChange(duration: $duration) {
currency
value
}
}
}
}
`
"""This directive allows results to be deferred during execution""" """This directive allows results to be deferred during execution"""
directive @defer on FIELD directive @defer on FIELD
"""
Tells the service which subscriptions will be published to when this mutation is
called. This directive is deprecated use @aws_susbscribe directive instead.
"""
directive @aws_publish(
"""
List of subscriptions which will be published to when this mutation is called.
"""
subscriptions: [String]
) on FIELD_DEFINITION
""" """
Tells the service this field/object has access authorized by sigv4 signing. Tells the service this field/object has access authorized by sigv4 signing.
""" """
directive @aws_iam on OBJECT | FIELD_DEFINITION directive @aws_iam on OBJECT | FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by a Lambda Authorizer.
"""
directive @aws_lambda on OBJECT | FIELD_DEFINITION
""" """
Tells the service this field/object has access authorized by an API key. Tells the service this field/object has access authorized by an API key.
""" """
directive @aws_api_key on OBJECT | FIELD_DEFINITION directive @aws_api_key on OBJECT | FIELD_DEFINITION
""" """
Tells the service this field/object has access authorized by an OIDC token. Tells the service this field/object has access authorized by a Lambda Authorizer.
""" """
directive @aws_oidc on OBJECT | FIELD_DEFINITION directive @aws_lambda on OBJECT | FIELD_DEFINITION
"""Directs the schema to enforce authorization on a field"""
directive @aws_auth(
"""List of cognito user pool groups which have access on this field"""
cognito_groups: [String]
) on FIELD_DEFINITION
"""Tells the service which mutation triggers this subscription.""" """Tells the service which mutation triggers this subscription."""
directive @aws_subscribe( directive @aws_subscribe(
...@@ -40,6 +30,17 @@ directive @aws_subscribe( ...@@ -40,6 +30,17 @@ directive @aws_subscribe(
mutations: [String] mutations: [String]
) on FIELD_DEFINITION ) on FIELD_DEFINITION
"""
Tells the service which subscriptions will be published to when this mutation is
called. This directive is deprecated use @aws_susbscribe directive instead.
"""
directive @aws_publish(
"""
List of subscriptions which will be published to when this mutation is called.
"""
subscriptions: [String]
) on FIELD_DEFINITION
""" """
Tells the service this field/object has access authorized by a Cognito User Pools token. Tells the service this field/object has access authorized by a Cognito User Pools token.
""" """
...@@ -48,11 +49,10 @@ directive @aws_cognito_user_pools( ...@@ -48,11 +49,10 @@ directive @aws_cognito_user_pools(
cognito_groups: [String] cognito_groups: [String]
) on OBJECT | FIELD_DEFINITION ) on OBJECT | FIELD_DEFINITION
"""Directs the schema to enforce authorization on a field""" """
directive @aws_auth( Tells the service this field/object has access authorized by an OIDC token.
"""List of cognito user pool groups which have access on this field""" """
cognito_groups: [String] directive @aws_oidc on OBJECT | FIELD_DEFINITION
) on FIELD_DEFINITION
enum ActivityType { enum ActivityType {
APPROVE APPROVE
...@@ -100,6 +100,7 @@ enum Chain { ...@@ -100,6 +100,7 @@ enum Chain {
ETHEREUM_GOERLI ETHEREUM_GOERLI
OPTIMISM OPTIMISM
POLYGON POLYGON
CELO
} }
input ContractInput { input ContractInput {
...@@ -112,6 +113,12 @@ enum Currency { ...@@ -112,6 +113,12 @@ enum Currency {
ETH ETH
} }
type Dimensions {
id: ID!
height: Float
width: Float
}
enum HighLow { enum HighLow {
HIGH HIGH
LOW LOW
...@@ -136,6 +143,12 @@ interface IContract { ...@@ -136,6 +143,12 @@ interface IContract {
address: String address: String
} }
type Image {
id: ID!
url: String
dimensions: Dimensions
}
enum MarketSortableField { enum MarketSortableField {
MARKET_CAP MARKET_CAP
VOLUME VOLUME
...@@ -168,6 +181,9 @@ type NftAsset { ...@@ -168,6 +181,9 @@ type NftAsset {
thumbnailUrl: String thumbnailUrl: String
animationUrl: String animationUrl: String
smallImageUrl: String smallImageUrl: String
image: Image
thumbnail: Image
smallImage: Image
name: String name: String
nftContract: NftContract nftContract: NftContract
...@@ -204,10 +220,12 @@ type NftCollection { ...@@ -204,10 +220,12 @@ type NftCollection {
assets(page: Int, pageSize: Int, orderBy: NftAssetSortableField): [NftAsset] assets(page: Int, pageSize: Int, orderBy: NftAssetSortableField): [NftAsset]
""" """
bannerImageUrl: String bannerImageUrl: String
bannerImage: Image
description: String description: String
discordUrl: String discordUrl: String
homepageUrl: String homepageUrl: String
imageUrl: String imageUrl: String
image: Image
instagramName: String instagramName: String
markets(currencies: [Currency!]!): [NftCollectionMarket] markets(currencies: [Currency!]!): [NftCollectionMarket]
name: String name: String
...@@ -285,12 +303,14 @@ type Portfolio { ...@@ -285,12 +303,14 @@ type Portfolio {
type Query { type Query {
tokens(contracts: [ContractInput!]!): [Token] tokens(contracts: [ContractInput!]!): [Token]
tokenProjects(contracts: [ContractInput!]!): [TokenProject] tokenProjects(contracts: [ContractInput!]!): [TokenProject]
topTokenProjects(orderBy: MarketSortableField, page: Int, pageSize: Int, currency: Currency): [TokenProject] topTokenProjects(orderBy: MarketSortableField!, page: Int!, pageSize: Int!, currency: Currency): [TokenProject]
searchTokens(searchQuery: String!): [Token] searchTokens(searchQuery: String!): [Token]
searchTokenProjects(searchQuery: String!): [TokenProject]
assetActivities(address: String!, page: Int, pageSize: Int): [AssetActivity] assetActivities(address: String!, page: Int, pageSize: Int): [AssetActivity]
portfolio(ownerAddress: String!): Portfolio portfolio(ownerAddress: String!): Portfolio
portfolios(ownerAddresses: [String!]!): [Portfolio] portfolios(ownerAddresses: [String!]!): [Portfolio]
nftCollectionsById(collectionIds: [String]): [NftCollection] nftCollectionsById(collectionIds: [String]): [NftCollection]
topTokens(chain: Chain, page: Int!, pageSize: Int!): [Token]
} }
type TimestampedAmount implements IAmount { type TimestampedAmount implements IAmount {
...@@ -308,6 +328,8 @@ type Token implements IContract { ...@@ -308,6 +328,8 @@ type Token implements IContract {
decimals: Int decimals: Int
name: String name: String
symbol: String symbol: String
project: TokenProject
market(currency: Currency): TokenMarket
} }
type TokenApproval { type TokenApproval {
...@@ -322,6 +344,8 @@ type TokenApproval { ...@@ -322,6 +344,8 @@ type TokenApproval {
type TokenBalance { type TokenBalance {
id: ID! id: ID!
blockNumber: Int
blockTimestamp: Int
quantity: Float quantity: Float
denominatedValue: Amount denominatedValue: Amount
ownerAddress: String! ownerAddress: String!
...@@ -329,6 +353,16 @@ type TokenBalance { ...@@ -329,6 +353,16 @@ type TokenBalance {
tokenProjectMarket: TokenProjectMarket tokenProjectMarket: TokenProjectMarket
} }
type TokenMarket {
id: ID!
token: Token!
price: Amount
totalValueLocked: Amount
volume(duration: HistoryDuration!): Amount
pricePercentChange(duration: HistoryDuration!): Amount
priceHistory(duration: HistoryDuration!): [TimestampedAmount]
}
type TokenProject { type TokenProject {
id: ID! id: ID!
name: String name: String
......
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import { Chain, HistoryDuration } from './__generated__/TokenQuery.graphql'
export enum TimePeriod {
HOUR,
DAY,
WEEK,
MONTH,
YEAR,
ALL,
}
export 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 const CHAIN_IDS_TO_BACKEND_NAME: { [key: number]: Chain } = {
[SupportedChainId.MAINNET]: 'ETHEREUM',
[SupportedChainId.GOERLI]: 'ETHEREUM_GOERLI',
[SupportedChainId.POLYGON]: 'POLYGON',
[SupportedChainId.POLYGON_MUMBAI]: 'POLYGON',
[SupportedChainId.CELO]: 'CELO',
[SupportedChainId.CELO_ALFAJORES]: 'CELO',
[SupportedChainId.ARBITRUM_ONE]: 'ARBITRUM',
[SupportedChainId.ARBITRUM_RINKEBY]: 'ARBITRUM',
[SupportedChainId.OPTIMISM]: 'OPTIMISM',
[SupportedChainId.OPTIMISTIC_KOVAN]: 'OPTIMISM',
}
export function useCurrentChainName() {
const { chainId } = useWeb3React()
return chainId && CHAIN_IDS_TO_BACKEND_NAME[chainId] ? CHAIN_IDS_TO_BACKEND_NAME[chainId] : 'ETHEREUM'
}
...@@ -18,6 +18,9 @@ type _Block_ { ...@@ -18,6 +18,9 @@ type _Block_ {
"""The block number""" """The block number"""
number: Int! number: Int!
"""Integer representation of the timestamp stored in blocks for the chain"""
timestamp: Int
} }
"""The type for the top-level _meta field""" """The type for the top-level _meta field"""
......
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