import HideMangoAccount from '@components/account/HideMangoAccount' import MangoAccountSizeModal from '@components/modals/MangoAccountSizeModal' import Button, { IconButton, 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, DocumentDuplicateIcon, PencilIcon, SquaresPlusIcon, TrashIcon, UserPlusIcon, } 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 { PRIVACY_MODE } from 'utils/constants' import mangoStore from '@store/mangoStore' import { PublicKey } 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' import { handleCopyAddress } from '@components/account/AccountActions' import AccountNameModal from '@components/modals/AccountNameModal' import DelegateModal, { DEFAULT_DELEGATE, } from '@components/modals/DelegateModal' import { abbreviateAddress } from 'utils/formatting' import CloseAccountModal from '@components/modals/CloseAccountModal' import useLocalStorageState from 'hooks/useLocalStorageState' import Switch from '@components/forms/Switch' import useUnownedAccount from 'hooks/useUnownedAccount' import { useWallet } from '@solana/wallet-adapter-react' import CreateAccountForm from '@components/account/CreateAccountForm' import ConnectEmptyState from '@components/shared/ConnectEmptyState' import { Serum3Market } from '@blockworks-foundation/mango-v4' export const SETTINGS_BUTTON_TITLE_CLASSES = 'text-th-fgd-1' 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 ROW_BUTTON_CLASSNAMES = 'flex items-center justify-between border-t border-th-bkg-3 py-4 w-full md:px-4 focus:outline-none md:hover:bg-th-bkg-2' const AccountSettings = () => { const { t } = useTranslation(['common', 'settings', 'trade']) const { mangoAccount, mangoAccountAddress } = useMangoAccount() const { group } = useMangoGroup() const { isDelegatedAccount, isUnownedAccount } = useUnownedAccount() const { connected } = useWallet() const [showAccountSizeModal, setShowAccountSizeModal] = useState(false) const [showEditAccountModal, setShowEditAccountModal] = useState(false) const [showCloseAccountModal, setShowCloseAccountModal] = useState(false) const [privacyMode, setPrivacyMode] = useLocalStorageState(PRIVACY_MODE) const [showDelegateModal, setShowDelegateModal] = 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 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', }) } }, [mangoAccount, setSubmitting], ) const handleCloseSerumOos = useCallback( async (market: Serum3Market) => { const client = mangoStore.getState().client const group = mangoStore.getState().group const actions = mangoStore.getState().actions if (!mangoAccount || !group) return setSubmitting(true) try { const ix = await client.serum3CloseOpenOrdersIx( group, mangoAccount, market.serumMarketExternal, ) const tx = await client.sendAndConfirmTransaction([ix]) notify({ title: 'Transaction confirmed', type: 'success', txid: tx.signature, }) await actions.reloadMangoAccount() } catch (e) { console.error(e) if (!isMangoError(e)) return notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } finally { setSubmitting(false) } }, [mangoAccount], ) const handleClosePerpAccounts = useCallback( async (marketIndex: number) => { const client = mangoStore.getState().client const group = mangoStore.getState().group const actions = mangoStore.getState().actions if (!mangoAccount || !group) return setSubmitting(true) try { const ix = await client.perpDeactivatePositionIx( group, mangoAccount, marketIndex, ) const tx = await client.sendAndConfirmTransaction([ix]) notify({ title: 'Transaction confirmed', type: 'success', txid: tx.signature, }) await actions.reloadMangoAccount() } catch (e) { console.error(e) if (!isMangoError(e)) return notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } finally { setSubmitting(false) } }, [mangoAccount], ) const getSlotsUsedColor = (used: number, total: number) => { if (used === total) { return 'text-th-error' } else if (used / total >= 0.75) { return 'text-th-warning' } else return 'text-th-success' } return mangoAccount && group && !isDelegatedAccount && !isUnownedAccount ? (

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

{mangoAccount.publicKey.toString()}

handleCopyAddress( mangoAccount.publicKey.toString(), t('copy-address-success', { pk: abbreviateAddress(mangoAccount.publicKey), }), ) } >

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

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

{!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-markets')}

{t('settings:slots-used', { used: usedSerum3.length, total: totalSerum3.length, type: t('settings:spot-markets').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}

{isUnused ? ( ) : ( )}
) }) ) : (

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

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

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

{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}

{isUnused ? ( ) : ( )}
) }) ) : (

{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')}...

)}
)}

{t('settings:privacy')}

{t('settings:privacy-mode')}

setPrivacyMode(!privacyMode)} />
{showAccountSizeModal ? ( setShowAccountSizeModal(false)} /> ) : null} {showEditAccountModal ? ( setShowEditAccountModal(false)} /> ) : null} {showDelegateModal ? ( setShowDelegateModal(false)} /> ) : null} {showCloseAccountModal ? ( setShowCloseAccountModal(false)} /> ) : null}
) : isUnownedAccount ? (

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

) : isDelegatedAccount ? (

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

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