Merge pull request #114 from blockworks-foundation/use-birdeye-prices

Use birdeye prices
This commit is contained in:
tlrsssss 2023-04-07 16:41:32 -04:00 committed by GitHub
commit 3d2b485cf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 283 additions and 201 deletions

View File

@ -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: {

View File

@ -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

View File

@ -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>
)
) : (

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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 ? (
<>

View File

@ -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>
)
})}

View File

@ -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 || [],
}
}

View File

@ -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
})