import { Bank, toUiDecimals, I80F48, PriceImpact, OracleProvider, toUiDecimalsForQuote, Group, } from '@blockworks-foundation/mango-v4' import ExplorerLink from '@components/shared/ExplorerLink' import { BorshAccountsCoder } from '@coral-xyz/anchor' import { coder } from '@project-serum/anchor/dist/cjs/spl/token' import mangoStore from '@store/mangoStore' import useMangoGroup from 'hooks/useMangoGroup' import type { NextPage } from 'next' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { ArrowTopRightOnSquareIcon, ChevronDownIcon, ExclamationTriangleIcon, } from '@heroicons/react/20/solid' import { Disclosure } from '@headlessui/react' import MarketLogos from '@components/trade/MarketLogos' import Button from '@components/shared/Button' import BN from 'bn.js' import { useRouter } from 'next/router' import Link from 'next/link' import { getApiTokenName, getFormattedBankValues, } from 'utils/governance/listingTools' import GovernancePageWrapper from '@components/governance/GovernancePageWrapper' import TokenLogo from '@components/shared/TokenLogo' import DashboardSuggestedValues from '@components/modals/DashboardSuggestedValuesModal' import { USDC_MINT } from 'utils/constants' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import useBanks from 'hooks/useBanks' import { PythHttpClient } from '@pythnetwork/client' import { MAINNET_PYTH_PROGRAM } from 'utils/governance/constants' import { PublicKey } from '@solana/web3.js' import { notify } from 'utils/notifications' import { LISTING_PRESETS, MidPriceImpact, getMidPriceImpacts, getProposedKey, } from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools' import Tooltip from '@components/shared/Tooltip' import SecurityCouncilModal from '@components/modals/SecurityCouncilModal' dayjs.extend(relativeTime) type BankWarningObject = { depositWeightScaleStartQuote?: string borrowWeightScaleStartQuote?: string depositLimit?: string netBorrowLimitPerWindowQuote?: string oracleConfFilter?: string oracleLiveliness?: string } const getSuggestedAndCurrentTier = ( bank: Bank, midPriceImp: MidPriceImpact[], ) => { const epsilon = 1e-8 let currentTier = Object.values(LISTING_PRESETS).find((x) => { if (bank?.name == 'USDC' || bank?.name == 'USDT') return true if (bank?.depositWeightScaleStartQuote != 20000000000) { if ( x.depositWeightScaleStartQuote === bank?.depositWeightScaleStartQuote ) { return true } } else { return ( Math.abs( x.loanOriginationFeeRate - bank?.loanOriginationFeeRate.toNumber(), ) < epsilon ) } }) if (currentTier == undefined) { currentTier = LISTING_PRESETS['asset_5000'] } const filteredResp = midPriceImp .filter((x) => x.avg_price_impact_percent < 1) .reduce((acc: { [key: string]: MidPriceImpact }, val: MidPriceImpact) => { if ( !acc[val.symbol] || val.target_amount > acc[val.symbol].target_amount ) { acc[val.symbol] = val } return acc }, {}) const priceImpact = filteredResp[getApiTokenName(bank.name)] const suggestedTierKey = getProposedKey(priceImpact?.target_amount) return { suggestedTierKey, currentTier, } } const getPythLink = async (pythOraclePk: PublicKey) => { const connection = mangoStore.getState().connection const pythClient = new PythHttpClient(connection, MAINNET_PYTH_PROGRAM) const pythAccounts = await pythClient.getData() const feed = pythAccounts.products.find( (x) => x.price_account === pythOraclePk.toBase58(), ) window.open( feed ? `https://pyth.network/price-feeds/${feed.asset_type.toLowerCase()}-${feed.base.toLowerCase()}-${feed.quote_currency.toLowerCase()}?cluster=solana-mainnet-beta` : `https://explorer.solana.com/address/${pythOraclePk.toBase58()}`, '_blank', ) } export async function getStaticProps({ locale }: { locale: string }) { return { props: { ...(await serverSideTranslations(locale, [ 'account', 'close-account', 'common', 'notifications', 'onboarding', 'profile', 'search', 'settings', 'token', 'trade', ])), }, } } const Dashboard: NextPage = () => { const { group } = useMangoGroup() const { banks } = useBanks() const [stickyIndex, setStickyIndex] = useState(-1) const midPriceImp = useMemo(() => { const priceImpacts: PriceImpact[] = group?.pis || [] return getMidPriceImpacts(priceImpacts.length ? priceImpacts : []) }, [group?.pis]) const handleScroll = useCallback(() => { for (let i = 0; i < banks.length; i++) { const element = document.getElementById(`parent-item-${i}`) if (element) { const rect = element.getBoundingClientRect() if (rect.top <= 0) { setStickyIndex(i) } } } }, [banks]) useEffect(() => { if (banks.length) { window.addEventListener('scroll', handleScroll) return () => { window.removeEventListener('scroll', handleScroll) } } }, [banks]) useEffect(() => { if (banks.length) { const banksWithZeroPrice = banks.filter((x) => x.price.isZero()) if (banksWithZeroPrice?.length) { banksWithZeroPrice.map((x) => notify({ type: 'error', description: `${x.name} reported price 0`, title: '0 price detected', }), ) } } }, [banks.length]) const sortByTier = (tier: string | undefined) => { const tierOrder: Record = { S: 0, AAA: 1, AA: 2, A: 3, 'A-': 4, BBB: 5, BB: 6, B: 7, C: 8, D: 9, } if (tier) { return tierOrder[tier] } return Infinity } return ( <> {group ? (

