import { FunctionComponent, useEffect, useMemo, useState } from 'react' import { ExternalLinkIcon, EyeOffIcon } from '@heroicons/react/outline' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { AreaChart, Area, XAxis, YAxis, Tooltip } from 'recharts' import useDimensions from 'react-cool-dimensions' import { IconButton } from './Button' import { LineChartIcon } from './icons' import { useTranslation } from 'next-i18next' import { ExpandableRow } from './TableElements' dayjs.extend(relativeTime) interface SwapTokenInfoProps { inputTokenId?: string outputTokenId?: string } export const numberFormatter = Intl.NumberFormat('en', { minimumFractionDigits: 1, maximumFractionDigits: 5, }) export const numberCompacter = Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2, }) const SwapTokenInfo: FunctionComponent = ({ inputTokenId, outputTokenId, }) => { const [chartData, setChartData] = useState([]) const [hideChart, setHideChart] = useState(false) const [baseTokenId, setBaseTokenId] = useState('') const [quoteTokenId, setQuoteTokenId] = useState('') const [inputTokenInfo, setInputTokenInfo] = useState(null) const [outputTokenInfo, setOutputTokenInfo] = useState(null) const [mouseData, setMouseData] = useState(null) const [daysToShow, setDaysToShow] = useState(1) const [topHolders, setTopHolders] = useState(null) const { observe, width, height } = useDimensions() const { t } = useTranslation(['common', 'swap']) const getTopHolders = async (inputMint, outputMint) => { const inputResponse = await fetch( `https://public-api.solscan.io/token/holders?tokenAddress=${inputMint}&offset=0&limit=10` ) const outputResponse = await fetch( `https://public-api.solscan.io/token/holders?tokenAddress=${outputMint}&offset=0&limit=10` ) const inputData = await inputResponse.json() const outputData = await outputResponse.json() setTopHolders({ inputHolders: inputData.data, outputHolders: outputData.data, }) } useEffect(() => { if (inputTokenInfo && outputTokenInfo) { getTopHolders( inputTokenInfo.contract_address, outputTokenInfo.contract_address ) } }, [inputTokenInfo, outputTokenInfo]) const handleMouseMove = (coords) => { if (coords.activePayload) { setMouseData(coords.activePayload[0].payload) } } const handleMouseLeave = () => { setMouseData(null) } useEffect(() => { if (!inputTokenId || !outputTokenId) { return } if (['usd-coin', 'tether'].includes(inputTokenId)) { setBaseTokenId(outputTokenId) setQuoteTokenId(inputTokenId) } else { setBaseTokenId(inputTokenId) setQuoteTokenId(outputTokenId) } }, [inputTokenId, outputTokenId]) // Use ohlc data const getChartData = async () => { const inputResponse = await fetch( `https://api.coingecko.com/api/v3/coins/${baseTokenId}/ohlc?vs_currency=usd&days=${daysToShow}` ) const outputResponse = await fetch( `https://api.coingecko.com/api/v3/coins/${quoteTokenId}/ohlc?vs_currency=usd&days=${daysToShow}` ) const inputData = await inputResponse.json() const outputData = await outputResponse.json() let data = [] if (Array.isArray(inputData)) { data = data.concat(inputData) } if (Array.isArray(outputData)) { data = data.concat(outputData) } const formattedData = data.reduce((a, c) => { const found = a.find((price) => price.time === c[0]) if (found) { if (['usd-coin', 'tether'].includes(quoteTokenId)) { found.price = found.inputPrice / c[4] } else { found.price = c[4] / found.inputPrice } } else { a.push({ time: c[0], inputPrice: c[4] }) } return a }, []) formattedData[formattedData.length - 1].time = Date.now() setChartData(formattedData.filter((d) => d.price)) } // Alternative chart data. Needs a timestamp tolerance to get data points for each asset // const getChartData = async () => { // const now = Date.now() / 1000 // const inputResponse = await fetch( // `https://api.coingecko.com/api/v3/coins/${inputTokenId}/market_chart/range?vs_currency=usd&from=${ // now - 1 * 86400 // }&to=${now}` // ) // const outputResponse = await fetch( // `https://api.coingecko.com/api/v3/coins/${outputTokenId}/market_chart/range?vs_currency=usd&from=${ // now - 1 * 86400 // }&to=${now}` // ) // const inputData = await inputResponse.json() // const outputData = await outputResponse.json() // const data = inputData?.prices.concat(outputData?.prices) // const formattedData = data.reduce((a, c) => { // const found = a.find( // (price) => c[0] >= price.time - 120000 && c[0] <= price.time + 120000 // ) // if (found) { // found.price = found.inputPrice / c[1] // } else { // a.push({ time: c[0], inputPrice: c[1] }) // } // return a // }, []) // setChartData(formattedData.filter((d) => d.price)) // } const getInputTokenInfo = async () => { const response = await fetch( `https://api.coingecko.com/api/v3/coins/${inputTokenId}?localization=false&tickers=false&developer_data=false&sparkline=false ` ) const data = await response.json() setInputTokenInfo(data) } const getOutputTokenInfo = async () => { const response = await fetch( `https://api.coingecko.com/api/v3/coins/${outputTokenId}?localization=false&tickers=false&developer_data=false&sparkline=false ` ) const data = await response.json() setOutputTokenInfo(data) } useMemo(() => { if (baseTokenId && quoteTokenId) { getChartData() } }, [daysToShow, baseTokenId, quoteTokenId]) useMemo(() => { if (baseTokenId) { getInputTokenInfo() } if (quoteTokenId) { getOutputTokenInfo() } }, [baseTokenId, quoteTokenId]) const chartChange = chartData.length ? ((chartData[chartData.length - 1]['price'] - chartData[0]['price']) / chartData[0]['price']) * 100 : 0 return (
{chartData.length && baseTokenId && quoteTokenId ? (
{inputTokenInfo && outputTokenInfo ? (
{`${outputTokenInfo?.symbol?.toUpperCase()}/${inputTokenInfo?.symbol?.toUpperCase()}`}
) : null} {mouseData ? ( <>
{numberFormatter.format(mouseData['price'])} = 0 ? 'text-th-green' : 'text-th-red' }`} > {chartChange.toFixed(2)}%
{dayjs(mouseData['time']).format('DD MMM YY, h:mma')}
) : ( <>
{numberFormatter.format( chartData[chartData.length - 1]['price'] )} = 0 ? 'text-th-green' : 'text-th-red' }`} > {chartChange.toFixed(2)}%
{dayjs(chartData[chartData.length - 1]['time']).format( 'DD MMM YY, h:mma' )}
)}
setHideChart(!hideChart)}> {hideChart ? ( ) : ( )}
{!hideChart ? (
} />
) : null}
) : (
{t('swap:chart-not-available')}
)}
{inputTokenInfo && outputTokenInfo && baseTokenId ? (
{inputTokenInfo.image?.small ? ( {inputTokenInfo.name} ) : null}

{inputTokenInfo?.symbol?.toUpperCase()}

{inputTokenInfo.name}
{inputTokenInfo.market_data?.current_price?.usd ? (
$ {numberFormatter.format( inputTokenInfo.market_data?.current_price.usd )}
) : null} {inputTokenInfo.market_data?.price_change_percentage_24h ? (
= 0 ? 'text-th-green' : 'text-th-red' }`} > {inputTokenInfo.market_data.price_change_percentage_24h.toFixed( 2 )} %
) : null}
} panelTemplate={
{t('market-data')}
{inputTokenInfo.market_cap_rank ? (
{t('swap:market-cap-rank')}
#{inputTokenInfo.market_cap_rank}
) : null} {inputTokenInfo.market_data?.market_cap && inputTokenInfo.market_data?.market_cap?.usd !== 0 ? (
{t('swap:market-cap')}
$ {numberCompacter.format( inputTokenInfo.market_data?.market_cap?.usd )}
) : null} {inputTokenInfo.market_data?.total_volume?.usd ? (
{t('daily-volume')}
$ {numberCompacter.format( inputTokenInfo.market_data?.total_volume?.usd )}
) : null} {inputTokenInfo.market_data?.circulating_supply ? (
{t('swap:token-supply')}
{numberCompacter.format( inputTokenInfo.market_data.circulating_supply )}
{inputTokenInfo.market_data?.max_supply ? (
{t('swap:max-supply')}:{' '} {numberCompacter.format( inputTokenInfo.market_data.max_supply )}
) : null}
) : null} {inputTokenInfo.market_data?.ath?.usd ? (
{t('swap:ath')}
$ {numberFormatter.format( inputTokenInfo.market_data.ath.usd )}
{inputTokenInfo.market_data?.ath_change_percentage ?.usd ? (
= 0 ? 'text-th-green' : 'text-th-red' }`} > {(inputTokenInfo.market_data?.ath_change_percentage?.usd).toFixed( 2 )} %
) : null}
{inputTokenInfo.market_data?.ath_date?.usd ? (
{dayjs( inputTokenInfo.market_data.ath_date.usd ).fromNow()}
) : null}
) : null} {inputTokenInfo.market_data?.atl?.usd ? (
{t('swap:atl')}
$ {numberFormatter.format( inputTokenInfo.market_data.atl.usd )}
{inputTokenInfo.market_data?.atl_change_percentage ?.usd ? (
= 0 ? 'text-th-green' : 'text-th-red' }`} > {(inputTokenInfo.market_data?.atl_change_percentage?.usd).toLocaleString( undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2, } )} %
) : null}
{inputTokenInfo.market_data?.atl_date?.usd ? (
{dayjs( inputTokenInfo.market_data.atl_date.usd ).fromNow()}
) : null}
) : null}
{topHolders?.inputHolders ? (
{t('swap:top-ten')}
{topHolders.inputHolders.map((holder) => (
{holder.owner.slice(0, 5) + '…' + holder.owner.slice(-5)}
{numberFormatter.format( holder.amount / Math.pow(10, holder.decimals) )}
))}
) : null}
} /> ) : (
{t('swap:input-info-unavailable')}
)} {outputTokenInfo && quoteTokenId ? (
{outputTokenInfo.image?.small ? ( {outputTokenInfo.name} ) : null}

{outputTokenInfo?.symbol?.toUpperCase()}

{outputTokenInfo.name}
{outputTokenInfo.market_data?.current_price?.usd ? (
$ {numberFormatter.format( outputTokenInfo.market_data?.current_price.usd )}
) : null} {outputTokenInfo.market_data ?.price_change_percentage_24h ? (
= 0 ? 'text-th-green' : 'text-th-red' }`} > {outputTokenInfo.market_data.price_change_percentage_24h.toFixed( 2 )} %
) : null}
} panelTemplate={
{t('market-data')}
{outputTokenInfo.market_cap_rank ? (
{t('swap:market-cap-rank')}
#{outputTokenInfo.market_cap_rank}
) : null} {outputTokenInfo.market_data?.market_cap && outputTokenInfo.market_data?.market_cap?.usd !== 0 ? (
{t('swap:market-cap')}
$ {numberCompacter.format( outputTokenInfo.market_data?.market_cap?.usd )}
) : null} {outputTokenInfo.market_data?.total_volume?.usd ? (
{t('daily-volume')}
$ {numberCompacter.format( outputTokenInfo.market_data?.total_volume?.usd )}
) : null} {outputTokenInfo.market_data?.circulating_supply ? (
{t('swap:token-supply')}
{numberCompacter.format( outputTokenInfo.market_data.circulating_supply )}
{outputTokenInfo.market_data?.max_supply ? (
{t('swap:max-supply')}:{' '} {numberCompacter.format( outputTokenInfo.market_data.max_supply )}
) : null}
) : null} {outputTokenInfo.market_data?.ath?.usd ? (
{t('swap:ath')}
$ {numberFormatter.format( outputTokenInfo.market_data.ath.usd )}
{outputTokenInfo.market_data?.ath_change_percentage ?.usd ? (
= 0 ? 'text-th-green' : 'text-th-red' }`} > {(outputTokenInfo.market_data?.ath_change_percentage?.usd).toFixed( 2 )} %
) : null}
{outputTokenInfo.market_data?.ath_date?.usd ? (
{dayjs( outputTokenInfo.market_data.ath_date.usd ).fromNow()}
) : null}
) : null} {outputTokenInfo.market_data?.atl?.usd ? (
{t('swap:atl')}
$ {numberFormatter.format( outputTokenInfo.market_data.atl.usd )}
{outputTokenInfo.market_data?.atl_change_percentage ?.usd ? (
= 0 ? 'text-th-green' : 'text-th-red' }`} > {(outputTokenInfo.market_data?.atl_change_percentage?.usd).toLocaleString( undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2, } )} %
) : null}
{outputTokenInfo.market_data?.atl_date?.usd ? (
{dayjs( outputTokenInfo.market_data.atl_date.usd ).fromNow()}
) : null}
) : null}
{topHolders?.inputHolders ? (
{t('swap:top-ten')}
{topHolders.inputHolders.map((holder) => (
{holder.owner.slice(0, 5) + '…' + holder.owner.slice(-5)}
{numberFormatter.format( holder.amount / Math.pow(10, holder.decimals) )}
))}
) : null}
} />
) : (
{t('swap:output-info-unavailable')}
)} ) } export default SwapTokenInfo