Commit b92c8007 authored by cartcrom's avatar cartcrom Committed by GitHub

feat: explore chain switching (#4710)

* initial commit
* replaced isUserAddedToken with chain-switching friendly version of hook
* reverted useTopTokens()
* addressed first round of PR comments
parent 029f3acb
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs' import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import React from 'react' import React, { useMemo } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import Logo from '../Logo' import Logo from '../Logo'
...@@ -27,17 +27,21 @@ export default function CurrencyLogo({ ...@@ -27,17 +27,21 @@ export default function CurrencyLogo({
symbol, symbol,
size = '24px', size = '24px',
style, style,
src,
...rest ...rest
}: { }: {
currency?: Currency | null currency?: Currency | null
symbol?: string | null symbol?: string | null
size?: string size?: string
style?: React.CSSProperties style?: React.CSSProperties
src?: string | null
}) { }) {
const logoURIs = useCurrencyLogoURIs(currency)
const srcs = useMemo(() => (src ? [src] : logoURIs), [src, logoURIs])
const props = { const props = {
alt: `${currency?.symbol ?? 'token'} logo`, alt: `${currency?.symbol ?? 'token'} logo`,
size, size,
srcs: useCurrencyLogoURIs(currency), srcs,
symbol: symbol ?? currency?.symbol, symbol: symbol ?? currency?.symbol,
style, style,
...rest, ...rest,
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import Web3Status from 'components/Web3Status' import Web3Status from 'components/Web3Status'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft' import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { useGlobalChainName } from 'graphql/data/util'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex' import { Row } from 'nft/components/Flex'
import { UniIcon } from 'nft/components/icons' import { UniIcon } from 'nft/components/icons'
...@@ -36,6 +37,7 @@ const MenuItem = ({ href, id, isActive, children }: MenuItemProps) => { ...@@ -36,6 +37,7 @@ const MenuItem = ({ href, id, isActive, children }: MenuItemProps) => {
const PageTabs = () => { const PageTabs = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const nftFlag = useNftFlag() const nftFlag = useNftFlag()
const chainName = useGlobalChainName()
const isPoolActive = const isPoolActive =
pathname.startsWith('/pool') || pathname.startsWith('/pool') ||
...@@ -49,7 +51,7 @@ const PageTabs = () => { ...@@ -49,7 +51,7 @@ const PageTabs = () => {
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}> <MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
<Trans>Swap</Trans> <Trans>Swap</Trans>
</MenuItem> </MenuItem>
<MenuItem href="/tokens" isActive={pathname.startsWith('/tokens')}> <MenuItem href={`/tokens/${chainName.toLowerCase()}`} isActive={pathname.startsWith('/tokens')}>
<Trans>Tokens</Trans> <Trans>Tokens</Trans>
</MenuItem> </MenuItem>
{nftFlag === NftVariant.Enabled && ( {nftFlag === NftVariant.Enabled && (
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core' import { NativeCurrency, Token } from '@uniswap/sdk-core'
import { ParentSize } from '@visx/responsive' import { ParentSize } from '@visx/responsive'
import { useWeb3React } from '@web3-react/core'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon' import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety' import { checkWarning } from 'constants/tokenSafety'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { SingleTokenData } from 'graphql/data/Token' import { SingleTokenData, useTokenPricesCached } from 'graphql/data/Token'
import { useCurrency } from 'hooks/Tokens' import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import useCurrencyLogoURIs, { getTokenLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { isAddress } from 'utils'
import { useIsFavorited, useToggleFavorite } from '../state' import { useIsFavorited, useToggleFavorite } from '../state'
import { ClickFavorited, FavoriteIcon } from '../TokenTable/TokenRow' import { ClickFavorited, FavoriteIcon, L2NetworkLogo, LogoContainer } from '../TokenTable/TokenRow'
import PriceChart from './PriceChart' import PriceChart from './PriceChart'
import ShareButton from './ShareButton' import ShareButton from './ShareButton'
...@@ -51,55 +52,52 @@ const TokenActions = styled.div` ...@@ -51,55 +52,52 @@ const TokenActions = styled.div`
gap: 16px; gap: 16px;
color: ${({ theme }) => theme.textSecondary}; color: ${({ theme }) => theme.textSecondary};
` `
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
border-radius: 5px;
padding: 4px 8px;
font-weight: 600;
font-size: 12px;
line-height: 12px;
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
`
export default function ChartSection({ token, tokenData }: { token: Token; tokenData: SingleTokenData | undefined }) { export function useTokenLogoURI(
const { chainId: connectedChainId } = useWeb3React() token: NonNullable<SingleTokenData> | NonNullable<TopToken>,
nativeCurrency?: Token | NativeCurrency
) {
const checksummedAddress = isAddress(token.address)
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
return (
useCurrencyLogoURIs(nativeCurrency)[0] ??
(checksummedAddress && getTokenLogoURI(checksummedAddress, chainId)) ??
token.project?.logoUrl
)
}
export default function ChartSection({
token,
nativeCurrency,
}: {
token: NonNullable<SingleTokenData>
nativeCurrency?: Token | NativeCurrency
}) {
const isFavorited = useIsFavorited(token.address) const isFavorited = useIsFavorited(token.address)
const toggleFavorite = useToggleFavorite(token.address) const toggleFavorite = useToggleFavorite(token.address)
const chainInfo = getChainInfo(token?.chainId) const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
const networkLabel = chainInfo?.label const L2Icon = getChainInfo(chainId).circleLogoUrl
const networkBadgebackgroundColor = chainInfo?.backgroundColor const warning = checkWarning(token.address ?? '')
const warning = checkWarning(token.address)
let currency = useCurrency(token.address)
if (connectedChainId) { const { prices } = useTokenPricesCached(token)
const wrappedNativeCurrency = WRAPPED_NATIVE_CURRENCY[connectedChainId]
const isWrappedNativeToken = wrappedNativeCurrency?.address === token?.address
if (isWrappedNativeToken) {
currency = nativeOnChain(connectedChainId)
}
}
const tokenName = tokenData?.name ?? token?.name const logoSrc = useTokenLogoURI(token, nativeCurrency)
const tokenSymbol = tokenData?.tokens?.[0]?.symbol ?? token?.symbol
return ( return (
<ChartHeader> <ChartHeader>
<TokenInfoContainer> <TokenInfoContainer>
<TokenNameCell> <TokenNameCell>
<CurrencyLogo currency={currency} size={'32px'} symbol={tokenSymbol} /> <LogoContainer>
{tokenName ?? <Trans>Name not found</Trans>} <CurrencyLogo src={logoSrc} size={'32px'} symbol={nativeCurrency?.symbol ?? token.symbol} />
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol> <L2NetworkLogo networkUrl={L2Icon} size={'16px'} />
</LogoContainer>
{nativeCurrency?.name ?? token.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{nativeCurrency?.symbol ?? token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
{!warning && <VerifiedIcon size="20px" />} {!warning && <VerifiedIcon size="20px" />}
{networkBadgebackgroundColor && (
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
{networkLabel}
</NetworkBadge>
)}
</TokenNameCell> </TokenNameCell>
<TokenActions> <TokenActions>
{tokenName && tokenSymbol && ( {token.name && token.symbol && token.address && (
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} tokenAddress={token.address} /> <ShareButton tokenName={token.name} tokenSymbol={token.symbol} tokenAddress={token.address} />
)} )}
{useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled && ( {useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled && (
<ClickFavorited onClick={toggleFavorite}> <ClickFavorited onClick={toggleFavorite}>
...@@ -110,9 +108,7 @@ export default function ChartSection({ token, tokenData }: { token: Token; token ...@@ -110,9 +108,7 @@ export default function ChartSection({ token, tokenData }: { token: Token; token
</TokenInfoContainer> </TokenInfoContainer>
<ChartContainer> <ChartContainer>
<ParentSize> <ParentSize>
{({ width, height }) => ( {({ width, height }) => prices && <PriceChart prices={prices} width={width} height={height} />}
<PriceChart tokenAddress={token.address} width={width} height={height} priceDataFragmentRef={null} />
)}
</ParentSize> </ParentSize>
</ChartContainer> </ChartContainer>
</ChartHeader> </ChartHeader>
......
...@@ -15,8 +15,6 @@ import { ...@@ -15,8 +15,6 @@ import {
timeMonth, timeMonth,
timeTicks, timeTicks,
} from 'd3' } from 'd3'
import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql'
import { useTokenPricesCached } from 'graphql/data/Token'
import { PricePoint } from 'graphql/data/Token' import { PricePoint } from 'graphql/data/Token'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale' import { useActiveLocale } from 'hooks/useActiveLocale'
...@@ -37,8 +35,6 @@ import { ...@@ -37,8 +35,6 @@ import {
import LineChart from '../../Charts/LineChart' import LineChart from '../../Charts/LineChart'
import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector' import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
// TODO: This should be combined with the logic in TimeSelector.
export const DATA_EMPTY = { value: 0, timestamp: 0 } export const DATA_EMPTY = { value: 0, timestamp: 0 }
export function getPriceBounds(pricePoints: PricePoint[]): [number, number] { export function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
...@@ -131,18 +127,14 @@ const timeOptionsHeight = 44 ...@@ -131,18 +127,14 @@ const timeOptionsHeight = 44
interface PriceChartProps { interface PriceChartProps {
width: number width: number
height: number height: number
tokenAddress: string prices: PricePoint[] | undefined
priceDataFragmentRef?: TokenPrices$key | null
} }
export function PriceChart({ width, height, tokenAddress, priceDataFragmentRef }: PriceChartProps) { export function PriceChart({ width, height, prices }: PriceChartProps) {
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom) const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
const locale = useActiveLocale() const locale = useActiveLocale()
const theme = useTheme() const theme = useTheme()
const { priceMap } = useTokenPricesCached(priceDataFragmentRef, tokenAddress, 'ETHEREUM', timePeriod)
const prices = priceMap.get(timePeriod)
// first price point on the x-axis of the current time period's chart // first price point on the x-axis of the current time period's chart
const startingPrice = prices?.[0] ?? DATA_EMPTY const startingPrice = prices?.[0] ?? DATA_EMPTY
// last price point on the x-axis of the current time period's chart // last price point on the x-axis of the current time period's chart
......
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains' import { BACKEND_CHAIN_NAMES, CHAIN_NAME_TO_CHAIN_ID, validateUrlChainParam } from 'graphql/data/util'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useAtom } from 'jotai'
import { useRef } from 'react' import { useRef } from 'react'
import { Check, ChevronDown, ChevronUp } from 'react-feather' import { Check, ChevronDown, ChevronUp } from 'react-feather'
import { useNavigate, useParams } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks' import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer' import { ApplicationModal } from 'state/application/reducer'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants' import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
import { filterNetworkAtom } from '../state'
import FilterOption from './FilterOption' import FilterOption from './FilterOption'
const NETWORKS = [
SupportedChainId.MAINNET,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.POLYGON,
SupportedChainId.OPTIMISM,
]
const InternalMenuItem = styled.div` const InternalMenuItem = styled.div`
flex: 1; flex: 1;
padding: 12px 8px; padding: 12px 8px;
...@@ -99,15 +91,18 @@ const CheckContainer = styled.div` ...@@ -99,15 +91,18 @@ const CheckContainer = styled.div`
flex-direction: flex-end; flex-direction: flex-end;
` `
// TODO: change this to reflect data pipeline
export default function NetworkFilter() { export default function NetworkFilter() {
const theme = useTheme() const theme = useTheme()
const node = useRef<HTMLDivElement | null>(null) const node = useRef<HTMLDivElement | null>(null)
const open = useModalIsOpen(ApplicationModal.NETWORK_FILTER) const open = useModalIsOpen(ApplicationModal.NETWORK_FILTER)
const toggleMenu = useToggleModal(ApplicationModal.NETWORK_FILTER) const toggleMenu = useToggleModal(ApplicationModal.NETWORK_FILTER)
useOnClickOutside(node, open ? toggleMenu : undefined) useOnClickOutside(node, open ? toggleMenu : undefined)
const [activeNetwork, setNetwork] = useAtom(filterNetworkAtom) const navigate = useNavigate()
const { label, circleLogoUrl, logoUrl } = getChainInfo(activeNetwork)
const { chainName } = useParams<{ chainName?: string }>()
const currentChainName = validateUrlChainParam(chainName)
const { label, circleLogoUrl, logoUrl } = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[currentChainName])
return ( return (
<StyledMenu ref={node}> <StyledMenu ref={node}>
...@@ -127,25 +122,28 @@ export default function NetworkFilter() { ...@@ -127,25 +122,28 @@ export default function NetworkFilter() {
</FilterOption> </FilterOption>
{open && ( {open && (
<MenuTimeFlyout> <MenuTimeFlyout>
{NETWORKS.map((network) => ( {BACKEND_CHAIN_NAMES.map((network) => {
<InternalLinkMenuItem const chainInfo = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[network])
key={network} return (
onClick={() => { <InternalLinkMenuItem
setNetwork(network) key={network}
toggleMenu() onClick={() => {
}} navigate(`/tokens/${network.toLowerCase()}`)
> toggleMenu()
<NetworkLabel> }}
<Logo src={getChainInfo(network).circleLogoUrl ?? getChainInfo(network).logoUrl} /> >
{getChainInfo(network).label} <NetworkLabel>
</NetworkLabel> <Logo src={chainInfo.circleLogoUrl ?? chainInfo.logoUrl} />
{network === activeNetwork && ( {chainInfo.label}
<CheckContainer> </NetworkLabel>
<Check size={16} color={theme.accentAction} /> {network === currentChainName && (
</CheckContainer> <CheckContainer>
)} <Check size={16} color={theme.accentAction} />
</InternalLinkMenuItem> </CheckContainer>
))} )}
</InternalLinkMenuItem>
)
})}
</MenuTimeFlyout> </MenuTimeFlyout>
)} )}
</StyledMenu> </StyledMenu>
......
...@@ -7,13 +7,12 @@ import CurrencyLogo from 'components/CurrencyLogo' ...@@ -7,13 +7,12 @@ import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens' import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod } from 'graphql/data/util'
import { useCurrency } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ForwardedRef, forwardRef } from 'react' import { ForwardedRef, forwardRef } from 'react'
import { CSSProperties, HTMLProps, ReactHTMLElement, ReactNode } from 'react' import { CSSProperties, ReactNode } from 'react'
import { ArrowDown, ArrowUp, Heart } from 'react-feather' import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components/macro' import styled, { css, useTheme } from 'styled-components/macro'
import { ClickableStyle } from 'theme' import { ClickableStyle } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt' import { formatDollarAmount } from 'utils/formatDollarAmt'
...@@ -26,7 +25,6 @@ import { ...@@ -26,7 +25,6 @@ import {
} from '../constants' } from '../constants'
import { LoadingBubble } from '../loading' import { LoadingBubble } from '../loading'
import { import {
filterNetworkAtom,
filterStringAtom, filterStringAtom,
filterTimeAtom, filterTimeAtom,
sortAscendingAtom, sortAscendingAtom,
...@@ -35,6 +33,7 @@ import { ...@@ -35,6 +33,7 @@ import {
useSetSortMethod, useSetSortMethod,
useToggleFavorite, useToggleFavorite,
} from '../state' } from '../state'
import { useTokenLogoURI } from '../TokenDetails/ChartSection'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart' import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { DISPLAYS } from './TimeSelector' import { DISPLAYS } from './TimeSelector'
...@@ -312,18 +311,18 @@ const SparkLineLoadingBubble = styled(LongLoadingBubble)` ...@@ -312,18 +311,18 @@ const SparkLineLoadingBubble = styled(LongLoadingBubble)`
height: 4px; height: 4px;
` `
const L2NetworkLogo = styled.div<{ networkUrl?: string }>` export const L2NetworkLogo = styled.div<{ networkUrl?: string; size?: string }>`
height: 12px; height: ${({ size }) => size ?? '12px'};
width: 12px; width: ${({ size }) => size ?? '12px'};
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
background: url(${({ networkUrl }) => networkUrl}); background: url(${({ networkUrl }) => networkUrl});
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 12px 12px; background-size: ${({ size }) => (size ? `${size} ${size}` : '12px 12px')};
display: ${({ networkUrl }) => !networkUrl && 'none'}; display: ${({ networkUrl }) => !networkUrl && 'none'};
` `
const LogoContainer = styled.div` export const LogoContainer = styled.div`
position: relative; position: relative;
align-items: center; align-items: center;
display: flex; display: flex;
...@@ -465,29 +464,31 @@ export function LoadingRow() { ...@@ -465,29 +464,31 @@ export function LoadingRow() {
) )
} }
interface LoadedRowProps extends HTMLProps<ReactHTMLElement<HTMLElement>> { interface LoadedRowProps {
tokenListIndex: number tokenListIndex: number
tokenListLength: number tokenListLength: number
token: TopToken token: NonNullable<TopToken>
} }
/* Loaded State: row component with token information */ /* Loaded State: row component with token information */
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => { export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
const { tokenListIndex, tokenListLength, token } = props const { tokenListIndex, tokenListLength, token } = props
const tokenAddress = token?.address const tokenAddress = token.address
const currency = useCurrency(tokenAddress) const tokenName = token.name
const tokenName = token?.name const tokenSymbol = token.symbol
const tokenSymbol = token?.symbol
const isFavorited = useIsFavorited(tokenAddress) const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress) const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
const filterNetwork = lowercaseChainName.toUpperCase()
const L2Icon = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[filterNetwork]).circleLogoUrl
const timePeriod = useAtomValue(filterTimeAtom) const timePeriod = useAtomValue(filterTimeAtom)
const delta = token?.market?.pricePercentChange?.value const delta = token.market?.pricePercentChange?.value
const arrow = delta ? getDeltaArrow(delta) : null const arrow = delta ? getDeltaArrow(delta) : null
const formattedDelta = delta ? formatDelta(delta) : null const formattedDelta = delta ? formatDelta(delta) : null
const sortAscending = useAtomValue(sortAscendingAtom) const sortAscending = useAtomValue(sortAscendingAtom)
const { chainName } = useParams<{ chainName?: string }>()
const exploreTokenSelectedEventProperties = { const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork, chain_id: filterNetwork,
...@@ -503,7 +504,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -503,7 +504,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
return ( return (
<div ref={ref}> <div ref={ref}>
<StyledLink <StyledLink
to={`/tokens/${tokenAddress}`} to={`/tokens/${chainName}/${tokenAddress}`}
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)} onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
> >
<TokenRow <TokenRow
...@@ -522,7 +523,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -522,7 +523,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
tokenInfo={ tokenInfo={
<ClickableName> <ClickableName>
<LogoContainer> <LogoContainer>
<CurrencyLogo currency={currency} symbol={tokenSymbol} /> <CurrencyLogo src={useTokenLogoURI(token)} symbol={tokenSymbol} />
<L2NetworkLogo networkUrl={L2Icon} /> <L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer> </LogoContainer>
<TokenInfoCell> <TokenInfoCell>
...@@ -534,7 +535,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -534,7 +535,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
price={ price={
<ClickableContent> <ClickableContent>
<PriceInfoCell> <PriceInfoCell>
{token?.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'} {token.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'}
<PercentChangeInfoCell> <PercentChangeInfoCell>
{formattedDelta} {formattedDelta}
{arrow} {arrow}
...@@ -550,12 +551,12 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -550,12 +551,12 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
} }
marketCap={ marketCap={
<ClickableContent> <ClickableContent>
{token?.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'} {token.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'}
</ClickableContent> </ClickableContent>
} }
volume={ volume={
<ClickableContent> <ClickableContent>
{token?.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'} {token.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'}
</ClickableContent> </ClickableContent>
} }
sparkLine={ sparkLine={
...@@ -566,7 +567,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -566,7 +567,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
width={width} width={width}
height={height} height={height}
tokenData={token} tokenData={token}
pricePercentChange={token?.market?.pricePercentChange?.value} pricePercentChange={token.market?.pricePercentChange?.value}
timePeriod={timePeriod} timePeriod={timePeriod}
/> />
)} )}
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { showFavoritesAtom } from 'components/Tokens/state' import { showFavoritesAtom } from 'components/Tokens/state'
import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens' import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens'
import { validateUrlChainParam } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactNode, useCallback, useRef } from 'react' import { ReactNode, useCallback, useRef } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import { useParams } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants' import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
...@@ -69,7 +71,8 @@ export default function TokenTable() { ...@@ -69,7 +71,8 @@ export default function TokenTable() {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom) const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s // TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const { loading, tokens, tokensWithoutPriceHistoryCount, hasMore, loadMoreTokens } = useTopTokens() const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
const { loading, tokens, tokensWithoutPriceHistoryCount, hasMore, loadMoreTokens } = useTopTokens(chainName)
const showMoreLoadingRows = Boolean(loading && hasMore) const showMoreLoadingRows = Boolean(loading && hasMore)
const observer = useRef<IntersectionObserver>() const observer = useRef<IntersectionObserver>()
...@@ -114,15 +117,18 @@ export default function TokenTable() { ...@@ -114,15 +117,18 @@ export default function TokenTable() {
<GridContainer> <GridContainer>
<HeaderRow /> <HeaderRow />
<TokenDataContainer> <TokenDataContainer>
{tokens.map((token, index) => ( {tokens.map(
<LoadedRow (token, index) =>
key={token?.address} token && (
tokenListIndex={index} <LoadedRow
tokenListLength={tokens?.length ?? 0} key={token?.address}
token={token} tokenListIndex={index}
ref={index + 1 === tokens.length ? lastTokenRef : undefined} tokenListLength={tokens?.length ?? 0}
/> token={token}
))} ref={index + 1 === tokens.length ? lastTokenRef : undefined}
/>
)
)}
{showMoreLoadingRows && LoadingMoreRows} {showMoreLoadingRows && LoadingMoreRows}
</TokenDataContainer> </TokenDataContainer>
</GridContainer> </GridContainer>
......
import { SupportedChainId } from 'constants/chains'
import { TokenSortMethod } from 'graphql/data/TopTokens' import { TokenSortMethod } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
...@@ -8,7 +7,6 @@ import { useCallback, useMemo } from 'react' ...@@ -8,7 +7,6 @@ import { useCallback, useMemo } from 'react'
export const favoritesAtom = atomWithStorage<string[]>('favorites', []) export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false) export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
export const filterStringAtom = atomWithReset<string>('') export const filterStringAtom = atomWithReset<string>('')
export const filterNetworkAtom = atom<SupportedChainId>(SupportedChainId.MAINNET)
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY) export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY)
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED) export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED)
export const sortAscendingAtom = atom<boolean>(false) export const sortAscendingAtom = atom<boolean>(false)
......
import graphql from 'babel-plugin-relay/macro' import graphql from 'babel-plugin-relay/macro'
import { filterTimeAtom } from 'components/Tokens/state'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay' import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
...@@ -34,78 +36,39 @@ The difference between Token and TokenProject: ...@@ -34,78 +36,39 @@ The difference between Token and TokenProject:
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko. TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/ */
const tokenQuery = graphql` const tokenQuery = graphql`
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!, $skip: Boolean = false) { query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!) {
tokenProjects(contracts: [$contract]) @skip(if: $skip) { tokens(contracts: [$contract]) {
description id
homepageUrl
twitterName
name name
tokens { chain
chain address
address symbol
symbol market(currency: USD) {
market { totalValueLocked {
totalValueLocked {
value
currency
}
}
}
prices: markets(currencies: [USD]) {
...TokenPrices
}
markets(currencies: [USD]) {
price {
value
currency
}
marketCap {
value
currency
}
fullyDilutedMarketCap {
value
currency
}
volume1D: volume(duration: DAY) {
value
currency
}
volume1W: volume(duration: WEEK) {
value
currency
}
volume1M: volume(duration: MONTH) {
value value
currency currency
} }
volume1Y: volume(duration: YEAR) { priceHistory(duration: $duration) {
timestamp
value value
currency
} }
pricePercentChange24h { price {
currency
value value
}
pricePercentChange1W: pricePercentChange(duration: WEEK) {
currency currency
value
} }
pricePercentChange1M: pricePercentChange(duration: MONTH) { volume24H: volume(duration: DAY) {
currency
value value
}
pricePercentChange1Y: pricePercentChange(duration: YEAR) {
currency currency
value
} }
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) { }
value project {
currency description
} homepageUrl
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) { twitterName
value logoUrl
currency tokens {
chain
address
} }
} }
} }
...@@ -114,9 +77,8 @@ const tokenQuery = graphql` ...@@ -114,9 +77,8 @@ const tokenQuery = graphql`
export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) { export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) {
const data = useLazyLoadQuery<TokenQuery>(tokenQuery, { const data = useLazyLoadQuery<TokenQuery>(tokenQuery, {
contract: { address, chain }, contract: { address: address.toLowerCase(), chain },
duration: toHistoryDuration(timePeriod), duration: toHistoryDuration(timePeriod),
skip: false,
}) })
return data return data
...@@ -132,8 +94,8 @@ const tokenPriceQuery = graphql` ...@@ -132,8 +94,8 @@ const tokenPriceQuery = graphql`
$skip1Y: Boolean! $skip1Y: Boolean!
$skipMax: Boolean! $skipMax: Boolean!
) { ) {
tokenProjects(contracts: [$contract]) { tokens(contracts: [$contract]) {
markets(currencies: [USD]) { market(currency: USD) {
priceHistory1H: priceHistory(duration: HOUR) @skip(if: $skip1H) { priceHistory1H: priceHistory(duration: HOUR) @skip(if: $skip1H) {
timestamp timestamp
value value
...@@ -172,19 +134,12 @@ export function useTokenPricesFromFragment(key: TokenPrices$key | null | undefin ...@@ -172,19 +134,12 @@ export function useTokenPricesFromFragment(key: TokenPrices$key | null | undefin
return filterPrices(fetchedTokenPrices) return filterPrices(fetchedTokenPrices)
} }
export function useTokenPricesCached( export function useTokenPricesCached(token: SingleTokenData) {
priceDataFragmentRef: TokenPrices$key | null | undefined,
address: string,
chain: Chain,
timePeriod: TimePeriod
) {
// Attempt to use token prices already provided by TokenDetails / TopToken queries // Attempt to use token prices already provided by TokenDetails / TopToken queries
const environment = useRelayEnvironment() const environment = useRelayEnvironment()
const fetchedTokenPrices = useFragment(tokenPricesFragment, priceDataFragmentRef ?? null)?.priceHistory const timePeriod = useAtomValue(filterTimeAtom)
const [priceMap, setPriceMap] = useState<Map<TimePeriod, PricePoint[] | undefined>>( const [priceMap, setPriceMap] = useState<Map<TimePeriod, PricePoint[] | undefined>>(new Map())
new Map([[timePeriod, filterPrices(fetchedTokenPrices)]])
)
const updatePrices = useCallback( const updatePrices = useCallback(
(key: TimePeriod, data?: PricePoint[]) => { (key: TimePeriod, data?: PricePoint[]) => {
...@@ -195,32 +150,36 @@ export function useTokenPricesCached( ...@@ -195,32 +150,36 @@ export function useTokenPricesCached(
// Fetch the other timePeriods after first render // Fetch the other timePeriods after first render
useEffect(() => { useEffect(() => {
const fetchedTokenPrices = token?.market?.priceHistory
updatePrices(timePeriod, filterPrices(fetchedTokenPrices))
// Fetch all time periods except the one already populated // Fetch all time periods except the one already populated
fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, { if (token?.chain && token?.address) {
contract: { address, chain }, fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, {
skip1H: timePeriod === TimePeriod.HOUR && !!fetchedTokenPrices, contract: { address: token.address, chain: token.chain },
skip1D: timePeriod === TimePeriod.DAY && !!fetchedTokenPrices, skip1H: timePeriod === TimePeriod.HOUR && !!fetchedTokenPrices,
skip1W: timePeriod === TimePeriod.WEEK && !!fetchedTokenPrices, skip1D: timePeriod === TimePeriod.DAY && !!fetchedTokenPrices,
skip1M: timePeriod === TimePeriod.MONTH && !!fetchedTokenPrices, skip1W: timePeriod === TimePeriod.WEEK && !!fetchedTokenPrices,
skip1Y: timePeriod === TimePeriod.YEAR && !!fetchedTokenPrices, skip1M: timePeriod === TimePeriod.MONTH && !!fetchedTokenPrices,
skipMax: timePeriod === TimePeriod.ALL && !!fetchedTokenPrices, skip1Y: timePeriod === TimePeriod.YEAR && !!fetchedTokenPrices,
}).subscribe({ skipMax: timePeriod === TimePeriod.ALL && !!fetchedTokenPrices,
next: (data) => { }).subscribe({
const markets = data.tokenProjects?.[0]?.markets?.[0] next: (data) => {
if (markets) { const market = data.tokens?.[0]?.market
markets.priceHistory1H && updatePrices(TimePeriod.HOUR, filterPrices(markets.priceHistory1H)) if (market) {
markets.priceHistory1D && updatePrices(TimePeriod.DAY, filterPrices(markets.priceHistory1D)) market.priceHistory1H && updatePrices(TimePeriod.HOUR, filterPrices(market.priceHistory1H))
markets.priceHistory1W && updatePrices(TimePeriod.WEEK, filterPrices(markets.priceHistory1W)) market.priceHistory1D && updatePrices(TimePeriod.DAY, filterPrices(market.priceHistory1D))
markets.priceHistory1M && updatePrices(TimePeriod.MONTH, filterPrices(markets.priceHistory1M)) market.priceHistory1W && updatePrices(TimePeriod.WEEK, filterPrices(market.priceHistory1W))
markets.priceHistory1Y && updatePrices(TimePeriod.YEAR, filterPrices(markets.priceHistory1Y)) market.priceHistory1M && updatePrices(TimePeriod.MONTH, filterPrices(market.priceHistory1M))
markets.priceHistoryMAX && updatePrices(TimePeriod.ALL, filterPrices(markets.priceHistoryMAX)) market.priceHistory1Y && updatePrices(TimePeriod.YEAR, filterPrices(market.priceHistory1Y))
} market.priceHistoryMAX && updatePrices(TimePeriod.ALL, filterPrices(market.priceHistoryMAX))
}, }
}) },
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [token?.chain, token?.address])
return { priceMap } return { prices: priceMap.get(timePeriod) }
} }
export type SingleTokenData = NonNullable<TokenQuery$data['tokenProjects']>[number] export type SingleTokenData = NonNullable<TokenQuery$data['tokens']>[number]
...@@ -11,13 +11,16 @@ import { useAtomValue } from 'jotai/utils' ...@@ -11,13 +11,16 @@ import { useAtomValue } from 'jotai/utils'
import { useCallback, useLayoutEffect, useMemo, useState } from 'react' import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay' import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { ContractInput, HistoryDuration, TopTokens_TokensQuery } from './__generated__/TopTokens_TokensQuery.graphql' import {
Chain,
ContractInput,
HistoryDuration,
TopTokens_TokensQuery,
} from './__generated__/TopTokens_TokensQuery.graphql'
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql' import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { toHistoryDuration } from './util' import { toHistoryDuration } from './util'
import { useCurrentChainName } from './util'
export function usePrefetchTopTokens(duration: HistoryDuration) { export function usePrefetchTopTokens(duration: HistoryDuration, chain: Chain) {
const chain = useCurrentChainName()
return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain }) return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain })
} }
...@@ -160,12 +163,12 @@ interface UseTopTokensReturnValue { ...@@ -160,12 +163,12 @@ interface UseTopTokensReturnValue {
hasMore: boolean hasMore: boolean
loadMoreTokens: () => void loadMoreTokens: () => void
} }
export function useTopTokens(): UseTopTokensReturnValue { export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom)) const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [tokens, setTokens] = useState<TopToken[]>() const [tokens, setTokens] = useState<TopToken[]>()
const [page, setPage] = useState(0) const [page, setPage] = useState(0)
const prefetchedData = usePrefetchTopTokens(duration) const prefetchedData = usePrefetchTopTokens(duration, chain)
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData.topTokens)) const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length
...@@ -269,6 +272,9 @@ export const tokensQuery = graphql` ...@@ -269,6 +272,9 @@ export const tokensQuery = graphql`
value value
} }
} }
project {
logoUrl
}
} }
} }
` `
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { useAppSelector } from 'state/hooks'
import { Chain, HistoryDuration } from './__generated__/TokenQuery.graphql' import { Chain, HistoryDuration } from './__generated__/TokenQuery.graphql'
...@@ -42,8 +42,37 @@ export const CHAIN_IDS_TO_BACKEND_NAME: { [key: number]: Chain } = { ...@@ -42,8 +42,37 @@ export const CHAIN_IDS_TO_BACKEND_NAME: { [key: number]: Chain } = {
[SupportedChainId.OPTIMISTIC_KOVAN]: 'OPTIMISM', [SupportedChainId.OPTIMISTIC_KOVAN]: 'OPTIMISM',
} }
export function useCurrentChainName() { export function useGlobalChainName() {
const { chainId } = useWeb3React() const chainId = useAppSelector((state) => state.application.chainId)
return chainId && CHAIN_IDS_TO_BACKEND_NAME[chainId] ? CHAIN_IDS_TO_BACKEND_NAME[chainId] : 'ETHEREUM' return chainId && CHAIN_IDS_TO_BACKEND_NAME[chainId] ? CHAIN_IDS_TO_BACKEND_NAME[chainId] : 'ETHEREUM'
} }
export const URL_CHAIN_PARAM_TO_BACKEND: { [key: string]: Chain } = {
ethereum: 'ETHEREUM',
polygon: 'POLYGON',
celo: 'CELO',
arbitrum: 'ARBITRUM',
optimism: 'OPTIMISM',
}
export function validateUrlChainParam(chainName: string | undefined) {
return chainName && URL_CHAIN_PARAM_TO_BACKEND[chainName] ? URL_CHAIN_PARAM_TO_BACKEND[chainName] : 'ETHEREUM'
}
export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
ETHEREUM: SupportedChainId.MAINNET,
POLYGON: SupportedChainId.POLYGON,
CELO: SupportedChainId.CELO,
ARBITRUM: SupportedChainId.ARBITRUM_ONE,
OPTIMISM: SupportedChainId.OPTIMISM,
}
export const BACKEND_CHAIN_NAMES: Chain[] = ['ARBITRUM', 'CELO', 'ETHEREUM', 'OPTIMISM', 'POLYGON']
export function isValidBackendChainName(chainName: string | undefined): chainName is Chain {
if (!chainName) return false
for (let i = 0; i < BACKEND_CHAIN_NAMES.length; i++) {
if (chainName === BACKEND_CHAIN_NAMES[i]) return true
}
return false
}
...@@ -9,7 +9,7 @@ import { isL2ChainId } from 'utils/chains' ...@@ -9,7 +9,7 @@ import { isL2ChainId } from 'utils/chains'
import { useAllLists, useCombinedActiveList, useInactiveListUrls } from '../state/lists/hooks' import { useAllLists, useCombinedActiveList, useInactiveListUrls } from '../state/lists/hooks'
import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo' import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from '../state/user/hooks' import { useUserAddedTokens, useUserAddedTokensOnChain } from '../state/user/hooks'
import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks' import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks'
// reduce token map into standard address <-> Token mapping, optionally include user added tokens // reduce token map into standard address <-> Token mapping, optionally include user added tokens
...@@ -160,6 +160,20 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool ...@@ -160,6 +160,20 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
return !!userAddedTokens.find((token) => currency.equals(token)) return !!userAddedTokens.find((token) => currency.equals(token))
} }
// Check if currency on specific chain is included in custom list from user storage
export function useIsUserAddedTokenOnChain(
address: string | undefined | null,
chain: number | undefined | null
): boolean {
const userAddedTokens = useUserAddedTokensOnChain(chain)
if (!address || !chain) {
return false
}
return !!userAddedTokens.find((token) => token.address === address)
}
// undefined if invalid or does not exist // undefined if invalid or does not exist
// null if loading or null was passed // null if loading or null was passed
// otherwise returns the token // otherwise returns the token
......
import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql'
import { useGlobalChainName } from 'graphql/data/util'
import { useEffect, useRef } from 'react'
export const useOnGlobalChainSwitch = (callback: (chain: Chain) => void) => {
const globalChainName = useGlobalChainName()
const prevGlobalChainRef = useRef(globalChainName)
useEffect(() => {
if (prevGlobalChainRef.current !== globalChainName) {
callback(globalChainName)
}
prevGlobalChainRef.current = globalChainName
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalChainName])
}
...@@ -39,7 +39,7 @@ export function getNativeLogoURI(chainId: SupportedChainId = SupportedChainId.MA ...@@ -39,7 +39,7 @@ export function getNativeLogoURI(chainId: SupportedChainId = SupportedChainId.MA
} }
} }
function getTokenLogoURI(address: string, chainId: SupportedChainId = SupportedChainId.MAINNET): string | void { export function getTokenLogoURI(address: string, chainId: SupportedChainId = SupportedChainId.MAINNET): string | void {
const networkName = chainIdToNetworkName(chainId) const networkName = chainIdToNetworkName(chainId)
const networksWithUrls = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.MAINNET, SupportedChainId.OPTIMISM] const networksWithUrls = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.MAINNET, SupportedChainId.OPTIMISM]
if (networksWithUrls.includes(chainId)) { if (networksWithUrls.includes(chainId)) {
......
...@@ -165,7 +165,7 @@ export default function App() { ...@@ -165,7 +165,7 @@ export default function App() {
{tokensFlag === TokensVariant.Enabled && ( {tokensFlag === TokensVariant.Enabled && (
<> <>
<Route <Route
path="/tokens" path="/tokens/:chainName"
element={ element={
<Suspense fallback={<LoadingTokens />}> <Suspense fallback={<LoadingTokens />}>
<Tokens /> <Tokens />
...@@ -173,7 +173,7 @@ export default function App() { ...@@ -173,7 +173,7 @@ export default function App() {
} }
/> />
<Route <Route
path="/tokens/:tokenAddress" path="/tokens/:chainName/:tokenAddress"
element={ element={
<Suspense fallback={<LoadingTokenDetails />}> <Suspense fallback={<LoadingTokenDetails />}>
<TokenDetails /> <TokenDetails />
......
import { Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { formatToDecimal } from 'analytics/utils' import { formatToDecimal } from 'analytics/utils'
import { import {
...@@ -20,9 +21,13 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal' ...@@ -20,9 +21,13 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget, { WIDGET_WIDTH } from 'components/Widget' import Widget, { WIDGET_WIDTH } from 'components/Widget'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains' import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains'
import { isCelo, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety' import { checkWarning } from 'constants/tokenSafety'
import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql'
import { useTokenQuery } from 'graphql/data/Token' import { useTokenQuery } from 'graphql/data/Token'
import { useIsUserAddedToken, useToken } from 'hooks/Tokens' import { CHAIN_NAME_TO_CHAIN_ID, validateUrlChainParam } from 'graphql/data/util'
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances' import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
import { useStablecoinValue } from 'hooks/useStablecoinPrice' import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
...@@ -90,14 +95,40 @@ function NetworkBalances(tokenAddress: string | undefined) { ...@@ -90,14 +95,40 @@ function NetworkBalances(tokenAddress: string | undefined) {
} }
export default function TokenDetails() { export default function TokenDetails() {
const { tokenAddress } = useParams<{ tokenAddress?: string }>() const { tokenAddress: tokenAddressParam, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
const token = useToken(tokenAddress) const chainId = CHAIN_NAME_TO_CHAIN_ID[validateUrlChainParam(chainName)]
let tokenAddress = tokenAddressParam
let nativeCurrency
if (tokenAddressParam === 'NATIVE') {
nativeCurrency = nativeOnChain(chainId)
tokenAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address?.toLowerCase()
}
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
const isBlockedToken = tokenWarning?.canProceed === false const isBlockedToken = tokenWarning?.canProceed === false
const timePeriod = useAtomValue(filterTimeAtom)
const currentChainName = validateUrlChainParam(chainName)
const token = useTokenQuery(tokenAddress ?? '', currentChainName, timePeriod).tokens?.[0]
const navigate = useNavigate() const navigate = useNavigate()
const switchChains = (newChain: Chain) => {
if (tokenAddressParam === 'NATIVE') {
navigate(`/tokens/${newChain.toLowerCase()}/NATIVE`)
} else {
token?.project?.tokens?.forEach((token) => {
if (token.chain === newChain && token.address) {
navigate(`/tokens/${newChain.toLowerCase()}/${token.address}`)
}
})
}
}
useOnGlobalChainSwitch(switchChains)
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>() const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
const shouldShowSpeedbump = !useIsUserAddedToken(token) && tokenWarning !== null
// TODO: Make useToken() work on non-mainenet chains or replace this logic
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(tokenAddress, chainId) && 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(() => {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
...@@ -114,10 +145,11 @@ export default function TokenDetails() { ...@@ -114,10 +145,11 @@ export default function TokenDetails() {
) )
/* network balance handling */ /* network balance handling */
const { data: networkData } = NetworkBalances(token?.address) const { data: networkData } = NetworkBalances(tokenAddress)
const { chainId: connectedChainId, account } = useWeb3React() const { chainId: connectedChainId, account } = useWeb3React()
const balanceValue = useTokenBalance(account, token ?? undefined) // TODO: consider updating useTokenBalance to work with just address/chain to avoid using Token data structure here
const balanceValue = useTokenBalance(account, new Token(chainId, tokenAddress ?? '', 18))
const balance = balanceValue ? formatToDecimal(balanceValue, Math.min(balanceValue.currency.decimals, 6)) : undefined const balance = balanceValue ? formatToDecimal(balanceValue, Math.min(balanceValue.currency.decimals, 6)) : undefined
const balanceUsdValue = useStablecoinValue(balanceValue)?.toFixed(2) const balanceUsdValue = useStablecoinValue(balanceValue)?.toFixed(2)
const balanceUsd = balanceUsdValue ? parseFloat(balanceUsdValue) : undefined const balanceUsd = balanceUsdValue ? parseFloat(balanceUsdValue) : undefined
...@@ -131,9 +163,6 @@ export default function TokenDetails() { ...@@ -131,9 +163,6 @@ export default function TokenDetails() {
return chainIds return chainIds
}, [connectedChainId]) }, [connectedChainId])
const timePeriod = useAtomValue(filterTimeAtom)
const query = useTokenQuery(tokenAddress ?? '', 'ETHEREUM', timePeriod)
const balancesByNetwork = networkData const balancesByNetwork = networkData
? chainsToList.map((chainId) => { ? chainsToList.map((chainId) => {
const amount = networkData[chainId] const amount = networkData[chainId]
...@@ -147,7 +176,7 @@ export default function TokenDetails() { ...@@ -147,7 +176,7 @@ export default function TokenDetails() {
key={chainId} key={chainId}
logoUrl={chainInfo.logoUrl} logoUrl={chainInfo.logoUrl}
balance={'1'} balance={'1'}
tokenSymbol={token?.symbol} tokenSymbol={token.symbol}
fiatValue={fiatValue.toSignificant(2)} fiatValue={fiatValue.toSignificant(2)}
label={chainInfo.label} label={chainInfo.label}
networkColor={networkColor} networkColor={networkColor}
...@@ -156,9 +185,11 @@ export default function TokenDetails() { ...@@ -156,9 +185,11 @@ export default function TokenDetails() {
}) })
: null : null
const tokenProject = query.tokenProjects?.[0] const defaultWidgetToken =
const tokenProjectMarket = tokenProject?.markets?.[0] nativeCurrency ??
const tokenMarket = tokenProject?.tokens?.[0]?.market (token?.address && token.symbol && token.name
? new Token(CHAIN_NAME_TO_CHAIN_ID[currentChainName], token.address, 18, token.symbol, token.name)
: undefined)
// TODO: Fix this logic to not automatically redirect on refresh, yet still catch invalid addresses // TODO: Fix this logic to not automatically redirect on refresh, yet still catch invalid addresses
//const location = useLocation() //const location = useLocation()
...@@ -171,32 +202,33 @@ export default function TokenDetails() { ...@@ -171,32 +202,33 @@ export default function TokenDetails() {
{token && ( {token && (
<> <>
<LeftPanel> <LeftPanel>
<BreadcrumbNavLink to="/tokens"> <BreadcrumbNavLink to={`/tokens/${chainName}`}>
<ArrowLeft size={14} /> Tokens <ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink> </BreadcrumbNavLink>
<ChartSection token={token} tokenData={tokenProject} /> <ChartSection token={token} nativeCurrency={nativeCurrency} />
<StatsSection <StatsSection
TVL={tokenMarket?.totalValueLocked?.value} TVL={token.market?.totalValueLocked?.value}
volume24H={tokenProjectMarket?.volume1D?.value} volume24H={token.market?.volume24H?.value}
priceHigh52W={tokenProjectMarket?.priceHigh52W?.value} // TODO: Reenable these values once they're available in schema
priceLow52W={tokenProjectMarket?.priceLow52W?.value} // priceHigh52W={token.market?.priceHigh52W?.value}
// priceLow52W={token.market?.priceLow52W?.value}
/> />
<AboutSection <AboutSection
address={token.address} address={token.address ?? ''}
description={tokenProject?.description} description={token.project?.description}
homepageUrl={tokenProject?.homepageUrl} homepageUrl={token.project?.homepageUrl}
twitterName={tokenProject?.twitterName} twitterName={token.project?.twitterName}
/> />
<AddressSection address={token.address} /> <AddressSection address={token.address ?? ''} />
</LeftPanel> </LeftPanel>
<RightPanel> <RightPanel>
<Widget defaultToken={token ?? undefined} onReviewSwapClick={onReviewSwap} /> <Widget defaultToken={!isCelo(chainId) ? defaultWidgetToken : undefined} onReviewSwapClick={onReviewSwap} />
{tokenWarning && <TokenSafetyMessage tokenAddress={token.address} warning={tokenWarning} />} {tokenWarning && <TokenSafetyMessage tokenAddress={token.address ?? ''} warning={tokenWarning} />}
<BalanceSummary address={token.address} balance={balance} balanceUsd={balanceUsd} /> <BalanceSummary address={token.address ?? ''} balance={balance} balanceUsd={balanceUsd} />
</RightPanel> </RightPanel>
<Footer> <Footer>
<FooterBalanceSummary <FooterBalanceSummary
address={token.address} address={token.address ?? ''}
networkBalances={balancesByNetwork} networkBalances={balancesByNetwork}
balance={balance} balance={balance}
balanceUsd={balanceUsd} balanceUsd={balanceUsd}
......
...@@ -9,9 +9,11 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar' ...@@ -9,9 +9,11 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar'
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector' import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable' import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { isValidBackendChainName } from 'graphql/data/util'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { useResetAtom } from 'jotai/utils' import { useResetAtom } from 'jotai/utils'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
...@@ -74,6 +76,11 @@ const Tokens = () => { ...@@ -74,6 +76,11 @@ const Tokens = () => {
resetFilterString() resetFilterString()
}, [location, resetFilterString]) }, [location, resetFilterString])
const navigate = useNavigate()
useOnGlobalChainSwitch((chain) => {
if (isValidBackendChainName(chain)) navigate(`/tokens/${chain.toLowerCase()}`)
})
return ( return (
<Trace page={PageName.TOKENS_PAGE} shouldLogImpression> <Trace page={PageName.TOKENS_PAGE} shouldLogImpression>
<ExploreContainer> <ExploreContainer>
......
...@@ -245,8 +245,7 @@ export function useRemoveUserAddedToken(): (chainId: number, address: string) => ...@@ -245,8 +245,7 @@ export function useRemoveUserAddedToken(): (chainId: number, address: string) =>
) )
} }
export function useUserAddedTokens(): Token[] { export function useUserAddedTokensOnChain(chainId: number | undefined | null): Token[] {
const { chainId } = useWeb3React()
const serializedTokensMap = useAppSelector(({ user: { tokens } }) => tokens) const serializedTokensMap = useAppSelector(({ user: { tokens } }) => tokens)
return useMemo(() => { return useMemo(() => {
...@@ -258,6 +257,10 @@ export function useUserAddedTokens(): Token[] { ...@@ -258,6 +257,10 @@ export function useUserAddedTokens(): Token[] {
}, [serializedTokensMap, chainId]) }, [serializedTokensMap, chainId])
} }
export function useUserAddedTokens(): Token[] {
return useUserAddedTokensOnChain(useWeb3React().chainId)
}
function serializePair(pair: Pair): SerializedPair { function serializePair(pair: Pair): SerializedPair {
return { return {
token0: serializeToken(pair.token0), token0: serializeToken(pair.token0),
......
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