Commit d954026c authored by Kaylee George's avatar Kaylee George Committed by GitHub

feat: add token details and token row table query (#4419)

* add query

* revert styles

* more style

* rename

* restructure

* fix

* more

* network

* uppercase symbol

* rm unused

* fix

* check nan

* add token row query

* small change

* nan?
parent 2c2dad14
...@@ -29,9 +29,9 @@ import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector' ...@@ -29,9 +29,9 @@ import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
// TODO: This should be combined with the logic in TimeSelector. // TODO: This should be combined with the logic in TimeSelector.
type PricePoint = { value: number; timestamp: number } export type PricePoint = { value: number; timestamp: number }
const DATA_EMPTY = { value: 0, timestamp: 0 } export const DATA_EMPTY = { value: 0, timestamp: 0 }
function getPriceBounds(pricePoints: PricePoint[]): [number, number] { function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
const prices = pricePoints.map((x) => x.value) const prices = pricePoints.map((x) => x.value)
...@@ -47,7 +47,7 @@ const StyledDownArrow = styled(ArrowDownRight)` ...@@ -47,7 +47,7 @@ const StyledDownArrow = styled(ArrowDownRight)`
color: ${({ theme }) => theme.accentFailure}; color: ${({ theme }) => theme.accentFailure};
` `
function getDelta(start: number, current: number) { export function getDelta(start: number, current: number) {
const delta = (current / start - 1) * 100 const delta = (current / start - 1) * 100
const isPositive = Math.sign(delta) > 0 const isPositive = Math.sign(delta) > 0
......
...@@ -6,6 +6,7 @@ import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon' ...@@ -6,6 +6,7 @@ import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal' import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety' import { checkWarning } from 'constants/tokenSafety'
import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetailQuery'
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens' import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { useCallback } from 'react' import { useCallback } from 'react'
...@@ -14,8 +15,9 @@ import { ArrowLeft, Heart, TrendingUp } from 'react-feather' ...@@ -14,8 +15,9 @@ import { ArrowLeft, Heart, TrendingUp } from 'react-feather'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ClickableStyle, CopyContractAddress } from 'theme' import { ClickableStyle, CopyContractAddress } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import { favoritesAtom, useToggleFavorite } from '../state' import { favoritesAtom, filterNetworkAtom, useToggleFavorite } from '../state'
import { ClickFavorited } from '../TokenTable/TokenRow' import { ClickFavorited } from '../TokenTable/TokenRow'
import { Wave } from './LoadingTokenDetail' import { Wave } from './LoadingTokenDetail'
import Resource from './Resource' import Resource from './Resource'
...@@ -95,7 +97,6 @@ const StatPrice = styled.span` ...@@ -95,7 +97,6 @@ const StatPrice = styled.span`
export const StatsSection = styled.div` export const StatsSection = styled.div`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
` `
export const StatPair = styled.div` export const StatPair = styled.div`
display: flex; display: flex;
...@@ -189,13 +190,15 @@ export default function LoadedTokenDetail({ address }: { address: string }) { ...@@ -189,13 +190,15 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
const chainInfo = getChainInfo(token?.chainId) const chainInfo = getChainInfo(token?.chainId)
const networkLabel = chainInfo?.label const networkLabel = chainInfo?.label
const networkBadgebackgroundColor = chainInfo?.backgroundColor const networkBadgebackgroundColor = chainInfo?.backgroundColor
const filterNetwork = useAtomValue(filterNetworkAtom)
const tokenDetailData = useTokenDetailQuery(address, chainIdToChainName(filterNetwork))
// catch token error and loading state // catch token error and loading state
if (!token || !token.name || !token.symbol) { if (!token || !token.name || !token.symbol) {
return ( return (
<TopArea> <TopArea>
<BreadcrumbNavLink to="/explore"> <BreadcrumbNavLink to="/tokens">
<ArrowLeft size={14} /> Explore <ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink> </BreadcrumbNavLink>
<ChartHeader> <ChartHeader>
<TokenInfoContainer> <TokenInfoContainer>
...@@ -229,7 +232,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) { ...@@ -229,7 +232,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<Trans>No token information available</Trans> <Trans>No token information available</Trans>
</NoInfoAvailable> </NoInfoAvailable>
<ResourcesContainer> <ResourcesContainer>
<Resource name={'Etherscan'} link={'https://etherscan.io/'} /> <Resource name={'Etherscan'} link={`https://etherscan.io/address/${address}`} />
<Resource name={'Protocol Info'} link={`https://info.uniswap.org/#/tokens/${address}`} /> <Resource name={'Protocol Info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
</ResourcesContainer> </ResourcesContainer>
</AboutSection> </AboutSection>
...@@ -256,25 +259,21 @@ export default function LoadedTokenDetail({ address }: { address: string }) { ...@@ -256,25 +259,21 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
</TopArea> </TopArea>
) )
} }
const tokenName = token.name
const tokenSymbol = token.symbol
// TODO: format price, add sparkline const tokenName = tokenDetailData.name
const aboutToken = const tokenSymbol = tokenDetailData.tokens?.[0].symbol?.toUpperCase()
'Ethereum is a decentralized computing platform that uses ETH (Ether) to pay transaction fees (gas). Developers can use Ethereum to run decentralized applications (dApps) and issue new crypto assets, known as Ethereum tokens.'
const tokenMarketCap = '23.02B'
const tokenVolume = '1.6B'
return ( return (
<TopArea> <TopArea>
<BreadcrumbNavLink to="/explore"> <BreadcrumbNavLink to="/tokens">
<ArrowLeft size={14} /> Explore <ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink> </BreadcrumbNavLink>
<ChartHeader> <ChartHeader>
<TokenInfoContainer> <TokenInfoContainer>
<TokenNameCell> <TokenNameCell>
<CurrencyLogo currency={currency} size={'32px'} /> <CurrencyLogo currency={currency} size={'32px'} />
{tokenName} <TokenSymbol>{tokenSymbol}</TokenSymbol> {tokenName ?? <Trans>Name not found</Trans>}
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
{!warning && <VerifiedIcon size="20px" />} {!warning && <VerifiedIcon size="20px" />}
{networkBadgebackgroundColor && ( {networkBadgebackgroundColor && (
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}> <NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
...@@ -283,7 +282,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) { ...@@ -283,7 +282,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
)} )}
</TokenNameCell> </TokenNameCell>
<TokenActions> <TokenActions>
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} /> {tokenName && tokenSymbol && <ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} />}
<ClickFavorited onClick={toggleFavorite}> <ClickFavorited onClick={toggleFavorite}>
<FavoriteIcon isFavorited={isFavorited} /> <FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited> </ClickFavorited>
...@@ -297,31 +296,43 @@ export default function LoadedTokenDetail({ address }: { address: string }) { ...@@ -297,31 +296,43 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<AboutHeader> <AboutHeader>
<Trans>About</Trans> <Trans>About</Trans>
</AboutHeader> </AboutHeader>
{aboutToken} {tokenDetailData.description}
<ResourcesContainer> <ResourcesContainer>
<Resource name={'Etherscan'} link={'https://etherscan.io/'} /> <Resource name={'Etherscan'} link={`https://etherscan.io/address/${address}`} />
<Resource name={'Protocol Info'} link={`https://info.uniswap.org/#/tokens/${address}`} /> <Resource name={'Protocol Info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
{tokenDetailData.homepageUrl && <Resource name={'Website'} link={tokenDetailData.homepageUrl} />}
{tokenDetailData.twitterName && (
<Resource name={'Twitter'} link={`https://twitter.com/${tokenDetailData.twitterName}`} />
)}
</ResourcesContainer> </ResourcesContainer>
</AboutSection> </AboutSection>
<StatsSection> <StatsSection>
<StatPair> <StatPair>
<Stat> <Stat>
Market cap<StatPrice>${tokenMarketCap}</StatPrice> Market cap
<StatPrice>
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
</StatPrice>
</Stat> </Stat>
<Stat> <Stat>
{/* TODO: connect to chart's selected time */}
24H volume 24H volume
<StatPrice>${tokenVolume}</StatPrice> <StatPrice>
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
</StatPrice>
</Stat> </Stat>
</StatPair> </StatPair>
<StatPair> <StatPair>
<Stat> <Stat>
52W low 52W low
<StatPrice>$1,790.01</StatPrice> <StatPrice>
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
</StatPrice>
</Stat> </Stat>
<Stat> <Stat>
52W high 52W high
<StatPrice>$4,420.71</StatPrice> <StatPrice>
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
</StatPrice>
</Stat> </Stat>
</StatPair> </StatPair>
</StatsSection> </StatsSection>
......
...@@ -5,15 +5,18 @@ import { EventName } from 'components/AmplitudeAnalytics/constants' ...@@ -5,15 +5,18 @@ import { EventName } from 'components/AmplitudeAnalytics/constants'
import SparklineChart from 'components/Charts/SparklineChart' import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { chainIdToChainName } from 'graphql/data/TokenDetailQuery'
import { useTokenPriceQuery } from 'graphql/data/TokenPriceQuery'
import { useTokenRowQuery } from 'graphql/data/TokenRowQuery'
import { useCurrency, useToken } from 'hooks/Tokens' import { useCurrency, useToken } from 'hooks/Tokens'
import { TimePeriod, TokenData } from 'hooks/useExplorePageQuery' import { TimePeriod, TokenData } from 'hooks/useExplorePageQuery'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { ArrowDown, ArrowDownRight, ArrowUp, ArrowUpRight, Heart } from 'react-feather' import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { formatAmount, formatDollarAmount } from 'utils/formatDollarAmt' import { formatDollarAmount } from 'utils/formatDollarAmt'
import { import {
LARGE_MEDIA_BREAKPOINT, LARGE_MEDIA_BREAKPOINT,
...@@ -32,13 +35,10 @@ import { ...@@ -32,13 +35,10 @@ import {
useSetSortCategory, useSetSortCategory,
useToggleFavorite, useToggleFavorite,
} from '../state' } from '../state'
import { DATA_EMPTY, getDelta, PricePoint } from '../TokenDetails/PriceChart'
import { Category, SortDirection } from '../types' import { Category, SortDirection } from '../types'
import { DISPLAYS } from './TimeSelector' import { DISPLAYS } from './TimeSelector'
const ArrowCell = styled.div`
padding-left: 2px;
display: flex;
`
const Cell = styled.div` const Cell = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -438,29 +438,24 @@ export default function LoadedRow({ ...@@ -438,29 +438,24 @@ export default function LoadedRow({
const currency = useCurrency(tokenAddress) const currency = useCurrency(tokenAddress)
const tokenName = token?.name ?? '' const tokenName = token?.name ?? ''
const tokenSymbol = token?.symbol ?? '' const tokenSymbol = token?.symbol ?? ''
const tokenData = data[tokenAddress]
const theme = useTheme() const theme = useTheme()
const [favoriteTokens] = useAtom(favoritesAtom) const [favoriteTokens] = useAtom(favoritesAtom)
const isFavorited = favoriteTokens.includes(tokenAddress) const isFavorited = favoriteTokens.includes(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress) const toggleFavorite = useToggleFavorite(tokenAddress)
const isPositive = Math.sign(tokenData.delta) > 0
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom) const filterNetwork = useAtomValue(filterNetworkAtom)
const filterTime = useAtomValue(filterTimeAtom) // filter time period for top tokens table
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const tokenPercentChangeInfo = ( // TODO: make delta shareable and fix based on future changes
<> const pricePoints: PricePoint[] = useTokenPriceQuery(tokenAddress, timePeriod, 'ETHEREUM').filter(
{tokenData.delta}% (p): p is PricePoint => Boolean(p && p.value)
<ArrowCell>
{isPositive ? (
<ArrowUpRight size={16} color={theme.accentSuccess} />
) : (
<ArrowDownRight size={16} color={theme.accentFailure} />
)}
</ArrowCell>
</>
) )
const hasData = pricePoints.length !== 0
/* TODO: Implement API calls & cache to use here */
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
const [delta, arrow] = getDelta(startingPrice.value, endingPrice.value)
const exploreTokenSelectedEventProperties = { const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork, chain_id: filterNetwork,
...@@ -468,11 +463,13 @@ export default function LoadedRow({ ...@@ -468,11 +463,13 @@ export default function LoadedRow({
token_symbol: token?.symbol, token_symbol: token?.symbol,
token_list_index: tokenListIndex, token_list_index: tokenListIndex,
token_list_length: tokenListLength, token_list_length: tokenListLength,
time_frame: filterTime, time_frame: timePeriod,
search_token_address_input: filterString, search_token_address_input: filterString,
} }
const heartColor = isFavorited ? theme.accentActive : undefined const heartColor = isFavorited ? theme.accentActive : undefined
// TODO: consider using backend network?
const tokenRowData = useTokenRowQuery(tokenAddress, timePeriod, chainIdToChainName(filterNetwork))
// TODO: currency logo sizing mobile (32px) vs. desktop (24px) // TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return ( return (
<StyledLink <StyledLink
...@@ -508,14 +505,30 @@ export default function LoadedRow({ ...@@ -508,14 +505,30 @@ export default function LoadedRow({
price={ price={
<ClickableContent> <ClickableContent>
<PriceInfoCell> <PriceInfoCell>
{formatDollarAmount(tokenData.price)} {tokenRowData.price?.value ? formatDollarAmount(tokenRowData.price?.value) : '-'}
<PercentChangeInfoCell>{tokenPercentChangeInfo}</PercentChangeInfoCell> <PercentChangeInfoCell>
{delta}
{arrow}
</PercentChangeInfoCell>
</PriceInfoCell> </PriceInfoCell>
</ClickableContent> </ClickableContent>
} }
percentChange={<ClickableContent>{tokenPercentChangeInfo}</ClickableContent>} percentChange={
marketCap={<ClickableContent>{formatAmount(tokenData.marketCap).toUpperCase()}</ClickableContent>} <ClickableContent>
volume={<ClickableContent>{formatAmount(tokenData.volume[timePeriod]).toUpperCase()}</ClickableContent>} {delta}
{arrow}
</ClickableContent>
}
marketCap={
<ClickableContent>
{tokenRowData.marketCap?.value ? formatDollarAmount(tokenRowData.marketCap?.value) : '-'}
</ClickableContent>
}
volume={
<ClickableContent>
{tokenRowData.volume?.value ? formatDollarAmount(tokenRowData.volume?.value) : '-'}
</ClickableContent>
}
sparkLine={ sparkLine={
<SparkLine> <SparkLine>
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize> <ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
......
import graphql from 'babel-plugin-relay/macro'
import { SupportedChainId } from 'constants/chains'
import { useLazyLoadQuery } from 'react-relay'
import type { Chain, TokenDetailQuery as TokenDetailQueryType } from './__generated__/TokenDetailQuery.graphql'
export function chainIdToChainName(networkId: SupportedChainId): Chain {
switch (networkId) {
case SupportedChainId.MAINNET:
return 'ETHEREUM'
case SupportedChainId.ARBITRUM_ONE:
return 'ARBITRUM'
case SupportedChainId.OPTIMISM:
return 'OPTIMISM'
case SupportedChainId.POLYGON:
return 'POLYGON'
default:
return 'ETHEREUM'
}
}
export function useTokenDetailQuery(address: string, chain: Chain) {
const tokenDetail = useLazyLoadQuery<TokenDetailQueryType>(
graphql`
query TokenDetailQuery($contract: ContractInput!) {
tokenProjects(contracts: [$contract]) {
description
homepageUrl
twitterName
name
markets(currencies: [USD]) {
price {
value
currency
}
marketCap {
value
currency
}
fullyDilutedMarketCap {
value
currency
}
volume24h: volume(duration: DAY) {
value
currency
}
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
value
currency
}
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
value
currency
}
}
tokens {
chain
address
symbol
decimals
}
}
}
`,
{
contract: {
address,
chain,
},
}
)
const { description, homepageUrl, twitterName, name, markets, tokens } = tokenDetail?.tokenProjects?.[0] ?? {}
const { price, marketCap, fullyDilutedMarketCap, volume24h, priceHigh52W, priceLow52W } = markets?.[0] ?? {}
return {
description,
homepageUrl,
twitterName,
name,
markets,
tokens,
price,
marketCap,
fullyDilutedMarketCap,
volume24h,
priceHigh52W,
priceLow52W,
}
}
import graphql from 'babel-plugin-relay/macro'
import { TimePeriod } from 'hooks/useExplorePageQuery'
import { useLazyLoadQuery } from 'react-relay'
import type { Chain, TokenRowQuery as TokenRowQueryType } from './__generated__/TokenRowQuery.graphql'
export function useTokenRowQuery(address: string, timePeriod: TimePeriod, chain: Chain) {
const tokenRowData = useLazyLoadQuery<TokenRowQueryType>(
graphql`
query TokenRowQuery($contract: ContractInput!) {
tokenProjects(contracts: [$contract]) {
markets(currencies: [USD]) {
price {
value
currency
}
marketCap {
value
currency
}
fullyDilutedMarketCap {
value
currency
}
volume1H: volume(duration: HOUR) {
value
currency
}
volume1D: volume(duration: DAY) {
value
currency
}
volume1W: volume(duration: WEEK) {
value
currency
}
volume1M: volume(duration: MONTH) {
value
currency
}
volume1Y: volume(duration: YEAR) {
value
currency
}
}
}
}
`,
{
contract: {
address,
chain,
},
}
)
const { price, marketCap, volume1H, volume1D, volume1W, volume1M, volume1Y } =
tokenRowData.tokenProjects?.[0]?.markets?.[0] ?? {}
switch (timePeriod) {
case TimePeriod.HOUR:
return { price, marketCap, volume: volume1H } ?? {}
case TimePeriod.DAY:
return { price, marketCap, volume: volume1D } ?? {}
case TimePeriod.WEEK:
return { price, marketCap, volume: volume1W } ?? {}
case TimePeriod.MONTH:
return { price, marketCap, volume: volume1M } ?? {}
case TimePeriod.YEAR:
return { price, marketCap, volume: volume1Y } ?? {}
case TimePeriod.ALL:
//TODO: Add functionality for ALL, without requesting it at same time as rest of data for performance reasons
return { price, marketCap, volume: volume1Y } ?? {}
}
}
...@@ -9,7 +9,8 @@ export const formatDollarAmount = (num: number | undefined, digits = 2, round = ...@@ -9,7 +9,8 @@ export const formatDollarAmount = (num: number | undefined, digits = 2, round =
return '<0.001' return '<0.001'
} }
return numbro(num).formatCurrency({ return numbro(num)
.formatCurrency({
average: round, average: round,
mantissa: num > 1000 ? 2 : digits, mantissa: num > 1000 ? 2 : digits,
abbreviations: { abbreviations: {
...@@ -17,6 +18,7 @@ export const formatDollarAmount = (num: number | undefined, digits = 2, round = ...@@ -17,6 +18,7 @@ export const formatDollarAmount = (num: number | undefined, digits = 2, round =
billion: 'B', billion: 'B',
}, },
}) })
.toUpperCase()
} }
// using a currency library here in case we want to add more in future // using a currency library here in case we want to add more in future
......
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