import FavoriteMarketButton from '@components/shared/FavoriteMarketButton' import { Popover } from '@headlessui/react' import { ChevronDownIcon, ExclamationTriangleIcon, MagnifyingGlassIcon, } from '@heroicons/react/20/solid' import useMangoGroup from 'hooks/useMangoGroup' import useSelectedMarket from 'hooks/useSelectedMarket' import { useTranslation } from 'next-i18next' import Link from 'next/link' import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react' import { countLeadingZeros, floorToDecimal, formatCurrencyValue, formatNumericValue, getDecimalCount, numberCompacter, } from 'utils/numbers' import MarketLogos from './MarketLogos' import SoonBadge from '@components/shared/SoonBadge' 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' import SheenLoader from '@components/shared/SheenLoader' import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData' import { AllowedKeys, sortPerpMarkets, sortSpotMarkets, startSearch, } from 'utils/markets' import Input from '@components/forms/Input' import { useSortableData } from 'hooks/useSortableData' import { SortableColumnHeader } from '@components/shared/TableElements' import { useViewport } from 'hooks/useViewport' import { useRouter } from 'next/router' import { TOKEN_REDUCE_ONLY_OPTIONS } from 'utils/constants' import { isBankVisibleForUser } from 'utils/bank' import Decimal from 'decimal.js' import useMangoAccount from 'hooks/useMangoAccount' type Currencies = { [key: string]: string } export const CURRENCY_SYMBOLS: Currencies = { 'wBTC (Portal)': '₿', SOL: '◎', } const MARKET_LINK_CLASSES = 'grid grid-cols-3 sm: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 = 'flex w-full items-center justify-between py-2 px-4 md:hover:cursor-not-allowed' export const DEFAULT_SORT_KEY: AllowedKeys = 'notionalQuoteVolume' const MarketSelectDropdown = () => { const { t } = useTranslation(['common', 'trade']) const { selectedMarket } = useSelectedMarket() const [spotOrPerp, setSpotOrPerp] = useState( selectedMarket instanceof PerpMarket ? 'perp' : 'spot', ) const [search, setSearch] = useState('') const [isOpen, setIsOpen] = useState(false) const { group } = useMangoGroup() const [spotBaseFilter, setSpotBaseFilter] = useState('All') const { perpMarketsWithData, serumMarketsWithData, isLoading } = useListedMarketsWithMarketData() const { isDesktop } = useViewport() const focusRef = useRef(null) const { query } = useRouter() const { mangoAccount } = useMangoAccount() // switch to spot tab on spot markets useEffect(() => { if (query?.name && !query.name.includes('PERP')) { setSpotOrPerp('spot') } else { setSpotOrPerp('perp') } }, [query]) const unsortedPerpMarketsToShow = useMemo(() => { if (!perpMarketsWithData.length) return [] return sortPerpMarkets(perpMarketsWithData, DEFAULT_SORT_KEY) }, [perpMarketsWithData]) const spotQuoteTokens: string[] = useMemo(() => { if (serumMarketsWithData.length && group) { const quoteTokens: string[] = ['All'] serumMarketsWithData.map((m) => { const quoteBank = group.getFirstBankByTokenIndex(m.quoteTokenIndex) const quote = quoteBank.name if (!quoteTokens.includes(quote)) { quoteTokens.push(quote) } }) return quoteTokens.sort((a, b) => a.localeCompare(b)) } return ['All'] }, [group, serumMarketsWithData]) const unsortedSerumMarketsToShow = useMemo(() => { if (!serumMarketsWithData.length || !group) return [] if (spotBaseFilter !== 'All') { const filteredMarkets = serumMarketsWithData.filter((m) => { const quoteBank = group.getFirstBankByTokenIndex(m.quoteTokenIndex) const quote = quoteBank.name return quote === spotBaseFilter }) return search ? startSearch(filteredMarkets, search) : sortSpotMarkets(filteredMarkets, DEFAULT_SORT_KEY) } else { return search ? startSearch(serumMarketsWithData, search) : sortSpotMarkets(serumMarketsWithData, DEFAULT_SORT_KEY) } }, [group, search, serumMarketsWithData, spotBaseFilter]) const handleUpdateSearch = (e: ChangeEvent) => { setSearch(e.target.value) } const { items: perpMarketsToShow, requestSort: requestPerpSort, sortConfig: perpSortConfig, } = useSortableData(unsortedPerpMarketsToShow) const { items: serumMarketsToShow, requestSort: requestSerumSort, sortConfig: serumSortConfig, } = useSortableData(unsortedSerumMarketsToShow) const filteredSerumMarkets = serumMarketsToShow.filter((x) => { const baseBank = group?.getFirstBankByTokenIndex(x.baseTokenIndex) if (baseBank?.reduceOnly === TOKEN_REDUCE_ONLY_OPTIONS.ENABLED) { if (!mangoAccount) { return false } const borrowedAmount = mangoAccount ? new Decimal(mangoAccount.getTokenBorrowsUi(baseBank)) .toDecimalPlaces(baseBank.mintDecimals, Decimal.ROUND_UP) .toNumber() : 0 const balance = mangoAccount ? mangoAccount.getTokenBalanceUi(baseBank) : 0 return isBankVisibleForUser(baseBank, borrowedAmount, balance) } else { return true } }) useEffect(() => { if (focusRef?.current && spotOrPerp === 'spot' && isDesktop && isOpen) { focusRef.current.focus() } }, [focusRef, isDesktop, isOpen, spotOrPerp]) return ( {({ open, close }) => (
setIsOpen(!isOpen)} >
{selectedMarket ? ( ) : ( )}
{selectedMarket?.name || ( {t('loading')} )} {selectedMarket?.reduceOnly ? (

{t('trade:reduce-only')}

) : null}
setSpotOrPerp(v)} values={[ ['perp', 0], ['spot', 0], ]} fillWidth />
{spotOrPerp === 'perp' && perpMarketsToShow.length ? ( <>
requestPerpSort('name')} sortConfig={perpSortConfig} title={t('market')} />

requestPerpSort('marketData.last_price')} sortConfig={perpSortConfig} title={t('price')} />

