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": "暗",