Merge pull request #352 from blockworks-foundation/saml33/birdeye-change

use birdeye for change values
This commit is contained in:
saml33 2023-12-20 08:29:31 +11:00 committed by GitHub
commit 300c8fcc00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 183 additions and 129 deletions

View File

@ -1,5 +1,5 @@
import Decimal from 'decimal.js'
import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices'
import { BirdeyePriceResponse } from 'types'
import { DAILY_SECONDS } from 'utils/constants'
/* eslint-disable @typescript-eslint/no-explicit-any */

View File

@ -33,10 +33,7 @@ const PerpMarketsTable = () => {
const showTableView = width ? width > breakpoints.md : false
const rate = usePerpFundingRate()
const router = useRouter()
const { perpMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const loadingMarketData = isLoading || isFetching
const { perpMarketsWithData, isLoading } = useListedMarketsWithMarketData()
return (
<ContentBox hideBorder hidePadding>
@ -126,8 +123,8 @@ const PerpMarketsTable = () => {
</div>
</Td>
<Td>
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
{!isLoading ? (
priceHistory && priceHistory?.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={
@ -236,7 +233,7 @@ const PerpMarketsTable = () => {
return (
<MobilePerpMarketItem
key={market.publicKey.toString()}
loadingMarketData={loadingMarketData}
loadingMarketData={isLoading}
market={market}
/>
)

View File

@ -98,10 +98,7 @@ const RecentGainersLosers = () => {
for (const token of banksWithMarketData) {
const volume = token.market?.marketData?.quote_volume_24h || 0
if (token.market?.quoteTokenIndex === 0 && volume > 0) {
const pastPrice = token.market?.marketData?.price_24h
const change = pastPrice
? ((token.bank.uiPrice - pastPrice) / pastPrice) * 100
: 0
const change = token.market?.rollingChange || 0
tradeableAssets.push({ bank: token.bank, change, type: 'spot' })
}
}

View File

@ -17,6 +17,7 @@ import Input from '@components/forms/Input'
import EmptyState from '@components/nftMarket/EmptyState'
import { Bank } from '@blockworks-foundation/mango-v4'
import useBanks from 'hooks/useBanks'
import SheenLoader from '@components/shared/SheenLoader'
export type BankWithMarketData = {
bank: Bank
@ -91,7 +92,8 @@ const Spot = () => {
const { t } = useTranslation(['common', 'explore', 'trade'])
const { group } = useMangoGroup()
const { banks } = useBanks()
const { serumMarketsWithData } = useListedMarketsWithMarketData()
const { serumMarketsWithData, isLoading: loadingMarketsData } =
useListedMarketsWithMarketData()
const [sortByKey, setSortByKey] = useState<AllowedKeys>('quote_volume_24h')
const [search, setSearch] = useState('')
const [showTableView, setShowTableView] = useState(true)
@ -169,7 +171,15 @@ const Spot = () => {
</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 ? (
<div className="mt-6 border-t border-th-bkg-3">
<SpotTable tokens={sortedTokensToShow} />

View File

@ -14,7 +14,6 @@ import Tooltip from '@components/shared/Tooltip'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { COLORS } from 'styles/colors'
import useThemeWrapper from 'hooks/useThemeWrapper'
import dayjs from 'dayjs'
import TokenReduceOnlyDesc from '@components/shared/TokenReduceOnlyDesc'
import CollateralWeightDisplay from '@components/shared/CollateralWeightDisplay'
@ -38,18 +37,15 @@ const SpotCards = ({ tokens }: { tokens: BankWithMarketData[] }) => {
).mul(bank.uiPrice)
const depositRate = bank.getDepositRateUi()
const borrowRate = bank.getBorrowRateUi()
const pastPrice = token.market?.marketData?.price_24h
const volume = token.market?.marketData?.quote_volume_24h || 0
const change =
volume > 0 && pastPrice
? ((bank.uiPrice - pastPrice) / pastPrice) * 100
: 0
const chartData = token?.market?.priceHistory?.length
? token.market.priceHistory
?.sort((a, b) => a.time - b.time)
.concat([{ price: bank.uiPrice, time: Date.now() }])
: []
const chartData =
token.market?.marketData?.price_history
?.sort((a, b) => a.time.localeCompare(b.time))
.concat([{ price: bank.uiPrice, time: dayjs().toISOString() }]) ||
[]
const volume = token.market?.marketData?.quote_volume_24h || 0
const change = token.market?.rollingChange || 0
return (
<div
@ -66,6 +62,7 @@ const SpotCards = ({ tokens }: { tokens: BankWithMarketData[] }) => {
<TokenReduceOnlyDesc bank={bank} />
</span>
</h3>
{bank.uiPrice ? (
<div className="flex items-center space-x-3">
<span className="font-mono">
<FormatNumericValue value={bank.uiPrice} isUsd />
@ -74,6 +71,7 @@ const SpotCards = ({ tokens }: { tokens: BankWithMarketData[] }) => {
<Change change={change} suffix="%" />
) : null}
</div>
) : null}
</div>
</div>
{chartData.length ? (
@ -98,11 +96,9 @@ const SpotCards = ({ tokens }: { tokens: BankWithMarketData[] }) => {
<p className="font-mono text-th-fgd-2">
{!token.market ? (
''
) : token.market?.marketData?.quote_volume_24h ? (
) : volume ? (
<span>
{numberCompacter.format(
token.market.marketData.quote_volume_24h,
)}{' '}
{numberCompacter.format(volume)}{' '}
<span className="font-body text-th-fgd-4">USDC</span>
</span>
) : (

View File

@ -30,7 +30,6 @@ import BankAmountWithValue from '@components/shared/BankAmountWithValue'
import { BankWithMarketData } from './Spot'
import { SerumMarketWithMarketData } from 'hooks/useListedMarketsWithMarketData'
import Tooltip from '@components/shared/Tooltip'
import dayjs from 'dayjs'
import TableTokenName from '@components/shared/TableTokenName'
import { LinkButton } from '@components/shared/Button'
import { formatTokenSymbol } from 'utils/tokens'
@ -48,7 +47,7 @@ type TableData = {
price: number
priceHistory: {
price: number
time: string
time: number
}[]
volume: number
isUp: boolean
@ -69,17 +68,15 @@ const SpotTable = ({ tokens }: { tokens: BankWithMarketData[] }) => {
const baseBank = token.bank
const price = baseBank.uiPrice
const pastPrice = token.market?.marketData?.price_24h
const priceHistory =
token.market?.marketData?.price_history
?.sort((a, b) => a.time.localeCompare(b.time))
.concat([{ price: price, time: dayjs().toISOString() }]) || []
const priceHistory = token?.market?.priceHistory?.length
? token.market.priceHistory
?.sort((a, b) => a.time - b.time)
.concat([{ price: price, time: Date.now() }])
: []
const volume = token.market?.marketData?.quote_volume_24h || 0
const change =
volume > 0 && pastPrice ? ((price - pastPrice) / pastPrice) * 100 : 0
const change = token.market?.rollingChange || 0
const tokenName = baseBank.name
@ -272,7 +269,7 @@ const SpotTable = ({ tokens }: { tokens: BankWithMarketData[] }) => {
</Td>
<Td>
<div className="flex flex-col items-end">
{market ? (
{market && price ? (
<Change change={change} suffix="%" />
) : (
<span></span>

View File

@ -13,7 +13,7 @@ const MarketChange = ({
market: PerpMarket | Serum3Market | undefined
size?: 'small'
}) => {
const { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching } =
const { perpMarketsWithData, serumMarketsWithData, isLoading } =
useListedMarketsWithMarketData()
const change = useMemo(() => {
@ -23,18 +23,16 @@ const MarketChange = ({
const perpMarket = perpMarketsWithData.find(
(m) => m.name.toLowerCase() === market.name.toLowerCase(),
)
return perpMarket ? perpMarket.rollingChange : 0
return perpMarket?.rollingChange ? perpMarket.rollingChange : 0
} else {
const spotMarket = serumMarketsWithData.find(
(m) => m.name.toLowerCase() === market.name.toLowerCase(),
)
return spotMarket ? spotMarket.rollingChange : 0
return spotMarket?.rollingChange ? spotMarket.rollingChange : 0
}
}, [perpMarketsWithData, serumMarketsWithData])
const loading = isLoading || isFetching
return loading ? (
return isLoading ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>

View File

@ -6,13 +6,13 @@ import { useQuery } from '@tanstack/react-query'
import { makeApiRequest } from 'apis/birdeye/helpers'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices'
import parse from 'html-react-parser'
import { useTranslation } from 'next-i18next'
import { useMemo, useState } from 'react'
import { DAILY_SECONDS } from 'utils/constants'
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
import { countLeadingZeros, formatCurrencyValue } from 'utils/numbers'
import { BirdeyePriceResponse } from 'types'
dayjs.extend(relativeTime)
const DEFAULT_COINGECKO_VALUES = {

View File

@ -55,7 +55,7 @@ const MarketSelectDropdown = () => {
const [isOpen, setIsOpen] = useState(false)
const { group } = useMangoGroup()
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
const { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching } =
const { perpMarketsWithData, serumMarketsWithData, isLoading } =
useListedMarketsWithMarketData()
const { isDesktop } = useViewport()
const focusRef = useRef<HTMLInputElement>(null)
@ -130,8 +130,6 @@ const MarketSelectDropdown = () => {
}
}, [focusRef, isDesktop, isOpen, spotOrPerp])
const loadingMarketData = isLoading || isFetching
return (
<Popover>
{({ open, close }) => (
@ -269,7 +267,7 @@ const MarketSelectDropdown = () => {
<MarketChange market={m} size="small" />
</div>
<div className="col-span-1 hidden sm:flex sm:justify-end">
{loadingMarketData ? (
{isLoading ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
@ -446,7 +444,7 @@ const MarketSelectDropdown = () => {
<MarketChange market={m} size="small" />
</div>
<div className="col-span-1 hidden sm:flex sm:justify-end">
{loadingMarketData ? (
{isLoading ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import useMarketsData from './useMarketsData'
import { useMemo } from 'react'
import mangoStore from '@store/mangoStore'
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
import { useBirdeye24hrPrices } from './useBirdeye24hrPrices'
type ApiData = {
marketData: MarketsDataItem | undefined
@ -10,6 +11,7 @@ type ApiData = {
type MarketRollingChange = {
rollingChange: number | undefined
priceHistory: Array<{ price: number; time: number }>
}
export type SerumMarketWithMarketData = Serum3Market &
@ -21,7 +23,12 @@ export type PerpMarketWithMarketData = PerpMarket &
MarketRollingChange
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 perpMarkets = mangoStore((s) => s.perpMarkets)
@ -62,27 +69,43 @@ export default function useListedMarketsWithMarketData() {
if (!serumMarkets || !serumMarkets.length) return []
const allSpotMarkets: SerumMarketWithMarketData[] =
serumMarkets as SerumMarketWithMarketData[]
if (spotData) {
if (spotData && birdeyeSpotDailyPrices?.length) {
for (const market of allSpotMarkets) {
const spotEntries = Object.entries(spotData).find(
(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
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 change =
dailyVolume > 0 ? ((currentPrice - pastPrice) / pastPrice) * 100 : 0
const change = currentPrice
? ((currentPrice - pastPrice) / pastPrice) * 100
: 0
market.rollingChange = change
market.priceHistory = priceHistory
market.marketData = spotEntries ? spotEntries[1][0] : undefined
}
}
return [...allSpotMarkets].sort((a, b) => a.name.localeCompare(b.name))
}, [spotData, serumMarkets])
}, [currentPrices, birdeyeSpotDailyPrices, spotData, serumMarkets])
const perpMarketsWithData = useMemo(() => {
if (!perpMarkets || !perpMarkets.length) return []
@ -111,5 +134,7 @@ export default function useListedMarketsWithMarketData() {
)
}, [perpData, perpMarkets])
return { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching }
const isLoading = loadingMarketsData || loadingBirdeyeSpotDailyPrices
return { perpMarketsWithData, serumMarketsWithData, isLoading }
}

View File

@ -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 MarketsDataItem = {