fix: fetch decimals from token query (#5016)

* feat: add decimals, wrapper to token query

* fix: construct token in details page
This commit is contained in:
Zach Pomerantz 2022-10-26 18:00:53 -07:00 committed by GitHub
parent 2e3950018a
commit 058aa52faf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 67 additions and 121 deletions

@ -1,11 +1,13 @@
import graphql from 'babel-plugin-relay/macro' import graphql from 'babel-plugin-relay/macro'
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery } from 'react-relay' import { fetchQuery, useLazyLoadQuery } from 'react-relay'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql' import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { ContractInput, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql' import { ContractInput, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import environment from './RelayEnvironment' import environment from './RelayEnvironment'
import { TimePeriod, toHistoryDuration } from './util' import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration } from './util'
/* /*
The difference between Token and TokenProject: The difference between Token and TokenProject:
@ -19,6 +21,7 @@ const tokenQuery = graphql`
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!) { query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) { tokens(contracts: [$contract]) {
id @required(action: LOG) id @required(action: LOG)
decimals
name name
chain @required(action: LOG) chain @required(action: LOG)
address @required(action: LOG) address @required(action: LOG)
@ -61,6 +64,42 @@ const tokenQuery = graphql`
} }
` `
const tokenPriceQuery = graphql`
query TokenPriceQuery(
$contract: ContractInput!
$skip1H: Boolean!
$skip1D: Boolean!
$skip1W: Boolean!
$skip1M: Boolean!
$skip1Y: Boolean!
) {
tokens(contracts: [$contract]) {
market(currency: USD) {
priceHistory1H: priceHistory(duration: HOUR) @skip(if: $skip1H) {
timestamp
value
}
priceHistory1D: priceHistory(duration: DAY) @skip(if: $skip1D) {
timestamp
value
}
priceHistory1W: priceHistory(duration: WEEK) @skip(if: $skip1W) {
timestamp
value
}
priceHistory1M: priceHistory(duration: MONTH) @skip(if: $skip1M) {
timestamp
value
}
priceHistory1Y: priceHistory(duration: YEAR) @skip(if: $skip1Y) {
timestamp
value
}
}
}
}
`
export type PricePoint = { value: number; timestamp: number } export type PricePoint = { value: number; timestamp: number }
export function filterPrices(prices: NonNullable<NonNullable<SingleTokenData>['market']>['priceHistory'] | undefined) { export function filterPrices(prices: NonNullable<NonNullable<SingleTokenData>['market']>['priceHistory'] | undefined) {
return prices?.filter((p): p is PricePoint => Boolean(p && p.value)) return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
@ -130,44 +169,22 @@ export function useTokenQuery(
current[originalTimePeriod] = filterPrices(token?.market?.priceHistory) current[originalTimePeriod] = filterPrices(token?.market?.priceHistory)
return current return current
}), }),
[token, originalTimePeriod] [originalTimePeriod, token?.market?.priceHistory]
) )
return [token, prices] return [token, prices]
} }
const tokenPriceQuery = graphql` // TODO: Return a QueryToken from useTokenQuery instead of SingleTokenData to make it more usable in Currency-centric interfaces.
query TokenPriceQuery( export class QueryToken extends WrappedTokenInfo {
$contract: ContractInput! constructor(data: NonNullable<SingleTokenData>) {
$skip1H: Boolean! super({
$skip1D: Boolean! chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain],
$skip1W: Boolean! address: data.address,
$skip1M: Boolean! decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
$skip1Y: Boolean! symbol: data.symbol ?? '',
) { name: data.name ?? '',
tokens(contracts: [$contract]) { logoURI: data.project?.logoUrl ?? undefined,
market(currency: USD) { })
priceHistory1H: priceHistory(duration: HOUR) @skip(if: $skip1H) {
timestamp
value
} }
priceHistory1D: priceHistory(duration: DAY) @skip(if: $skip1D) { }
timestamp
value
}
priceHistory1W: priceHistory(duration: WEEK) @skip(if: $skip1W) {
timestamp
value
}
priceHistory1M: priceHistory(duration: MONTH) @skip(if: $skip1M) {
timestamp
value
}
priceHistory1Y: priceHistory(duration: YEAR) @skip(if: $skip1Y) {
timestamp
value
}
}
}
}
`

@ -2,91 +2,17 @@ import { arrayify } from '@ethersproject/bytes'
import { parseBytes32String } from '@ethersproject/strings' import { parseBytes32String } from '@ethersproject/strings'
import { Currency, Token } from '@uniswap/sdk-core' import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import ERC20_ABI from 'abis/erc20.json' import { isSupportedChain } from 'constants/chains'
import { Erc20 } from 'abis/types'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract' import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall' import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useEffect, useMemo, useState } from 'react' import { useMemo } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { DEFAULT_ERC20_DECIMALS } from '../../constants/tokens' import { DEFAULT_ERC20_DECIMALS } from '../../constants/tokens'
import { TOKEN_SHORTHANDS } from '../../constants/tokens' import { TOKEN_SHORTHANDS } from '../../constants/tokens'
import { getContract, isAddress } from '../../utils' import { isAddress } from '../../utils'
import { supportedChainId } from '../../utils/supportedChainId' import { supportedChainId } from '../../utils/supportedChainId'
/**
* Returns a Token from query data.
* Data should already include all fields except decimals, or it will be considered invalid.
* Returns null if the token is loading or null was passed.
* Returns undefined if invalid or the token does not exist.
*/
export function useTokenFromQuery({
address: tokenAddress,
chainId,
symbol,
name,
project,
}: {
address?: string
chainId?: SupportedChainId
symbol?: string | null
name?: string | null
project?: { logoUrl?: string | null } | null
} = {}): Token | null | undefined {
const { chainId: activeChainId } = useWeb3React()
const address = isAddress(tokenAddress)
const [decimals, setDecimals] = useState<number | null | undefined>(null)
const tokenContract = useTokenContract(chainId === activeChainId ? (address ? address : undefined) : undefined, false)
const { loading, result: [decimalsResult] = [] } = useSingleCallResult(
tokenContract,
'decimals',
undefined,
NEVER_RELOAD
)
useEffect(() => {
if (loading) {
setDecimals(null)
} else if (decimalsResult) {
setDecimals(decimalsResult)
} else if (!address || !chainId || chainId === activeChainId) {
setDecimals(undefined)
} else {
setDecimals(null)
// Load decimals from a cross-chain RPC provider.
const provider = RPC_PROVIDERS[chainId]
const contract = getContract(address, ERC20_ABI, provider) as Erc20
contract
.decimals()
.then((value) => {
if (!stale) setDecimals(value)
})
.catch(() => undefined)
}
let stale = false
return () => {
stale = true
}
}, [activeChainId, address, chainId, decimalsResult, loading])
return useMemo(() => {
if (!chainId || !address) return undefined
if (decimals === null || decimals === undefined) return decimals
if (!symbol || !name) {
return new Token(chainId, address, decimals, symbol ?? undefined, name ?? undefined)
} else {
const logoURI = project?.logoUrl ?? undefined
return new WrappedTokenInfo({ chainId, address, decimals, symbol, name, logoURI })
}
}, [address, chainId, decimals, name, project?.logoUrl, symbol])
}
// parse a name or symbol from a token response // parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/ const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/

