diff --git a/components/forms/Select.tsx b/components/forms/Select.tsx index 8ff43661..37f165c4 100644 --- a/components/forms/Select.tsx +++ b/components/forms/Select.tsx @@ -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 diff --git a/components/shared/SoonBadge.tsx b/components/shared/SoonBadge.tsx index 51bdb127..29fd318e 100644 --- a/components/shared/SoonBadge.tsx +++ b/components/shared/SoonBadge.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'next-i18next' const SoonBadge = () => { const { t } = useTranslation('common') return ( -
+
{t('soon')}™
) diff --git a/components/stats/PerpMarketsOverviewTable.tsx b/components/stats/PerpMarketsOverviewTable.tsx index 7267784c..3474f803 100644 --- a/components/stats/PerpMarketsOverviewTable.tsx +++ b/components/stats/PerpMarketsOverviewTable.tsx @@ -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 = () => { - {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 ( - goToPerpMarketDetails(market, router)} - > - -
- -

- {market.name} -

- {isComingSoon ? : null} -
- - -
-

- {market.uiPrice ? ( - - ) : ( - '–' - )} -

-
- - -
- -
- - - {!loadingMarketData ? ( - priceHistory && priceHistory.length ? ( -
- -
- ) : symbol === 'USDC' || symbol === 'USDT' ? null : ( -

{t('unavailable')}

- ) - ) : ( -
- )} - - -
-

- {volume ? `$${numberCompacter.format(volume)}` : '$0'} -

-
- - -
- {fundingRate !== '–' ? ( - - {fundingRateApr ? ( -
- The 1hr rate as an APR is{' '} - - {fundingRateApr} - -
- ) : null} -
- Funding is paid continuously. The 1hr rate - displayed is a rolling average of the past 60 - mins. -
-
- When positive, longs will pay shorts and when - negative shorts pay longs. -
- - } - > -

{fundingRate}

-
- ) : ( -

- )} -
- - -
- {openInterest ? ( - <> -

- goToPerpMarketDetails(market, router)} + > + +

+ +

+ {market.name} +

+ {isComingSoon ? : null} +
+ + +
+

+ {market.uiPrice ? ( + + ) : ( + '–' + )} +

+
+ + +
+ +
+ + + {!loadingMarketData ? ( + priceHistory && priceHistory.length ? ( +
+ +
+ ) : symbol === 'USDC' || symbol === 'USDT' ? null : ( +

+ {t('unavailable')}

-

- $ - {numberCompacter.format( - openInterest * market.uiPrice, - )} -

- + ) ) : ( - <> -

-

- +
)} -
- - -
- -
- - - ) - })} + + +
+

+ {volume ? `$${numberCompacter.format(volume)}` : '$0'} +

