import HideMangoAccount from '@components/account/HideMangoAccount' import MangoAccountSizeModal from '@components/modals/MangoAccountSizeModal' import Button, { LinkButton } from '@components/shared/Button' import TokenLogo from '@components/shared/TokenLogo' import Tooltip from '@components/shared/Tooltip' import MarketLogos from '@components/trade/MarketLogos' import { Disclosure } from '@headlessui/react' import { ChevronDownIcon, SquaresPlusIcon } from '@heroicons/react/20/solid' import useMangoAccount from 'hooks/useMangoAccount' import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts' import useMangoGroup from 'hooks/useMangoGroup' import { useTranslation } from 'next-i18next' import { useCallback, useMemo, useState } from 'react' import { MAX_ACCOUNTS } from 'utils/constants' import mangoStore from '@store/mangoStore' import { PublicKey, TransactionInstruction } from '@solana/web3.js' import { notify } from 'utils/notifications' import { isMangoError } from 'types' import { getMaxWithdrawForBank } from '@components/swap/useTokenMax' import Decimal from 'decimal.js' import { formatTokenSymbol } from 'utils/tokens' import { handleCancelAll } from '@components/swap/SwapTriggerOrders' enum CLOSE_TYPE { TOKEN, PERP, SERUMOO, PERPOO, } const CLOSE_WRAPPER_CLASSNAMES = 'mb-4 flex flex-col md:flex-row md:items-center md:justify-between rounded-md bg-th-bkg-2 px-4 py-3' const SLOT_ROW_CLASSNAMES = 'flex items-center justify-between border-t border-th-bkg-3 py-3' const AccountSettings = () => { const { t } = useTranslation(['common', 'settings', 'trade']) const { mangoAccountAddress } = useMangoAccount() const { group } = useMangoGroup() const [showAccountSizeModal, setShowAccountSizeModal] = useState(false) const [submitting, setSubmitting] = useState(false) const [cancelTcs, setCancelTcs] = useState('') const { usedTokens, usedSerum3, usedPerps, usedPerpOo, usedTcs, emptySerum3, emptyPerps, totalTokens, totalSerum3, totalPerps, totalPerpOpenOrders, isAccountFull, } = useMangoAccountAccounts() const tokenStatus = useMemo(() => { const mangoAccount = mangoStore.getState().mangoAccount.current if (!group || !mangoAccount || !usedTokens.length) return [] const tokens = [] for (const token of usedTokens) { const bank = group.getFirstBankByTokenIndex(token.tokenIndex) const tokenMax = getMaxWithdrawForBank(group, bank, mangoAccount) const balance = mangoAccount.getTokenBalanceUi(bank) const isClosable = tokenMax.eq(new Decimal(balance)) && !token.inUseCount tokens.push({ isClosable, balance, tokenIndex: token.tokenIndex }) } return tokens }, [group, mangoAccountAddress, usedTokens]) const handleCloseToken = useCallback( async (tokenMint: PublicKey) => { const client = mangoStore.getState().client const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current const actions = mangoStore.getState().actions if (!mangoAccount || !group) return setSubmitting(true) try { const { signature: tx, slot } = await client.tokenWithdrawAllDepositForMint( group, mangoAccount, tokenMint, ) notify({ title: 'Transaction confirmed', type: 'success', txid: tx, }) await actions.reloadMangoAccount(slot) setSubmitting(false) } catch (e) { console.error(e) setSubmitting(false) if (!isMangoError(e)) return notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } }, [setSubmitting], ) const handleCloseSlots = useCallback( async (closeType: CLOSE_TYPE) => { const client = mangoStore.getState().client const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current const actions = mangoStore.getState().actions if (!mangoAccount || !group) return setSubmitting(true) try { let ixs: TransactionInstruction[] = [] if (closeType === CLOSE_TYPE.PERP) { try { ixs = await Promise.all( emptyPerps.map((p) => client.perpDeactivatePositionIx( group, mangoAccount, p.marketIndex, ), ), ) } catch (e) { console.log('error closing unused perp positions', e) } } else if (closeType === CLOSE_TYPE.SERUMOO) { try { ixs = await Promise.all( emptySerum3.map((s) => { const market = group.getSerum3MarketByMarketIndex(s.marketIndex) return client.serum3CloseOpenOrdersIx( group, mangoAccount, market.serumMarketExternal, ) }), ) } catch (e) { console.log('error closing unused serum open orders', e) } } else if (closeType === CLOSE_TYPE.PERPOO) { // No instruction yet } if (ixs.length === 0) return const tx = await client.sendAndConfirmTransaction(ixs) notify({ title: 'Transaction confirmed', type: 'success', txid: tx.signature, }) await actions.reloadMangoAccount() setSubmitting(false) } catch (e) { console.error(e) setSubmitting(false) if (!isMangoError(e)) return notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } }, [emptyPerps, emptySerum3], ) return mangoAccountAddress && group ? (