requestPerpSort('rollingChange')} sortConfig={perpSortConfig} title={t('rolling-change')} />

requestPerpSort('marketData.quote_volume_24h') } sortConfig={perpSortConfig} title={t('daily-volume')} />

{perpMarketsToShow.map((m) => { const isComingSoon = m.oracleLastUpdatedSlot == 0 const volumeData = m?.marketData?.quote_volume_24h const volume = volumeData ? volumeData : 0 const leverage = 1 / (m.maintBaseLiabWeight.toNumber() - 1) return (
{!isComingSoon ? ( <> { close() setSearch('') }} shallow={true} >
{m.name} {leverage ? ( ) : null}
{formatCurrencyValue( m.uiPrice, getDecimalCount(m.tickSize), )}
{isLoading ? (
) : ( {volume ? ( ${numberCompacter.format(volume)} ) : ( $0 )} )}
) : (
{m.name}
)}
) })} ) : null} {spotOrPerp === 'spot' ? ( <>
{spotQuoteTokens.map((tab) => ( ))}

requestSerumSort('name')} sortConfig={serumSortConfig} title={t('market')} />

requestSerumSort('marketData.last_price')} sortConfig={serumSortConfig} title={t('price')} />

requestSerumSort('rollingChange')} sortConfig={serumSortConfig} title={t('rolling-change')} />

requestSerumSort('marketData.notionalQuoteVolume') } sortConfig={serumSortConfig} title={t('daily-volume')} />

{filteredSerumMarkets.length ? ( filteredSerumMarkets.map((m) => { const baseBank = group?.getFirstBankByTokenIndex( m.baseTokenIndex, ) const quoteBank = group?.getFirstBankByTokenIndex( m.quoteTokenIndex, ) const market = group?.getSerum3ExternalMarket( m.serumMarketExternal, ) let leverage if (group) { leverage = m.maxBidLeverage(group) } 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() setSearch('') }} shallow={true} >
{m.name} {leverage ? ( ) : null}
{price && market?.tickSize ? ( {quoteBank?.name === 'USDC' ? '$' : ''} {countLeadingZeros(price) <= 4 ? formatNumericValue( price, getDecimalCount(market.tickSize), ) : price.toExponential(3)} {quoteBank?.name && quoteBank.name !== 'USDC' ? ( {' '} {CURRENCY_SYMBOLS[quoteBank.name] || quoteBank.name} ) : null} ) : null}
{isLoading ? (
) : ( {quoteBank?.name === 'USDC' ? '$' : ''} {volume ? numberCompacter.format(volume) : 0} {quoteBank?.name && quoteBank.name !== 'USDC' ? ( {' '} {CURRENCY_SYMBOLS[quoteBank.name] || quoteBank.name} ) : null} )}
) }) ) : (

{t('trade:no-markets-found')}

)} ) : null}
)} ) } export default MarketSelectDropdown const LeverageBadge = ({ leverage }: { leverage: number }) => { return (
{leverage < 1 ? leverage.toFixed(1) : leverage.toFixed()}x
) }