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 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}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user