import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' import useSelectedMarket from 'hooks/useSelectedMarket' import Tooltip from '@components/shared/Tooltip' import { useTranslation } from 'next-i18next' import mangoStore from '@store/mangoStore' import { useEffect, useState } from 'react' import { PerpMarket, Bank } from '@blockworks-foundation/mango-v4' import { BorshAccountsCoder } from '@coral-xyz/anchor' import { floorToDecimal, formatNumericValue, getDecimalCount, } from 'utils/numbers' import dayjs from 'dayjs' import duration from 'dayjs/plugin/duration' import relativeTime from 'dayjs/plugin/relativeTime' import useOracleProvider from 'hooks/useOracleProvider' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid' const OraclePrice = () => { const { serumOrPerpMarket, price: stalePrice, selectedMarket, quoteBank, } = useSelectedMarket() dayjs.extend(duration) dayjs.extend(relativeTime) const connection = mangoStore((s) => s.connection) const [price, setPrice] = useState(stalePrice) const [oracleLastUpdatedSlot, setOracleLastUpdatedSlot] = useState(0) const [highestSlot, setHighestSlot] = useState(0) const [isStale, setIsStale] = useState(false) const { oracleProvider, oracleLinkPath } = useOracleProvider() const { t } = useTranslation(['common', 'trade']) //subscribe to the market oracle account useEffect(() => { const client = mangoStore.getState().client const group = mangoStore.getState().group if (!group || !selectedMarket) return let marketOrBank: PerpMarket | Bank let decimals: number if (selectedMarket instanceof PerpMarket) { marketOrBank = selectedMarket decimals = selectedMarket.baseDecimals } else { const baseBank = group.getFirstBankByTokenIndex( selectedMarket.baseTokenIndex, ) marketOrBank = baseBank decimals = group.getMintDecimals(baseBank.mint) } const coder = new BorshAccountsCoder(client.program.idl) setPrice(stalePrice) const subId = connection.onAccountChange( marketOrBank.oracle, async (info, context) => { const { price, uiPrice, lastUpdatedSlot } = await group.decodePriceFromOracleAi( coder, marketOrBank.oracle, info, decimals, client, ) marketOrBank._price = price marketOrBank._uiPrice = uiPrice marketOrBank._oracleLastUpdatedSlot = lastUpdatedSlot setOracleLastUpdatedSlot(lastUpdatedSlot) const marketSlot = mangoStore.getState().selectedMarket.lastSeenSlot const oracleWriteSlot = context.slot const accountSlot = mangoStore.getState().mangoAccount.lastSlot const highestSlot = Math.max( marketSlot.bids, marketSlot.asks, oracleWriteSlot, accountSlot, ) const maxStalenessSlots = marketOrBank.oracleConfig.maxStalenessSlots.toNumber() setHighestSlot(highestSlot) setIsStale( maxStalenessSlots > 0 && highestSlot - lastUpdatedSlot > maxStalenessSlots, ) if (selectedMarket instanceof PerpMarket) { setPrice(uiPrice) } else { let price if (quoteBank && serumOrPerpMarket) { price = floorToDecimal( uiPrice / quoteBank.uiPrice, getDecimalCount(serumOrPerpMarket.tickSize), ).toNumber() } else { price = 0 } setPrice(price) } }, 'processed', ) return () => { if (typeof subId !== 'undefined') { connection.removeAccountChangeListener(subId) } } }, [connection, selectedMarket, serumOrPerpMarket, quoteBank, stalePrice]) const oracleDecimals = getDecimalCount(serumOrPerpMarket?.tickSize || 0.01) return ( <>
{t('trade:price-provided-by')} {oracleLinkPath ? ( {oracleProvider} ) : ( {oracleProvider} )}
{t('trade:last-updated')}{' '} {dayjs .duration({ seconds: -((highestSlot - oracleLastUpdatedSlot) * 0.5), }) .humanize(true)} .
{isStale ? (
{t('trade:oracle-not-updated')}
{t('trade:oracle-not-updated-warning')}
) : undefined} } >
{t('trade:oracle-price')}
{isStale ? ( ) : null}
{price ? ( <> {quoteBank?.name === 'USDC' ? '$' : ''} {formatNumericValue(price, oracleDecimals)}{' '} {quoteBank?.name !== 'USDC' ? ( {quoteBank?.name} ) : null} ) : ( )}
) } export default OraclePrice