+
+ + +
+ {fundingRate !== '–' ? ( + + {fundingRateApr ? ( +
+ The 1hr rate as an APR is{' '} + + {fundingRateApr} + +
+ ) : null} +
+ Funding is paid continuously. The 1hr rate + displayed is a rolling average of the past 60 + mins. +
+
+ When positive, longs will pay shorts and when + negative shorts pay longs. +
+ + } + > +

{fundingRate}

+
+ ) : ( +

+ )} +
+ + +
+ {openInterest ? ( + <> +

+ +

+

+ $ + {numberCompacter.format( + openInterest * market.uiPrice, + )} +

+ + ) : ( + <> +

+

+ + )} +
+ + +
+ +
+ + + ) + }, + )} ) : (
- {perpMarkets.map((market) => { - return ( - - ) - })} + {sortPerpMarkets(perpMarketsWithData, 'quote_volume_24h').map( + (market) => { + return ( + + ) + }, + )}
)} @@ -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] diff --git a/components/stats/SpotMarketsTable.tsx b/components/stats/SpotMarketsTable.tsx index 44b569fc..dc1aeac7 100644 --- a/components/stats/SpotMarketsTable.tsx +++ b/components/stats/SpotMarketsTable.tsx @@ -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 = () => { - {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 = () => { ) - })} + }, + )} ) : (
- {serumMarkets - .slice() - .sort((a, b) => a.name.localeCompare(b.name)) - .map((market) => { + {sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h').map( + (market) => { return ( ) - })} + }, + )}
)} @@ -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 diff --git a/components/trade/MarketSelectDropdown.tsx b/components/trade/MarketSelectDropdown.tsx index 14793094..c5fb0058 100644 --- a/components/trade/MarketSelectDropdown.tsx +++ b/components/trade/MarketSelectDropdown.tsx @@ -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('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 ( @@ -107,7 +113,7 @@ const MarketSelectDropdown = () => { } mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-2`} /> - +
{ fillWidth />
-
- {spotOrPerp === 'perp' && perpMarkets?.length - ? perpMarkets.map((m) => { +
+ {spotOrPerp === 'perp' && perpMarketsToShow.length ? ( + <> +
+

{t('market')}

+

{t('price')}

+

+ {t('rolling-change')} +

+

+ {t('daily-volume')} +

+
+ {perpMarketsToShow.map((m) => { const isComingSoon = m.oracleLastUpdatedSlot == 0 + + const volumeData = m?.marketData?.quote_volume_24h + + const volume = volumeData ? volumeData : 0 + return ( -
+
{!isComingSoon ? ( <> { }} shallow={true} > -
- - {m.name} +
+ + + {m.name} +
-
- +
+ {formatCurrencyValue( m.uiPrice, getDecimalCount(m.tickSize), )} +
+
+
+ {loadingMarketData ? ( + +
+ + ) : ( + + {volume ? ( + + ${numberCompacter.format(volume)} + + ) : ( + + $0 + + )} + + )} +
- +
+ +
) : (
- - {m.name} + + {m.name}
)}
) - }) - : null} - {spotOrPerp === 'spot' && serumMarkets?.length ? ( + })} + + ) : null} + {spotOrPerp === 'spot' && serumMarketsToShow.length ? ( <> -
- {spotBaseTokens.map((tab) => ( - - ))} -
- {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 ( -
+
+ {spotBaseTokens.map((tab) => ( + + ))} +
+ {/* need to sort out change before enabling more sorting options */} + {/*
+ +
*/} +
+
+

{t('market')}

+

{t('price')}

+

+ {t('rolling-change')} +

+

+ {t('daily-volume')} +

+
+ {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 ( +
+ { + close() + }} + shallow={true} + > +
+ + + {m.name} + +
+
+ {price && market?.tickSize ? ( + + {quoteBank?.name === 'USDC' ? '$' : ''} + {getDecimalCount(market.tickSize) <= 6 + ? formatNumericValue( + price, + getDecimalCount(market.tickSize), + ) + : price.toExponential(3)} + {quoteBank?.name !== 'USDC' ? ( + + {' '} + {quoteBank?.name} + + ) : null} + + ) : null} +
+
+ +
+
+ {loadingMarketData ? ( + +
+ + ) : ( + + {quoteBank?.name === 'USDC' ? '$' : ''} + {volume ? numberCompacter.format(volume) : 0} + {quoteBank?.name !== 'USDC' ? ( + + {' '} + {quoteBank?.name} + + ) : null} + + )} +
+ +
- ) - })} +
+ ) + })} ) : null}
diff --git a/hooks/useListedMarketsWithMarketData.ts b/hooks/useListedMarketsWithMarketData.ts new file mode 100644 index 00000000..712d5f1f --- /dev/null +++ b/hooks/useListedMarketsWithMarketData.ts @@ -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 } +} diff --git a/hooks/useMarketsData.ts b/hooks/useMarketsData.ts index 734fe797..889fd8f4 100644 --- a/hooks/useMarketsData.ts +++ b/hooks/useMarketsData.ts @@ -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, }) } diff --git a/utils/markets.ts b/utils/markets.ts new file mode 100644 index 00000000..ec003a3c --- /dev/null +++ b/utils/markets.ts @@ -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 + }, + ) +}