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?
This commit is contained in:
Kaylee George 2022-08-22 11:52:24 -07:00 committed by GitHub
parent 2c2dad1415
commit d954026cea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 248 additions and 60 deletions

@ -29,9 +29,9 @@ import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/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] {
const prices = pricePoints.map((x) => x.value)
@ -47,7 +47,7 @@ const StyledDownArrow = styled(ArrowDownRight)`
color: ${({ theme }) => theme.accentFailure};
`
function getDelta(start: number, current: number) {
export function getDelta(start: number, current: number) {
const delta = (current / start - 1) * 100
const isPositive = Math.sign(delta) > 0

@ -6,6 +6,7 @@ import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetailQuery'
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils'
import { useCallback } from 'react'
@ -14,8 +15,9 @@ import { ArrowLeft, Heart, TrendingUp } from 'react-feather'
import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
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 { Wave } from './LoadingTokenDetail'
import Resource from './Resource'
@ -95,7 +97,6 @@ const StatPrice = styled.span`
export const StatsSection = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
`
export const StatPair = styled.div`
display: flex;
@ -189,13 +190,15 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
const chainInfo = getChainInfo(token?.chainId)
const networkLabel = chainInfo?.label
const networkBadgebackgroundColor = chainInfo?.backgroundColor
const filterNetwork = useAtomValue(filterNetworkAtom)
const tokenDetailData = useTokenDetailQuery(address, chainIdToChainName(filterNetwork))
// catch token error and loading state
if (!token || !token.name || !token.symbol) {
return (
<TopArea>
<BreadcrumbNavLink to="/explore">
<ArrowLeft size={14} /> Explore
<BreadcrumbNavLink to="/tokens">
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<ChartHeader>
<TokenInfoContainer>
@ -229,7 +232,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<Trans>No token information available</Trans>
</NoInfoAvailable>
<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}`} />
</ResourcesContainer>
</AboutSection>
@ -256,25 +259,21 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
</TopArea>
)
}
const tokenName = token.name
const tokenSymbol = token.symbol
// TODO: format price, add sparkline
const aboutToken =
'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'
const tokenName = tokenDetailData.name
const tokenSymbol = tokenDetailData.tokens?.[0].symbol?.toUpperCase()
return (
<TopArea>
<BreadcrumbNavLink to="/explore">
<ArrowLeft size={14} /> Explore
<BreadcrumbNavLink to="/tokens">
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<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" />}
{networkBadgebackgroundColor && (
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
@ -283,7 +282,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
)}
</TokenNameCell>
<TokenActions>
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} />
{tokenName && tokenSymbol && <ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} />}
<ClickFavorited onClick={toggleFavorite}>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
@ -297,31 +296,43 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<AboutHeader>
<Trans>About</Trans>
</AboutHeader>
{aboutToken}
{tokenDetailData.description}
<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}`} />
{tokenDetailData.homepageUrl && <Resource name={'Website'} link={tokenDetailData.homepageUrl} />}
{tokenDetailData.twitterName && (
<Resource name={'Twitter'} link={`https://twitter.com/${tokenDetailData.twitterName}`} />
)}
</ResourcesContainer>
</AboutSection>
<StatsSection>
<StatPair>
<Stat>
Market cap<StatPrice>${tokenMarketCap}</StatPrice>
Market cap
<StatPrice>
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
</StatPrice>
</Stat>
<Stat>
{/* TODO: connect to chart's selected time */}
24H volume
<StatPrice>${tokenVolume}</StatPrice>
<StatPrice>
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
</StatPrice>
</Stat>
</StatPair>
<StatPair>
<Stat>
52W low
<StatPrice>$1,790.01</StatPrice>
<StatPrice>
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
</StatPrice>
</Stat>
<Stat>
52W high
<StatPrice>$4,420.71</StatPrice>
<StatPrice>
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
</StatPrice>
</Stat>
</StatPair>
</StatsSection>

