import { Bank } from '@blockworks-foundation/mango-v4' import { Disclosure, Popover, Transition } from '@headlessui/react' import { ChevronDownIcon, CurrencyDollarIcon, EllipsisHorizontalIcon, XMarkIcon, } from '@heroicons/react/20/solid' import { useTranslation } from 'next-i18next' import { useRouter } from 'next/router' import { Fragment, useCallback, useMemo, useState } from 'react' import { useViewport } from '../hooks/useViewport' import mangoStore from '@store/mangoStore' import { breakpoints } from '../utils/theme' import Switch from './forms/Switch' import ContentBox from './shared/ContentBox' import Tooltip from './shared/Tooltip' import { formatTokenSymbol } from 'utils/tokens' import useMangoAccount from 'hooks/useMangoAccount' import { SortableColumnHeader, Table, Td, Th, TrBody, TrHead, } from './shared/TableElements' import DepositWithdrawModal from './modals/DepositWithdrawModal' import BorrowRepayModal from './modals/BorrowRepayModal' import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions' import { SHOW_ZERO_BALANCES_KEY, TOKEN_REDUCE_ONLY_OPTIONS, USDC_MINT, } from 'utils/constants' import { PublicKey } from '@solana/web3.js' import ActionsLinkButton from './account/ActionsLinkButton' import FormatNumericValue from './shared/FormatNumericValue' import BankAmountWithValue from './shared/BankAmountWithValue' import useBanksWithBalances, { BankWithBalance, } from 'hooks/useBanksWithBalances' import useUnownedAccount from 'hooks/useUnownedAccount' import useLocalStorageState from 'hooks/useLocalStorageState' import TokenLogo from './shared/TokenLogo' import useHealthContributions from 'hooks/useHealthContributions' import { useSortableData } from 'hooks/useSortableData' import TableTokenName from './shared/TableTokenName' import CloseBorrowModal from './modals/CloseBorrowModal' import { floorToDecimal } from 'utils/numbers' import SheenLoader from './shared/SheenLoader' import useAccountInterest from 'hooks/useAccountInterest' import { handleGoToTradePage } from 'utils/markets' import TableRatesDisplay from './shared/TableRatesDisplay' import useCollateralFeePopupConditions from 'hooks/useCollateralFeePositions' import { LinkButton } from './shared/Button' import CollateralFeeWarningModal from './modals/CollateralFeeWarningModal' import InlineNotification from './shared/InlineNotification' export const handleOpenCloseBorrowModal = (borrowBank: Bank) => { const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current let repayBank: Bank | undefined if (borrowBank.name === 'USDC') { const solBank = group?.getFirstBankByMint(WRAPPED_SOL_MINT) repayBank = solBank } else { const usdcBank = group?.getFirstBankByMint(new PublicKey(USDC_MINT)) repayBank = usdcBank } if (mangoAccount && repayBank) { const borrowBalance = mangoAccount.getTokenBalanceUi(borrowBank) const roundedBorrowBalance = floorToDecimal( borrowBalance, borrowBank.mintDecimals, ).toNumber() const repayBalance = mangoAccount.getTokenBalanceUi(repayBank) const roundedRepayBalance = floorToDecimal( repayBalance, repayBank.mintDecimals, ).toNumber() const hasSufficientRepayBalance = Math.abs(roundedRepayBalance) * repayBank.uiPrice > Math.abs(roundedBorrowBalance) * borrowBank.uiPrice set((state) => { state.swap.swapMode = hasSufficientRepayBalance ? 'ExactOut' : 'ExactIn' state.swap.inputBank = repayBank state.swap.outputBank = borrowBank if (hasSufficientRepayBalance) { state.swap.amountOut = Math.abs(roundedBorrowBalance).toString() } else { state.swap.amountIn = Math.abs(roundedRepayBalance).toString() } }) } } export const handleCloseBorrowModal = () => { set((state) => { state.swap.inputBank = undefined state.swap.outputBank = undefined state.swap.amountIn = '' state.swap.amountOut = '' state.swap.swapMode = 'ExactIn' }) } type TableData = { bank: Bank balance: number symbol: string interestAmount: number interestValue: number inOrders: number unsettled: number collateralValue: number assetWeight: string liabWeight: string depositRate: number borrowRate: number } const set = mangoStore.getState().set const TokenList = () => { const { t } = useTranslation(['common', 'account', 'token', 'trade']) const [showCloseBorrowModal, setCloseBorrowModal] = useState(false) const [showCollateralFeeModal, setShowCollateralFeeModal] = useState(false) const [closeBorrowBank, setCloseBorrowBank] = useState() const [showZeroBalances, setShowZeroBalances] = useLocalStorageState( SHOW_ZERO_BALANCES_KEY, true, ) const { mangoAccountAddress } = useMangoAccount() const { initContributions } = useHealthContributions() const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances) const { width } = useViewport() const showTableView = width ? width > breakpoints.md : false const banks = useBanksWithBalances('balance') const { isCharged, collateralFeeBanks } = useCollateralFeePopupConditions() const { data: totalInterestData, isInitialLoading: loadingTotalInterestData, } = useAccountInterest() const formattedTableData = useCallback( (banks: BankWithBalance[]) => { const formatted = [] for (const b of banks) { const bank = b.bank const roundedBalance = floorToDecimal( b.balance, bank.mintDecimals, ).toNumber() let balance = roundedBalance if (b.balance && !roundedBalance) { balance = b.balance } const balanceValue = balance * bank.uiPrice const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name const hasInterestEarned = totalInterestData?.find( (d) => d.symbol.toLowerCase() === symbol.toLowerCase() || (symbol === 'ETH (Portal)' && d.symbol === 'ETH'), ) const interestAmount = hasInterestEarned ? hasInterestEarned.borrow_interest * -1 + hasInterestEarned.deposit_interest : 0 const interestValue = hasInterestEarned ? hasInterestEarned.borrow_interest_usd * -1 + hasInterestEarned.deposit_interest_usd : 0.0 const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0 const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0 const collateralValue = initContributions.find((val) => val.asset === bank.name) ?.contribution || 0 const assetWeight = bank.scaledInitAssetWeight(bank.price).toFixed(2) const liabWeight = bank.scaledInitLiabWeight(bank.price).toFixed(2) const depositRate = bank.getDepositRateUi() const borrowRate = bank.getBorrowRateUi() const data = { balance, balanceValue, bank, symbol, interestAmount, interestValue, inOrders, unsettled, collateralValue, assetWeight, liabWeight, depositRate, borrowRate, } formatted.push(data) } return formatted }, [initContributions, spotBalances, totalInterestData], ) const unsortedTableData = useMemo(() => { if (!banks.length) return [] if (showZeroBalances || !mangoAccountAddress) { return formattedTableData(banks) } else { const filtered = banks.filter((b) => Math.abs(b.balance) > 0) return formattedTableData(filtered) } }, [banks, mangoAccountAddress, showZeroBalances, totalInterestData]) const { items: tableData, requestSort, sortConfig, } = useSortableData(unsortedTableData) const openCloseBorrowModal = (borrowBank: Bank) => { setCloseBorrowModal(true) setCloseBorrowBank(borrowBank) handleOpenCloseBorrowModal(borrowBank) } const closeBorrowModal = () => { setCloseBorrowModal(false) handleCloseBorrowModal() } const balancesNumber = useMemo(() => { if (!banks.length) return 0 const activeBalancesNumber = banks.filter( (bank) => Math.abs(bank.balance) > 0, ).length return activeBalancesNumber }, [banks]) return ( {mangoAccountAddress ? (

