diff --git a/components/settings/AccountSettings.tsx b/components/settings/AccountSettings.tsx index 5fb58c6d..478b54a8 100644 --- a/components/settings/AccountSettings.tsx +++ b/components/settings/AccountSettings.tsx @@ -1,22 +1,38 @@ import MangoAccountSizeModal from '@components/modals/MangoAccountSizeModal' +import ActionsLinkButton from '@components/account/ActionsLinkButton' import { 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 { Disclosure, Popover, Transition } from '@headlessui/react' +import { + ChevronDownIcon, + MinusCircleIcon, + SquaresPlusIcon, +} from '@heroicons/react/20/solid' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import mangoStore from '@store/mangoStore' import useMangoAccount from 'hooks/useMangoAccount' import useMangoAccountAccounts, { getAvaialableAccountsColor, } from 'hooks/useMangoAccountAccounts' import useMangoGroup from 'hooks/useMangoGroup' import { useTranslation } from 'next-i18next' -import { useState } from 'react' +import { Fragment, useState } from 'react' import { MAX_ACCOUNTS } from 'utils/constants' +import { isMangoError } from 'types' +import { notify } from 'utils/notifications' + +enum CLOSE_TYPE { + TOKEN, + PERP, + SERUMOO, + PERPOO, +} const AccountSettings = () => { const { t } = useTranslation(['common', 'settings']) - const { mangoAccountAddress } = useMangoAccount() + const { mangoAccountAddress, mangoAccount } = useMangoAccount() const { group } = useMangoGroup() const [showAccountSizeModal, setShowAccountSizeModal] = useState(false) const { @@ -24,6 +40,10 @@ const AccountSettings = () => { usedSerum3, usedPerps, usedPerpOo, + // emptyTokens, + emptySerum3, + emptyPerps, + // emptyPerpOo, totalTokens, totalSerum3, totalPerps, @@ -31,6 +51,57 @@ const AccountSettings = () => { isAccountFull, } = useMangoAccountAccounts() + const handleCloseSlots = 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 + try { + let ixs: TransactionInstruction[] = [] + if (closeType == CLOSE_TYPE.TOKEN) { + // No instruction yet + } else if (closeType === CLOSE_TYPE.PERP) { + ixs = await Promise.all( + emptyPerps.map((p) => + client.perpDeactivatePositionIx(group, mangoAccount, p.marketIndex), + ), + ) + } else if (closeType === CLOSE_TYPE.SERUMOO) { + ixs = await Promise.all( + emptySerum3.map((s) => + client.serum3CloseOpenOrdersIx( + group, + mangoAccount, + new PublicKey(s), + ), + ), + ) + } 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() + } catch (e) { + console.error(e) + if (!isMangoError(e)) return + notify({ + title: 'Transaction failed', + description: e.message, + txid: e?.txid, + type: 'error', + }) + } + } + return mangoAccountAddress && group ? ( <>

{t('account')}

@@ -45,6 +116,67 @@ const AccountSettings = () => { {t('settings:increase-account-size')} ) : null} + {emptySerum3.length > 0 || emptyPerps.length > 0 ? ( + + {({ open }) => ( + <> + + + + {t('settings:close-unused-slots')} + + + + + {/* console.log('handle close tokens')} + > + {'Close unused token positions'} + */} + handleCloseSlots(CLOSE_TYPE.SERUMOO)} + > + + {t('settings:close-spot-oo')} + + + handleCloseSlots(CLOSE_TYPE.PERP)} + > + {t('settings:close-perp')} + + {/* console.log('close perp oos')} + > + {'Close unused perp OOs'} + */} + + + + )} + + ) : null} {({ open }) => ( diff --git a/hooks/useMangoAccountAccounts.ts b/hooks/useMangoAccountAccounts.ts index cdc279b1..89dcc740 100644 --- a/hooks/useMangoAccountAccounts.ts +++ b/hooks/useMangoAccountAccounts.ts @@ -8,6 +8,9 @@ import { TokenPosition, } from '@blockworks-foundation/mango-v4' import { MAX_ACCOUNTS } from 'utils/constants' +import useBanksWithBalances from './useBanksWithBalances' +import { OpenOrders } from '@project-serum/serum' +import { BN } from '@coral-xyz/anchor' export const getAvaialableAccountsColor = (used: number, total: number) => { const remaining = total - used @@ -44,11 +47,22 @@ const getIsAccountSizeFull = () => { } export default function useMangoAccountAccounts() { - const { mangoAccountAddress } = useMangoAccount() + const { mangoAccountAddress, mangoAccount } = useMangoAccount() + const banks = useBanksWithBalances() + + const [ + usedTokens, + usedSerum3, + usedPerps, + usedPerpOo, + emptyTokens, + emptySerum3, + emptyPerps, + emptyPerpOo, + ] = useMemo(() => { + if (!mangoAccountAddress || !mangoAccount) + return [[], [], [], [], [], [], [], []] - const [usedTokens, usedSerum3, usedPerps, usedPerpOo] = useMemo(() => { - const mangoAccount = mangoStore.getState().mangoAccount.current - if (!mangoAccountAddress || !mangoAccount) return [[], [], [], []] const { tokens, serum3, perps, perpOpenOrders } = mangoAccount const usedTokens: TokenPosition[] = tokens.filter((t) => t.inUseCount) const usedSerum3: Serum3Orders[] = serum3.filter( @@ -60,8 +74,48 @@ export default function useMangoAccountAccounts() { const usedPerpOo: PerpOo[] = perpOpenOrders.filter( (p) => p.orderMarket !== 65535, ) - return [usedTokens, usedSerum3, usedPerps, usedPerpOo] - }, [mangoAccountAddress]) + + // const emptyPerpOo = [] // No instruction for closing perp oo + const emptyTokens = usedTokens.filter((t) => { + const bank = banks.find((b) => b.bank.tokenIndex === t.tokenIndex) + if (!bank) return false + return t.inUseCount && bank.balance === 0 && bank.borrowedAmount === 0 + }) + + const emptyPerps = usedPerps.filter( + (p) => + p.asksBaseLots.isZero() && + p.bidsBaseLots.isZero() && + p.takerBaseLots.isZero && + p.takerQuoteLots.isZero() && + p.basePositionLots.isZero() && + p.quotePositionNative.isZero(), + ) + + const usedOpenOrders = usedSerum3 + .map((s) => mangoAccount.serum3OosMapByMarketIndex.get(s.marketIndex)) + .filter((o) => o !== undefined) as OpenOrders[] + const maxFreeSlotBits = new BN(2).pow(new BN(128)).sub(new BN(1)) // 2^128 - 1 + const emptySerum3 = usedOpenOrders + .filter( + (o) => + o.baseTokenTotal.isZero() && + o.quoteTokenTotal.isZero() && + o.freeSlotBits.eq(maxFreeSlotBits), + ) + .map((f) => f.market) + + return [ + usedTokens, + usedSerum3, + usedPerps, + usedPerpOo, + emptyTokens, + emptySerum3, + emptyPerps, + [], + ] + }, [mangoAccountAddress, mangoAccount]) const [totalTokens, totalSerum3, totalPerps, totalPerpOpenOrders] = useMemo(() => { @@ -73,7 +127,7 @@ export default function useMangoAccountAccounts() { const totalPerps = perps const totalPerpOpenOrders = perpOpenOrders return [totalTokens, totalSerum3, totalPerps, totalPerpOpenOrders] - }, [mangoAccountAddress]) + }, [mangoAccountAddress, mangoAccount]) // const [availableTokens, availableSerum3, availablePerps, availablePerpOo] = // useMemo(() => { @@ -99,6 +153,10 @@ export default function useMangoAccountAccounts() { usedSerum3, usedPerps, usedPerpOo, + emptyTokens, + emptySerum3, + emptyPerps, + emptyPerpOo, totalTokens, totalSerum3, totalPerps, diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index f7323349..ddb4f1a3 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -19,6 +19,9 @@ "chart-right": "Chart Right", "chinese": "简体中文", "chinese-traditional": "繁體中文", + "close-unused-slots": "Close Unused Slots", + "close-spot-oo": "Close Spot Open Orders", + "close-perp": "Close Perp Positions", "connect-notifications": "Connect to update your notification settings", "custom": "Custom", "dark": "Dark", diff --git a/public/locales/es/settings.json b/public/locales/es/settings.json index f7323349..ddb4f1a3 100644 --- a/public/locales/es/settings.json +++ b/public/locales/es/settings.json @@ -19,6 +19,9 @@ "chart-right": "Chart Right", "chinese": "简体中文", "chinese-traditional": "繁體中文", + "close-unused-slots": "Close Unused Slots", + "close-spot-oo": "Close Spot Open Orders", + "close-perp": "Close Perp Positions", "connect-notifications": "Connect to update your notification settings", "custom": "Custom", "dark": "Dark", diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json index f7323349..ddb4f1a3 100644 --- a/public/locales/ru/settings.json +++ b/public/locales/ru/settings.json @@ -19,6 +19,9 @@ "chart-right": "Chart Right", "chinese": "简体中文", "chinese-traditional": "繁體中文", + "close-unused-slots": "Close Unused Slots", + "close-spot-oo": "Close Spot Open Orders", + "close-perp": "Close Perp Positions", "connect-notifications": "Connect to update your notification settings", "custom": "Custom", "dark": "Dark", diff --git a/public/locales/zh/settings.json b/public/locales/zh/settings.json index 6e27d9ef..e75bfeef 100644 --- a/public/locales/zh/settings.json +++ b/public/locales/zh/settings.json @@ -19,6 +19,9 @@ "chart-right": "图表右", "chinese": "简体中文", "chinese-traditional": "繁體中文", + "close-unused-slots": "Close Unused Slots", + "close-spot-oo": "Close Spot Open Orders", + "close-perp": "Close Perp Positions", "connect-notifications": "Connect to update your notification settings", "custom": "自定", "dark": "暗", diff --git a/public/locales/zh_tw/settings.json b/public/locales/zh_tw/settings.json index d591cfa1..c1c4ca62 100644 --- a/public/locales/zh_tw/settings.json +++ b/public/locales/zh_tw/settings.json @@ -19,6 +19,9 @@ "chart-right": "圖表右", "chinese": "简体中文", "chinese-traditional": "繁體中文", + "close-unused-slots": "Close Unused Slots", + "close-spot-oo": "Close Spot Open Orders", + "close-perp": "Close Perp Positions", "connect-notifications": "連接錢包來切換通知設定", "custom": "自定", "dark": "暗",