{t('settings:account-slots')}

{!isAccountFull ? ( setShowAccountSizeModal(true)} > {t('settings:increase-account-slots')} ) : null}
{({ open }) => ( <>

{t('tokens')}

{t('settings:slots-used', { used: usedTokens.length, total: totalTokens.length, type: t('tokens').toLowerCase(), })}

{t('settings:close-token-positions-desc')}

{usedTokens.length ? ( usedTokens.map((token, i) => { const tokenBank = group.getFirstBankByTokenIndex( token.tokenIndex, ) const status = tokenStatus.find( (t) => t.tokenIndex === token.tokenIndex, ) const isCollateral = tokenBank .scaledInitAssetWeight(tokenBank.price) .toNumber() > 0 return (

{i + 1}.

{tokenBank.name}

{status?.balance}

{status?.isClosable ? ( ) : (

{t('settings:close-instructions')}

)}
) }) ) : (

{t('notifications:empty-state-title')}...

)}
)}
{({ open }) => ( <>

{t('settings:spot-open-orders')}

{t('settings:slots-used', { used: usedSerum3.length, total: totalSerum3.length, type: t('settings:spot-open-orders').toLowerCase(), })}

{t('settings:close-spot-oo-desc')}

{usedSerum3.length ? ( usedSerum3.map((mkt, i) => { const market = group.getSerum3MarketByMarketIndex( mkt.marketIndex, ) const isUnused = !!emptySerum3.find( (m) => m.marketIndex === mkt.marketIndex, ) return (

{i + 1}.

{market.name}

) }) ) : (

{t('notifications:empty-state-title')}...

)}
)}
{({ open }) => ( <>

{t('settings:perp-positions')}

{t('settings:slots-used', { used: usedPerps.length, total: totalPerps.length, type: t('settings:perp-positions').toLowerCase(), })}

{t('settings:close-perp-desc')}

{usedPerps.length ? ( usedPerps.map((perp, i) => { const market = group.getPerpMarketByMarketIndex( perp.marketIndex, ) const isUnused = !!emptyPerps.find( (mkt) => mkt.marketIndex === perp.marketIndex, ) return (

{i + 1}.

{market.name}

) }) ) : (

{t('notifications:empty-state-title')}...

)}
)}
{({ open }) => ( <>

{t('settings:perp-open-orders')}

{t('settings:slots-used', { used: usedPerpOo.length, total: totalPerpOpenOrders.length, type: t('settings:perp-open-orders').toLowerCase(), })}

{usedPerpOo.length ? ( usedPerpOo.map((perp, i) => { const market = group.getPerpMarketByMarketIndex( perp.orderMarket, ) return (

{i + 1}.

{market.name}

) }) ) : (

{t('notifications:empty-state-title')}...

)}
)}
{({ open }) => ( <>

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

{t('settings:trigger-orders-used', { orders: usedTcs.length, })}

{usedTcs.length ? ( usedTcs.map((tcs, i) => { const buyBank = group.getFirstBankByTokenIndex( tcs.buyTokenIndex, ) const sellBank = group.getFirstBankByTokenIndex( tcs.sellTokenIndex, ) const maxBuy = tcs.getMaxBuyUi(group) const maxSell = tcs.getMaxSellUi(group) let side if (maxBuy === 0 || maxBuy > maxSell) { side = 'sell' } else { side = 'buy' } const formattedBuyTokenName = formatTokenSymbol(buyBank.name) const formattedSellTokenName = formatTokenSymbol( sellBank.name, ) const pair = side === 'sell' ? `${formattedSellTokenName}/${formattedBuyTokenName}` : `${formattedBuyTokenName}/${formattedSellTokenName}` return (

{i + 1}.

{pair}

) }) ) : (

{t('notifications:empty-state-title')}...

)}
)}
{showAccountSizeModal ? ( setShowAccountSizeModal(false)} /> ) : null}
) : null } export default AccountSettings const IsUnusedBadge = ({ isUnused }: { isUnused: boolean }) => { const { t } = useTranslation('settings') return (
{isUnused ? ( t('settings:unused') ) : ( {t('settings:in-use')} )}
) }