{`${balancesNumber} ${t( 'balances', )}`}

setShowZeroBalances(!showZeroBalances)} > {t('account:zero-balances')}
) : null} {showTableView ? ( <> {tableData.map((data) => { const { balance, bank, symbol, interestValue, inOrders, collateralValue, assetWeight, liabWeight, depositRate, borrowRate, } = data const decimals = floorToDecimal( balance, bank.mintDecimals, ).toNumber() ? bank.mintDecimals : undefined return ( ) })}
requestSort('symbol')} sortConfig={sortConfig} title={t('token')} />
requestSort('balanceValue')} sortConfig={sortConfig} title={t('balance')} titleClass="tooltip-underline" />
requestSort('collateralValue')} sortConfig={sortConfig} title={t('collateral-value')} titleClass="tooltip-underline" />
requestSort('inOrders')} sortConfig={sortConfig} title={t('trade:in-orders')} />
requestSort('interestValue')} sortConfig={sortConfig} title={t('interest-earned-paid')} titleClass="tooltip-underline" />
requestSort('depositRate')} sortConfig={sortConfig} title={t('rates')} titleClass="tooltip-underline" />
{t('actions')}
{isCharged && collateralFeeBanks.find( (b) => b.bank.name === bank.name, ) ? ( {t('account:tooltip-collateral-fees-charged')} setShowCollateralFeeModal(true) } > {t('view-fees')} } > ) : null}

x