@ -5,15 +5,18 @@ import { EventName } from 'components/AmplitudeAnalytics/constants'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
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 { TimePeriod, TokenData } from 'hooks/useExplorePageQuery'
import { useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
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 styled, { useTheme } from 'styled-components/macro'
import { formatAmount, formatDollarAmount } from 'utils/formatDollarAmt'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import {
LARGE_MEDIA_BREAKPOINT,
@ -32,13 +35,10 @@ import {
useSetSortCategory,
useToggleFavorite,
} from '../state'
import { DATA_EMPTY, getDelta, PricePoint } from '../TokenDetails/PriceChart'
import { Category, SortDirection } from '../types'
import { DISPLAYS } from './TimeSelector'
const ArrowCell = styled.div`
padding-left: 2px;
display: flex;
`
const Cell = styled.div`
display: flex;
align-items: center;
@ -438,29 +438,24 @@ export default function LoadedRow({
const currency = useCurrency(tokenAddress)
const tokenName = token?.name ?? ''
const tokenSymbol = token?.symbol ?? ''
const tokenData = data[tokenAddress]
const theme = useTheme()
const [favoriteTokens] = useAtom(favoritesAtom)
const isFavorited = favoriteTokens.includes(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress)
const isPositive = Math.sign(tokenData.delta) > 0
const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
const filterTime = useAtomValue(filterTimeAtom) // filter time period for top tokens table
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const tokenPercentChangeInfo = (
<>
{tokenData.delta}%
<ArrowCell>
{isPositive ? (
<ArrowUpRight size={16} color={theme.accentSuccess} />
) : (
<ArrowDownRight size={16} color={theme.accentFailure} />
)}
</ArrowCell>
</>
// TODO: make delta shareable and fix based on future changes
const pricePoints: PricePoint[] = useTokenPriceQuery(tokenAddress, timePeriod, 'ETHEREUM').filter(
(p): p is PricePoint => Boolean(p && p.value)
)
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 = {
chain_id: filterNetwork,
@ -468,11 +463,13 @@ export default function LoadedRow({
token_symbol: token?.symbol,
token_list_index: tokenListIndex,
token_list_length: tokenListLength,
time_frame: filterTime,
time_frame: timePeriod,
search_token_address_input: filterString,
}
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)
return (
<StyledLink
@ -508,14 +505,30 @@ export default function LoadedRow({
price={
<ClickableContent>
<PriceInfoCell>
{formatDollarAmount(tokenData.price)}
<PercentChangeInfoCell>{tokenPercentChangeInfo}</PercentChangeInfoCell>
{tokenRowData.price?.value ? formatDollarAmount(tokenRowData.price?.value) : '-'}
<PercentChangeInfoCell>
{delta}
{arrow}
</PercentChangeInfoCell>
</PriceInfoCell>
</ClickableContent>
}
percentChange={<ClickableContent>{tokenPercentChangeInfo}</ClickableContent>}
marketCap={<ClickableContent>{formatAmount(tokenData.marketCap).toUpperCase()}</ClickableContent>}
volume={<ClickableContent>{formatAmount(tokenData.volume[timePeriod]).toUpperCase()}</ClickableContent>}
percentChange={
<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>
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>

@ -0,0 +1,89 @@
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,
}
}

@ -0,0 +1,73 @@
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,14 +9,16 @@ export const formatDollarAmount = (num: number | undefined, digits = 2, round =
return '<0.001'
}
return numbro(num).formatCurrency({
average: round,
mantissa: num > 1000 ? 2 : digits,
abbreviations: {
million: 'M',
billion: 'B',
},
})
return numbro(num)
.formatCurrency({
average: round,
mantissa: num > 1000 ? 2 : digits,
abbreviations: {
million: 'M',
billion: 'B',
},
})
.toUpperCase()
}
// using a currency library here in case we want to add more in future