From 058aa52faf4a1997ecface9c7fe710b8293ff57d Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 26 Oct 2022 18:00:53 -0700 Subject: [PATCH] fix: fetch decimals from token query (#5016) * feat: add decimals, wrapper to token query * fix: construct token in details page --- src/graphql/data/Token.ts | 89 +++++++++++++++++++------------- src/lib/hooks/useCurrency.ts | 80 ++-------------------------- src/pages/TokenDetails/index.tsx | 19 ++++--- 3 files changed, 67 insertions(+), 121 deletions(-) diff --git a/src/graphql/data/Token.ts b/src/graphql/data/Token.ts index 8f7f1289ce..97016adceb 100644 --- a/src/graphql/data/Token.ts +++ b/src/graphql/data/Token.ts @@ -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['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) { + 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, + }) } -` +} diff --git a/src/lib/hooks/useCurrency.ts b/src/lib/hooks/useCurrency.ts index b805146d9b..959362971c 100644 --- a/src/lib/hooks/useCurrency.ts +++ b/src/lib/hooks/useCurrency.ts @@ -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(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}$/ diff --git a/src/pages/TokenDetails/index.tsx b/src/pages/TokenDetails/index.tsx index 719b208dd1..4104df7b10 100644 --- a/src/pages/TokenDetails/index.tsx +++ b/src/pages/TokenDetails/index.tsx @@ -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() {