Commit b65fffc5 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by Connor McEwen

feat: load token from query data (#4904)

* fix: do not fetch balances cross-chain

* build: updgrade widget

* feat: cleanly load and switch chains from widget

* fix: load token from query data

* build: trigger checks

* fix: do not override native token from query

* fix: catch error on switch chain

* refactor: useTokenFromActiveNetwork

* refactor: defaultToken behavior clarification
parent a3a3e934
......@@ -2,13 +2,20 @@
// eslint-disable-next-line no-restricted-imports
import '@uniswap/widgets/dist/fonts.css'
import { Currency, EMPTY_TOKEN_LIST, OnReviewSwapClick, SwapWidget, SwapWidgetSkeleton } from '@uniswap/widgets'
import {
AddEthereumChainParameter,
Currency,
EMPTY_TOKEN_LIST,
OnReviewSwapClick,
SwapWidget,
SwapWidgetSkeleton,
} from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { networkConnection } from 'connection'
import { RPC_PROVIDERS } from 'constants/providers'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useCallback } from 'react'
import { useIsDarkMode } from 'state/user/hooks'
import { DARK_THEME, LIGHT_THEME } from 'theme/widget'
import { switchChain } from 'utils/switchChain'
import { useSyncWidgetInputs } from './inputs'
import { useSyncWidgetSettings } from './settings'
......@@ -32,19 +39,29 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
const { settings } = useSyncWidgetSettings()
const { transactions } = useSyncWidgetTransactions()
const onSwitchChain = useCallback(
// TODO: Widget should not break if this rejects - upstream the catch to ignore it.
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
[connector]
)
if (!inputs.value.INPUT && !inputs.value.OUTPUT) {
return <WidgetSkeleton />
}
return (
<>
<SwapWidget
disableBranding
hideConnectionUI
jsonRpcUrlMap={RPC_PROVIDERS}
routerUrl={WIDGET_ROUTER_URL}
width={WIDGET_WIDTH}
locale={locale}
theme={theme}
onReviewSwapClick={onReviewSwapClick}
// defaultChainId is excluded - it is always inferred from the passed provider
provider={connector === networkConnection.connector ? null : provider} // use jsonRpcUrlMap for network providers
provider={provider}
onSwitchChain={onSwitchChain}
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
{...inputs}
{...settings}
......
......@@ -21,6 +21,8 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
})
useEffect(() => {
// Avoid overwriting tokens if none are specified, so that a loading token does not cause layout flashing.
if (!defaultToken) return
setTokens({
[Field.OUTPUT]: defaultToken,
})
......
......@@ -2,16 +2,90 @@ import { arrayify } from '@ethersproject/bytes'
import { parseBytes32String } from '@ethersproject/strings'
import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { isSupportedChain } from 'constants/chains'
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 { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
import { isAddress } from '../../utils'
import { getContract, 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}$/
......@@ -29,10 +103,7 @@ function parseStringOrBytes32(str: string | undefined, bytes32: string | undefin
* Returns null if token is loading or null was passed.
* Returns undefined if tokenAddress is invalid or token does not exist.
*/
export function useTokenFromNetwork(
tokenAddress: string | null | undefined,
tokenChainId?: number
): Token | null | undefined {
export function useTokenFromActiveNetwork(tokenAddress: string | undefined): Token | null | undefined {
const { chainId } = useWeb3React()
const formattedAddress = isAddress(tokenAddress)
......@@ -62,14 +133,13 @@ export function useTokenFromNetwork(
return useMemo(() => {
// If the token is on another chain, we cannot fetch it on-chain, and it is invalid.
if (tokenChainId !== undefined && tokenChainId !== chainId) return undefined
if (typeof tokenAddress !== 'string' || !isSupportedChain(chainId) || !formattedAddress) return undefined
if (isLoading || !chainId) return null
if (!parsedDecimals) return undefined
return new Token(chainId, formattedAddress, parsedDecimals, parsedSymbol, parsedName)
}, [tokenChainId, chainId, tokenAddress, formattedAddress, isLoading, parsedDecimals, parsedSymbol, parsedName])
}, [chainId, tokenAddress, formattedAddress, isLoading, parsedDecimals, parsedSymbol, parsedName])
}
type TokenMap = { [address: string]: Token }
......@@ -83,7 +153,7 @@ export function useTokenFromMapOrNetwork(tokens: TokenMap, tokenAddress?: string
const address = isAddress(tokenAddress)
const token: Token | undefined = address ? tokens[address] : undefined
const tokenFromNetwork = useTokenFromNetwork(token ? undefined : address ? address : undefined)
const tokenFromNetwork = useTokenFromActiveNetwork(token ? undefined : address ? address : undefined)
return tokenFromNetwork ?? token
}
......
......@@ -56,9 +56,10 @@ export function useTokenBalancesWithLoadingIndicator(
address?: string,
tokens?: (Token | undefined)[]
): [{ [tokenAddress: string]: CurrencyAmount<Token> | undefined }, boolean] {
const { chainId } = useWeb3React() // we cannot fetch balances cross-chain
const validatedTokens: Token[] = useMemo(
() => tokens?.filter((t?: Token): t is Token => isAddress(t?.address) !== false) ?? [],
[tokens]
() => tokens?.filter((t?: Token): t is Token => isAddress(t?.address) !== false && t?.chainId === chainId) ?? [],
[chainId, tokens]
)
const validatedTokenAddresses = useMemo(() => validatedTokens.map((vt) => vt.address), [validatedTokens])
......
......@@ -10,7 +10,7 @@ import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget, { WIDGET_WIDTH } from 'components/Widget'
import { isCelo, NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { 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'
......@@ -18,9 +18,9 @@ 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 { useTokenFromNetwork } from 'lib/hooks/useCurrency'
import { useTokenFromQuery } from 'lib/hooks/useCurrency'
import useCurrencyBalance, { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { useNavigate, useParams } from 'react-router-dom'
import styled from 'styled-components/macro'
......@@ -66,23 +66,25 @@ export const RightPanel = styled.div`
`
export default function TokenDetails() {
const { tokenAddress: tokenAddressParam, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
const { account } = useWeb3React()
const currentChainName = validateUrlChainParam(chainName)
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[currentChainName]
const nativeCurrency = nativeOnChain(pageChainId)
const timePeriod = useAtomValue(filterTimeAtom)
const isNative = tokenAddressParam === NATIVE_CHAIN_ID
const tokenQueryAddress = isNative ? nativeCurrency.wrapped.address : tokenAddressParam
const [tokenQueryData, prices] = useTokenQuery(tokenQueryAddress ?? '', currentChainName, timePeriod)
const pageToken = useTokenFromNetwork(tokenAddressParam, CHAIN_NAME_TO_CHAIN_ID[currentChainName])
const isNative = tokenAddress === NATIVE_CHAIN_ID
const [tokenQueryData, prices] = useTokenQuery(
isNative ? nativeCurrency.wrapped.address : tokenAddress ?? '',
currentChainName,
timePeriod
)
const queryToken = useTokenFromQuery(isNative ? undefined : { ...tokenQueryData, chainId: pageChainId })
const token = isNative ? nativeCurrency : queryToken
const nativeCurrencyBalance = useCurrencyBalance(account, nativeCurrency)
const tokenBalance = useTokenBalance(account, token?.wrapped)
const tokenBalance = useTokenBalance(account, isNative ? nativeCurrency.wrapped : pageToken ?? undefined)
const tokenWarning = tokenAddressParam ? checkWarning(tokenAddressParam) : null
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
const isBlockedToken = tokenWarning?.canProceed === false
const navigate = useNavigate()
......@@ -105,7 +107,7 @@ export default function TokenDetails() {
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(tokenAddressParam, pageChainId) && tokenWarning !== null
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(tokenAddress, pageChainId) && tokenWarning !== null
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
const onReviewSwap = useCallback(
() => new Promise<boolean>((resolve) => (shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true))),
......@@ -120,17 +122,6 @@ export default function TokenDetails() {
[continueSwap, setContinueSwap]
)
const widgetToken = useMemo(() => {
if (pageToken) {
return pageToken
}
if (nativeCurrency) {
if (isCelo(pageChainId)) return undefined
return nativeCurrency
}
return undefined
}, [nativeCurrency, pageChainId, pageToken])
return (
<TokenDetailsLayout>
{tokenQueryData && (
......@@ -160,7 +151,11 @@ export default function TokenDetails() {
<AddressSection address={tokenQueryData.address ?? ''} />
</LeftPanel>
<RightPanel>
<Widget defaultToken={widgetToken} onReviewSwapClick={onReviewSwap} />
<Widget
// A null token is still loading, and should not be overridden.
defaultToken={token === null ? undefined : token ?? nativeCurrency}
onReviewSwapClick={onReviewSwap}
/>
{tokenWarning && <TokenSafetyMessage tokenAddress={tokenQueryData.address ?? ''} warning={tokenWarning} />}
<BalanceSummary
tokenAmount={tokenBalance}
......
......@@ -4305,10 +4305,10 @@
"@uniswap/v3-core" "1.0.0"
"@uniswap/v3-periphery" "^1.0.1"
"@uniswap/widgets@^2.14.1":
version "2.14.1"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.14.1.tgz#82fb36d2258dd673e9e4c74df191f37cb4ac794c"
integrity sha512-uVf8wWjpkODNW1+GWKYzkDp+uFuctyFz0wj2UlNmhmgmKx4aSXeZKHMhDTgtlj5Xbb2RiKDvKnI0Ko4GCjGJGw==
"@uniswap/widgets@^2.15.1":
version "2.15.1"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.15.1.tgz#34632d74048d32e1842765f9a3d1c35aa955847c"
integrity sha512-1DCanZVpGoifykZSEjJmXesTQE+Gmm66mo7hZ2CVLSZxRCsQLFSPaZ8FWTRU2p3FW8pyECnEpe79CpkkK22WIg==
dependencies:
"@babel/runtime" ">=7.17.0"
"@fontsource/ibm-plex-mono" "^4.5.1"
......
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