import React, { MouseEventHandler, useCallback, useMemo, useState } from 'react' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { AreaChart, Area, XAxis, YAxis, Tooltip as RechartsTooltip, ResponsiveContainer, Text, ReferenceDot, ReferenceDotProps, } from 'recharts' import FlipNumbers from 'react-flip-numbers' import ContentBox from '../shared/ContentBox' import { formatCurrencyValue, formatNumericValue } from '../../utils/numbers' import SheenLoader from '../shared/SheenLoader' import { COLORS } from '../../styles/colors' import Change from '../shared/Change' import ChartRangeButtons from '../shared/ChartRangeButtons' import { useViewport } from 'hooks/useViewport' import { formatTokenSymbol } from 'utils/tokens' import { useQuery } from '@tanstack/react-query' import mangoStore from '@store/mangoStore' import useLocalStorageState from 'hooks/useLocalStorageState' import { ANIMATION_SETTINGS_KEY } from 'utils/constants' import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { useTranslation } from 'next-i18next' import { ArrowsRightLeftIcon, EyeIcon, EyeSlashIcon, NoSymbolIcon, } from '@heroicons/react/20/solid' import FormatNumericValue from '@components/shared/FormatNumericValue' import { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart' import { interpolateNumber } from 'd3-interpolate' import { IconButton } from '@components/shared/Button' import Tooltip from '@components/shared/Tooltip' import { SwapHistoryItem } from 'types' import useThemeWrapper from 'hooks/useThemeWrapper' import FavoriteSwapButton from './FavoriteSwapButton' import { SwapChartDataItem, fetchSwapChartPrices } from 'apis/birdeye/helpers' import Image from 'next/image' dayjs.extend(relativeTime) const set = mangoStore.getState().set const CustomizedLabel = ({ chartData, x, y, value, index, }: { chartData: SwapChartDataItem[] x?: number y?: string | number value?: number index?: number }) => { const { width } = useViewport() const [min, max] = useMemo(() => { if (chartData.length) { const prices = chartData.map((d) => d.price) return [Math.min(...prices), Math.max(...prices)] } return ['', ''] }, [chartData]) const [minIndex, maxIndex] = useMemo(() => { const minIndex = chartData.findIndex((d) => d.price === min) const maxIndex = chartData.findIndex((d) => d.price === max) return [minIndex, maxIndex] }, [min, max, chartData]) if (value && (minIndex === index || maxIndex === index)) { return ( width / 3 ? 'end' : 'start'} className="font-mono" > {formatNumericValue(value)} ) } else return
} interface ExtendedReferenceDotProps extends ReferenceDotProps { swapHistory: SwapHistoryItem[] swapMarketName: string flipPrices: boolean mouseEnter: ( swap: SwapHistoryItem | undefined, birdeyePrice: string | number | undefined, ) => void mouseLeave: MouseEventHandler } const SwapHistoryArrows = (props: ExtendedReferenceDotProps) => { const { cx, cy, x, y, swapHistory, swapMarketName, flipPrices, mouseEnter, mouseLeave, } = props const swapDetails = swapHistory.find( (swap) => dayjs(swap.block_datetime).unix() * 1000 === x, ) const side = swapDetails?.swap_in_symbol.toLowerCase() === swapMarketName.split('/')[0].toLowerCase() ? !flipPrices ? 'sell' : 'buy' : !flipPrices ? 'buy' : 'sell' const buy = { pathCoords: 'M11 0.858312L0.857867 15.0004H21.1421L11 0.858312Z', fill: 'var(--up)', yOffset: 0, } const sell = { pathCoords: 'M11 14.1427L21.1421 0.000533306L0.857865 0.000529886L11 14.1427Z', fill: 'var(--down)', yOffset: -15, } const sideArrowProps = side === 'buy' ? (!flipPrices ? buy : sell) : !flipPrices ? sell : buy const birdeyePrice = y ? Number(y) : 0 return cx && cy ? ( mouseEnter(swapDetails, birdeyePrice)} onMouseLeave={mouseLeave} > ) : (
) } const SwapTokenChart = () => { const { t } = useTranslation('common') const { amountIn, amountOut, inputBank, outputBank, flipPrices, swapMode, swapOrTrigger, } = mangoStore((s) => s.swap) const { isDesktop } = useViewport() const [mouseData, setMouseData] = useState() const [daysToShow, setDaysToShow] = useState('1') const { theme } = useThemeWrapper() const [animationSettings] = useLocalStorageState( ANIMATION_SETTINGS_KEY, INITIAL_ANIMATION_SETTINGS, ) const swapHistory = mangoStore((s) => s.mangoAccount.swapHistory.data) const loadSwapHistory = mangoStore((s) => s.mangoAccount.swapHistory.loading) const [showSwaps, setShowSwaps] = useState(true) const [swapTooltipData, setSwapTooltipData] = useState(null) const [swapTooltipBirdeyePrice, setSwapTooltipBirdeyePrice] = useState< string | number | undefined >(undefined) const [inputBankName, outputBankName] = useMemo(() => { if (!inputBank || !outputBank) return ['', ''] return [inputBank.name, outputBank.name] }, [inputBank, outputBank]) const swapMarketName = useMemo(() => { if (!inputBankName || !outputBankName) return '' const inputSymbol = formatTokenSymbol(inputBankName) const outputSymbol = formatTokenSymbol(outputBankName) return flipPrices ? `${outputSymbol}/${inputSymbol}` : `${inputSymbol}/${outputSymbol}` }, [flipPrices, inputBankName, outputBankName]) const handleSwapMouseEnter = useCallback( ( swap: SwapHistoryItem | undefined, birdeyePrice: string | number | undefined, ) => { if (swap) { setSwapTooltipData(swap) } if (birdeyePrice) { setSwapTooltipBirdeyePrice(birdeyePrice) } }, [setSwapTooltipData, setSwapTooltipBirdeyePrice], ) const handleSwapMouseLeave = useCallback(() => { setSwapTooltipData(null) }, [setSwapTooltipData]) const renderTooltipContent = useCallback( (swap: SwapHistoryItem) => { const { swap_in_amount, swap_in_symbol, swap_out_price_usd, swap_out_amount, swap_out_symbol, } = swap const swapOutValue = swap_out_price_usd * swap_out_amount const baseMarketToken = swapMarketName.split('/')[0].toLowerCase() const swapSide = swap_in_symbol.toLowerCase() === baseMarketToken ? !flipPrices ? 'sell' : 'buy' : !flipPrices ? 'buy' : 'sell' const buy = { price: swap_in_amount / swap_out_amount, priceSymbol: formatTokenSymbol(swap_in_symbol), amount: swap_out_amount, side: 'buy', symbol: formatTokenSymbol(swap_out_symbol), value: swapOutValue, } const sell = { price: swap_out_amount / swap_in_amount, priceSymbol: formatTokenSymbol(swap_out_symbol), amount: swap_in_amount, side: 'sell', symbol: formatTokenSymbol(swap_in_symbol), value: swapOutValue, } const swapProps = swapSide === 'buy' ? !flipPrices ? buy : sell : !flipPrices ? sell : buy const { amount, price, priceSymbol, side, symbol, value } = swapProps let birdeyePercentageDifference = 0 if ( swapTooltipBirdeyePrice && typeof swapTooltipBirdeyePrice === 'number' ) { const difference = ((price - swapTooltipBirdeyePrice) / price) * 100 birdeyePercentageDifference = difference } const betterThanBirdeye = swapSide === 'buy' ? flipPrices ? birdeyePercentageDifference > 0 : birdeyePercentageDifference < 0 : flipPrices ? birdeyePercentageDifference < 0 : birdeyePercentageDifference > 0 return ( <>

{`${t( side, )} ${amount} ${symbol} at ${formatNumericValue( price, )} ${priceSymbol} for ${formatCurrencyValue(value)}`}

{birdeyePercentageDifference ? (

{birdeyePercentageDifference.toFixed(2)}% {' '} {betterThanBirdeye ? 'better than' : 'worse than'} Birdeye

) : null} ) }, [flipPrices, swapMarketName, swapTooltipBirdeyePrice], ) const { data: birdeyePriceData, isLoading: loadingBirdeye, isFetching: fetchingBirdeye, } = useQuery( ['swap-chart-price-data', inputBank?.mint, outputBank?.mint, daysToShow], () => fetchSwapChartPrices( inputBank?.mint.toString(), outputBank?.mint.toString(), daysToShow, ), { cacheTime: 1000 * 60 * 15, staleTime: 1000 * 60 * 1, enabled: !!(inputBank && outputBank), refetchOnWindowFocus: false, }, ) const handleFlippedPriceData = useMemo(() => { if ( !birdeyePriceData || !birdeyePriceData.length || birdeyePriceData.length < 2 ) return [] if (flipPrices) { const flippedPrices = [] for (const item of birdeyePriceData) { const flippedPrice = item.outputTokenPrice / item.inputTokenPrice flippedPrices.push({ ...item, price: flippedPrice }) } return flippedPrices } else { return birdeyePriceData } }, [birdeyePriceData, flipPrices]) const chartSwapTimes = useMemo(() => { if ( loadSwapHistory || !swapHistory || !swapHistory.length || !inputBankName || !outputBankName ) return [] const chartSymbols = [ inputBankName === 'ETH (Portal)' ? 'ETH' : inputBankName, outputBankName === 'ETH (Portal)' ? 'ETH' : outputBankName, ] return swapHistory .filter( (swap) => chartSymbols.includes(swap.swap_in_symbol) && chartSymbols.includes(swap.swap_out_symbol), ) .map((val) => dayjs(val.block_datetime).unix() * 1000) }, [swapHistory, loadSwapHistory, inputBankName, outputBankName]) const swapHistoryPoints = useMemo(() => { if (!handleFlippedPriceData.length || !chartSwapTimes.length) return [] return chartSwapTimes.map((x) => { const makeSwapChartDataItem = { inputTokenPrice: 1, outputTokenPrice: 1 } const index = handleFlippedPriceData.findIndex((d) => d.time > x) // find index of data point with x value greater than highlight x if (index === 0) { return { time: x, price: handleFlippedPriceData[0].price, ...makeSwapChartDataItem, } // return first data point y value if highlight x is less than first data point x } else if (index === -1) { return { time: x, price: handleFlippedPriceData[handleFlippedPriceData.length - 1].price, ...makeSwapChartDataItem, } // return last data point y value if highlight x is greater than last data point x } else { const x0 = handleFlippedPriceData[index - 1].time const x1 = handleFlippedPriceData[index].time const y0 = handleFlippedPriceData[index - 1].price const y1 = handleFlippedPriceData[index].price const interpolateY = interpolateNumber(y0, y1) // create interpolate function for y values const y = interpolateY((x - x0) / (x1 - x0)) // estimate y value at highlight x using interpolate function return { time: x, price: y, ...makeSwapChartDataItem } } }) }, [handleFlippedPriceData, chartSwapTimes]) const chartData = useMemo(() => { if (!handleFlippedPriceData.length) return [] const minTime = handleFlippedPriceData[0].time const maxTime = handleFlippedPriceData[handleFlippedPriceData.length - 1].time let data = handleFlippedPriceData if (swapHistoryPoints.length && showSwaps) { const swapPoints = swapHistoryPoints.filter( (point) => point.time >= minTime && point.time <= maxTime, ) data = handleFlippedPriceData .concat(swapPoints) .sort((a, b) => a.time - b.time) } if (amountIn && amountOut && swapOrTrigger === 'swap') { const latestPrice = flipPrices ? parseFloat(amountIn) / parseFloat(amountOut) : parseFloat(amountOut) / parseFloat(amountIn) const item: SwapChartDataItem[] = [ { price: latestPrice, time: Date.now(), inputTokenPrice: 0, outputTokenPrice: 0, }, ] return data.concat(item) } else if (inputBank && outputBank) { const latestPrice = flipPrices ? outputBank.uiPrice / inputBank.uiPrice : inputBank.uiPrice / outputBank.uiPrice const item: SwapChartDataItem[] = [ { price: latestPrice, time: Date.now(), inputTokenPrice: inputBank.uiPrice, outputTokenPrice: outputBank.uiPrice, }, ] return data.concat(item) } return data }, [ amountIn, amountOut, handleFlippedPriceData, flipPrices, inputBank, outputBank, showSwaps, swapHistoryPoints, swapOrTrigger, ]) const handleMouseMove: CategoricalChartFunc = useCallback( (coords) => { if (coords.activePayload) { setMouseData(coords.activePayload[0].payload) } }, [setMouseData], ) const handleMouseLeave = useCallback(() => { setMouseData(undefined) }, [setMouseData]) const calculateChartChange = useCallback(() => { if (!chartData?.length) return 0 if (mouseData) { const index = chartData.findIndex((d) => d.time === mouseData.time) if (index === -1) return 0 return ( ((chartData[index]['price'] - chartData[0]['price']) / chartData[0]['price']) * 100 ) } else { return ( ((chartData[chartData.length - 1]['price'] - chartData[0]['price']) / chartData[0]['price']) * 100 ) } }, [chartData, mouseData]) const loadingSwapPrice = useMemo(() => { return ( !!(swapMode === 'ExactIn' && Number(amountIn) && !Number(amountOut)) || !!(swapMode === 'ExactOut' && Number(amountOut) && !Number(amountIn)) ) }, [amountIn, amountOut, swapMode]) const chartNumberHeight = isDesktop ? 48 : 40 const chartNumberWidth = isDesktop ? 35 : 27 return ( {loadingBirdeye || fetchingBirdeye ? ( <>
{!isDesktop ? (
) : null} ) : chartData?.length ? (
{swapTooltipData ? (
{renderTooltipContent(swapTooltipData)}
) : null}
{inputBankName && outputBankName ? (
birdeye

{swapMarketName}

set((state) => { state.swap.flipPrices = !flipPrices }) } hideBg >
) : null} {mouseData ? ( <>
{animationSettings['number-scroll'] ? ( ) : ( )}

{dayjs(mouseData.time).format('DD MMM YY, h:mma')}

) : ( <>
{loadingSwapPrice ? (
) : animationSettings['number-scroll'] ? ( ) : ( )}

{dayjs(chartData[chartData.length - 1].time).format( 'DD MMM YY, h:mma', )}

)}
setShowSwaps(!showSwaps)} > {showSwaps ? ( ) : ( )} setDaysToShow(v)} />
} /> = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] } stopOpacity={0.25} /> = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] } stopOpacity={0} /> = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] } strokeWidth={1.5} fill="url(#gradientArea)" label={} /> {showSwaps && swapHistoryPoints.length ? swapHistoryPoints.map((point, index) => ( } /> )) : null}
) : (

{t('chart-unavailable')}

)} ) } export default React.memo(SwapTokenChart)