Merge pull request #352 from blockworks-foundation/saml33/birdeye-change
use birdeye for change values
This commit is contained in:
commit
300c8fcc00
|
@ -1,5 +1,5 @@
|
||||||
import Decimal from 'decimal.js'
|
import Decimal from 'decimal.js'
|
||||||
import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices'
|
import { BirdeyePriceResponse } from 'types'
|
||||||
import { DAILY_SECONDS } from 'utils/constants'
|
import { DAILY_SECONDS } from 'utils/constants'
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
|
@ -33,10 +33,7 @@ const PerpMarketsTable = () => {
|
||||||
const showTableView = width ? width > breakpoints.md : false
|
const showTableView = width ? width > breakpoints.md : false
|
||||||
const rate = usePerpFundingRate()
|
const rate = usePerpFundingRate()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { perpMarketsWithData, isLoading, isFetching } =
|
const { perpMarketsWithData, isLoading } = useListedMarketsWithMarketData()
|
||||||
useListedMarketsWithMarketData()
|
|
||||||
|
|
||||||
const loadingMarketData = isLoading || isFetching
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentBox hideBorder hidePadding>
|
<ContentBox hideBorder hidePadding>
|
||||||
|
@ -126,8 +123,8 @@ const PerpMarketsTable = () => {
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{!loadingMarketData ? (
|
{!isLoading ? (
|
||||||
priceHistory && priceHistory.length ? (
|
priceHistory && priceHistory?.length ? (
|
||||||
<div className="h-10 w-24">
|
<div className="h-10 w-24">
|
||||||
<SimpleAreaChart
|
<SimpleAreaChart
|
||||||
color={
|
color={
|
||||||
|
@ -236,7 +233,7 @@ const PerpMarketsTable = () => {
|
||||||
return (
|
return (
|
||||||
<MobilePerpMarketItem
|
<MobilePerpMarketItem
|
||||||
key={market.publicKey.toString()}
|
key={market.publicKey.toString()}
|
||||||
loadingMarketData={loadingMarketData}
|
loadingMarketData={isLoading}
|
||||||
market={market}
|
market={market}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -98,10 +98,7 @@ const RecentGainersLosers = () => {
|
||||||
for (const token of banksWithMarketData) {
|
for (const token of banksWithMarketData) {
|
||||||
const volume = token.market?.marketData?.quote_volume_24h || 0
|
const volume = token.market?.marketData?.quote_volume_24h || 0
|
||||||
if (token.market?.quoteTokenIndex === 0 && volume > 0) {
|
if (token.market?.quoteTokenIndex === 0 && volume > 0) {
|
||||||
const pastPrice = token.market?.marketData?.price_24h
|
const change = token.market?.rollingChange || 0
|
||||||
const change = pastPrice
|
|
||||||
? ((token.bank.uiPrice - pastPrice) / pastPrice) * 100
|
|
||||||
: 0
|
|
||||||
tradeableAssets.push({ bank: token.bank, change, type: 'spot' })
|
tradeableAssets.push({ bank: token.bank, change, type: 'spot' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import Input from '@components/forms/Input'
|
||||||
import EmptyState from '@components/nftMarket/EmptyState'
|
import EmptyState from '@components/nftMarket/EmptyState'
|
||||||
import { Bank } from '@blockworks-foundation/mango-v4'
|
import { Bank } from '@blockworks-foundation/mango-v4'
|
||||||
import useBanks from 'hooks/useBanks'
|
import useBanks from 'hooks/useBanks'
|
||||||
|
import SheenLoader from '@components/shared/SheenLoader'
|
||||||
|
|
||||||
export type BankWithMarketData = {
|
export type BankWithMarketData = {
|
||||||
bank: Bank
|
bank: Bank
|
||||||
|
@ -91,7 +92,8 @@ const Spot = () => {
|
||||||
const { t } = useTranslation(['common', 'explore', 'trade'])
|
const { t } = useTranslation(['common', 'explore', 'trade'])
|
||||||
const { group } = useMangoGroup()
|
const { group } = useMangoGroup()
|
||||||
const { banks } = useBanks()
|
const { banks } = useBanks()
|
||||||
const { serumMarketsWithData } = useListedMarketsWithMarketData()
|
const { serumMarketsWithData, isLoading: loadingMarketsData } =
|
||||||
|
useListedMarketsWithMarketData()
|
||||||
const [sortByKey, setSortByKey] = useState<AllowedKeys>('quote_volume_24h')
|
const [sortByKey, setSortByKey] = useState<AllowedKeys>('quote_volume_24h')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [showTableView, setShowTableView] = useState(true)
|
const [showTableView, setShowTableView] = useState(true)
|
||||||
|
@ -169,7 +171,15 @@ const Spot = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{sortedTokensToShow.length ? (
|
{loadingMarketsData ? (
|
||||||
|
<div className="mx-4 my-6 space-y-1 md:mx-6">
|
||||||
|
{[...Array(4)].map((x, i) => (
|
||||||
|
<SheenLoader className="flex flex-1" key={i}>
|
||||||
|
<div className="h-16 w-full bg-th-bkg-2" />
|
||||||
|
</SheenLoader>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : sortedTokensToShow.length ? (
|
||||||
showTableView ? (
|
showTableView ? (
|
||||||
<div className="mt-6 border-t border-th-bkg-3">
|
<div className="mt-6 border-t border-th-bkg-3">
|
||||||
<SpotTable tokens={sortedTokensToShow} />
|
<SpotTable tokens={sortedTokensToShow} />
|
||||||
|
|
|
@ -14,7 +14,6 @@ import Tooltip from '@components/shared/Tooltip'
|
||||||
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
|
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
|
||||||
import { COLORS } from 'styles/colors'
|
import { COLORS } from 'styles/colors'
|
||||||
import useThemeWrapper from 'hooks/useThemeWrapper'
|
import useThemeWrapper from 'hooks/useThemeWrapper'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import TokenReduceOnlyDesc from '@components/shared/TokenReduceOnlyDesc'
|
import TokenReduceOnlyDesc from '@components/shared/TokenReduceOnlyDesc'
|
||||||
import CollateralWeightDisplay from '@components/shared/CollateralWeightDisplay'
|
import CollateralWeightDisplay from '@components/shared/CollateralWeightDisplay'
|
||||||
|
|
||||||
|
@ -38,18 +37,15 @@ const SpotCards = ({ tokens }: { tokens: BankWithMarketData[] }) => {
|
||||||
).mul(bank.uiPrice)
|
).mul(bank.uiPrice)
|
||||||
const depositRate = bank.getDepositRateUi()
|
const depositRate = bank.getDepositRateUi()
|
||||||
const borrowRate = bank.getBorrowRateUi()
|
const borrowRate = bank.getBorrowRateUi()
|
||||||
const pastPrice = token.market?.marketData?.price_24h
|
const chartData = token?.market?.priceHistory?.length
|
||||||
const volume = token.market?.marketData?.quote_volume_24h || 0
|
? token.market.priceHistory
|
||||||
const change =
|
?.sort((a, b) => a.time - b.time)
|
||||||
volume > 0 && pastPrice
|
.concat([{ price: bank.uiPrice, time: Date.now() }])
|
||||||
? ((bank.uiPrice - pastPrice) / pastPrice) * 100
|
: []
|
||||||
: 0
|
|
||||||
|
|
||||||
const chartData =
|
const volume = token.market?.marketData?.quote_volume_24h || 0
|
||||||
token.market?.marketData?.price_history
|
|
||||||
?.sort((a, b) => a.time.localeCompare(b.time))
|
const change = token.market?.rollingChange || 0
|
||||||
.concat([{ price: bank.uiPrice, time: dayjs().toISOString() }]) ||
|
|
||||||
[]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -66,14 +62,16 @@ const SpotCards = ({ tokens }: { tokens: BankWithMarketData[] }) => {
|
||||||
<TokenReduceOnlyDesc bank={bank} />
|
<TokenReduceOnlyDesc bank={bank} />
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center space-x-3">
|
{bank.uiPrice ? (
|
||||||
<span className="font-mono">
|
<div className="flex items-center space-x-3">
|
||||||
<FormatNumericValue value={bank.uiPrice} isUsd />
|
<span className="font-mono">
|
||||||
</span>
|
<FormatNumericValue value={bank.uiPrice} isUsd />
|
||||||
{token.market ? (
|
</span>
|
||||||
<Change change={change} suffix="%" />
|
{token.market ? (
|
||||||
) : null}
|
<Change change={change} suffix="%" />
|
||||||
</div>
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{chartData.length ? (
|
{chartData.length ? (
|
||||||
|
@ -98,11 +96,9 @@ const SpotCards = ({ tokens }: { tokens: BankWithMarketData[] }) => {
|
||||||
<p className="font-mono text-th-fgd-2">
|
<p className="font-mono text-th-fgd-2">
|
||||||
{!token.market ? (
|
{!token.market ? (
|
||||||
'–'
|
'–'
|
||||||
) : token.market?.marketData?.quote_volume_24h ? (
|
) : volume ? (
|
||||||
<span>
|
<span>
|
||||||
{numberCompacter.format(
|
{numberCompacter.format(volume)}{' '}
|
||||||
token.market.marketData.quote_volume_24h,
|
|
||||||
)}{' '}
|
|
||||||
<span className="font-body text-th-fgd-4">USDC</span>
|
<span className="font-body text-th-fgd-4">USDC</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -30,7 +30,6 @@ import BankAmountWithValue from '@components/shared/BankAmountWithValue'
|
||||||
import { BankWithMarketData } from './Spot'
|
import { BankWithMarketData } from './Spot'
|
||||||
import { SerumMarketWithMarketData } from 'hooks/useListedMarketsWithMarketData'
|
import { SerumMarketWithMarketData } from 'hooks/useListedMarketsWithMarketData'
|
||||||
import Tooltip from '@components/shared/Tooltip'
|
import Tooltip from '@components/shared/Tooltip'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import TableTokenName from '@components/shared/TableTokenName'
|
import TableTokenName from '@components/shared/TableTokenName'
|
||||||
import { LinkButton } from '@components/shared/Button'
|
import { LinkButton } from '@components/shared/Button'
|
||||||
import { formatTokenSymbol } from 'utils/tokens'
|
import { formatTokenSymbol } from 'utils/tokens'
|
||||||
|
@ -48,7 +47,7 @@ type TableData = {
|
||||||
price: number
|
price: number
|
||||||
priceHistory: {
|
priceHistory: {
|
||||||
price: number
|
price: number
|
||||||
time: string
|
time: number
|
||||||
}[]
|
}[]
|
||||||
volume: number
|
volume: number
|
||||||
isUp: boolean
|
isUp: boolean
|
||||||
|
@ -69,17 +68,15 @@ const SpotTable = ({ tokens }: { tokens: BankWithMarketData[] }) => {
|
||||||
const baseBank = token.bank
|
const baseBank = token.bank
|
||||||
const price = baseBank.uiPrice
|
const price = baseBank.uiPrice
|
||||||
|
|
||||||
const pastPrice = token.market?.marketData?.price_24h
|
const priceHistory = token?.market?.priceHistory?.length
|
||||||
|
? token.market.priceHistory
|
||||||
const priceHistory =
|
?.sort((a, b) => a.time - b.time)
|
||||||
token.market?.marketData?.price_history
|
.concat([{ price: price, time: Date.now() }])
|
||||||
?.sort((a, b) => a.time.localeCompare(b.time))
|
: []
|
||||||
.concat([{ price: price, time: dayjs().toISOString() }]) || []
|
|
||||||
|
|
||||||
const volume = token.market?.marketData?.quote_volume_24h || 0
|
const volume = token.market?.marketData?.quote_volume_24h || 0
|
||||||
|
|
||||||
const change =
|
const change = token.market?.rollingChange || 0
|
||||||
volume > 0 && pastPrice ? ((price - pastPrice) / pastPrice) * 100 : 0
|
|
||||||
|
|
||||||
const tokenName = baseBank.name
|
const tokenName = baseBank.name
|
||||||
|
|
||||||
|
@ -272,7 +269,7 @@ const SpotTable = ({ tokens }: { tokens: BankWithMarketData[] }) => {
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex flex-col items-end">
|
<div className="flex flex-col items-end">
|
||||||
{market ? (
|
{market && price ? (
|
||||||
<Change change={change} suffix="%" />
|
<Change change={change} suffix="%" />
|
||||||
) : (
|
) : (
|
||||||
<span>–</span>
|
<span>–</span>
|
||||||
|
|
|
@ -13,7 +13,7 @@ const MarketChange = ({
|
||||||
market: PerpMarket | Serum3Market | undefined
|
market: PerpMarket | Serum3Market | undefined
|
||||||
size?: 'small'
|
size?: 'small'
|
||||||
}) => {
|
}) => {
|
||||||
const { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching } =
|
const { perpMarketsWithData, serumMarketsWithData, isLoading } =
|
||||||
useListedMarketsWithMarketData()
|
useListedMarketsWithMarketData()
|
||||||
|
|
||||||
const change = useMemo(() => {
|
const change = useMemo(() => {
|
||||||
|
@ -23,18 +23,16 @@ const MarketChange = ({
|
||||||
const perpMarket = perpMarketsWithData.find(
|
const perpMarket = perpMarketsWithData.find(
|
||||||
(m) => m.name.toLowerCase() === market.name.toLowerCase(),
|
(m) => m.name.toLowerCase() === market.name.toLowerCase(),
|
||||||
)
|
)
|
||||||
return perpMarket ? perpMarket.rollingChange : 0
|
return perpMarket?.rollingChange ? perpMarket.rollingChange : 0
|
||||||
} else {
|
} else {
|
||||||
const spotMarket = serumMarketsWithData.find(
|
const spotMarket = serumMarketsWithData.find(
|
||||||
(m) => m.name.toLowerCase() === market.name.toLowerCase(),
|
(m) => m.name.toLowerCase() === market.name.toLowerCase(),
|
||||||
)
|
)
|
||||||
return spotMarket ? spotMarket.rollingChange : 0
|
return spotMarket?.rollingChange ? spotMarket.rollingChange : 0
|
||||||
}
|
}
|
||||||
}, [perpMarketsWithData, serumMarketsWithData])
|
}, [perpMarketsWithData, serumMarketsWithData])
|
||||||
|
|
||||||
const loading = isLoading || isFetching
|
return isLoading ? (
|
||||||
|
|
||||||
return loading ? (
|
|
||||||
<SheenLoader className="mt-0.5">
|
<SheenLoader className="mt-0.5">
|
||||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||||
</SheenLoader>
|
</SheenLoader>
|
||||||
|
|
|
@ -6,13 +6,13 @@ import { useQuery } from '@tanstack/react-query'
|
||||||
import { makeApiRequest } from 'apis/birdeye/helpers'
|
import { makeApiRequest } from 'apis/birdeye/helpers'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices'
|
|
||||||
import parse from 'html-react-parser'
|
import parse from 'html-react-parser'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { DAILY_SECONDS } from 'utils/constants'
|
import { DAILY_SECONDS } from 'utils/constants'
|
||||||
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
|
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
|
||||||
import { countLeadingZeros, formatCurrencyValue } from 'utils/numbers'
|
import { countLeadingZeros, formatCurrencyValue } from 'utils/numbers'
|
||||||
|
import { BirdeyePriceResponse } from 'types'
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const DEFAULT_COINGECKO_VALUES = {
|
const DEFAULT_COINGECKO_VALUES = {
|
||||||
|
|
|
@ -55,7 +55,7 @@ const MarketSelectDropdown = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const { group } = useMangoGroup()
|
const { group } = useMangoGroup()
|
||||||
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
|
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
|
||||||
const { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching } =
|
const { perpMarketsWithData, serumMarketsWithData, isLoading } =
|
||||||
useListedMarketsWithMarketData()
|
useListedMarketsWithMarketData()
|
||||||
const { isDesktop } = useViewport()
|
const { isDesktop } = useViewport()
|
||||||
const focusRef = useRef<HTMLInputElement>(null)
|
const focusRef = useRef<HTMLInputElement>(null)
|
||||||
|
@ -130,8 +130,6 @@ const MarketSelectDropdown = () => {
|
||||||
}
|
}
|
||||||
}, [focusRef, isDesktop, isOpen, spotOrPerp])
|
}, [focusRef, isDesktop, isOpen, spotOrPerp])
|
||||||
|
|
||||||
const loadingMarketData = isLoading || isFetching
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
{({ open, close }) => (
|
{({ open, close }) => (
|
||||||
|
@ -269,7 +267,7 @@ const MarketSelectDropdown = () => {
|
||||||
<MarketChange market={m} size="small" />
|
<MarketChange market={m} size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 hidden sm:flex sm:justify-end">
|
<div className="col-span-1 hidden sm:flex sm:justify-end">
|
||||||
{loadingMarketData ? (
|
{isLoading ? (
|
||||||
<SheenLoader className="mt-0.5">
|
<SheenLoader className="mt-0.5">
|
||||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||||
</SheenLoader>
|
</SheenLoader>
|
||||||
|
@ -446,7 +444,7 @@ const MarketSelectDropdown = () => {
|
||||||
<MarketChange market={m} size="small" />
|
<MarketChange market={m} size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 hidden sm:flex sm:justify-end">
|
<div className="col-span-1 hidden sm:flex sm:justify-end">
|
||||||
{loadingMarketData ? (
|
{isLoading ? (
|
||||||
<SheenLoader className="mt-0.5">
|
<SheenLoader className="mt-0.5">
|
||||||
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
||||||
</SheenLoader>
|
</SheenLoader>
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { Group, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||||
|
import mangoStore from '@store/mangoStore'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { makeApiRequest } from 'apis/birdeye/helpers'
|
||||||
|
import useMangoGroup from './useMangoGroup'
|
||||||
|
import { DAILY_SECONDS } from 'utils/constants'
|
||||||
|
|
||||||
|
const fetchBirdeye24hrPrices = async (
|
||||||
|
group: Group | undefined,
|
||||||
|
spotMarkets: Serum3Market[],
|
||||||
|
) => {
|
||||||
|
if (!group) return []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const queryEnd = Math.floor(Date.now() / 1000)
|
||||||
|
const queryStart = queryEnd - DAILY_SECONDS
|
||||||
|
|
||||||
|
// collect unique quote tokens
|
||||||
|
const uniqueQuoteTokens = Array.from(
|
||||||
|
new Set(
|
||||||
|
spotMarkets.map((market) => {
|
||||||
|
const quoteBank = group.getFirstBankByTokenIndex(
|
||||||
|
market.quoteTokenIndex,
|
||||||
|
)
|
||||||
|
return quoteBank?.mint
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).filter(Boolean) // remove any undefined values
|
||||||
|
|
||||||
|
// fetch responses for unique quote tokens
|
||||||
|
const quoteResponses = await Promise.all(
|
||||||
|
uniqueQuoteTokens.map(async (quoteToken) => {
|
||||||
|
const quoteQuery = `defi/history_price?address=${quoteToken}&address_type=token&type=1H&time_from=${queryStart}&time_to=${queryEnd}`
|
||||||
|
const quoteResponse = await makeApiRequest(quoteQuery)
|
||||||
|
return {
|
||||||
|
quoteToken,
|
||||||
|
items: quoteResponse?.data?.items?.length
|
||||||
|
? quoteResponse.data.items
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// create a map for quick access to quote items based on quoteToken
|
||||||
|
const quoteItemsMap = new Map(
|
||||||
|
quoteResponses.map((response) => [response.quoteToken, response.items]),
|
||||||
|
)
|
||||||
|
|
||||||
|
// fetch base responses and match them with quote items
|
||||||
|
const promises = spotMarkets.map(async (market) => {
|
||||||
|
const baseBank = group.getFirstBankByTokenIndex(market.baseTokenIndex)
|
||||||
|
const quoteBank = group.getFirstBankByTokenIndex(market.quoteTokenIndex)
|
||||||
|
|
||||||
|
const baseQuery = `defi/history_price?address=${baseBank?.mint}&address_type=token&type=1H&time_from=${queryStart}&time_to=${queryEnd}`
|
||||||
|
|
||||||
|
const baseResponse = await makeApiRequest(baseQuery)
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: baseResponse?.data?.items?.length ? baseResponse.data.items : [],
|
||||||
|
quote: quoteItemsMap.get(quoteBank?.mint) || [],
|
||||||
|
marketIndex: market.marketIndex,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const responses = await Promise.all(promises)
|
||||||
|
|
||||||
|
return responses
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error fetching 24-hour price data from birdeye', e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBirdeye24hrPrices = () => {
|
||||||
|
const spotMarkets = mangoStore((s) => s.serumMarkets)
|
||||||
|
const { group } = useMangoGroup()
|
||||||
|
return useQuery(
|
||||||
|
['birdeye-daily-prices'],
|
||||||
|
() => fetchBirdeye24hrPrices(group, spotMarkets),
|
||||||
|
{
|
||||||
|
cacheTime: 1000 * 60 * 15,
|
||||||
|
staleTime: 1000 * 60 * 10,
|
||||||
|
retry: 3,
|
||||||
|
enabled: !!(group && spotMarkets?.length),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,58 +0,0 @@
|
||||||
import { Serum3Market } from '@blockworks-foundation/mango-v4'
|
|
||||||
import mangoStore from '@store/mangoStore'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { makeApiRequest } from 'apis/birdeye/helpers'
|
|
||||||
import { DAILY_SECONDS } from 'utils/constants'
|
|
||||||
|
|
||||||
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 - DAILY_SECONDS
|
|
||||||
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 || [],
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ import useMarketsData from './useMarketsData'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import mangoStore from '@store/mangoStore'
|
import mangoStore from '@store/mangoStore'
|
||||||
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
|
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||||
|
import { useBirdeye24hrPrices } from './useBirdeye24hrPrices'
|
||||||
|
|
||||||
type ApiData = {
|
type ApiData = {
|
||||||
marketData: MarketsDataItem | undefined
|
marketData: MarketsDataItem | undefined
|
||||||
|
@ -10,6 +11,7 @@ type ApiData = {
|
||||||
|
|
||||||
type MarketRollingChange = {
|
type MarketRollingChange = {
|
||||||
rollingChange: number | undefined
|
rollingChange: number | undefined
|
||||||
|
priceHistory: Array<{ price: number; time: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerumMarketWithMarketData = Serum3Market &
|
export type SerumMarketWithMarketData = Serum3Market &
|
||||||
|
@ -21,7 +23,12 @@ export type PerpMarketWithMarketData = PerpMarket &
|
||||||
MarketRollingChange
|
MarketRollingChange
|
||||||
|
|
||||||
export default function useListedMarketsWithMarketData() {
|
export default function useListedMarketsWithMarketData() {
|
||||||
const { data: marketsData, isLoading, isFetching } = useMarketsData()
|
const { data: marketsData, isInitialLoading: loadingMarketsData } =
|
||||||
|
useMarketsData()
|
||||||
|
const {
|
||||||
|
data: birdeyeSpotDailyPrices,
|
||||||
|
isInitialLoading: loadingBirdeyeSpotDailyPrices,
|
||||||
|
} = useBirdeye24hrPrices()
|
||||||
const serumMarkets = mangoStore((s) => s.serumMarkets)
|
const serumMarkets = mangoStore((s) => s.serumMarkets)
|
||||||
const perpMarkets = mangoStore((s) => s.perpMarkets)
|
const perpMarkets = mangoStore((s) => s.perpMarkets)
|
||||||
|
|
||||||
|
@ -62,27 +69,43 @@ export default function useListedMarketsWithMarketData() {
|
||||||
if (!serumMarkets || !serumMarkets.length) return []
|
if (!serumMarkets || !serumMarkets.length) return []
|
||||||
const allSpotMarkets: SerumMarketWithMarketData[] =
|
const allSpotMarkets: SerumMarketWithMarketData[] =
|
||||||
serumMarkets as SerumMarketWithMarketData[]
|
serumMarkets as SerumMarketWithMarketData[]
|
||||||
if (spotData) {
|
if (spotData && birdeyeSpotDailyPrices?.length) {
|
||||||
for (const market of allSpotMarkets) {
|
for (const market of allSpotMarkets) {
|
||||||
const spotEntries = Object.entries(spotData).find(
|
const spotEntries = Object.entries(spotData).find(
|
||||||
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
|
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
|
||||||
)
|
)
|
||||||
|
const birdeyePrices = birdeyeSpotDailyPrices.find(
|
||||||
|
(prices) => prices.marketIndex === market.marketIndex,
|
||||||
|
)
|
||||||
|
const priceHistory = []
|
||||||
|
let pastPrice = 0
|
||||||
|
if (birdeyePrices?.base?.length && birdeyePrices?.quote?.length) {
|
||||||
|
pastPrice =
|
||||||
|
birdeyePrices.base[0]?.value / birdeyePrices.quote[0]?.value
|
||||||
|
for (let i = 0; i < birdeyePrices.base.length; i++) {
|
||||||
|
const base = birdeyePrices.base[i]
|
||||||
|
const quote = birdeyePrices.quote[i]
|
||||||
|
if (base.unixTime === quote?.unixTime && quote?.value) {
|
||||||
|
const price = base.value / quote.value
|
||||||
|
const time = base.unixTime
|
||||||
|
priceHistory.push({ price, time })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// calculate price change
|
// calculate price change
|
||||||
const pastPrice = spotEntries ? spotEntries[1][0]?.price_24h : 0
|
|
||||||
const dailyVolume = spotEntries
|
|
||||||
? spotEntries[1][0]?.quote_volume_24h
|
|
||||||
: 0
|
|
||||||
const currentPrice = currentPrices[market.name]
|
const currentPrice = currentPrices[market.name]
|
||||||
const change =
|
const change = currentPrice
|
||||||
dailyVolume > 0 ? ((currentPrice - pastPrice) / pastPrice) * 100 : 0
|
? ((currentPrice - pastPrice) / pastPrice) * 100
|
||||||
|
: 0
|
||||||
|
|
||||||
market.rollingChange = change
|
market.rollingChange = change
|
||||||
|
market.priceHistory = priceHistory
|
||||||
market.marketData = spotEntries ? spotEntries[1][0] : undefined
|
market.marketData = spotEntries ? spotEntries[1][0] : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...allSpotMarkets].sort((a, b) => a.name.localeCompare(b.name))
|
return [...allSpotMarkets].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}, [spotData, serumMarkets])
|
}, [currentPrices, birdeyeSpotDailyPrices, spotData, serumMarkets])
|
||||||
|
|
||||||
const perpMarketsWithData = useMemo(() => {
|
const perpMarketsWithData = useMemo(() => {
|
||||||
if (!perpMarkets || !perpMarkets.length) return []
|
if (!perpMarkets || !perpMarkets.length) return []
|
||||||
|
@ -111,5 +134,7 @@ export default function useListedMarketsWithMarketData() {
|
||||||
)
|
)
|
||||||
}, [perpData, perpMarkets])
|
}, [perpData, perpMarkets])
|
||||||
|
|
||||||
return { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching }
|
const isLoading = loadingMarketsData || loadingBirdeyeSpotDailyPrices
|
||||||
|
|
||||||
|
return { perpMarketsWithData, serumMarketsWithData, isLoading }
|
||||||
}
|
}
|
||||||
|
|
|
@ -498,6 +498,12 @@ export function isMangoError(error: unknown): error is MangoError {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BirdeyePriceResponse {
|
||||||
|
address: string
|
||||||
|
unixTime: number
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
export type MarketData = { [key: string]: MarketsDataItem[] }
|
export type MarketData = { [key: string]: MarketsDataItem[] }
|
||||||
|
|
||||||
export type MarketsDataItem = {
|
export type MarketsDataItem = {
|
||||||
|
|
Loading…
Reference in New Issue