sort markets by volume

This commit is contained in:
saml33 2023-07-24 22:14:45 +10:00
parent 5b38739e1b
commit 3c492c893e
8 changed files with 565 additions and 363 deletions

View File

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Listbox } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { ReactNode } from 'react'
interface SelectProps {
value: string | ReactNode
onChange: (x: string) => void
onChange: (x: any) => void
children: ReactNode
className?: string
dropdownPanelClassName?: string

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'next-i18next'
const SoonBadge = () => {
const { t } = useTranslation('common')
return (
<div className="flex items-center rounded-full border border-th-active px-1.5 py-0.5 text-xs uppercase text-th-active">
<div className="flex items-center rounded-full border border-th-active px-1.5 py-0.5 text-xxs uppercase text-th-active leading-none">
{t('soon')}&trade;
</div>
)

View File

@ -1,7 +1,6 @@
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next'
import { useViewport } from '../../hooks/useViewport'
import mangoStore from '@store/mangoStore'
import { COLORS } from '../../styles/colors'
import { breakpoints } from '../../utils/theme'
import ContentBox from '../shared/ContentBox'
@ -15,16 +14,17 @@ import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { getDecimalCount, numberCompacter } from 'utils/numbers'
import Tooltip from '@components/shared/Tooltip'
import { MarketData } from 'types'
import { NextRouter, useRouter } from 'next/router'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { Disclosure, Transition } from '@headlessui/react'
import { LinkButton } from '@components/shared/Button'
import SoonBadge from '@components/shared/SoonBadge'
import useMarketsData from 'hooks/useMarketsData'
import { useMemo } from 'react'
import MarketChange from '@components/shared/MarketChange'
import useThemeWrapper from 'hooks/useThemeWrapper'
import useListedMarketsWithMarketData, {
PerpMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
import { sortPerpMarkets } from 'utils/markets'
export const goToPerpMarketDetails = (
market: PerpMarket,
@ -36,18 +36,13 @@ export const goToPerpMarketDetails = (
const PerpMarketsOverviewTable = () => {
const { t } = useTranslation(['common', 'trade'])
const perpMarkets = mangoStore((s) => s.perpMarkets)
const { theme } = useThemeWrapper()
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const rate = usePerpFundingRate()
const router = useRouter()
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const perpData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.perpData || []
}, [marketsData])
const { perpMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const loadingMarketData = isLoading || isFetching
@ -68,191 +63,193 @@ const PerpMarketsOverviewTable = () => {
</TrHead>
</thead>
<tbody>
{perpMarkets.map((market) => {
const symbol = market.name.split('-')[0]
{sortPerpMarkets(perpMarketsWithData, 'quote_volume_24h').map(
(market) => {
const symbol = market.name.split('-')[0]
const perpDataEntries = Object.entries(perpData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
const marketData = perpDataEntries
? perpDataEntries[1][0]
: undefined
const priceHistory = market?.marketData?.price_history
const priceHistory = marketData?.price_history
const volumeData = market?.marketData?.quote_volume_24h
const volume = marketData?.quote_volume_24h
? marketData.quote_volume_24h
: 0
const volume = volumeData ? volumeData : 0
let fundingRate
let fundingRateApr
if (rate.isSuccess) {
const marketRate = rate?.data?.find(
(r) => r.market_index === market.perpMarketIndex,
)
if (marketRate) {
fundingRate = formatFunding.format(
marketRate.funding_rate_hourly,
)
fundingRateApr = formatFunding.format(
marketRate.funding_rate_hourly * 8760,
let fundingRate
let fundingRateApr
if (rate.isSuccess) {
const marketRate = rate?.data?.find(
(r) => r.market_index === market.perpMarketIndex,
)
if (marketRate) {
fundingRate = formatFunding.format(
marketRate.funding_rate_hourly,
)
fundingRateApr = formatFunding.format(
marketRate.funding_rate_hourly * 8760,
)
} else {
fundingRate = ''
fundingRateApr = ''
}
} else {
fundingRate = ''
fundingRateApr = ''
}
} else {
fundingRate = ''
fundingRateApr = ''
}
const openInterest = market.baseLotsToUi(market.openInterest)
const isComingSoon = market.oracleLastUpdatedSlot == 0
const isUp =
priceHistory && priceHistory.length
? market.uiPrice >= priceHistory[0].price
: false
const openInterest = market.baseLotsToUi(market.openInterest)
const isComingSoon = market.oracleLastUpdatedSlot == 0
const isUp =
priceHistory && priceHistory.length
? market.uiPrice >= priceHistory[0].price
: false
return (
<TrBody
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
key={market.publicKey.toString()}
onClick={() => goToPerpMarketDetails(market, router)}
>
<Td>
<div className="flex items-center">
<MarketLogos market={market} size="large" />
<p className="mr-2 whitespace-nowrap font-body">
{market.name}
</p>
{isComingSoon ? <SoonBadge /> : null}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{market.uiPrice ? (
<FormatNumericValue value={market.uiPrice} isUsd />
) : (
''
)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<MarketChange market={market} />
</div>
</Td>
<Td>
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
data={priceHistory.concat([
{
time: new Date().toString(),
price: market.uiPrice,
},
])}
name={symbol}
xKey="time"
yKey="price"
/>
</div>
) : symbol === 'USDC' || symbol === 'USDT' ? null : (
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
)
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{volume ? `$${numberCompacter.format(volume)}` : '$0'}
</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end">
{fundingRate !== '' ? (
<Tooltip
content={
<>
{fundingRateApr ? (
<div className="">
The 1hr rate as an APR is{' '}
<span className="font-mono text-th-fgd-2">
{fundingRateApr}
</span>
</div>
) : null}
<div className="mt-2">
Funding is paid continuously. The 1hr rate
displayed is a rolling average of the past 60
mins.
</div>
<div className="mt-2">
When positive, longs will pay shorts and when
negative shorts pay longs.
</div>
</>
}
>
<p className="tooltip-underline">{fundingRate}</p>
</Tooltip>
) : (
<p></p>
)}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
{openInterest ? (
<>
<p>
<FormatNumericValue
value={openInterest}
decimals={getDecimalCount(market.minOrderSize)}
return (
<TrBody
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
key={market.publicKey.toString()}
onClick={() => goToPerpMarketDetails(market, router)}
>
<Td>
<div className="flex items-center">
<MarketLogos market={market} size="large" />
<p className="mr-2 whitespace-nowrap font-body">
{market.name}
</p>
{isComingSoon ? <SoonBadge /> : null}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{market.uiPrice ? (
<FormatNumericValue value={market.uiPrice} isUsd />
) : (
''
)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<MarketChange market={market} />
</div>
</Td>
<Td>
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={
isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]
}
data={priceHistory.concat([
{
time: new Date().toString(),
price: market.uiPrice,
},
])}
name={symbol}
xKey="time"
yKey="price"
/>
</div>
) : symbol === 'USDC' || symbol === 'USDT' ? null : (
<p className="mb-0 text-th-fgd-4">
{t('unavailable')}
</p>
<p className="text-th-fgd-4">
$
{numberCompacter.format(
openInterest * market.uiPrice,
)}
</p>
</>
)
) : (
<>
<p></p>
<p className="text-th-fgd-4"></p>
</>
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</div>
</Td>
<Td>
<div className="flex justify-end">
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
</div>
</Td>
</TrBody>
)
})}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{volume ? `$${numberCompacter.format(volume)}` : '$0'}
</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end">
{fundingRate !== '' ? (
<Tooltip
content={
<>
{fundingRateApr ? (
<div className="">
The 1hr rate as an APR is{' '}
<span className="font-mono text-th-fgd-2">
{fundingRateApr}
</span>
</div>
) : null}
<div className="mt-2">
Funding is paid continuously. The 1hr rate
displayed is a rolling average of the past 60
mins.
</div>
<div className="mt-2">
When positive, longs will pay shorts and when
negative shorts pay longs.
</div>
</>
}
>
<p className="tooltip-underline">{fundingRate}</p>
</Tooltip>
) : (
<p></p>
)}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
{openInterest ? (
<>
<p>
<FormatNumericValue
value={openInterest}
decimals={getDecimalCount(market.minOrderSize)}
/>
</p>
<p className="text-th-fgd-4">
$
{numberCompacter.format(
openInterest * market.uiPrice,
)}
</p>
</>
) : (
<>
<p></p>
<p className="text-th-fgd-4"></p>
</>
)}
</div>
</Td>
<Td>
<div className="flex justify-end">
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
</div>
</Td>
</TrBody>
)
},
)}
</tbody>
</Table>
) : (
<div className="border-b border-th-bkg-3">
{perpMarkets.map((market) => {
return (
<MobilePerpMarketItem
key={market.publicKey.toString()}
market={market}
/>
)
})}
{sortPerpMarkets(perpMarketsWithData, 'quote_volume_24h').map(
(market) => {
return (
<MobilePerpMarketItem
key={market.publicKey.toString()}
loadingMarketData={loadingMarketData}
market={market}
/>
)
},
)}
</div>
)}
</ContentBox>
@ -261,28 +258,23 @@ const PerpMarketsOverviewTable = () => {
export default PerpMarketsOverviewTable
const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
const MobilePerpMarketItem = ({
market,
loadingMarketData,
}: {
market: PerpMarketWithMarketData
loadingMarketData: boolean
}) => {
const { t } = useTranslation('common')
const { theme } = useThemeWrapper()
const router = useRouter()
const rate = usePerpFundingRate()
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const perpData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.perpData || []
}, [marketsData])
const priceHistory = market?.marketData?.price_history
const perpDataEntries = Object.entries(perpData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
const marketData = perpDataEntries ? perpDataEntries[1][0] : undefined
const volumeData = market?.marketData?.quote_volume_24h
const priceHistory = marketData?.price_history
const volume = marketData?.quote_volume_24h ? marketData.quote_volume_24h : 0
const loadingMarketData = isLoading || isFetching
const volume = volumeData ? volumeData : 0
const symbol = market.name.split('-')[0]

View File

@ -1,8 +1,6 @@
import { Serum3Market } from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next'
import { useMemo } from 'react'
import { useViewport } from '../../hooks/useViewport'
import mangoStore from '@store/mangoStore'
import { COLORS } from '../../styles/colors'
import { breakpoints } from '../../utils/theme'
import ContentBox from '../shared/ContentBox'
@ -12,26 +10,23 @@ import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { floorToDecimal, getDecimalCount, numberCompacter } from 'utils/numbers'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { MarketData } from 'types'
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import MarketChange from '@components/shared/MarketChange'
import useMarketsData from 'hooks/useMarketsData'
import useThemeWrapper from 'hooks/useThemeWrapper'
import useListedMarketsWithMarketData, {
SerumMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
import { sortSpotMarkets } from 'utils/markets'
const SpotMarketsTable = () => {
const { t } = useTranslation('common')
const { group } = useMangoGroup()
const serumMarkets = mangoStore((s) => s.serumMarkets)
const { theme } = useThemeWrapper()
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const spotData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.spotData || []
}, [marketsData])
const { serumMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const loadingMarketData = isLoading || isFetching
@ -49,10 +44,8 @@ const SpotMarketsTable = () => {
</TrHead>
</thead>
<tbody>
{serumMarkets
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((mkt) => {
{sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h').map(
(mkt) => {
const baseBank = group?.getFirstBankByTokenIndex(
mkt.baseTokenIndex,
)
@ -70,18 +63,11 @@ const SpotMarketsTable = () => {
).toNumber()
}
const spotDataEntries = Object.entries(spotData).find(
(e) => e[0].toLowerCase() === mkt.name.toLowerCase(),
)
const marketData = spotDataEntries
? spotDataEntries[1][0]
: undefined
const priceHistory = mkt?.marketData?.price_history
const priceHistory = marketData?.price_history
const volumeData = mkt?.marketData?.quote_volume_24h
const volume = marketData?.quote_volume_24h
? marketData.quote_volume_24h
: 0
const volume = volumeData ? volumeData : 0
const isUp =
price && priceHistory && priceHistory.length
@ -169,22 +155,23 @@ const SpotMarketsTable = () => {
</Td>
</TrBody>
)
})}
},
)}
</tbody>
</Table>
) : (
<div className="border-b border-th-bkg-3">
{serumMarkets
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((market) => {
{sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h').map(
(market) => {
return (
<MobileSpotMarketItem
key={market.publicKey.toString()}
loadingMarketData={loadingMarketData}
market={market}
/>
)
})}
},
)}
</div>
)}
</ContentBox>
@ -193,21 +180,19 @@ const SpotMarketsTable = () => {
export default SpotMarketsTable
const MobileSpotMarketItem = ({ market }: { market: Serum3Market }) => {
const MobileSpotMarketItem = ({
market,
loadingMarketData,
}: {
market: SerumMarketWithMarketData
loadingMarketData: boolean
}) => {
const { t } = useTranslation('common')
const { group } = useMangoGroup()
const { theme } = useThemeWrapper()
const baseBank = group?.getFirstBankByTokenIndex(market.baseTokenIndex)
const quoteBank = group?.getFirstBankByTokenIndex(market.quoteTokenIndex)
const serumMarket = group?.getSerum3ExternalMarket(market.serumMarketExternal)
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const spotData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.spotData || []
}, [marketsData])
const loadingMarketData = isLoading || isFetching
const price = useMemo(() => {
if (!baseBank || !quoteBank || !serumMarket) return 0
@ -217,14 +202,11 @@ const MobileSpotMarketItem = ({ market }: { market: Serum3Market }) => {
).toNumber()
}, [baseBank, quoteBank, serumMarket])
const spotDataEntries = Object.entries(spotData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
const marketData = spotDataEntries ? spotDataEntries[1][0] : undefined
const priceHistory = market?.marketData?.price_history
const priceHistory = marketData?.price_history
const volueData = market?.marketData?.quote_volume_24h
const volume = marketData?.quote_volume_24h ? marketData.quote_volume_24h : 0
const volume = volueData ? volueData : 0
const isUp =
price && priceHistory && priceHistory.length

View File

@ -1,7 +1,6 @@
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
import { Popover } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import useMangoGroup from 'hooks/useMangoGroup'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
@ -12,6 +11,7 @@ import {
formatCurrencyValue,
formatNumericValue,
getDecimalCount,
numberCompacter,
} from 'utils/numbers'
import MarketLogos from './MarketLogos'
import SoonBadge from '@components/shared/SoonBadge'
@ -19,15 +19,23 @@ import TabButtons from '@components/shared/TabButtons'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import Loading from '@components/shared/Loading'
import MarketChange from '@components/shared/MarketChange'
const MARKET_LINK_WRAPPER_CLASSES =
'flex items-center justify-between px-4 md:pl-6 md:pr-4'
import SheenLoader from '@components/shared/SheenLoader'
// import Select from '@components/forms/Select'
import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData'
import { AllowedKeys, sortPerpMarkets, sortSpotMarkets } from 'utils/markets'
const MARKET_LINK_CLASSES =
'mr-1 -ml-3 flex w-full items-center justify-between rounded-md py-2 px-3 focus:outline-none focus-visible:text-th-active md:hover:cursor-pointer md:hover:bg-th-bkg-3 md:hover:text-th-fgd-1'
'grid grid-cols-3 md:grid-cols-4 flex items-center w-full py-2 px-4 rounded-r-md focus:outline-none focus-visible:text-th-active md:hover:cursor-pointer md:hover:bg-th-bkg-3 md:hover:text-th-fgd-1'
const MARKET_LINK_DISABLED_CLASSES =
'mr-2 -ml-3 flex w-full items-center justify-between rounded-md py-2 px-3 md:hover:cursor-not-allowed'
'flex w-full items-center justify-between py-2 px-4 md:hover:cursor-not-allowed'
// const SORT_KEYS = [
// 'quote_volume_24h',
// 'quote_volume_1h',
// 'change_24h',
// 'change_1h',
// ]
const MarketSelectDropdown = () => {
const { t } = useTranslation('common')
@ -35,27 +43,21 @@ const MarketSelectDropdown = () => {
const [spotOrPerp, setSpotOrPerp] = useState(
selectedMarket instanceof PerpMarket ? 'perp' : 'spot',
)
const serumMarkets = mangoStore((s) => s.serumMarkets)
const allPerpMarkets = mangoStore((s) => s.perpMarkets)
const [sortByKey] = useState<AllowedKeys>('quote_volume_24h')
const { group } = useMangoGroup()
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
const { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const perpMarkets = useMemo(() => {
return allPerpMarkets
.filter(
(p) =>
p.publicKey.toString() !==
'9Y8paZ5wUpzLFfQuHz8j2RtPrKsDtHx9sbgFmWb5abCw',
)
.sort((a, b) =>
a.oracleLastUpdatedSlot == 0 ? -1 : a.name.localeCompare(b.name),
)
}, [allPerpMarkets])
const perpMarketsToShow = useMemo(() => {
if (!perpMarketsWithData.length) return []
return sortPerpMarkets(perpMarketsWithData, sortByKey)
}, [perpMarketsWithData, sortByKey])
const spotBaseTokens: string[] = useMemo(() => {
if (serumMarkets.length) {
if (serumMarketsWithData.length) {
const baseTokens: string[] = ['All']
serumMarkets.map((m) => {
serumMarketsWithData.map((m) => {
const base = m.name.split('/')[1]
if (!baseTokens.includes(base)) {
baseTokens.push(base)
@ -64,19 +66,23 @@ const MarketSelectDropdown = () => {
return baseTokens.sort((a, b) => a.localeCompare(b))
}
return ['All']
}, [serumMarkets])
}, [serumMarketsWithData])
const serumMarketsToShow = useMemo(() => {
if (!serumMarkets || !serumMarkets.length) return []
if (!serumMarketsWithData.length) return []
if (spotBaseFilter !== 'All') {
return serumMarkets.filter((m) => {
const filteredMarkets = serumMarketsWithData.filter((m) => {
const base = m.name.split('/')[1]
return base === spotBaseFilter
})
return sortSpotMarkets(filteredMarkets, sortByKey)
} else {
return serumMarkets
return sortSpotMarkets(serumMarketsWithData, sortByKey)
}
}, [serumMarkets, spotBaseFilter])
}, [serumMarketsWithData, sortByKey, spotBaseFilter])
const loadingMarketData = isLoading || isFetching
return (
<Popover>
@ -107,7 +113,7 @@ 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 border-y border-r border-th-bkg-3 bg-th-bkg-2 md:-left-6 md:w-[420px] md:rounded-br-md">
<Popover.Panel className="absolute top-12 z-40 w-screen border-y md:border-r border-th-bkg-3 bg-th-bkg-2 -left-4 md:w-[560px]">
<div className="border-b border-th-bkg-3">
<TabButtons
activeValue={spotOrPerp}
@ -119,15 +125,28 @@ const MarketSelectDropdown = () => {
fillWidth
/>
</div>
<div className="py-3">
{spotOrPerp === 'perp' && perpMarkets?.length
? perpMarkets.map((m) => {
<div className="py-3 max-h-[calc(100vh-160px)] thin-scroll overflow-auto">
{spotOrPerp === 'perp' && perpMarketsToShow.length ? (
<>
<div className="grid grid-cols-3 md:grid-cols-4 pl-4 pr-14 text-xxs border-b border-th-bkg-3 pb-1 mb-2">
<p className="col-span-1">{t('market')}</p>
<p className="col-span-1 text-right">{t('price')}</p>
<p className="col-span-1 text-right">
{t('rolling-change')}
</p>
<p className="col-span-1 text-right hidden md:block">
{t('daily-volume')}
</p>
</div>
{perpMarketsToShow.map((m) => {
const isComingSoon = m.oracleLastUpdatedSlot == 0
const volumeData = m?.marketData?.quote_volume_24h
const volume = volumeData ? volumeData : 0
return (
<div
className={MARKET_LINK_WRAPPER_CLASSES}
key={m.publicKey.toString()}
>
<div className="flex items-center w-full" key={m.name}>
{!isComingSoon ? (
<>
<Link
@ -141,116 +160,196 @@ const MarketSelectDropdown = () => {
}}
shallow={true}
>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="text-th-fgd-2">{m.name}</span>
<div className="col-span-1 flex items-center">
<MarketLogos market={m} size="small" />
<span className="text-th-fgd-2 text-xs">
{m.name}
</span>
</div>
<div className="flex items-center">
<span className="mr-3 font-mono text-xs text-th-fgd-2">
<div className="col-span-1 flex justify-end">
<span className="font-mono text-xs text-th-fgd-2">
{formatCurrencyValue(
m.uiPrice,
getDecimalCount(m.tickSize),
)}
</span>
</div>
<div className="col-span-1 flex justify-end">
<MarketChange market={m} size="small" />
</div>
<div className="col-span-1 md:flex justify-end hidden">
{loadingMarketData ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
) : (
<span>
{volume ? (
<span className="font-mono text-xs text-th-fgd-2">
${numberCompacter.format(volume)}
</span>
) : (
<span className="font-mono text-xs text-th-fgd-2">
$0
</span>
)}
</span>
)}
</div>
</Link>
<FavoriteMarketButton market={m} />
<div className="px-3">
<FavoriteMarketButton market={m} />
</div>
</>
) : (
<span className={MARKET_LINK_DISABLED_CLASSES}>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="mr-2">{m.name}</span>
<MarketLogos market={m} size="small" />
<span className="mr-2 text-xs">{m.name}</span>
<SoonBadge />
</div>
</span>
)}
</div>
)
})
: null}
{spotOrPerp === 'spot' && serumMarkets?.length ? (
})}
</>
) : null}
{spotOrPerp === 'spot' && serumMarketsToShow.length ? (
<>
<div className="mb-3 px-4 md:px-6">
{spotBaseTokens.map((tab) => (
<button
className={`rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
spotBaseFilter === tab
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
}`}
onClick={() => setSpotBaseFilter(tab)}
key={tab}
>
{t(tab)}
</button>
))}
</div>
{serumMarketsToShow
.map((x) => x)
.sort((a, b) => a.name.localeCompare(b.name))
.map((m) => {
const baseBank = group?.getFirstBankByTokenIndex(
m.baseTokenIndex,
)
const quoteBank = group?.getFirstBankByTokenIndex(
m.quoteTokenIndex,
)
const market = group?.getSerum3ExternalMarket(
m.serumMarketExternal,
)
let price
if (baseBank && market && quoteBank) {
price = floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize),
).toNumber()
}
return (
<div
className={MARKET_LINK_WRAPPER_CLASSES}
key={m.publicKey.toString()}
<div className="flex items-center justify-between mb-3 px-4">
<div>
{spotBaseTokens.map((tab) => (
<button
className={`rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
spotBaseFilter === tab
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
}`}
onClick={() => setSpotBaseFilter(tab)}
key={tab}
>
<Link
className={MARKET_LINK_CLASSES}
href={{
pathname: '/trade',
query: { name: m.name },
}}
onClick={() => {
close()
}}
shallow={true}
>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="text-th-fgd-2">{m.name}</span>
</div>
<div className="flex items-center">
{price && market?.tickSize ? (
<span className="mr-3 font-mono text-xs text-th-fgd-2">
{quoteBank?.name === 'USDC' ? '$' : ''}
{getDecimalCount(market.tickSize) <= 6
? formatNumericValue(
price,
getDecimalCount(market.tickSize),
)
: price.toExponential(3)}{' '}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
{quoteBank?.name}
</span>
) : null}
</span>
) : null}
<MarketChange market={m} size="small" />
</div>
</Link>
{t(tab)}
</button>
))}
</div>
{/* need to sort out change before enabling more sorting options */}
{/* <div>
<Select
value={sortByKey}
onChange={(sortBy) => setSortByKey(sortBy)}
className="w-full"
>
{SORT_KEYS.map((sortBy) => {
return (
<Select.Option key={sortBy} value={sortBy}>
<div className="flex w-full items-center justify-between">
{sortBy}
</div>
</Select.Option>
)
})}
</Select>
</div> */}
</div>
<div className="grid grid-cols-3 md:grid-cols-4 pl-4 pr-14 text-xxs border-b border-th-bkg-3 pb-1 mb-2">
<p className="col-span-1">{t('market')}</p>
<p className="col-span-1 text-right">{t('price')}</p>
<p className="col-span-1 text-right">
{t('rolling-change')}
</p>
<p className="col-span-1 text-right hidden md:block">
{t('daily-volume')}
</p>
</div>
{serumMarketsToShow.map((m) => {
const baseBank = group?.getFirstBankByTokenIndex(
m.baseTokenIndex,
)
const quoteBank = group?.getFirstBankByTokenIndex(
m.quoteTokenIndex,
)
const market = group?.getSerum3ExternalMarket(
m.serumMarketExternal,
)
let price
if (baseBank && market && quoteBank) {
price = floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize),
).toNumber()
}
const volumeData = m?.marketData?.quote_volume_24h
const volume = volumeData ? volumeData : 0
return (
<div className="flex items-center w-full" key={m.name}>
<Link
className={MARKET_LINK_CLASSES}
href={{
pathname: '/trade',
query: { name: m.name },
}}
onClick={() => {
close()
}}
shallow={true}
>
<div className="col-span-1 flex items-center">
<MarketLogos market={m} size="small" />
<span className="text-th-fgd-2 text-xs">
{m.name}
</span>
</div>
<div className="col-span-1 flex justify-end">
{price && market?.tickSize ? (
<span className="font-mono text-xs text-th-fgd-2">
{quoteBank?.name === 'USDC' ? '$' : ''}
{getDecimalCount(market.tickSize) <= 6
? formatNumericValue(
price,
getDecimalCount(market.tickSize),
)
: price.toExponential(3)}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
{' '}
{quoteBank?.name}
</span>
) : null}
</span>
) : null}
</div>
<div className="col-span-1 flex justify-end">
<MarketChange market={m} size="small" />
</div>
<div className="col-span-1 md:flex justify-end hidden">
{loadingMarketData ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
) : (
<span className="font-mono text-xs text-th-fgd-2">
{quoteBank?.name === 'USDC' ? '$' : ''}
{volume ? numberCompacter.format(volume) : 0}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
{' '}
{quoteBank?.name}
</span>
) : null}
</span>
)}
</div>
</Link>
<div className="px-3">
<FavoriteMarketButton market={m} />
</div>
)
})}
</div>
)
})}
</>
) : null}
</div>

View File

@ -0,0 +1,69 @@
import { MarketData, MarketsDataItem } from 'types'
import useMarketsData from './useMarketsData'
import { useMemo } from 'react'
import mangoStore from '@store/mangoStore'
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
type ApiData = {
marketData: MarketsDataItem | undefined
}
export type SerumMarketWithMarketData = Serum3Market & ApiData
export type PerpMarketWithMarketData = PerpMarket & ApiData
export default function useListedMarketsWithMarketData() {
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const serumMarkets = mangoStore((s) => s.serumMarkets)
const perpMarkets = mangoStore((s) => s.perpMarkets)
const perpData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.perpData || []
}, [marketsData])
const spotData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.spotData || []
}, [marketsData])
const serumMarketsWithData = useMemo(() => {
if (!serumMarkets || !serumMarkets.length) return []
const allSpotMarkets: SerumMarketWithMarketData[] =
serumMarkets as SerumMarketWithMarketData[]
if (spotData) {
for (const market of allSpotMarkets) {
const spotEntries = Object.entries(spotData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
market.marketData = spotEntries ? spotEntries[1][0] : undefined
}
}
return [...allSpotMarkets].sort((a, b) => a.name.localeCompare(b.name))
}, [spotData, serumMarkets])
const perpMarketsWithData = useMemo(() => {
if (!perpMarkets || !perpMarkets.length) return []
const allPerpMarkets: PerpMarketWithMarketData[] =
perpMarkets as PerpMarketWithMarketData[]
if (perpData) {
for (const market of allPerpMarkets) {
const perpEntries = Object.entries(perpData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
market.marketData = perpEntries ? perpEntries[1][0] : undefined
}
}
return allPerpMarkets
.filter(
(p) =>
p.publicKey.toString() !==
'9Y8paZ5wUpzLFfQuHz8j2RtPrKsDtHx9sbgFmWb5abCw',
)
.sort((a, b) =>
a.oracleLastUpdatedSlot == 0 ? -1 : a.name.localeCompare(b.name),
)
}, [perpData, perpMarkets])
return { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching }
}

View File

@ -17,12 +17,11 @@ const fetchMarketData = async () => {
}
}
export default function useMarketData() {
export default function useMarketsData() {
return useQuery(['market-data'], () => fetchMarketData(), {
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
// enabled: market && market instanceof Serum3Market,
})
}

60
utils/markets.ts Normal file
View File

@ -0,0 +1,60 @@
import {
PerpMarketWithMarketData,
SerumMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
export type AllowedKeys =
| 'quote_volume_24h'
| 'quote_volume_1h'
| 'change_24h'
| 'change_1h'
export const sortSpotMarkets = (
spotMarkets: SerumMarketWithMarketData[],
sortByKey: AllowedKeys,
) => {
return spotMarkets.sort(
(a: SerumMarketWithMarketData, b: SerumMarketWithMarketData) => {
const aValue: number | undefined = a?.marketData?.[sortByKey]
const bValue: number | undefined = b?.marketData?.[sortByKey]
// Handle marketData[sortByKey] is undefined
if (typeof aValue === 'undefined' && typeof bValue === 'undefined') {
return 0 // Consider them equal
}
if (typeof aValue === 'undefined') {
return 1 // b should come before a
}
if (typeof bValue === 'undefined') {
return -1 // a should come before b
}
return bValue - aValue
},
)
}
export const sortPerpMarkets = (
perpMarkets: PerpMarketWithMarketData[],
sortByKey: AllowedKeys,
) => {
return perpMarkets.sort(
(a: PerpMarketWithMarketData, b: PerpMarketWithMarketData) => {
const aValue: number | undefined = a?.marketData?.[sortByKey]
const bValue: number | undefined = b?.marketData?.[sortByKey]
// Handle marketData[sortByKey] is undefined
if (typeof aValue === 'undefined' && typeof bValue === 'undefined') {
return 0 // Consider them equal
}
if (typeof aValue === 'undefined') {
return 1 // b should come before a
}
if (typeof bValue === 'undefined') {
return -1 // a should come before b
}
return bValue - aValue
},
)
}