mango-v4-ui/components/shared/BalancesTable.tsx

498 lines
17 KiB
TypeScript
Raw Normal View History

2023-01-29 20:13:38 -08:00
import { Serum3Market } from '@blockworks-foundation/mango-v4'
2023-07-04 21:40:47 -07:00
import { ChevronDownIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
2022-11-18 09:09:39 -08:00
import useMangoAccount from 'hooks/useMangoAccount'
2022-10-03 19:26:50 -07:00
import { useViewport } from 'hooks/useViewport'
import { useTranslation } from 'next-i18next'
import { useRouter } from 'next/router'
import { useCallback, useMemo } from 'react'
2023-07-17 21:40:24 -07:00
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
2022-10-03 19:26:50 -07:00
import { breakpoints } from 'utils/theme'
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
import { LinkButton } from './Button'
2023-07-27 21:00:51 -07:00
import {
SortableColumnHeader,
Table,
Td,
Th,
TrBody,
TrHead,
} from './TableElements'
2022-11-20 12:20:27 -08:00
import useSelectedMarket from 'hooks/useSelectedMarket'
2023-01-19 17:45:08 -08:00
import ConnectEmptyState from './ConnectEmptyState'
import { useWallet } from '@solana/wallet-adapter-react'
2023-01-23 21:17:08 -08:00
import FormatNumericValue from './FormatNumericValue'
2023-01-28 17:13:36 -08:00
import BankAmountWithValue from './BankAmountWithValue'
2023-01-29 20:13:38 -08:00
import useBanksWithBalances, {
BankWithBalance,
} from 'hooks/useBanksWithBalances'
import useUnownedAccount from 'hooks/useUnownedAccount'
import { Disclosure, Transition } from '@headlessui/react'
2023-07-17 19:58:20 -07:00
import useHealthContributions from 'hooks/useHealthContributions'
import Tooltip from './Tooltip'
2023-07-17 21:40:24 -07:00
import { PublicKey } from '@solana/web3.js'
import { USDC_MINT } from 'utils/constants'
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'
2023-07-27 21:00:51 -07:00
import { useSortableData } from 'hooks/useSortableData'
import TableTokenName from './TableTokenName'
const BalancesTable = () => {
2023-07-17 19:58:20 -07:00
const { t } = useTranslation(['common', 'account', 'trade'])
2023-01-19 19:10:15 -08:00
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances)
2022-10-03 19:26:50 -07:00
const { width } = useViewport()
2023-01-19 17:45:08 -08:00
const { connected } = useWallet()
2022-10-03 19:26:50 -07:00
const showTableView = width ? width > breakpoints.md : false
2023-01-29 20:13:38 -08:00
const banks = useBanksWithBalances('balance')
2023-07-17 19:58:20 -07:00
const { initContributions } = useHealthContributions()
2023-01-29 20:13:38 -08:00
const filteredBanks = useMemo(() => {
2023-10-17 21:05:22 -07:00
if (banks.length && mangoAccountAddress) {
2023-01-29 20:13:38 -08:00
return banks.filter((b) => {
return (
Math.abs(floorToDecimal(b.balance, b.bank.mintDecimals).toNumber()) >
0 ||
spotBalances[b.bank.mint.toString()]?.unsettled > 0 ||
spotBalances[b.bank.mint.toString()]?.inOrders > 0
)
})
}
return []
2023-10-17 21:05:22 -07:00
}, [banks, mangoAccountAddress])
2023-07-27 22:04:38 -07:00
const formattedTableData = useCallback(() => {
const formatted = []
for (const b of filteredBanks) {
const bank = b.bank
const balance = b.balance
2023-07-31 19:26:08 -07:00
const balanceValue = balance * bank.uiPrice
2023-07-27 22:04:38 -07:00
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
2023-07-27 21:00:51 -07:00
2023-07-27 22:04:38 -07:00
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
2023-07-27 21:00:51 -07:00
2023-07-27 22:04:38 -07:00
const collateralValue =
initContributions.find((val) => val.asset === bank.name)
?.contribution || 0
2023-07-27 21:00:51 -07:00
2023-07-27 22:04:38 -07:00
const assetWeight = bank.scaledInitAssetWeight(bank.price)
const liabWeight = bank.scaledInitLiabWeight(bank.price)
2023-07-27 21:00:51 -07:00
2023-07-27 22:04:38 -07:00
const data = {
assetWeight,
balance,
2023-07-31 19:26:08 -07:00
balanceValue,
2023-07-27 22:04:38 -07:00
bankWithBalance: b,
collateralValue,
inOrders,
liabWeight,
symbol,
unsettled,
2023-07-27 21:00:51 -07:00
}
2023-07-27 22:04:38 -07:00
formatted.push(data)
}
return formatted
}, [filteredBanks])
2023-07-27 21:00:51 -07:00
const {
items: tableData,
requestSort,
sortConfig,
2023-07-27 22:04:38 -07:00
} = useSortableData(formattedTableData())
2023-07-27 21:00:51 -07:00
2023-01-29 20:13:38 -08:00
return filteredBanks.length ? (
2022-11-04 06:14:50 -07:00
showTableView ? (
2022-11-20 02:44:14 -08:00
<Table>
2022-11-04 06:14:50 -07:00
<thead>
2022-11-20 02:44:14 -08:00
<TrHead>
2023-07-27 21:00:51 -07:00
<Th className="text-left">
<SortableColumnHeader
sortKey="symbol"
sort={() => requestSort('symbol')}
sortConfig={sortConfig}
title={t('token')}
/>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
2023-07-31 19:26:08 -07:00
sortKey="balanceValue"
sort={() => requestSort('balanceValue')}
2023-07-27 21:00:51 -07:00
sortConfig={sortConfig}
title={t('balance')}
/>
</div>
</Th>
2023-07-17 19:58:20 -07:00
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-collateral-value')}>
2023-07-27 21:00:51 -07:00
<SortableColumnHeader
sortKey="collateralValue"
sort={() => requestSort('collateralValue')}
sortConfig={sortConfig}
title={t('collateral-value')}
titleClass="tooltip-underline"
/>
2023-07-17 19:58:20 -07:00
</Tooltip>
</div>
</Th>
2023-07-27 21:00:51 -07:00
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="inOrders"
sort={() => requestSort('inOrders')}
sortConfig={sortConfig}
title={t('trade:in-orders')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="unsettled"
sort={() => requestSort('unsettled')}
sortConfig={sortConfig}
title={t('trade:unsettled')}
/>
</div>
2022-11-20 02:44:14 -08:00
</Th>
</TrHead>
2022-11-04 06:14:50 -07:00
</thead>
<tbody>
2023-07-27 21:00:51 -07:00
{tableData.map((data) => {
const {
assetWeight,
balance,
bankWithBalance,
collateralValue,
inOrders,
liabWeight,
symbol,
unsettled,
} = data
const bank = bankWithBalance.bank
2023-07-17 19:58:20 -07:00
2022-11-04 06:14:50 -07:00
return (
2023-01-29 20:13:38 -08:00
<TrBody key={bank.name} className="text-sm">
2022-11-20 02:44:14 -08:00
<Td>
<TableTokenName bank={bank} symbol={symbol} />
2022-11-20 02:44:14 -08:00
</Td>
<Td className="text-right">
2023-07-27 21:00:51 -07:00
<Balance bank={bankWithBalance} />
2022-11-04 06:14:50 -07:00
<p className="text-sm text-th-fgd-4">
2023-01-23 21:17:08 -08:00
<FormatNumericValue
2023-07-27 21:00:51 -07:00
value={mangoAccount ? balance * bank.uiPrice : 0}
2023-01-23 21:17:08 -08:00
isUsd
/>
2022-11-04 06:14:50 -07:00
</p>
2022-11-20 02:44:14 -08:00
</Td>
2023-07-17 19:58:20 -07:00
<Td className="text-right">
<p>
2023-07-17 20:09:57 -07:00
<FormatNumericValue
value={collateralValue}
decimals={2}
isUsd
/>
2023-07-17 19:58:20 -07:00
</p>
<p className="text-sm text-th-fgd-4">
<FormatNumericValue
2023-07-17 20:09:57 -07:00
value={
2023-07-27 21:00:51 -07:00
collateralValue <= -0.01
? liabWeight.toFixed(2)
: assetWeight.toFixed(2)
2023-07-17 20:09:57 -07:00
}
2023-07-17 19:58:20 -07:00
/>
x
</p>
</Td>
2023-01-08 19:38:09 -08:00
<Td className="text-right">
2023-01-28 17:13:36 -08:00
<BankAmountWithValue amount={inOrders} bank={bank} stacked />
2022-11-20 02:44:14 -08:00
</Td>
2023-01-08 19:38:09 -08:00
<Td className="text-right">
2023-01-28 17:13:36 -08:00
<BankAmountWithValue amount={unsettled} bank={bank} stacked />
2022-11-20 02:44:14 -08:00
</Td>
</TrBody>
2022-11-04 06:14:50 -07:00
)
})}
</tbody>
2022-11-20 02:44:14 -08:00
</Table>
2022-11-04 06:14:50 -07:00
) : (
<div className="border-b border-th-bkg-3">
2023-07-27 21:00:51 -07:00
{tableData.map((data, i) => {
const {
assetWeight,
balance,
bankWithBalance,
collateralValue,
inOrders,
liabWeight,
symbol,
unsettled,
} = data
const bank = bankWithBalance.bank
2023-07-17 19:58:20 -07:00
return (
<Disclosure key={bank.name}>
{({ open }) => (
<>
<Disclosure.Button
2023-08-30 21:42:29 -07:00
as="div"
2023-03-19 03:28:13 -07:00
className={`w-full border-t border-th-bkg-3 p-4 text-left focus:outline-none ${
i === 0 ? 'border-t-0' : ''
}`}
>
<div className="flex items-center justify-between">
<TableTokenName bank={bank} symbol={symbol} />
<div className="flex items-center space-x-2">
<div className="text-right">
2023-07-27 21:00:51 -07:00
<Balance bank={bankWithBalance} />
<span className="font-mono text-xs text-th-fgd-3">
<FormatNumericValue
2023-07-27 21:00:51 -07:00
value={mangoAccount ? balance * bank.uiPrice : 0}
isUsd
/>
</span>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-0'
} h-6 w-6 shrink-0 text-th-fgd-3`}
/>
</div>
</div>
</Disclosure.Button>
<Transition
enter="transition ease-in duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
>
<Disclosure.Panel>
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pb-4 pt-4">
2023-07-17 19:58:20 -07:00
<div className="col-span-1">
<Tooltip
content={t('account:tooltip-collateral-value')}
>
<p className="tooltip-underline text-xs text-th-fgd-3">
{t('collateral-value')}
2023-07-17 19:58:20 -07:00
</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">
2023-07-17 20:09:57 -07:00
<FormatNumericValue
value={collateralValue}
decimals={2}
isUsd
/>
2023-07-17 19:58:20 -07:00
<span className="text-th-fgd-3">
{' '}
<FormatNumericValue
value={
2023-07-17 20:09:57 -07:00
collateralValue <= -0.01
2023-07-27 21:00:51 -07:00
? liabWeight.toFixed(2)
: assetWeight.toFixed(2)
2023-07-17 19:58:20 -07:00
}
/>
x
</span>
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('trade:in-orders')}
</p>
<BankAmountWithValue amount={inOrders} bank={bank} />
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('trade:unsettled')}
</p>
<BankAmountWithValue amount={unsettled} bank={bank} />
</div>
</div>
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
2022-11-04 06:14:50 -07:00
)
})}
</div>
2022-11-04 06:14:50 -07:00
)
2023-01-19 19:10:15 -08:00
) : mangoAccountAddress || connected ? (
2022-11-04 06:14:50 -07:00
<div className="flex flex-col items-center p-8">
2023-01-15 01:53:34 -08:00
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
2022-11-04 06:14:50 -07:00
<p>{t('trade:no-balances')}</p>
</div>
2023-01-19 17:45:08 -08:00
) : (
<div className="p-8">
<ConnectEmptyState text={t('connect-balances')} />
</div>
)
}
export default BalancesTable
2023-01-29 20:13:38 -08:00
const Balance = ({ bank }: { bank: BankWithBalance }) => {
2022-11-20 12:20:27 -08:00
const { selectedMarket } = useSelectedMarket()
const { asPath } = useRouter()
const { isUnownedAccount } = useUnownedAccount()
2023-08-31 20:22:50 -07:00
const { isDesktop } = useViewport()
2023-01-29 20:13:38 -08:00
const tokenBank = bank.bank
const handleTradeFormBalanceClick = useCallback(
(balance: number, type: 'base' | 'quote') => {
const set = mangoStore.getState().set
const group = mangoStore.getState().group
const tradeForm = mangoStore.getState().tradeForm
if (!group || !selectedMarket) return
let price: number
if (tradeForm.tradeType === 'Market') {
const orderbook = mangoStore.getState().selectedMarket.orderbook
const side =
(balance > 0 && type === 'quote') || (balance < 0 && type === 'base')
? 'buy'
: 'sell'
price = calculateLimitPriceForMarketOrder(orderbook, balance, side)
} else {
price = Number(tradeForm.price)
}
let minOrderDecimals: number
2023-01-24 19:11:42 -08:00
let tickDecimals: number
if (selectedMarket instanceof Serum3Market) {
const market = group.getSerum3ExternalMarket(
2023-07-21 11:47:53 -07:00
selectedMarket.serumMarketExternal,
)
minOrderDecimals = getDecimalCount(market.minOrderSize)
2023-01-24 19:11:42 -08:00
tickDecimals = getDecimalCount(market.tickSize)
} else {
minOrderDecimals = getDecimalCount(selectedMarket.minOrderSize)
2023-01-24 19:11:42 -08:00
tickDecimals = getDecimalCount(selectedMarket.tickSize)
}
if (type === 'quote') {
2023-01-24 19:11:42 -08:00
const floorBalance = floorToDecimal(balance, tickDecimals).toNumber()
const baseSize = floorToDecimal(
floorBalance / price,
2023-07-21 11:47:53 -07:00
minOrderDecimals,
2023-01-24 19:11:42 -08:00
).toNumber()
const quoteSize = floorToDecimal(baseSize * price, tickDecimals)
set((s) => {
s.tradeForm.baseSize = baseSize.toString()
s.tradeForm.quoteSize = quoteSize.toString()
})
} else {
2023-01-24 19:11:42 -08:00
const baseSize = floorToDecimal(balance, minOrderDecimals).toNumber()
const quoteSize = floorToDecimal(baseSize * price, tickDecimals)
set((s) => {
s.tradeForm.baseSize = baseSize.toString()
s.tradeForm.quoteSize = quoteSize.toString()
})
}
},
2023-07-21 11:47:53 -07:00
[selectedMarket],
)
const handleSwapFormBalanceClick = useCallback(
(balance: number) => {
const set = mangoStore.getState().set
2023-07-17 21:40:24 -07:00
const group = mangoStore.getState().group
const swap = mangoStore.getState().swap
const usdcBank = group?.getFirstBankByMint(new PublicKey(USDC_MINT))
const solBank = group?.getFirstBankByMint(WRAPPED_SOL_MINT)
if (balance >= 0) {
set((s) => {
2023-01-29 20:13:38 -08:00
s.swap.inputBank = tokenBank
s.swap.amountIn = balance.toString()
s.swap.amountOut = ''
s.swap.swapMode = 'ExactIn'
2023-07-17 21:40:24 -07:00
if (tokenBank.name === swap.outputBank?.name) {
s.swap.outputBank =
swap.outputBank.name === 'USDC' ? solBank : usdcBank
}
2023-09-07 20:35:30 -07:00
s.swap.triggerPrice = ''
})
} else {
set((s) => {
2023-09-07 20:35:30 -07:00
if (swap.swapOrTrigger === 'swap') {
s.swap.outputBank = tokenBank
s.swap.amountIn = ''
s.swap.amountOut = Math.abs(balance).toString()
s.swap.swapMode = 'ExactOut'
if (tokenBank.name === swap.inputBank?.name) {
s.swap.inputBank =
swap.inputBank.name === 'USDC' ? solBank : usdcBank
}
} else {
s.swap.inputBank = tokenBank
s.swap.amountIn = Math.abs(balance).toString()
s.swap.amountOut = ''
if (tokenBank.name === swap.outputBank?.name) {
s.swap.outputBank =
swap.outputBank.name === 'USDC' ? solBank : usdcBank
}
2023-07-17 21:40:24 -07:00
}
})
}
},
2023-07-21 11:47:53 -07:00
[bank],
)
2023-01-29 20:13:38 -08:00
const balance = bank.balance
const isBaseOrQuote = useMemo(() => {
2022-12-16 09:27:12 -08:00
if (selectedMarket instanceof Serum3Market) {
2023-01-29 20:13:38 -08:00
if (tokenBank.tokenIndex === selectedMarket.baseTokenIndex) {
return 'base'
2023-01-29 20:13:38 -08:00
} else if (tokenBank.tokenIndex === selectedMarket.quoteTokenIndex) {
return 'quote'
} else return ''
}
2023-01-29 20:13:38 -08:00
}, [tokenBank, selectedMarket])
2023-07-06 06:16:19 -07:00
if (!balance) return <p className="md:flex md:justify-end">0</p>
return (
<p className="font-mono text-th-fgd-2 md:flex md:justify-end">
2023-08-31 20:22:50 -07:00
{!isUnownedAccount && isDesktop ? (
asPath.includes('/trade') && isBaseOrQuote ? (
<LinkButton
2023-04-20 19:32:20 -07:00
className="font-normal underline underline-offset-2 md:underline-offset-4 md:hover:no-underline"
onClick={() =>
handleTradeFormBalanceClick(Math.abs(balance), isBaseOrQuote)
}
>
<FormatNumericValue
value={balance}
decimals={tokenBank.mintDecimals}
/>
</LinkButton>
) : asPath.includes('/swap') ? (
<LinkButton
2023-04-20 19:32:20 -07:00
className="font-normal underline underline-offset-2 md:underline-offset-4 md:hover:no-underline"
onClick={() =>
handleSwapFormBalanceClick(
2023-07-21 11:47:53 -07:00
Number(floorToDecimal(balance, tokenBank.mintDecimals)),
)
}
>
<FormatNumericValue
value={balance}
decimals={tokenBank.mintDecimals}
/>
</LinkButton>
) : (
2023-01-29 20:13:38 -08:00
<FormatNumericValue
value={balance}
decimals={tokenBank.mintDecimals}
/>
)
) : (
2023-01-29 20:13:38 -08:00
<FormatNumericValue value={balance} decimals={tokenBank.mintDecimals} />
)}
</p>
)
}