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 { 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 { useTheme } from 'styled-components/macro'
......@@ -11,7 +13,7 @@ type PricePoint = { value: number; timestamp: number }
interface SparklineChartProps {
width: number
height: number
tokenData: SingleTokenData
tokenData: TopToken
pricePercentChange: number | undefined | null
timePeriod: TimePeriod
}
......@@ -19,7 +21,7 @@ interface SparklineChartProps {
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) {
const theme = useTheme()
// for sparkline
const pricePoints = useTokenPricesFromFragment(tokenData?.prices?.[0]) ?? []
const pricePoints = filterPrices(tokenData?.market?.priceHistory) ?? []
const hasData = pricePoints.length !== 0
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
......
......@@ -111,7 +111,7 @@ export default function ChartSection({ token, tokenData }: { token: Token; token
<ChartContainer>
<ParentSize>
{({ 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>
</ChartContainer>
......
......@@ -17,7 +17,8 @@ import {
} from 'd3'
import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql'
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 { useAtom } from 'jotai'
import { useCallback, useEffect, useMemo, useState } from 'react'
......@@ -131,15 +132,15 @@ interface PriceChartProps {
width: number
height: number
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 locale = useActiveLocale()
const theme = useTheme()
const { priceMap } = useTokenPricesCached(priceData, tokenAddress, 'ETHEREUM', timePeriod)
const { priceMap } = useTokenPricesCached(priceDataFragmentRef, tokenAddress, 'ETHEREUM', timePeriod)
const prices = priceMap.get(timePeriod)
// 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 { useAtom } from 'jotai'
import { useRef } from 'react'
......
......@@ -6,7 +6,8 @@ import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
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 { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react'
......@@ -27,14 +28,13 @@ import {
filterNetworkAtom,
filterStringAtom,
filterTimeAtom,
sortCategoryAtom,
sortDirectionAtom,
sortAscendingAtom,
sortMethodAtom,
useIsFavorited,
useSetSortCategory,
useSetSortMethod,
useToggleFavorite,
} from '../state'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { Category, SortDirection } from '../types'
import { DISPLAYS } from './TimeSelector'
const Cell = styled.div`
......@@ -329,9 +329,10 @@ const LogoContainer = styled.div`
`
/* formatting for volume with timeframe header display */
function getHeaderDisplay(category: string, timeframe: TimePeriod): string {
if (category === Category.volume || category === Category.percentChange) return `${DISPLAYS[timeframe]} ${category}`
return category
function getHeaderDisplay(method: string, timeframe: TimePeriod): string {
if (method === TokenSortMethod.VOLUME || method === TokenSortMethod.PERCENT_CHANGE)
return `${DISPLAYS[timeframe]} ${method}`
return method
}
/* Get singular header cell for header row */
......@@ -339,20 +340,20 @@ function HeaderCell({
category,
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
}) {
const theme = useTheme()
const sortDirection = useAtomValue<SortDirection>(sortDirectionAtom)
const handleSortCategory = useSetSortCategory(category)
const sortCategory = useAtomValue<Category>(sortCategoryAtom)
const timeframe = useAtomValue<TimePeriod>(filterTimeAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
const handleSortCategory = useSetSortMethod(category)
const sortMethod = useAtomValue(sortMethodAtom)
const timeframe = useAtomValue(filterTimeAtom)
if (sortCategory === category) {
if (sortMethod === category) {
return (
<HeaderCellWrapper onClick={handleSortCategory}>
<SortArrowCell>
{sortDirection === SortDirection.increasing ? (
{sortAscending ? (
<ArrowUp size={14} color={theme.accentActive} />
) : (
<ArrowDown size={14} color={theme.accentActive} />
......@@ -430,10 +431,10 @@ export function HeaderRow() {
favorited={null}
listNumber="#"
tokenInfo={<Trans>Token Name</Trans>}
price={<HeaderCell category={Category.price} sortable />}
percentChange={<HeaderCell category={Category.percentChange} sortable />}
marketCap={<HeaderCell category={Category.marketCap} sortable />}
volume={<HeaderCell category={Category.volume} sortable />}
price={<HeaderCell category={TokenSortMethod.PRICE} sortable />}
percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} sortable />}
marketCap={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} sortable />}
volume={<HeaderCell category={TokenSortMethod.VOLUME} sortable />}
sparkLine={null}
/>
)
......@@ -462,31 +463,28 @@ export function LoadingRow() {
)
}
/* Loaded State: row component with token information */
export default function LoadedRow({
tokenListIndex,
tokenListLength,
tokenData,
timePeriod,
}: {
interface LoadedRowProps {
tokenListIndex: number
tokenListLength: number
tokenData: SingleTokenData
timePeriod: TimePeriod
}) {
const tokenAddress = tokenData?.tokens?.[0].address
token: TopToken
}
/* Loaded State: row component with token information */
export default function LoadedRow({ tokenListIndex, tokenListLength, token }: LoadedRowProps) {
const tokenAddress = token?.address
const currency = useCurrency(tokenAddress)
const tokenName = tokenData?.name
const tokenSymbol = tokenData?.tokens?.[0].symbol
const tokenName = token?.name
const tokenSymbol = token?.symbol
const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const tokenDetails = tokenData?.markets?.[0]
const { volume, pricePercentChange } = getDurationDetails(tokenData, timePeriod)
const arrow = pricePercentChange ? getDeltaArrow(pricePercentChange) : null
const formattedDelta = pricePercentChange ? formatDelta(pricePercentChange) : null
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token?.market?.pricePercentChange?.value
const arrow = delta ? getDeltaArrow(delta) : null
const formattedDelta = delta ? formatDelta(delta) : null
const sortAscending = useAtomValue(sortAscendingAtom)
const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork,
......@@ -516,7 +514,7 @@ export default function LoadedRow({
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
}
listNumber={tokenListIndex + 1}
listNumber={sortAscending ? 100 - tokenListIndex : tokenListIndex + 1}
tokenInfo={
<ClickableName>
<LogoContainer>
......@@ -532,7 +530,7 @@ export default function LoadedRow({
price={
<ClickableContent>
<PriceInfoCell>
{tokenDetails?.price?.value ? formatDollarAmount(tokenDetails?.price?.value) : '-'}
{token?.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'}
<PercentChangeInfoCell>
{formattedDelta}
{arrow}
......@@ -542,16 +540,20 @@ export default function LoadedRow({
}
percentChange={
<ClickableContent>
{formattedDelta}
{formattedDelta ?? '-'}
{arrow}
</ClickableContent>
}
marketCap={
<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>
}
volume={<ClickableContent>{volume ? formatDollarAmount(volume ?? undefined) : '-'}</ClickableContent>}
sparkLine={
<SparkLine>
<ParentSize>
......@@ -559,8 +561,8 @@ export default function LoadedRow({
<SparklineChart
width={width}
height={height}
tokenData={tokenData}
pricePercentChange={pricePercentChange}
tokenData={token}
pricePercentChange={token?.market?.pricePercentChange?.value}
timePeriod={timePeriod}
/>
)}
......
import { Trans } from '@lingui/macro'
import {
favoritesAtom,
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 { showFavoritesAtom } from 'components/Tokens/state'
import { usePrefetchTopTokens, useTopTokens } from 'graphql/data/TopTokens'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
import { Category, SortDirection } from '../types'
import LoadedRow, { HeaderRow, LoadingRow } from './TokenRow'
const GridContainer = styled.div`
......@@ -49,93 +39,6 @@ const TokenRowsContainer = styled.div`
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 }) {
return (
<GridContainer>
......@@ -160,13 +63,16 @@ export function LoadingTokenTable() {
export default function TokenTable() {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const topTokens = useTopTokenQuery(1, timePeriod)
const filteredTokens = useFilteredTokens(topTokens)
const sortedFilteredTokens = useSortedTokens(filteredTokens)
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const prefetchedTokens = usePrefetchTopTokens()
const { loading, tokens, loadMoreTokens } = useTopTokens(prefetchedTokens)
/* loading and error state */
if (topTokens === null) {
if (loading) {
return <LoadingTokenTable />
} else {
if (!tokens) {
return (
<NoTokensState
message={
......@@ -177,32 +83,26 @@ export default function TokenTable() {
}
/>
)
}
if (showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
}
if (!showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>No tokens found</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 (
<Suspense fallback={<LoadingTokenTable />}>
<>
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{sortedFilteredTokens?.map((token, index) => (
<LoadedRow
key={token?.name}
tokenListIndex={index}
tokenListLength={sortedFilteredTokens.length}
tokenData={token}
timePeriod={timePeriod}
/>
{tokens?.map((token, index) => (
<LoadedRow key={token?.name} tokenListIndex={index} tokenListLength={tokens.length} token={token} />
))}
</TokenRowsContainer>
</GridContainer>
</Suspense>
<button onClick={loadMoreTokens}>load more</button>
</>
)
}
}
}
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 { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } from 'react'
import { Category, SortDirection } from './types'
export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
export const filterStringAtom = atomWithReset<string>('')
export const filterNetworkAtom = atom<SupportedChainId>(SupportedChainId.MAINNET)
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY)
export const sortCategoryAtom = atom<Category>(Category.marketCap)
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing)
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED)
export const sortAscendingAtom = atom<boolean>(false)
/* for favoriting tokens */
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 */
export function useSetSortCategory(category: Category) {
const [sortCategory, setSortCategory] = useAtom(sortCategoryAtom)
const [sortDirection, setDirectionCategory] = useAtom(sortDirectionAtom)
export function useSetSortMethod(newSortMethod: TokenSortMethod) {
const [sortMethod, setSortMethod] = useAtom(sortMethodAtom)
const [sortAscending, setSortAscending] = useAtom(sortAscendingAtom)
return useCallback(() => {
if (category === sortCategory) {
const oppositeDirection =
sortDirection === SortDirection.increasing ? SortDirection.decreasing : SortDirection.increasing
setDirectionCategory(oppositeDirection)
if (sortMethod === newSortMethod) {
setSortAscending(!sortAscending)
} else {
setSortCategory(category)
setDirectionCategory(SortDirection.decreasing)
setSortMethod(newSortMethod)
setSortAscending(false)
}
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
}, [sortMethod, setSortMethod, setSortAscending, sortAscending, newSortMethod])
}
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
// and reusing cached data if its available/fresh.
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)
// Export a singleton instance of Relay Environment configured with our network function:
......
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 { TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { Chain, 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'
}
}
import { TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { TimePeriod, toHistoryDuration } from './util'
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) {
export const projectMetaDataFragment = graphql`
fragment Token_TokenProject_Metadata on TokenProject {
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`
......@@ -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`
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!, $skip: Boolean = false) {
......@@ -206,14 +100,14 @@ const tokenQuery = graphql`
`
export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) {
const cachedTopToken = cachedTopTokens[address]
//const cachedTopToken = cachedTopTokens[address]
const data = useLazyLoadQuery<TokenQuery>(tokenQuery, {
contract: { address, chain },
duration: toHistoryDuration(timePeriod),
skip: !!cachedTopToken,
skip: false,
})
return !cachedTopToken ? data : { tokenProjects: [{ ...cachedTopToken }] }
return data
}
const tokenPriceQuery = graphql`
......@@ -267,22 +161,25 @@ export function useTokenPricesFromFragment(key: TokenPrices$key | null | undefin
}
export function useTokenPricesCached(
key: TokenPrices$key | null | undefined,
priceDataFragmentRef: 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 fetchedTokenPrices = useFragment(tokenPricesFragment, priceDataFragmentRef ?? null)?.priceHistory
const [priceMap, setPriceMap] = useState(
new Map<TimePeriod, PricePoint[] | undefined>([[timePeriod, filterPrices(fetchedTokenPrices)]])
const [priceMap, setPriceMap] = useState<Map<TimePeriod, PricePoint[] | undefined>>(
new Map([[timePeriod, filterPrices(fetchedTokenPrices)]])
)
function updatePrices(key: TimePeriod, data?: PricePoint[]) {
const updatePrices = useCallback(
(key: TimePeriod, data?: PricePoint[]) => {
setPriceMap(new Map(priceMap.set(key, data)))
}
},
[priceMap]
)
// Fetch the other timePeriods after first render
useEffect(() => {
......@@ -315,38 +212,3 @@ export function useTokenPricesCached(
}
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"""
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.
"""
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.
"""
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."""
directive @aws_subscribe(
......@@ -40,6 +30,17 @@ directive @aws_subscribe(
mutations: [String]
) 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.
"""
......@@ -48,11 +49,10 @@ directive @aws_cognito_user_pools(
cognito_groups: [String]
) 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 this field/object has access authorized by an OIDC token.
"""
directive @aws_oidc on OBJECT | FIELD_DEFINITION
enum ActivityType {
APPROVE
......@@ -100,6 +100,7 @@ enum Chain {
ETHEREUM_GOERLI
OPTIMISM
POLYGON
CELO
}
input ContractInput {
......@@ -112,6 +113,12 @@ enum Currency {
ETH
}
type Dimensions {
id: ID!
height: Float
width: Float
}
enum HighLow {
HIGH
LOW
......@@ -136,6 +143,12 @@ interface IContract {
address: String
}
type Image {
id: ID!
url: String
dimensions: Dimensions
}
enum MarketSortableField {
MARKET_CAP
VOLUME
......@@ -168,6 +181,9 @@ type NftAsset {
thumbnailUrl: String
animationUrl: String
smallImageUrl: String
image: Image
thumbnail: Image
smallImage: Image
name: String
nftContract: NftContract
......@@ -204,10 +220,12 @@ type NftCollection {
assets(page: Int, pageSize: Int, orderBy: NftAssetSortableField): [NftAsset]
"""
bannerImageUrl: String
bannerImage: Image
description: String
discordUrl: String
homepageUrl: String
imageUrl: String
image: Image
instagramName: String
markets(currencies: [Currency!]!): [NftCollectionMarket]
name: String
......@@ -285,12 +303,14 @@ type Portfolio {
type Query {
tokens(contracts: [ContractInput!]!): [Token]
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]
searchTokenProjects(searchQuery: String!): [TokenProject]
assetActivities(address: String!, page: Int, pageSize: Int): [AssetActivity]
portfolio(ownerAddress: String!): Portfolio
portfolios(ownerAddresses: [String!]!): [Portfolio]
nftCollectionsById(collectionIds: [String]): [NftCollection]
topTokens(chain: Chain, page: Int!, pageSize: Int!): [Token]
}
type TimestampedAmount implements IAmount {
......@@ -308,6 +328,8 @@ type Token implements IContract {
decimals: Int
name: String
symbol: String
project: TokenProject
market(currency: Currency): TokenMarket
}
type TokenApproval {
......@@ -322,6 +344,8 @@ type TokenApproval {
type TokenBalance {
id: ID!
blockNumber: Int
blockTimestamp: Int
quantity: Float
denominatedValue: Amount
ownerAddress: String!
......@@ -329,6 +353,16 @@ type TokenBalance {
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 {
id: ID!
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_ {
"""The block number"""
number: Int!
"""Integer representation of the timestamp stored in blocks for the chain"""
timestamp: Int
}
"""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