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