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:
parent
2e3950018a
commit
058aa52faf
@ -1,11 +1,13 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { fetchQuery, useLazyLoadQuery } from 'react-relay'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
|
||||
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
|
||||
import { ContractInput, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
|
||||
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:
|
||||
@ -19,6 +21,7 @@ const tokenQuery = graphql`
|
||||
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
id @required(action: LOG)
|
||||
decimals
|
||||
name
|
||||
chain @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 function filterPrices(prices: NonNullable<NonNullable<SingleTokenData>['market']>['priceHistory'] | undefined) {
|
||||
return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
|
||||
@ -130,44 +169,22 @@ export function useTokenQuery(
|
||||
current[originalTimePeriod] = filterPrices(token?.market?.priceHistory)
|
||||
return current
|
||||
}),
|
||||
[token, originalTimePeriod]
|
||||
[originalTimePeriod, token?.market?.priceHistory]
|
||||
)
|
||||
|
||||
return [token, prices]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Return a QueryToken from useTokenQuery instead of SingleTokenData to make it more usable in Currency-centric interfaces.
|
||||
export class QueryToken extends WrappedTokenInfo {
|
||||
constructor(data: NonNullable<SingleTokenData>) {
|
||||
super({
|
||||
chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain],
|
||||
address: data.address,
|
||||
decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
|
||||
symbol: data.symbol ?? '',
|
||||
name: data.name ?? '',
|
||||
logoURI: data.project?.logoUrl ?? undefined,
|
||||
})
|
||||
}
|
||||
`
|
||||
}
|
||||
|
@ -2,91 +2,17 @@ import { arrayify } from '@ethersproject/bytes'
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import ERC20_ABI from 'abis/erc20.json'
|
||||
import { Erc20 } from 'abis/types'
|
||||
import { isSupportedChain, SupportedChainId } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { DEFAULT_ERC20_DECIMALS } from '../../constants/tokens'
|
||||
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
|
||||
import { getContract, isAddress } from '../../utils'
|
||||
import { isAddress } from '../../utils'
|
||||
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
|
||||
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 { Trace } from 'analytics/Trace'
|
||||
import { filterTimeAtom } from 'components/Tokens/state'
|
||||
@ -18,16 +18,15 @@ import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
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 { 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 { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useTokenFromQuery } from 'lib/hooks/useCurrency'
|
||||
import { useCallback, useState, useTransition } from 'react'
|
||||
import { useCallback, useMemo, useState, useTransition } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
@ -43,8 +42,12 @@ export default function TokenDetails() {
|
||||
chain,
|
||||
timePeriod
|
||||
)
|
||||
const queryToken = useTokenFromQuery(isNative ? undefined : { ...tokenQueryData, chainId: pageChainId })
|
||||
const token = isNative ? nativeCurrency : queryToken
|
||||
const token = useMemo(() => {
|
||||
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 isBlockedToken = tokenWarning?.canProceed === false
|
||||
@ -129,7 +132,7 @@ export default function TokenDetails() {
|
||||
|
||||
<RightPanel>
|
||||
<Widget
|
||||
defaultToken={token === null ? undefined : token ?? nativeCurrency} // a null token is still loading, and should not be overridden.
|
||||
defaultToken={token ?? nativeCurrency}
|
||||
onTokensChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user