Merge pull request #114 from blockworks-foundation/use-birdeye-prices
Use birdeye prices
This commit is contained in:
commit
3d2b485cf7
|
@ -9,7 +9,7 @@ export const API_URL = 'https://public-api.birdeye.so/'
|
|||
|
||||
export const socketUrl = `wss://public-api.birdeye.so/socket?x-api-key=${NEXT_PUBLIC_BIRDEYE_API_KEY}`
|
||||
|
||||
// Make requests to CryptoCompare API
|
||||
// Make requests to Birdeye API
|
||||
export async function makeApiRequest(path: string) {
|
||||
const response = await fetch(`${API_URL}${path}`, {
|
||||
headers: {
|
||||
|
|
|
@ -25,7 +25,7 @@ const SimpleAreaChart = ({
|
|||
<AreaChart data={data}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`gradientArea-${name}`}
|
||||
id={`gradientArea-${name.replace(/[^a-zA-Z]/g, '')}`}
|
||||
x1="0"
|
||||
y1={flipGradientCoords ? '0' : '1'}
|
||||
x2="0"
|
||||
|
@ -39,7 +39,7 @@ const SimpleAreaChart = ({
|
|||
type="monotone"
|
||||
dataKey={yKey}
|
||||
stroke={color}
|
||||
fill={`url(#gradientArea-${name})`}
|
||||
fill={`url(#gradientArea-${name.replace(/[^a-zA-Z]/g, '')}`}
|
||||
/>
|
||||
<XAxis dataKey={xKey} hide />
|
||||
<YAxis
|
||||
|
|
|
@ -10,10 +10,10 @@ import ContentBox from '../shared/ContentBox'
|
|||
import Change from '../shared/Change'
|
||||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useCoingecko } from 'hooks/useCoingecko'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices'
|
||||
const SimpleAreaChart = dynamic(
|
||||
() => import('@components/shared/SimpleAreaChart'),
|
||||
{ ssr: false }
|
||||
|
@ -21,12 +21,13 @@ const SimpleAreaChart = dynamic(
|
|||
|
||||
const SpotMarketsTable = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko()
|
||||
const { group } = useMangoGroup()
|
||||
const serumMarkets = mangoStore((s) => s.serumMarkets)
|
||||
const { theme } = useTheme()
|
||||
const { width } = useViewport()
|
||||
const showTableView = width ? width > breakpoints.md : false
|
||||
const { data: birdeyePrices, isLoading: loadingPrices } =
|
||||
useBirdeyeMarketPrices()
|
||||
|
||||
return (
|
||||
<ContentBox hideBorder hidePadding>
|
||||
|
@ -50,21 +51,18 @@ const SpotMarketsTable = () => {
|
|||
)
|
||||
const oraclePrice = bank?.uiPrice
|
||||
|
||||
const coingeckoData = coingeckoPrices.find(
|
||||
(asset) =>
|
||||
asset.symbol.toUpperCase() === bank?.name.toUpperCase()
|
||||
const birdeyeData = birdeyePrices.find(
|
||||
(m) => m.mint === market.serumMarketExternal.toString()
|
||||
)
|
||||
|
||||
const change =
|
||||
coingeckoData && oraclePrice
|
||||
? ((oraclePrice - coingeckoData.prices[0][1]) /
|
||||
coingeckoData.prices[0][1]) *
|
||||
birdeyeData && oraclePrice
|
||||
? ((oraclePrice - birdeyeData.data[0].value) /
|
||||
birdeyeData.data[0].value) *
|
||||
100
|
||||
: 0
|
||||
|
||||
const chartData = coingeckoData
|
||||
? coingeckoData.prices
|
||||
: undefined
|
||||
const chartData = birdeyeData ? birdeyeData.data : undefined
|
||||
|
||||
return (
|
||||
<TrBody key={market.publicKey.toString()}>
|
||||
|
@ -97,8 +95,8 @@ const SpotMarketsTable = () => {
|
|||
}
|
||||
data={chartData}
|
||||
name={bank!.name}
|
||||
xKey="0"
|
||||
yKey="1"
|
||||
xKey="unixTime"
|
||||
yKey="value"
|
||||
/>
|
||||
</div>
|
||||
) : bank?.name === 'USDC' ||
|
||||
|
@ -123,14 +121,17 @@ const SpotMarketsTable = () => {
|
|||
</Table>
|
||||
) : (
|
||||
<div>
|
||||
{serumMarkets.map((market) => {
|
||||
return (
|
||||
<MobileSpotMarketItem
|
||||
key={market.publicKey.toString()}
|
||||
market={market}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{serumMarkets
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((market) => {
|
||||
return (
|
||||
<MobileSpotMarketItem
|
||||
key={market.publicKey.toString()}
|
||||
market={market}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ContentBox>
|
||||
|
@ -141,38 +142,38 @@ export default SpotMarketsTable
|
|||
|
||||
const MobileSpotMarketItem = ({ market }: { market: Serum3Market }) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko()
|
||||
const { data: birdeyePrices, isLoading: loadingPrices } =
|
||||
useBirdeyeMarketPrices()
|
||||
const { group } = useMangoGroup()
|
||||
const { theme } = useTheme()
|
||||
const bank = group?.getFirstBankByTokenIndex(market.baseTokenIndex)
|
||||
|
||||
const coingeckoData = useMemo(() => {
|
||||
const birdeyeData = useMemo(() => {
|
||||
if (!loadingPrices && bank) {
|
||||
return coingeckoPrices.find(
|
||||
(asset) => asset.symbol.toUpperCase() === bank?.name
|
||||
return birdeyePrices.find(
|
||||
(m) => m.mint === market.serumMarketExternal.toString()
|
||||
)
|
||||
}
|
||||
return null
|
||||
}, [loadingPrices, bank])
|
||||
|
||||
const change = useMemo(() => {
|
||||
if (coingeckoData) {
|
||||
if (birdeyeData && bank) {
|
||||
return (
|
||||
((coingeckoData.prices[coingeckoData.prices.length - 1][1] -
|
||||
coingeckoData.prices[0][1]) /
|
||||
coingeckoData.prices[0][1]) *
|
||||
((bank.uiPrice - birdeyeData.data[0].value) /
|
||||
birdeyeData.data[0].value) *
|
||||
100
|
||||
)
|
||||
}
|
||||
return 0
|
||||
}, [coingeckoData])
|
||||
}, [birdeyeData, bank])
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (coingeckoData) {
|
||||
return coingeckoData.prices
|
||||
if (birdeyeData) {
|
||||
return birdeyeData.data
|
||||
}
|
||||
return undefined
|
||||
}, [coingeckoData])
|
||||
}, [birdeyeData])
|
||||
|
||||
return (
|
||||
<div className="border-b border-th-bkg-3 px-6 py-4">
|
||||
|
@ -200,11 +201,11 @@ const MobileSpotMarketItem = ({ market }: { market: Serum3Market }) => {
|
|||
color={change >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]}
|
||||
data={chartData}
|
||||
name={bank!.name}
|
||||
xKey="0"
|
||||
yKey="1"
|
||||
xKey="unixTime"
|
||||
yKey="value"
|
||||
/>
|
||||
</div>
|
||||
) : bank?.name === 'USDC' || bank?.name === 'USDT' ? null : (
|
||||
) : (
|
||||
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
|
||||
)
|
||||
) : (
|
||||
|
|
|
@ -4,9 +4,11 @@ import ChartRangeButtons from '@components/shared/ChartRangeButtons'
|
|||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import { ArrowSmallUpIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { makeApiRequest } from 'apis/birdeye/helpers'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useCoingecko } from 'hooks/useCoingecko'
|
||||
import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices'
|
||||
import parse from 'html-react-parser'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
@ -34,40 +36,55 @@ const DEFAULT_COINGECKO_VALUES = {
|
|||
total_volume: 0,
|
||||
}
|
||||
|
||||
interface BirdeyeResponse {
|
||||
data: { items: BirdeyePriceResponse[] }
|
||||
success: boolean
|
||||
}
|
||||
|
||||
const fetchBirdeyePrices = async (
|
||||
daysToShow: string,
|
||||
mint: string
|
||||
): Promise<BirdeyePriceResponse[] | []> => {
|
||||
const interval = daysToShow === '1' ? '30m' : daysToShow === '7' ? '1H' : '4H'
|
||||
const queryEnd = Math.floor(Date.now() / 1000)
|
||||
const queryStart = queryEnd - parseInt(daysToShow) * 86400
|
||||
const query = `defi/history_price?address=${mint}&address_type=token&type=${interval}&time_from=${queryStart}&time_to=${queryEnd}`
|
||||
const response: BirdeyeResponse = await makeApiRequest(query)
|
||||
|
||||
if (response.success && response?.data?.items) {
|
||||
return response.data.items
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const CoingeckoStats = ({
|
||||
bank,
|
||||
coingeckoData,
|
||||
coingeckoId,
|
||||
}: {
|
||||
bank: Bank
|
||||
// TODO: Add Coingecko api types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
coingeckoData: any
|
||||
coingeckoId: string
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'token'])
|
||||
const [showFullDesc, setShowFullDesc] = useState(false)
|
||||
const [daysToShow, setDaysToShow] = useState<string>('1')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [chartData, setChartData] = useState<{ prices: any[] } | null>(null)
|
||||
const [loadChartData, setLoadChartData] = useState(true)
|
||||
const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko()
|
||||
|
||||
const handleDaysToShow = async (days: string) => {
|
||||
if (days !== '1') {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setLoadChartData(false)
|
||||
setChartData(data)
|
||||
} catch {
|
||||
setLoadChartData(false)
|
||||
}
|
||||
const {
|
||||
data: birdeyePrices,
|
||||
isLoading: loadingBirdeyePrices,
|
||||
isFetching: fetchingBirdeyePrices,
|
||||
} = useQuery(
|
||||
['birdeye-token-prices', daysToShow, bank],
|
||||
() => fetchBirdeyePrices(daysToShow, bank.mint.toString()),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 15,
|
||||
staleTime: 1000 * 60 * 10,
|
||||
retry: 3,
|
||||
enabled: !!bank,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
setDaysToShow(days)
|
||||
}
|
||||
)
|
||||
|
||||
const {
|
||||
ath,
|
||||
|
@ -82,28 +99,7 @@ const CoingeckoStats = ({
|
|||
max_supply,
|
||||
total_supply,
|
||||
total_volume,
|
||||
} = coingeckoData ? coingeckoData.market_data : DEFAULT_COINGECKO_VALUES
|
||||
|
||||
const loadingChart = useMemo(() => {
|
||||
return daysToShow == '1' ? loadingPrices : loadChartData
|
||||
}, [loadChartData, loadingPrices])
|
||||
|
||||
const coingeckoTokenPrices = useMemo(() => {
|
||||
if (daysToShow === '1' && coingeckoPrices.length && bank) {
|
||||
const tokenPriceData = coingeckoPrices.find(
|
||||
(asset) => asset.symbol.toUpperCase() === bank.name.toUpperCase()
|
||||
)
|
||||
|
||||
if (tokenPriceData) {
|
||||
return tokenPriceData.prices
|
||||
}
|
||||
} else {
|
||||
if (chartData && !loadingChart) {
|
||||
return chartData.prices
|
||||
}
|
||||
}
|
||||
return []
|
||||
}, [coingeckoPrices, bank, daysToShow, chartData, loadingChart])
|
||||
} = coingeckoData ? coingeckoData : DEFAULT_COINGECKO_VALUES
|
||||
|
||||
const truncateDescription = (desc: string) =>
|
||||
desc.substring(0, (desc + ' ').lastIndexOf(' ', 144))
|
||||
|
@ -140,35 +136,30 @@ const CoingeckoStats = ({
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!loadingChart ? (
|
||||
coingeckoTokenPrices.length ? (
|
||||
<>
|
||||
<div className="mt-4 flex w-full items-center justify-between px-6">
|
||||
<h2 className="text-base">{bank.name} Price Chart</h2>
|
||||
<ChartRangeButtons
|
||||
activeValue={daysToShow}
|
||||
names={['24H', '7D', '30D']}
|
||||
values={['1', '7', '30']}
|
||||
onChange={(v) => handleDaysToShow(v)}
|
||||
/>
|
||||
</div>
|
||||
<PriceChart
|
||||
daysToShow={parseInt(daysToShow)}
|
||||
prices={coingeckoTokenPrices}
|
||||
/>
|
||||
</>
|
||||
) : bank?.name === 'USDC' || bank?.name === 'USDT' ? null : (
|
||||
<div className="flex flex-col items-center p-6">
|
||||
<NoSymbolIcon className="mb-1 h-6 w-6 text-th-fgd-4" />
|
||||
<p className="mb-0 text-th-fgd-4">{t('token:chart-unavailable')}</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="mt-4 flex w-full items-center justify-between px-6">
|
||||
<h2 className="text-base">{bank.name} Price Chart</h2>
|
||||
<ChartRangeButtons
|
||||
activeValue={daysToShow}
|
||||
names={['24H', '7D', '30D']}
|
||||
values={['1', '7', '30']}
|
||||
onChange={(v) => setDaysToShow(v)}
|
||||
/>
|
||||
</div>
|
||||
{birdeyePrices?.length ? (
|
||||
<PriceChart daysToShow={parseInt(daysToShow)} prices={birdeyePrices} />
|
||||
) : loadingBirdeyePrices || fetchingBirdeyePrices ? (
|
||||
<div className="p-6">
|
||||
<SheenLoader className="flex flex-1">
|
||||
<div className="h-72 w-full rounded-md bg-th-bkg-2" />
|
||||
<div className="h-72 w-full rounded-lg bg-th-bkg-2 md:h-80" />
|
||||
</SheenLoader>
|
||||
</div>
|
||||
) : (
|
||||
<div className="m-6 flex h-72 items-center justify-center rounded-lg border border-th-bkg-3 md:h-80">
|
||||
<div className="flex flex-col items-center">
|
||||
<NoSymbolIcon className="mb-2 h-7 w-7 text-th-fgd-4" />
|
||||
<p>{t('chart-unavailable')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 border-b border-th-bkg-3 md:grid-cols-2">
|
||||
<div className="col-span-1 border-y border-th-bkg-3 px-6 py-4 md:col-span-2">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { formatDateAxis } from '@components/shared/DetailedAreaChart'
|
||||
import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useMemo } from 'react'
|
||||
import { Area, AreaChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'
|
||||
|
@ -9,13 +10,13 @@ const PriceChart = ({
|
|||
prices,
|
||||
daysToShow,
|
||||
}: {
|
||||
prices: number[][]
|
||||
prices: BirdeyePriceResponse[]
|
||||
daysToShow: number
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const change = useMemo(() => {
|
||||
return prices[prices.length - 1][1] - prices[0][1]
|
||||
return prices[prices.length - 1].value - prices[0].value
|
||||
}, [prices])
|
||||
|
||||
return (
|
||||
|
@ -44,14 +45,14 @@ const PriceChart = ({
|
|||
<Area
|
||||
isAnimationActive={false}
|
||||
type="monotone"
|
||||
dataKey="1"
|
||||
dataKey="value"
|
||||
stroke={change >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]}
|
||||
strokeWidth={1.5}
|
||||
fill="url(#gradientArea)"
|
||||
/>
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
dataKey="0"
|
||||
dataKey="unixTime"
|
||||
minTickGap={20}
|
||||
padding={{ left: 20, right: 20 }}
|
||||
tick={{
|
||||
|
@ -63,7 +64,7 @@ const PriceChart = ({
|
|||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
dataKey={'1'}
|
||||
dataKey="value"
|
||||
type="number"
|
||||
domain={['dataMin', 'dataMax']}
|
||||
padding={{ top: 20, bottom: 20 }}
|
||||
|
@ -73,7 +74,7 @@ const PriceChart = ({
|
|||
}}
|
||||
tickFormatter={(x) => formatCurrencyValue(x)}
|
||||
tickLine={false}
|
||||
width={prices[0][1] < 0.00001 ? 100 : 60}
|
||||
width={prices[0].value < 0.00001 ? 100 : 60}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
|
|
|
@ -106,21 +106,22 @@ const TokenPage = () => {
|
|||
}
|
||||
}, [bank, mangoTokens])
|
||||
|
||||
const coingeckoTokenInfo = useQuery<CoingeckoDataType, Error>(
|
||||
['coingecko-token-info', coingeckoId],
|
||||
() => fetchTokenInfo(coingeckoId),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 15,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !!coingeckoId,
|
||||
}
|
||||
)
|
||||
const { data: coingeckoTokenInfo, isLoading: loadingCoingeckoInfo } =
|
||||
useQuery<CoingeckoDataType, Error>(
|
||||
['coingecko-token-info', coingeckoId],
|
||||
() => fetchTokenInfo(coingeckoId),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 15,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !!coingeckoId,
|
||||
}
|
||||
)
|
||||
|
||||
const { high_24h, low_24h, price_change_percentage_24h } =
|
||||
coingeckoTokenInfo.data
|
||||
? coingeckoTokenInfo.data.market_data
|
||||
coingeckoTokenInfo?.market_data
|
||||
? coingeckoTokenInfo.market_data
|
||||
: DEFAULT_COINGECKO_VALUES
|
||||
|
||||
return (
|
||||
|
@ -131,9 +132,9 @@ const TokenPage = () => {
|
|||
<div className="mb-4 md:mb-1">
|
||||
<div className="mb-1.5 flex items-center space-x-2">
|
||||
<Image src={logoURI!} height="20" width="20" />
|
||||
{coingeckoTokenInfo.data ? (
|
||||
{coingeckoTokenInfo ? (
|
||||
<h1 className="text-base font-normal">
|
||||
{coingeckoTokenInfo.data.name}{' '}
|
||||
{coingeckoTokenInfo.name}{' '}
|
||||
<span className="text-th-fgd-4">{bank.name}</span>
|
||||
</h1>
|
||||
) : (
|
||||
|
@ -155,13 +156,13 @@ const TokenPage = () => {
|
|||
<FormatNumericValue value={bank.uiPrice} isUsd />
|
||||
)}
|
||||
</div>
|
||||
{coingeckoTokenInfo.data ? (
|
||||
{coingeckoTokenInfo?.market_data ? (
|
||||
<div className="mb-2">
|
||||
<Change change={price_change_percentage_24h} suffix="%" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{coingeckoTokenInfo.data ? (
|
||||
{coingeckoTokenInfo?.market_data ? (
|
||||
<DailyRange
|
||||
high={high_24h.usd}
|
||||
low={low_24h.usd}
|
||||
|
@ -191,12 +192,17 @@ const TokenPage = () => {
|
|||
</span>
|
||||
</div>
|
||||
{bank ? <TopTokenAccounts bank={bank} /> : null}
|
||||
{coingeckoTokenInfo.data && coingeckoId ? (
|
||||
{coingeckoTokenInfo?.market_data ? (
|
||||
<CoingeckoStats
|
||||
bank={bank}
|
||||
coingeckoData={coingeckoTokenInfo.data}
|
||||
coingeckoId={coingeckoId}
|
||||
coingeckoData={coingeckoTokenInfo.market_data}
|
||||
/>
|
||||
) : loadingCoingeckoInfo && coingeckoId ? (
|
||||
<div className="p-6">
|
||||
<SheenLoader className="flex flex-1">
|
||||
<div className="h-72 w-full rounded-lg bg-th-bkg-2 md:h-80" />
|
||||
</SheenLoader>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center p-6">
|
||||
<span className="mb-0.5 text-2xl">🦎</span>
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { Bank, PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import { Bank, PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import { IconButton, LinkButton } from '@components/shared/Button'
|
||||
import Change from '@components/shared/Change'
|
||||
import { getOneDayPerpStats } from '@components/stats/PerpMarketsTable'
|
||||
import { ChartBarIcon, InformationCircleIcon } from '@heroicons/react/20/solid'
|
||||
import { Market } from '@project-serum/serum'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import useJupiterMints from 'hooks/useJupiterMints'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Token } from 'types/jupiter'
|
||||
import {
|
||||
formatCurrencyValue,
|
||||
getDecimalCount,
|
||||
|
@ -19,31 +15,11 @@ import {
|
|||
import MarketSelectDropdown from './MarketSelectDropdown'
|
||||
import PerpFundingRate from './PerpFundingRate'
|
||||
import { BorshAccountsCoder } from '@coral-xyz/anchor'
|
||||
import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import usePrevious from '@components/shared/usePrevious'
|
||||
import PerpMarketDetailsModal from '@components/modals/PerpMarketDetailsModal.tsx'
|
||||
|
||||
type ResponseType = {
|
||||
prices: [number, number][]
|
||||
market_caps: [number, number][]
|
||||
total_volumes: [number, number][]
|
||||
}
|
||||
|
||||
const fetchTokenChange = async (
|
||||
mangoTokens: Token[],
|
||||
baseAddress: string
|
||||
): Promise<ResponseType> => {
|
||||
let coingeckoId = mangoTokens.find((t) => t.address === baseAddress)
|
||||
?.extensions?.coingeckoId
|
||||
|
||||
if (baseAddress === '3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh') {
|
||||
coingeckoId = 'bitcoin'
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=1`
|
||||
)
|
||||
const data = await response.json()
|
||||
return data
|
||||
}
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
|
||||
const AdvancedMarketHeader = ({
|
||||
showChart,
|
||||
|
@ -54,16 +30,20 @@ const AdvancedMarketHeader = ({
|
|||
}) => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
|
||||
const {
|
||||
serumOrPerpMarket,
|
||||
price: stalePrice,
|
||||
selectedMarket,
|
||||
} = useSelectedMarket()
|
||||
const selectedMarketName = mangoStore((s) => s.selectedMarket.name)
|
||||
const { mangoTokens } = useJupiterMints()
|
||||
const connection = mangoStore((s) => s.connection)
|
||||
const [price, setPrice] = useState(stalePrice)
|
||||
const { data: birdeyePrices, isLoading: loadingPrices } =
|
||||
useBirdeyeMarketPrices()
|
||||
const previousMarketName = usePrevious(selectedMarketName)
|
||||
const [showMarketDetails, setShowMarketDetails] = useState(false)
|
||||
const { group } = useMangoGroup()
|
||||
|
||||
//subscribe to the market oracle account
|
||||
useEffect(() => {
|
||||
|
@ -112,57 +92,48 @@ const AdvancedMarketHeader = ({
|
|||
}, [connection, selectedMarket])
|
||||
|
||||
useEffect(() => {
|
||||
if (serumOrPerpMarket instanceof PerpMarket) {
|
||||
if (group) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchPerpStats()
|
||||
}
|
||||
}, [serumOrPerpMarket])
|
||||
}, [group])
|
||||
|
||||
const spotBaseAddress = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (group && selectedMarket && selectedMarket instanceof Serum3Market) {
|
||||
return group
|
||||
.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
|
||||
.mint.toString()
|
||||
}
|
||||
}, [selectedMarket])
|
||||
|
||||
const spotChangeResponse = useQuery(
|
||||
['coingecko-tokens', spotBaseAddress],
|
||||
() => fetchTokenChange(mangoTokens, spotBaseAddress!),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 15,
|
||||
staleTime: 1000 * 60 * 10,
|
||||
retry: 3,
|
||||
enabled:
|
||||
!!spotBaseAddress &&
|
||||
serumOrPerpMarket instanceof Market &&
|
||||
mangoTokens.length > 0,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
)
|
||||
const birdeyeData = useMemo(() => {
|
||||
if (
|
||||
!birdeyePrices?.length ||
|
||||
!selectedMarket ||
|
||||
selectedMarket instanceof PerpMarket
|
||||
)
|
||||
return
|
||||
return birdeyePrices.find(
|
||||
(m) => m.mint === selectedMarket.serumMarketExternal.toString()
|
||||
)
|
||||
}, [birdeyePrices, selectedMarket])
|
||||
|
||||
const change = useMemo(() => {
|
||||
if (!price || !serumOrPerpMarket) return 0
|
||||
if (
|
||||
!price ||
|
||||
!serumOrPerpMarket ||
|
||||
selectedMarketName !== previousMarketName
|
||||
)
|
||||
return 0
|
||||
if (serumOrPerpMarket instanceof PerpMarket) {
|
||||
const changeData = getOneDayPerpStats(perpStats, selectedMarketName)
|
||||
|
||||
return changeData.length
|
||||
? ((price - changeData[0].price) / changeData[0].price) * 100
|
||||
: 0
|
||||
} else {
|
||||
if (!spotChangeResponse.data) return 0
|
||||
if (!birdeyeData) return 0
|
||||
return (
|
||||
((price - spotChangeResponse.data.prices?.[0][1]) /
|
||||
spotChangeResponse.data.prices?.[0][1]) *
|
||||
100
|
||||
((price - birdeyeData.data[0].value) / birdeyeData.data[0].value) * 100
|
||||
)
|
||||
}
|
||||
}, [
|
||||
spotChangeResponse,
|
||||
birdeyeData,
|
||||
price,
|
||||
serumOrPerpMarket,
|
||||
perpStats,
|
||||
previousMarketName,
|
||||
selectedMarketName,
|
||||
])
|
||||
|
||||
|
@ -194,7 +165,13 @@ const AdvancedMarketHeader = ({
|
|||
</div>
|
||||
<div className="ml-6 flex-col whitespace-nowrap">
|
||||
<div className="text-xs text-th-fgd-4">{t('rolling-change')}</div>
|
||||
<Change change={change} size="small" suffix="%" />
|
||||
{!loadingPrices && !loadingPerpStats ? (
|
||||
<Change change={change} size="small" suffix="%" />
|
||||
) : (
|
||||
<SheenLoader className="mt-0.5">
|
||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
</div>
|
||||
{serumOrPerpMarket instanceof PerpMarket ? (
|
||||
<>
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
// import ChartRangeButtons from '@components/shared/ChartRangeButtons'
|
||||
import Change from '@components/shared/Change'
|
||||
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import { getOneDayPerpStats } from '@components/stats/PerpMarketsTable'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Link from 'next/link'
|
||||
|
@ -15,6 +20,11 @@ const MarketSelectDropdown = () => {
|
|||
const { selectedMarket } = useSelectedMarket()
|
||||
const serumMarkets = mangoStore((s) => s.serumMarkets)
|
||||
const allPerpMarkets = mangoStore((s) => s.perpMarkets)
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
|
||||
const { group } = useMangoGroup()
|
||||
const { data: birdeyePrices, isLoading: loadingPrices } =
|
||||
useBirdeyeMarketPrices()
|
||||
// const [spotBaseFilter, setSpotBaseFilter] = useState('All')
|
||||
|
||||
const perpMarkets = useMemo(() => {
|
||||
|
@ -61,10 +71,17 @@ const MarketSelectDropdown = () => {
|
|||
} mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-2`}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute -left-4 top-12 z-40 mr-4 w-screen rounded-none bg-th-bkg-2 pb-4 pt-2 md:-left-6 md:w-72 md:rounded-br-md">
|
||||
<Popover.Panel className="absolute -left-4 top-12 z-40 mr-4 w-screen rounded-none bg-th-bkg-2 pb-4 pt-2 md:-left-6 md:w-96 md:rounded-br-md">
|
||||
<p className="my-2 ml-4 text-xs md:ml-6">{t('perp')}</p>
|
||||
{perpMarkets?.length
|
||||
? perpMarkets.map((m) => {
|
||||
const changeData = getOneDayPerpStats(perpStats, m.name)
|
||||
|
||||
const change = changeData.length
|
||||
? ((m.uiPrice - changeData[0].price) /
|
||||
changeData[0].price) *
|
||||
100
|
||||
: 0
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-4 md:px-6"
|
||||
|
@ -93,7 +110,16 @@ const MarketSelectDropdown = () => {
|
|||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
<div className="flex items-center space-x-3">
|
||||
{!loadingPerpStats ? (
|
||||
<Change change={change} suffix="%" />
|
||||
) : (
|
||||
<SheenLoader className="mt-0.5">
|
||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
<FavoriteMarketButton market={m} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@ -105,6 +131,21 @@ const MarketSelectDropdown = () => {
|
|||
.map((x) => x)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((m) => {
|
||||
const birdeyeData = birdeyePrices?.length
|
||||
? birdeyePrices.find(
|
||||
(market) =>
|
||||
market.mint === m.serumMarketExternal.toString()
|
||||
)
|
||||
: null
|
||||
const bank = group?.getFirstBankByTokenIndex(
|
||||
m.baseTokenIndex
|
||||
)
|
||||
const change =
|
||||
birdeyeData && bank
|
||||
? ((bank.uiPrice - birdeyeData.data[0].value) /
|
||||
birdeyeData.data[0].value) *
|
||||
100
|
||||
: 0
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-4 md:px-6"
|
||||
|
@ -133,7 +174,16 @@ const MarketSelectDropdown = () => {
|
|||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
<div className="flex items-center space-x-3">
|
||||
{!loadingPrices ? (
|
||||
<Change change={change} suffix="%" />
|
||||
) : (
|
||||
<SheenLoader className="mt-0.5">
|
||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
)}
|
||||
<FavoriteMarketButton market={m} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { makeApiRequest } from 'apis/birdeye/helpers'
|
||||
|
||||
export interface BirdeyePriceResponse {
|
||||
address: string
|
||||
unixTime: number
|
||||
value: number
|
||||
}
|
||||
|
||||
const fetchBirdeyePrices = async (
|
||||
spotMarkets: Serum3Market[]
|
||||
): Promise<{ data: BirdeyePriceResponse[]; mint: string }[]> => {
|
||||
const mints = spotMarkets.map((market) =>
|
||||
market.serumMarketExternal.toString()
|
||||
)
|
||||
|
||||
const promises = []
|
||||
const queryEnd = Math.floor(Date.now() / 1000)
|
||||
const queryStart = queryEnd - 86400
|
||||
for (const mint of mints) {
|
||||
const query = `defi/history_price?address=${mint}&address_type=pair&type=30m&time_from=${queryStart}&time_to=${queryEnd}`
|
||||
promises.push(makeApiRequest(query))
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises)
|
||||
if (responses?.length) {
|
||||
return responses.map((res) => ({
|
||||
data: res.data.items,
|
||||
mint: res.data.items[0].address,
|
||||
}))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export const useBirdeyeMarketPrices = () => {
|
||||
const spotMarkets = mangoStore((s) => s.serumMarkets)
|
||||
const res = useQuery(
|
||||
['birdeye-market-prices'],
|
||||
() => fetchBirdeyePrices(spotMarkets),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 15,
|
||||
staleTime: 1000 * 60 * 10,
|
||||
retry: 3,
|
||||
enabled: !!spotMarkets?.length,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
isFetching: res?.isFetching,
|
||||
isLoading: res?.isLoading,
|
||||
data: res?.data || [],
|
||||
}
|
||||
}
|
|
@ -748,8 +748,7 @@ const mangoStore = create<MangoStore>()(
|
|||
fetchPerpStats: async () => {
|
||||
const set = get().set
|
||||
const group = get().group
|
||||
const stats = get().perpStats.data
|
||||
if ((stats && stats.length) || !group) return
|
||||
if (!group) return []
|
||||
set((state) => {
|
||||
state.perpStats.loading = true
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue