Commit 13221e69 authored by cartcrom's avatar cartcrom Committed by GitHub

feat: caching and polling on apollo token queries (#5874)

* fix: caching on apollo token queries

* refactor: rename state variable

* added documentation for state variable purpose

* added documentation for nullish operator usage
parent 26fc3caa
...@@ -13,7 +13,7 @@ import TimePeriodSelector from './TimeSelector' ...@@ -13,7 +13,7 @@ import TimePeriodSelector from './TimeSelector'
function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined { function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
// Appends the current price to the end of the priceHistory array // Appends the current price to the end of the priceHistory array
const priceHistory = useMemo(() => { const priceHistory = useMemo(() => {
const market = tokenPriceData.tokens?.[0]?.market const market = tokenPriceData.token?.market
const priceHistory = market?.priceHistory?.filter(isPricePoint) const priceHistory = market?.priceHistory?.filter(isPricePoint)
const currentPrice = market?.price?.value const currentPrice = market?.price?.value
if (Array.isArray(priceHistory) && currentPrice !== undefined) { if (Array.isArray(priceHistory) && currentPrice !== undefined) {
......
...@@ -111,7 +111,7 @@ export default function TokenDetails({ ...@@ -111,7 +111,7 @@ export default function TokenDetails({
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain] const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const tokenQueryData = tokenQuery.tokens?.[0] const tokenQueryData = tokenQuery.token
const crossChainMap = useMemo( const crossChainMap = useMemo(
() => () =>
tokenQueryData?.project?.tokens.reduce((map, current) => { tokenQueryData?.project?.tokens.reduce((map, current) => {
......
...@@ -76,12 +76,11 @@ function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) { ...@@ -76,12 +76,11 @@ function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
} }
export default function TokenTable() { export default function TokenTable() {
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName) const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
const { tokens, tokenVolumeRank, loadingTokens, sparklines } = useTopTokens(chainName) const { tokens, tokenVolumeRank, loadingTokens, sparklines } = useTopTokens(chainName)
/* loading and error state */ /* loading and error state */
if (loadingTokens) { if (loadingTokens && !tokens) {
return <LoadingTokenTable rowCount={PAGE_SIZE} /> return <LoadingTokenTable rowCount={PAGE_SIZE} />
} else if (!tokens) { } else if (!tokens) {
return ( return (
......
...@@ -14,8 +14,8 @@ The difference between Token and TokenProject: ...@@ -14,8 +14,8 @@ The difference between Token and TokenProject:
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko. TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/ */
gql` gql`
query Token($contract: ContractInput!) { query Token($chain: Chain!, $address: String) {
tokens(contracts: [$contract]) { token(chain: $chain, address: $address) {
id id
decimals decimals
name name
...@@ -23,31 +23,39 @@ gql` ...@@ -23,31 +23,39 @@ gql`
address address
symbol symbol
market(currency: USD) { market(currency: USD) {
id
totalValueLocked { totalValueLocked {
id
value value
currency currency
} }
price { price {
id
value value
currency currency
} }
volume24H: volume(duration: DAY) { volume24H: volume(duration: DAY) {
id
value value
currency currency
} }
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) { priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
id
value value
} }
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) { priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
id
value value
} }
} }
project { project {
id
description description
homepageUrl homepageUrl
twitterName twitterName
logoUrl logoUrl
tokens { tokens {
id
chain chain
address address
} }
...@@ -58,7 +66,7 @@ gql` ...@@ -58,7 +66,7 @@ gql`
export type { Chain, TokenQuery } from './__generated__/types-and-hooks' export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number] export type TokenQueryData = TokenQuery['token']
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces. // TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo { export class QueryToken extends WrappedTokenInfo {
......
import gql from 'graphql-tag' import gql from 'graphql-tag'
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
gql` gql`
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) { query TokenPrice($chain: Chain!, $address: String, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) { token(chain: $chain, address: $address) {
id
address
chain
market(currency: USD) { market(currency: USD) {
id
price { price {
id
value value
} }
priceHistory(duration: $duration) { priceHistory(duration: $duration) {
id
timestamp timestamp
value value
} }
......
...@@ -15,7 +15,15 @@ import { ...@@ -15,7 +15,15 @@ import {
useTopTokens100Query, useTopTokens100Query,
useTopTokensSparklineQuery, useTopTokensSparklineQuery,
} from './__generated__/types-and-hooks' } from './__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util' import {
CHAIN_NAME_TO_CHAIN_ID,
isPricePoint,
PollingInterval,
PricePoint,
toHistoryDuration,
unwrapToken,
usePollQueryWhileMounted,
} from './util'
gql` gql`
query TopTokens100($duration: HistoryDuration!, $chain: Chain!) { query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
...@@ -26,24 +34,30 @@ gql` ...@@ -26,24 +34,30 @@ gql`
address address
symbol symbol
market(currency: USD) { market(currency: USD) {
id
totalValueLocked { totalValueLocked {
id
value value
currency currency
} }
price { price {
id
value value
currency currency
} }
pricePercentChange(duration: $duration) { pricePercentChange(duration: $duration) {
id
currency currency
value value
} }
volume(duration: $duration) { volume(duration: $duration) {
id
value value
currency currency
} }
} }
project { project {
id
logoUrl logoUrl
} }
} }
...@@ -53,9 +67,13 @@ gql` ...@@ -53,9 +67,13 @@ gql`
gql` gql`
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) { query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) { topTokens(pageSize: 100, page: 1, chain: $chain) {
id
address address
chain
market(currency: USD) { market(currency: USD) {
id
priceHistory(duration: $duration) { priceHistory(duration: $duration) {
id
timestamp timestamp
value value
} }
...@@ -64,11 +82,12 @@ gql` ...@@ -64,11 +82,12 @@ gql`
} }
` `
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) { function useSortedTokens(tokens: TopTokens100Query['topTokens']) {
const sortMethod = useAtomValue(sortMethodAtom) const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom) const sortAscending = useAtomValue(sortAscendingAtom)
return useMemo(() => { return useMemo(() => {
if (!tokens) return undefined
let tokenArray = Array.from(tokens) let tokenArray = Array.from(tokens)
switch (sortMethod) { switch (sortMethod) {
case TokenSortMethod.PRICE: case TokenSortMethod.PRICE:
...@@ -93,12 +112,13 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) { ...@@ -93,12 +112,13 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
}, [tokens, sortMethod, sortAscending]) }, [tokens, sortMethod, sortAscending])
} }
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) { function useFilteredTokens(tokens: TopTokens100Query['topTokens']) {
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString]) const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
return useMemo(() => { return useMemo(() => {
if (!tokens) return undefined
let returnTokens = tokens let returnTokens = tokens
if (lowercaseFilterString) { if (lowercaseFilterString) {
returnTokens = returnTokens?.filter((token) => { returnTokens = returnTokens?.filter((token) => {
...@@ -128,9 +148,12 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue { ...@@ -128,9 +148,12 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain] const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const duration = toHistoryDuration(useAtomValue(filterTimeAtom)) const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const { data: sparklineQuery } = useTopTokensSparklineQuery({ const { data: sparklineQuery } = usePollQueryWhileMounted(
variables: { duration, chain }, useTopTokensSparklineQuery({
}) variables: { duration, chain },
}),
PollingInterval.Slow
)
const sparklines = useMemo(() => { const sparklines = useMemo(() => {
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken)) const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
...@@ -141,17 +164,18 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue { ...@@ -141,17 +164,18 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
return map return map
}, [chainId, sparklineQuery?.topTokens]) }, [chainId, sparklineQuery?.topTokens])
const { data, loading: loadingTokens } = useTopTokens100Query({ const { data, loading: loadingTokens } = usePollQueryWhileMounted(
variables: { duration, chain }, useTopTokens100Query({
}) variables: { duration, chain },
const unwrappedTokens = useMemo( }),
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [], PollingInterval.Fast
[chainId, data]
) )
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data])
const tokenVolumeRank = useMemo( const tokenVolumeRank = useMemo(
() => () =>
unwrappedTokens unwrappedTokens
.sort((a, b) => { ?.sort((a, b) => {
if (!a.market?.volume || !b.market?.volume) return 0 if (!a.market?.volume || !b.market?.volume) return 0
return a.market.volume.value > b.market.volume.value ? -1 : 1 return a.market.volume.value > b.market.volume.value ? -1 : 1
}) })
...@@ -161,7 +185,7 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue { ...@@ -161,7 +185,7 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
...acc, ...acc,
[cur.address]: i + 1, [cur.address]: i + 1,
} }
}, {}), }, {}) ?? {},
[unwrappedTokens] [unwrappedTokens]
) )
const filteredTokens = useFilteredTokens(unwrappedTokens) const filteredTokens = useFilteredTokens(unwrappedTokens)
......
...@@ -827,19 +827,21 @@ export enum TransactionStatus { ...@@ -827,19 +827,21 @@ export enum TransactionStatus {
} }
export type TokenQueryVariables = Exact<{ export type TokenQueryVariables = Exact<{
contract: ContractInput; chain: Chain;
address?: InputMaybe<Scalars['String']>;
}>; }>;
export type TokenQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', value: number }, priceLow52W?: { __typename?: 'Amount', value: number } }, project?: { __typename?: 'TokenProject', description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', chain: Chain, address?: string }> } }> }; export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
export type TokenPriceQueryVariables = Exact<{ export type TokenPriceQueryVariables = Exact<{
contract: ContractInput; chain: Chain;
address?: InputMaybe<Scalars['String']>;
duration: HistoryDuration; duration: HistoryDuration;
}>; }>;
export type TokenPriceQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', market?: { __typename?: 'TokenMarket', price?: { __typename?: 'Amount', value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> }; export type TokenPriceQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } } };
export type TopTokens100QueryVariables = Exact<{ export type TopTokens100QueryVariables = Exact<{
duration: HistoryDuration; duration: HistoryDuration;
...@@ -847,7 +849,7 @@ export type TopTokens100QueryVariables = Exact<{ ...@@ -847,7 +849,7 @@ export type TopTokens100QueryVariables = Exact<{
}>; }>;
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', currency?: Currency, value: number }, volume?: { __typename?: 'Amount', value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', logoUrl?: string } }> }; export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
export type TopTokensSparklineQueryVariables = Exact<{ export type TopTokensSparklineQueryVariables = Exact<{
duration: HistoryDuration; duration: HistoryDuration;
...@@ -855,7 +857,7 @@ export type TopTokensSparklineQueryVariables = Exact<{ ...@@ -855,7 +857,7 @@ export type TopTokensSparklineQueryVariables = Exact<{
}>; }>;
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', address?: string, market?: { __typename?: 'TokenMarket', priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> }; export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } }> };
export type AssetQueryVariables = Exact<{ export type AssetQueryVariables = Exact<{
address: Scalars['String']; address: Scalars['String'];
...@@ -900,8 +902,8 @@ export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename ...@@ -900,8 +902,8 @@ export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename
export const TokenDocument = gql` export const TokenDocument = gql`
query Token($contract: ContractInput!) { query Token($chain: Chain!, $address: String) {
tokens(contracts: [$contract]) { token(chain: $chain, address: $address) {
id id
decimals decimals
name name
...@@ -909,31 +911,39 @@ export const TokenDocument = gql` ...@@ -909,31 +911,39 @@ export const TokenDocument = gql`
address address
symbol symbol
market(currency: USD) { market(currency: USD) {
id
totalValueLocked { totalValueLocked {
id
value value
currency currency
} }
price { price {
id
value value
currency currency
} }
volume24H: volume(duration: DAY) { volume24H: volume(duration: DAY) {
id
value value
currency currency
} }
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) { priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
id
value value
} }
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) { priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
id
value value
} }
} }
project { project {
id
description description
homepageUrl homepageUrl
twitterName twitterName
logoUrl logoUrl
tokens { tokens {
id
chain chain
address address
} }
...@@ -954,7 +964,8 @@ export const TokenDocument = gql` ...@@ -954,7 +964,8 @@ export const TokenDocument = gql`
* @example * @example
* const { data, loading, error } = useTokenQuery({ * const { data, loading, error } = useTokenQuery({
* variables: { * variables: {
* contract: // value for 'contract' * chain: // value for 'chain'
* address: // value for 'address'
* }, * },
* }); * });
*/ */
...@@ -970,13 +981,19 @@ export type TokenQueryHookResult = ReturnType<typeof useTokenQuery>; ...@@ -970,13 +981,19 @@ export type TokenQueryHookResult = ReturnType<typeof useTokenQuery>;
export type TokenLazyQueryHookResult = ReturnType<typeof useTokenLazyQuery>; export type TokenLazyQueryHookResult = ReturnType<typeof useTokenLazyQuery>;
export type TokenQueryResult = Apollo.QueryResult<TokenQuery, TokenQueryVariables>; export type TokenQueryResult = Apollo.QueryResult<TokenQuery, TokenQueryVariables>;
export const TokenPriceDocument = gql` export const TokenPriceDocument = gql`
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) { query TokenPrice($chain: Chain!, $address: String, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) { token(chain: $chain, address: $address) {
id
address
chain
market(currency: USD) { market(currency: USD) {
id
price { price {
id
value value
} }
priceHistory(duration: $duration) { priceHistory(duration: $duration) {
id
timestamp timestamp
value value
} }
...@@ -997,7 +1014,8 @@ export const TokenPriceDocument = gql` ...@@ -997,7 +1014,8 @@ export const TokenPriceDocument = gql`
* @example * @example
* const { data, loading, error } = useTokenPriceQuery({ * const { data, loading, error } = useTokenPriceQuery({
* variables: { * variables: {
* contract: // value for 'contract' * chain: // value for 'chain'
* address: // value for 'address'
* duration: // value for 'duration' * duration: // value for 'duration'
* }, * },
* }); * });
...@@ -1022,24 +1040,30 @@ export const TopTokens100Document = gql` ...@@ -1022,24 +1040,30 @@ export const TopTokens100Document = gql`
address address
symbol symbol
market(currency: USD) { market(currency: USD) {
id
totalValueLocked { totalValueLocked {
id
value value
currency currency
} }
price { price {
id
value value
currency currency
} }
pricePercentChange(duration: $duration) { pricePercentChange(duration: $duration) {
id
currency currency
value value
} }
volume(duration: $duration) { volume(duration: $duration) {
id
value value
currency currency
} }
} }
project { project {
id
logoUrl logoUrl
} }
} }
...@@ -1077,9 +1101,13 @@ export type TopTokens100QueryResult = Apollo.QueryResult<TopTokens100Query, TopT ...@@ -1077,9 +1101,13 @@ export type TopTokens100QueryResult = Apollo.QueryResult<TopTokens100Query, TopT
export const TopTokensSparklineDocument = gql` export const TopTokensSparklineDocument = gql`
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) { query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) { topTokens(pageSize: 100, page: 1, chain: $chain) {
id
address address
chain
market(currency: USD) { market(currency: USD) {
id
priceHistory(duration: $duration) { priceHistory(duration: $duration) {
id
timestamp timestamp
value value
} }
......
import { ApolloClient, InMemoryCache } from '@apollo/client' import { ApolloClient, InMemoryCache } from '@apollo/client'
import { relayStylePagination } from '@apollo/client/utilities' import { Reference, relayStylePagination } from '@apollo/client/utilities'
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
if (!GRAPHQL_URL) { if (!GRAPHQL_URL) {
...@@ -7,6 +7,7 @@ if (!GRAPHQL_URL) { ...@@ -7,6 +7,7 @@ if (!GRAPHQL_URL) {
} }
export const apolloClient = new ApolloClient({ export const apolloClient = new ApolloClient({
connectToDevTools: true,
uri: GRAPHQL_URL, uri: GRAPHQL_URL,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -18,6 +19,33 @@ export const apolloClient = new ApolloClient({ ...@@ -18,6 +19,33 @@ export const apolloClient = new ApolloClient({
fields: { fields: {
nftBalances: relayStylePagination(), nftBalances: relayStylePagination(),
nftAssets: relayStylePagination(), nftAssets: relayStylePagination(),
// tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[]
token: {
read(_, { args, toReference }): Reference | undefined {
return toReference({
__typename: 'Token',
chain: args?.chain,
address: args?.address,
})
},
},
},
},
Token: {
// key by chain, address combination so that Token(chain, address) endpoint can read from cache
/**
* NOTE: In any query for `token` or `tokens`, you must include the `chain` and `address` fields
* in order for result to normalize properly in the cache.
*/
keyFields: ['chain', 'address'],
fields: {
address: {
read(address: string | null): string | null {
// backend endpoint sometimes returns checksummed, sometimes lowercased addresses
// always use lowercased addresses in our app for consistency
return address?.toLowerCase() ?? null
},
},
}, },
}, },
}, },
......
import { QueryResult } from '@apollo/client'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc' import { ZERO_ADDRESS } from 'constants/misc'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import ms from 'ms.macro'
import { useEffect } from 'react'
import { Chain, HistoryDuration } from './__generated__/types-and-hooks' import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
export enum PollingInterval {
Slow = ms`5m`,
Normal = ms`1m`,
Fast = ms`12s`, // 12 seconds, block times for mainnet
LightningMcQueen = ms`3s`, // 3 seconds, approx block times for polygon
}
// Polls a query only when the current component is mounted, as useQuery's pollInterval prop will continue to poll after unmount
export function usePollQueryWhileMounted<T, K>(queryResult: QueryResult<T, K>, interval: PollingInterval) {
const { startPolling, stopPolling } = queryResult
useEffect(() => {
startPolling(interval)
return stopPolling
}, [interval, startPolling, stopPolling])
return queryResult
}
export enum TimePeriod { export enum TimePeriod {
HOUR, HOUR,
DAY, DAY,
......
import TokenDetails from 'components/Tokens/TokenDetails' import TokenDetails from 'components/Tokens/TokenDetails'
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton' import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { useTokenPriceQuery, useTokenQuery } from 'graphql/data/__generated__/types-and-hooks' import { useTokenPriceQuery, useTokenQuery } from 'graphql/data/__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util' import { TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils' import { atomWithStorage } from 'jotai/utils'
import { useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { getNativeTokenDBAddress } from 'utils/nativeTokens' import { getNativeTokenDBAddress } from 'utils/nativeTokens'
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY) export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
export default function TokenDetailsPage() { export default function TokenDetailsPage() {
const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>() const { tokenAddress, chainName } = useParams<{ tokenAddress: string; chainName?: string }>()
const chain = validateUrlChainParam(chainName) const chain = validateUrlChainParam(chainName)
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const isNative = tokenAddress === NATIVE_CHAIN_ID const isNative = tokenAddress === NATIVE_CHAIN_ID
const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom) const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom)
const [contract, duration] = useMemo( const [address, duration] = useMemo(
() => [ /* tokenAddress will always be defined in the path for for this page to render, but useParams will always
{ address: isNative ? nativeOnChain(pageChainId).wrapped.address : tokenAddress ?? '', chain }, return optional arguments; nullish coalescing operator is present here to appease typechecker */
toHistoryDuration(timePeriod), () => [isNative ? getNativeTokenDBAddress(chain) : tokenAddress ?? '', toHistoryDuration(timePeriod)],
], [chain, isNative, timePeriod, tokenAddress]
[chain, isNative, pageChainId, timePeriod, tokenAddress]
) )
const { data: tokenQuery, loading: tokenQueryLoading } = useTokenQuery({ const { data: tokenQuery } = useTokenQuery({
variables: { variables: {
contract: isNative ? { address: getNativeTokenDBAddress(chain), chain } : contract, address,
chain,
}, },
}) })
const { data: tokenPriceQuery } = useTokenPriceQuery({ const { data: tokenPriceQuery } = useTokenPriceQuery({
variables: { variables: {
contract, address,
chain,
duration, duration,
}, },
}) })
if (!tokenQuery || tokenQueryLoading) return <TokenDetailsPageSkeleton /> // Saves already-loaded chart data into state to display while tokenPriceQuery is undefined timePeriod input changes
const [currentPriceQuery, setCurrentPriceQuery] = useState(tokenPriceQuery)
useEffect(() => {
if (tokenPriceQuery) setCurrentPriceQuery(tokenPriceQuery)
}, [setCurrentPriceQuery, tokenPriceQuery])
if (!tokenQuery) return <TokenDetailsPageSkeleton />
return ( return (
<TokenDetails <TokenDetails
urlAddress={tokenAddress} urlAddress={tokenAddress}
chain={chain} chain={chain}
tokenQuery={tokenQuery} tokenQuery={tokenQuery}
tokenPriceQuery={tokenPriceQuery} tokenPriceQuery={currentPriceQuery}
onChangeTimePeriod={setTimePeriod} onChangeTimePeriod={setTimePeriod}
/> />
) )
......
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