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:
parent
2c2dad1415
commit
d954026cea
@ -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>
|
||||
|
89
src/graphql/data/TokenDetailQuery.ts
Normal file
89
src/graphql/data/TokenDetailQuery.ts
Normal file
@ -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,
|
||||
}
|
||||
}
|
73
src/graphql/data/TokenRowQuery.ts
Normal file
73
src/graphql/data/TokenRowQuery.ts
Normal file
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user