@ -1,4 +1,4 @@
import { Currency } from '@uniswap/sdk-core' import { Currency, Token } from '@uniswap/sdk-core'
import { PageName } from 'analytics/constants' import { PageName } from 'analytics/constants'
import { Trace } from 'analytics/Trace' import { Trace } from 'analytics/Trace'
import { filterTimeAtom } from 'components/Tokens/state' import { filterTimeAtom } from 'components/Tokens/state'
@ -18,16 +18,15 @@ import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage' import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal' import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget from 'components/Widget' import Widget from 'components/Widget'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' import { DEFAULT_ERC20_DECIMALS, NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety' import { checkWarning } from 'constants/tokenSafety'
import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql' import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql'
import { useTokenQuery } from 'graphql/data/Token' import { QueryToken, useTokenQuery } from 'graphql/data/Token'
import { CHAIN_NAME_TO_CHAIN_ID, validateUrlChainParam } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID, validateUrlChainParam } from 'graphql/data/util'
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens' import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { useTokenFromQuery } from 'lib/hooks/useCurrency' import { useCallback, useMemo, useState, useTransition } from 'react'
import { useCallback, useState, useTransition } from 'react'
import { ArrowLeft } from 'react-feather' import { ArrowLeft } from 'react-feather'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
@ -43,8 +42,12 @@ export default function TokenDetails() {
chain, chain,
timePeriod timePeriod
) )
const queryToken = useTokenFromQuery(isNative ? undefined : { ...tokenQueryData, chainId: pageChainId }) const token = useMemo(() => {
const token = isNative ? nativeCurrency : queryToken if (!tokenAddress) return undefined
if (isNative) return nativeCurrency
if (tokenQueryData) return new QueryToken(tokenQueryData)
return new Token(pageChainId, tokenAddress, DEFAULT_ERC20_DECIMALS)
}, [isNative, nativeCurrency, pageChainId, tokenAddress, tokenQueryData])
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
const isBlockedToken = tokenWarning?.canProceed === false const isBlockedToken = tokenWarning?.canProceed === false
@ -129,7 +132,7 @@ export default function TokenDetails() {
<RightPanel> <RightPanel>
<Widget <Widget
defaultToken={token === null ? undefined : token ?? nativeCurrency} // a null token is still loading, and should not be overridden. defaultToken={token ?? nativeCurrency}
onTokensChange={navigateToWidgetSelectedToken} onTokensChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick} onReviewSwapClick={onReviewSwapClick}
/> />