Banks Current / Detected{' '}

{banks .sort((a, b) => { const aIsReduceOnly = a.areDepositsReduceOnly() const bIsReduceOnly = b.areDepositsReduceOnly() if (aIsReduceOnly && !bIsReduceOnly) { return 1 } else if (!aIsReduceOnly && bIsReduceOnly) { return -1 } else { return sortByTier(a.tier) - sortByTier(b.tier) } }) .map((bank, i) => { return ( ) })}

Perp Markets

{Array.from(group.perpMarketsMapByOracle) .filter(([_, perpMarket]) => !perpMarket.name.includes('OLD')) .map(([oracle, perpMarket]) => { return ( {({ open }) => ( <>

{perpMarket.name}

} /> } /> } /> } /> } /> )}
) })}

Spot Markets

{Array.from(group.serum3MarketsMapByExternal.values()).map( (market) => { const externalMarket = group.getSerum3ExternalMarket( market.serumMarketExternal, ) return ( {({ open }) => ( <>

{market.name}

} /> } /> } /> } /> )}
) }, )}
) : ( 'Loading' )}
) } const BankDisclosure = ({ bank, index, midPriceImp, isSticky, }: { bank: Bank index: number midPriceImp: MidPriceImpact[] isSticky: boolean }) => { const { group } = useMangoGroup() const [isStale, setIsStale] = useState(false) const [openedSuggestedModal, setOpenedSuggestedModal] = useState< string | null >(null) const [securityModalOpen, setSecurityModalOpen] = useState( null, ) const { currentTier, suggestedTierKey } = getSuggestedAndCurrentTier( bank, midPriceImp, ) const warnings = useMemo(() => { const warnings: BankWarningObject = {} if (bank.areDepositsReduceOnly()) return const deposits = toUiDecimals( bank.indexedDeposits.mul(bank.depositIndex).toNumber(), bank.mintDecimals, ) const depositLimit = toUiDecimals(bank.depositLimit, bank.mintDecimals) const depositsValue = deposits * bank.uiPrice if (depositsValue < 10) return const depositsScaleStart = toUiDecimalsForQuote( bank.depositWeightScaleStartQuote, ) const netBorrowsInWindow = toUiDecimalsForQuote( I80F48.fromI64(bank.netBorrowsInWindow).mul(bank.price), ) const netBorrowLimitPerWindowQuote = toUiDecimals( bank.netBorrowLimitPerWindowQuote, 6, ) const borrowsValue = toUiDecimals( bank.indexedBorrows.mul(bank.borrowIndex).toNumber(), bank.mintDecimals, ) * bank.uiPrice const borrowsScaleStart = toUiDecimalsForQuote( bank.borrowWeightScaleStartQuote, ) const oracleConfFilter = 100 * bank.oracleConfig.confFilter.toNumber() const lastKnownConfidence = bank._oracleLastKnownDeviation instanceof I80F48 && !bank._oracleLastKnownDeviation.isZero() ? bank._oracleLastKnownDeviation ?.div(bank.price) .mul(I80F48.fromNumber(100)) .toNumber() : 0 if (depositsValue > depositsScaleStart) { warnings.depositWeightScaleStartQuote = 'Deposits value exceeds scaling start quote' } if (borrowsValue > borrowsScaleStart) { warnings.borrowWeightScaleStartQuote = 'Borrows value exceeds scaling start quote' } if (depositLimit && deposits >= depositLimit) { warnings.depositLimit = 'Deposits are at capacity' } if (netBorrowsInWindow >= netBorrowLimitPerWindowQuote) { warnings.netBorrowLimitPerWindowQuote = 'Net borrows in current window are at capacity' } if (lastKnownConfidence && lastKnownConfidence > oracleConfFilter) { warnings.oracleConfFilter = `Oracle confidence is outside the limit. Current: ${lastKnownConfidence.toFixed( 2, )}% limit: ${oracleConfFilter.toFixed(2)}%` } if (isStale) { warnings.oracleLiveliness = 'Oracle is stale' } return warnings }, [bank, isStale]) useEffect(() => { const client = mangoStore.getState().client const connection = mangoStore.getState().connection const group = mangoStore.getState().group if (!group) return const coder = new BorshAccountsCoder(client.program.idl) const decimals = group.getMintDecimals(bank.mint) const subId = connection.onAccountChange( bank.oracle, async (info, context) => { const { lastUpdatedSlot } = await Group.decodePriceFromOracleAi( group, coder, bank.oracle, info, decimals, client, ) const oracleWriteSlot = context.slot const accountSlot = mangoStore.getState().mangoAccount.lastSlot const highestSlot = Math.max(oracleWriteSlot, accountSlot) const maxStalenessSlots = bank.oracleConfig.maxStalenessSlots.toNumber() setIsStale( maxStalenessSlots > 0 && highestSlot - lastUpdatedSlot > maxStalenessSlots, ) }, 'processed', ) return () => { if (typeof subId !== 'undefined') { connection.removeAccountChangeListener(subId) } } }, [bank]) const depositLimitWarning = warnings?.depositLimit ?? null const depositWeightScaleStartQuoteWarning = warnings?.depositWeightScaleStartQuote ?? null const depositWarnings = [ depositLimitWarning, depositWeightScaleStartQuoteWarning, ].filter(Boolean) const borrowWeightScaleStartQuoteWarning = warnings?.borrowWeightScaleStartQuote ?? null const netBorrowLimitPerWindowQuoteWarning = warnings?.netBorrowLimitPerWindowQuote ?? null const borrowWarnings = [ borrowWeightScaleStartQuoteWarning, netBorrowLimitPerWindowQuoteWarning, ].filter(Boolean) const oracleConfFilterWarning = warnings?.oracleConfFilter ?? null const oracleLivelinessWarning = warnings?.oracleLiveliness ?? null const oracleWarnings = [ oracleConfFilterWarning, oracleLivelinessWarning, ].filter(Boolean) const showWarningTooltip = warnings && Object.keys(warnings).length if (!group) return null const mintInfo = group.mintInfosMapByMint.get(bank.mint.toString()) const formattedBankValues = getFormattedBankValues(group, bank) return ( {({ open }) => ( <>
{Object.values(warnings).map((value, i) => (

{i + 1}. {value}

))}
) : ( '' ) } >

{formattedBankValues.name} Bank

{showWarningTooltip ? ( ) : null}
{formattedBankValues.tier}
/
{currentTier?.preset_name}
{bank.mint.toBase58() !== USDC_MINT ? (
) : null}
} /> } /> } /> } /> {bank.oracle.toString()} ) : ( getPythLink(bank.oracle)} className={`flex cursor-pointer items-center break-all text-th-fgd-2 hover:text-th-fgd-3`} target="_blank" rel="noreferrer" > {bank.oracle.toString()} ) } /> } /> {`${formattedBankValues.rate0}% @ ${formattedBankValues.util0}% util, `} {`${formattedBankValues.rate1}% @ ${formattedBankValues.util1}% util, `} {`${formattedBankValues.maxRate}% @ 100% util`} } />
)}
) } const KeyValuePair = ({ label, value, warnings, }: { label: string value: number | ReactNode | string warnings?: (string | null)[] }) => { const isOracleWarning = warnings?.length ? warnings.some((str) => str && str.toLowerCase().includes('oracle')) : false return (
{warnings.map((value, index) => (

{index + 1}. {value}

))}
) : ( '' ) } >
{label} {warnings?.length ? ( ) : null}
{value}
) } type Vault = { amount: BN } const VaultData = ({ bank }: { bank: Bank }) => { const [vault, setVault] = useState() const client = mangoStore((s) => s.client) const getVaultData = useCallback(async () => { const res = await client.program.provider.connection.getAccountInfo( bank.vault, ) const v = res?.data ? coder().accounts.decode('token', res.data) : undefined setVault(v) }, [bank.vault]) useEffect(() => { getVaultData() }, [getVaultData]) return ( ) } export const DashboardNavbar = () => { const { asPath } = useRouter() return (

Groups

Risks

Slippage

Prospective

Marketing

Mango Account

) } export default Dashboard