{inOrders ? ( ) : (

)}
{loadingTotalInterestData ? (
) : Math.abs(interestValue) > 0 ? (

) : (

)}
{balance < 0 ? ( ) : null}
) : (
{tableData.map((data) => { return ( ) })}
)} {showCloseBorrowModal ? ( ) : null} {showCollateralFeeModal ? ( setShowCollateralFeeModal(false)} /> ) : null}
) } export default TokenList const MobileTokenListItem = ({ data, setShowCollateralFeeModal, }: { data: TableData setShowCollateralFeeModal: (show: boolean) => void }) => { const { t } = useTranslation(['common', 'token']) const { mangoAccount } = useMangoAccount() const { isCharged, collateralFeeBanks } = useCollateralFeePopupConditions() const { bank, balance, symbol, interestAmount, interestValue, inOrders, unsettled, collateralValue, assetWeight, liabWeight, depositRate, borrowRate, } = data const decimals = floorToDecimal(balance, bank.mintDecimals).toNumber() ? bank.mintDecimals : undefined return ( {({ open }) => ( <>

{isCharged && collateralFeeBanks.find((b) => b.bank.name === bank.name) ? (
{t('account:tooltip-collateral-fees-charged')} setShowCollateralFeeModal(true)} > {t('view-fees')} } type="info" />
) : null}

{t('collateral-value')}

{' '} x

{t('trade:in-orders')}

{t('trade:unsettled')}

{t('interest-earned-paid')}

{t('rates')}

)}
) } export const ActionsMenu = ({ bank, showText, }: { bank: Bank showText?: boolean }) => { const { t } = useTranslation('common') const { mangoAccountAddress } = useMangoAccount() const [showDepositModal, setShowDepositModal] = useState(false) const [showWithdrawModal, setShowWithdrawModal] = useState(false) const [showBorrowModal, setShowBorrowModal] = useState(false) const [showRepayModal, setShowRepayModal] = useState(false) const [selectedToken, setSelectedToken] = useState('') const set = mangoStore.getState().set const router = useRouter() const spotMarkets = mangoStore((s) => s.serumMarkets) const { isUnownedAccount } = useUnownedAccount() const { isDesktop } = useViewport() const hasSpotMarket = useMemo(() => { const markets = spotMarkets.filter( (m) => m.baseTokenIndex === bank?.tokenIndex, ) if (markets?.length) return true return false }, [spotMarkets]) const handleShowActionModals = useCallback( (token: string, action: 'borrow' | 'deposit' | 'withdraw' | 'repay') => { setSelectedToken(token) action === 'borrow' ? setShowBorrowModal(true) : action === 'deposit' ? setShowDepositModal(true) : action === 'withdraw' ? setShowWithdrawModal(true) : setShowRepayModal(true) }, [], ) const balance = useMemo(() => { if (!mangoAccountAddress || !bank) return 0 const mangoAccount = mangoStore.getState().mangoAccount.current if (mangoAccount) { return mangoAccount.getTokenBalanceUi(bank) } else return 0 }, [bank, mangoAccountAddress]) const handleSwap = useCallback(() => { const group = mangoStore.getState().group if (balance > 0) { if (bank.name === 'USDC') { const solBank = group?.getFirstBankByMint(WRAPPED_SOL_MINT) set((s) => { s.swap.inputBank = bank s.swap.outputBank = solBank }) } else { const usdcBank = group?.getFirstBankByMint(new PublicKey(USDC_MINT)) set((s) => { s.swap.inputBank = bank s.swap.outputBank = usdcBank }) } } else { if (bank.name === 'USDC') { const solBank = group?.getFirstBankByMint(WRAPPED_SOL_MINT) set((s) => { s.swap.inputBank = solBank s.swap.outputBank = bank }) } else { const usdcBank = group?.getFirstBankByMint(new PublicKey(USDC_MINT)) set((s) => { s.swap.inputBank = usdcBank s.swap.outputBank = bank }) } } router.push('/swap', undefined, { shallow: true }) }, [bank, router, set]) return ( <> {isUnownedAccount ? null : ( {({ open }) => (
{showText || !isDesktop ? ( {t('actions')} ) : null} {open ? ( ) : ( )} {!showText && isDesktop ? (

{formatTokenSymbol(bank.name)}

) : null} {bank.reduceOnly !== TOKEN_REDUCE_ONLY_OPTIONS.ENABLED ? ( handleShowActionModals(bank.name, 'deposit') } > {t('deposit')} ) : null} {balance < 0 ? ( handleShowActionModals(bank.name, 'repay')} > {t('repay')} ) : null} {balance && balance > 0 ? ( handleShowActionModals(bank.name, 'withdraw') } > {t('withdraw')} ) : null} {bank.reduceOnly === TOKEN_REDUCE_ONLY_OPTIONS.DISABLED ? ( handleShowActionModals(bank.name, 'borrow') } > {t('borrow')} ) : null} {t('swap')} {hasSpotMarket ? ( handleGoToTradePage(bank, spotMarkets, router) } > {t('trade')} ) : null}
)}
)} {showDepositModal ? ( setShowDepositModal(false)} token={selectedToken} /> ) : null} {showWithdrawModal ? ( setShowWithdrawModal(false)} token={selectedToken} /> ) : null} {showBorrowModal ? ( setShowBorrowModal(false)} token={selectedToken} /> ) : null} {showRepayModal ? ( setShowRepayModal(false)} token={selectedToken} /> ) : null} ) }