Add perps balances table (#714)

* Add perps balances table

* fix: updated svg

---------

Co-authored-by: Linkie Link <linkielink.dev@gmail.com>
This commit is contained in:
Bob van der Helm 2024-01-08 10:14:32 +01:00 committed by GitHub
parent 7707586c57
commit 117de1e3e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 443 additions and 15 deletions

View File

@ -3,6 +3,7 @@ import { getCreditManagerQueryClient } from 'api/cosmwasm-client'
import getDepositedVaults from 'api/vaults/getDepositedVaults'
import { BNCoin } from 'types/classes/BNCoin'
import { Positions } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { resolvePerpsPositions } from 'utils/resolvers'
export default async function getAccount(
chainConfig: ChainConfig,
@ -29,7 +30,7 @@ export default async function getAccount(
lends: accountPosition.lends.map((lend) => new BNCoin(lend)),
deposits: accountPosition.deposits.map((deposit) => new BNCoin(deposit)),
vaults: depositedVaults,
perps: accountPosition.perps,
perps: resolvePerpsPositions(accountPosition.perps),
kind: accountKind,
}
}

View File

@ -0,0 +1,8 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.9999 8.33424L11.6666 5.0009M2.08325 17.9176L4.90356 17.6042C5.24813 17.5659 5.42042 17.5468 5.58146 17.4946C5.72433 17.4484 5.86029 17.383 5.98566 17.3004C6.12696 17.2072 6.24954 17.0846 6.49469 16.8395L17.4999 5.83424C18.4204 4.91376 18.4204 3.42138 17.4999 2.5009C16.5795 1.58043 15.0871 1.58043 14.1666 2.5009L3.16136 13.5061C2.91621 13.7513 2.79363 13.8739 2.70045 14.0152C2.61778 14.1405 2.55243 14.2765 2.50618 14.4194C2.45405 14.5804 2.43491 14.7527 2.39662 15.0973L2.08325 17.9176Z"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View File

@ -21,6 +21,7 @@ export { default as Compass } from 'components/Icons/Compass.svg'
export { default as Copy } from 'components/Icons/Copy.svg'
export { default as Cross } from 'components/Icons/Cross.svg'
export { default as CrossCircled } from 'components/Icons/CrossCircled.svg'
export { default as Edit } from 'components/Icons/Edit.svg'
export { default as Enter } from 'components/Icons/Enter.svg'
export { default as ExclamationMarkCircled } from 'components/Icons/ExclamationMarkCircled.svg'
export { default as ExclamationMarkTriangle } from 'components/Icons/ExclamationMarkTriangle.svg'

View File

@ -0,0 +1,32 @@
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import usePrice from 'hooks/usePrice'
export const ENTRY_PRICE_META = {
accessorKey: 'entryPrice',
header: () => (
<div className='flex flex-col gap-1'>
<Text size='xs'>Entry Price</Text>
<Text size='xs' className='text-white/40'>
Current Price
</Text>
</div>
),
}
type Props = {
entryPrice: BigNumber
asset: Asset
}
export default function EntryPrice(props: Props) {
const price = usePrice(props.asset.denom)
return (
<TitleAndSubCell
title={<FormattedNumber amount={props.entryPrice.toNumber()} options={{ prefix: '$' }} />}
sub={<FormattedNumber amount={price.toNumber()} options={{ prefix: '$' }} />}
/>
)
}

View File

@ -0,0 +1,46 @@
import React, { useMemo } from 'react'
import DropDownButton from 'components/Button/DropDownButton'
import { Cross, Edit } from 'components/Icons'
import { PerpPositionRow } from 'components/Perps/BalancesTable/usePerpsBalancesData'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store'
export const MANAGE_META = { id: 'manage', header: 'Manage' }
interface Props {
perpPosition: PerpPositionRow
}
export default function Manage(props: Props) {
const currentAccount = useCurrentAccount()
const closePerpPosition = useStore((s) => s.closePerpPosition)
const ITEMS: DropDownItem[] = useMemo(
() => [
{
icon: <Edit />,
text: 'Edit Position Size',
onClick: () => {},
},
{
icon: <Cross width={16} />,
text: 'Close Position',
onClick: async () => {
if (!currentAccount) return
await closePerpPosition({
accountId: currentAccount.id,
denom: props.perpPosition.asset.denom,
})
},
},
],
[closePerpPosition, currentAccount, props.perpPosition.asset.denom],
)
return (
<div className='flex justify-end'>
<DropDownButton items={ITEMS} text='Manage' color='tertiary' />
</div>
)
}

View File

@ -0,0 +1,16 @@
import AssetImage from 'components/Asset/AssetImage'
import TitleAndSubCell from 'components/TitleAndSubCell'
export const PERP_NAME_META = { accessorKey: 'asset.symbol', header: 'Asset', id: 'symbol' }
type Props = {
asset: Asset
}
export function PerpName(props: Props) {
return (
<div className='flex gap-3'>
<AssetImage asset={props.asset} size={32} />
<TitleAndSubCell title={props.asset.name} sub={`${props.asset.symbol}-USD`} />
</div>
)
}

View File

@ -0,0 +1,24 @@
import classNames from 'classnames'
import Text from 'components/Text'
export const PERP_TYPE_META = { accessorKey: 'type', header: 'Side' }
type Props = {
type: PerpsType
}
export default function PerpType(props: Props) {
return (
<Text
size='xs'
className={classNames(
'capitalize px-1 py-0.5 rounded-sm inline',
props.type === 'short' && 'text-error bg-error/20',
props.type === 'long' && 'text-success bg-success/20',
)}
>
{props.type}
</Text>
)
}

View File

@ -0,0 +1,28 @@
import classNames from 'classnames'
import DisplayCurrency from 'components/DisplayCurrency'
import { BNCoin } from 'types/classes/BNCoin'
export const PNL_META = { accessorKey: 'pnl', header: 'Total PnL', id: 'pnl' }
type Props = {
pnl: BNCoin
}
export default function PnL(props: Props) {
const isNegative = props.pnl.amount.isNegative()
return (
<span
className={classNames(
'text-xs',
isNegative ? 'text-error' : props.pnl.amount.isZero() ? '' : 'text-success',
)}
>
{isNegative ? '-' : props.pnl.amount.isZero() ? '' : '+'}
<DisplayCurrency
className='inline'
coin={BNCoin.fromDenomAndBigNumber(props.pnl.denom, props.pnl.amount.abs())}
/>
</span>
)
}

View File

@ -0,0 +1,45 @@
import { useMemo } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import usePrice from 'hooks/usePrice'
import { demagnify } from 'utils/formatters'
export const SIZE_META = {
accessorKey: 'size',
header: () => (
<div className='flex flex-col gap-1'>
<Text size='xs'>Size</Text>
<Text size='xs' className='text-white/40'>
Value
</Text>
</div>
),
}
type Props = {
size: BigNumber
asset: Asset
}
export default function Size(props: Props) {
const price = usePrice(props.asset.denom)
const amount = useMemo(
() => demagnify(props.size.toString(), props.asset),
[props.asset, props.size],
)
const value = useMemo(() => price.times(amount).toNumber(), [amount, price])
return (
<TitleAndSubCell
title={
<FormattedNumber
amount={amount}
options={{ maxDecimals: amount < 0.0001 ? props.asset.decimals : 4 }}
/>
}
sub={<FormattedNumber amount={value} options={{ prefix: '$' }} />}
/>
)
}

View File

@ -0,0 +1,43 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import EntryPrice, { ENTRY_PRICE_META } from 'components/Perps/BalancesTable/Columns/EntryPrice'
import Manage, { MANAGE_META } from 'components/Perps/BalancesTable/Columns/Manage'
import { PERP_NAME_META, PerpName } from 'components/Perps/BalancesTable/Columns/PerpName'
import PerpType, { PERP_TYPE_META } from 'components/Perps/BalancesTable/Columns/PerpType'
import PnL, { PNL_META } from 'components/Perps/BalancesTable/Columns/PnL'
import Size, { SIZE_META } from 'components/Perps/BalancesTable/Columns/Size'
import { PerpPositionRow } from 'components/Perps/BalancesTable/usePerpsBalancesData'
export default function usePerpsBalancesTable() {
return useMemo<ColumnDef<PerpPositionRow>[]>(() => {
return [
{
...PERP_NAME_META,
cell: ({ row }) => <PerpName asset={row.original.asset} />,
},
{
...PERP_TYPE_META,
cell: ({ row }) => <PerpType type={row.original.type} />,
},
{
...SIZE_META,
cell: ({ row }) => <Size size={row.original.size} asset={row.original.asset} />,
},
{
...ENTRY_PRICE_META,
cell: ({ row }) => (
<EntryPrice entryPrice={row.original.entryPrice} asset={row.original.asset} />
),
},
{
...PNL_META,
cell: ({ row }) => <PnL pnl={row.original.pnl} />,
},
{
...MANAGE_META,
cell: ({ row }) => <Manage perpPosition={row.original} />,
},
]
}, [])
}

View File

@ -0,0 +1,10 @@
import usePerpsBalancesColumns from 'components/Perps/BalancesTable/Columns/usePerpsBalancesColumns'
import usePerpsBalancesData from 'components/Perps/BalancesTable/usePerpsBalancesData'
import Table from 'components/Table'
export default function PerpsBalancesTable() {
const data = usePerpsBalancesData()
const columns = usePerpsBalancesColumns()
return <Table title='Perp Positions' columns={columns} data={data} initialSorting={[]} />
}

View File

@ -0,0 +1,34 @@
import { useMemo } from 'react'
import usePerpsEnabledAssets from 'hooks/assets/usePerpsEnabledAssets'
import useCurrentAccount from 'hooks/useCurrentAccount'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
export default function usePerpsBalancesTable() {
const currentAccount = useCurrentAccount()
const perpAssets = usePerpsEnabledAssets()
return useMemo<PerpPositionRow[]>(() => {
if (!currentAccount) return []
return currentAccount.perps.map((position) => {
const asset = perpAssets.find(byDenom(position.denom))
return {
asset,
type: position.type,
size: position.size,
pnl: position.pnl,
entryPrice: position.entryPrice,
} as PerpPositionRow
})
}, [currentAccount, perpAssets])
}
export type PerpPositionRow = {
asset: Asset
type: 'long' | 'short'
size: BigNumber
pnl: BNCoin
entryPrice: BigNumber
}

View File

@ -1,4 +1,4 @@
import { useState } from 'react'
import { useCallback, useState } from 'react'
import Button from 'components/Button'
import Card from 'components/Card'
@ -15,12 +15,26 @@ import { AvailableOrderType } from 'components/Trade/TradeModule/SwapForm/OrderT
import { BN_ZERO } from 'constants/math'
import useBaseAsset from 'hooks/assets/useBasetAsset'
import usePerpsAsset from 'hooks/perps/usePerpsAsset'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { BN } from 'utils/helpers'
export function PerpsModule() {
const [selectedOrderType, setSelectedOrderType] = useState<AvailableOrderType>('Market')
const [selectedOrderDirection, setSelectedOrderDirection] = useState<OrderDirection>('long')
const baseAsset = useBaseAsset()
const { perpsAsset } = usePerpsAsset()
const openPerpPosition = useStore((s) => s.openPerpPosition)
const currentAccount = useCurrentAccount()
const onConfirm = useCallback(async () => {
if (!currentAccount) return
await openPerpPosition({
accountId: currentAccount.id,
coin: BNCoin.fromDenomAndBigNumber(perpsAsset.denom, BN(1000)),
})
}, [currentAccount, openPerpPosition, perpsAsset.denom])
if (!perpsAsset) return null
@ -50,7 +64,7 @@ export function PerpsModule() {
<RangeInput max={0} value={0} onChange={() => {}} />
<LeverageButtons />
<Spacer />
<Button>{selectedOrderDirection} ETH</Button>
<Button onClick={onConfirm}>{selectedOrderDirection} ETH</Button>
</Card>
)
}

View File

@ -1,5 +1,5 @@
import AccountDetailsCard from 'components/Trade/AccountDetailsCard'
import PerpsBalancesTable from './BalancesTable'
export function PerpsPositions() {
return <AccountDetailsCard />
return <PerpsBalancesTable />
}

View File

@ -80,7 +80,7 @@ export default function Table<T>(props: Props<T>) {
'align-center',
)}
>
<span className='w-5 h-5 text-white'>
<span className='w-5 h-5 text-white my-auto'>
{header.column.getCanSort()
? {
asc: <SortAsc size={16} />,

View File

@ -132,14 +132,17 @@ export default function useHealthComputer(account?: Account) {
return null
return {
denoms_data: { params: denomsData, prices: priceData },
kind: account.kind,
vaults_data: {
vault_configs: vaultConfigsData,
vault_values: vaultPositionValues,
},
denoms_data: {
params: denomsData,
prices: priceData,
},
positions: positions,
kind: account.kind,
}
} as HealthComputer
}, [account, positions, vaultPositionValues, vaultConfigsData, denomsData, priceData])
useEffect(() => {

View File

@ -6,13 +6,13 @@ import { PerpsPositions } from 'components/Perps/PerpsPositions'
export default function PerpsPage() {
return (
<div className='flex flex-col w-full h-full gap-4'>
<div className='grid w-full grid-cols-[auto_346px] gap-4 pb-4'>
<div className='grid grid-cols-1 grid-rows-[min-content_auto_min-content] gap-4 h-[calc(100dvh-93px)] pb-4'>
<div className='grid w-full grid-cols-[auto_346px] gap-4'>
<div className='flex flex-col gap-4'>
<PerpsInfo />
<PerpsChart />
<PerpsPositions />
</div>
<PerpsModule />
<PerpsPositions />
</div>
</div>
)

View File

@ -117,6 +117,19 @@ export default function createBroadcastSlice(
})
break
case 'open-perp':
toast.content.push({
coins: changes.deposits?.map((deposit) => deposit.toCoin()) ?? [],
text: 'Opened perp position',
})
break
case 'close-perp':
toast.content.push({
coins: changes.deposits?.map((deposit) => deposit.toCoin()) ?? [],
text: 'Closed perp position',
})
break
case 'swap':
if (changes.debts) {
toast.content.push({
@ -916,5 +929,65 @@ export default function createBroadcastSlice(
return { result: undefined, error: e.message }
}
},
openPerpPosition: async (options: { accountId: string; coin: BNCoin }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
{
open_perp: options.coin.toSignedCoin(),
},
],
},
}
const cmContract = get().chainConfig.contracts.creditManager
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, cmContract, msg, [])],
})
get().setToast({
response,
options: {
action: 'open-perp',
target: 'account',
accountId: options.accountId,
changes: { deposits: [options.coin] },
},
})
return response.then((response) => !!response.result)
},
closePerpPosition: async (options: { accountId: string; denom: string }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
{
close_perp: { denom: options.denom },
},
],
},
}
const cmContract = get().chainConfig.contracts.creditManager
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, cmContract, msg, [])],
})
get().setToast({
response,
options: {
action: 'close-perp',
target: 'account',
accountId: options.accountId,
changes: { deposits: [] },
},
})
return response.then((response) => !!response.result)
},
}
}

View File

@ -37,4 +37,11 @@ export class BNCoin {
},
}
}
toSignedCoin(): any {
return {
denom: this.denom,
size: this.amount.toString(),
}
}
}

View File

@ -1,7 +1,19 @@
type OrderDirection = 'long' | 'short' | 'buy' | 'sell'
const BNCoin = import('types/classes/BNCoin').BNCoin
type OrderDirection = PerpsType | ('buy' | 'sell')
type PerpsType = 'long' | 'short'
// TODO: 📈Remove this type when healthcomputer is implemented
type PositionsWithoutPerps = Omit<
import('types/generated/mars-credit-manager/MarsCreditManager.types').Positions,
'perps'
>
type PerpsPosition = {
denom: string
baseDenom: string
type: PerpsType
size: BigNumber
// closingFee: BNCoin
}

View File

@ -72,6 +72,8 @@ interface HandleResponseProps {
| 'swap'
| 'oracle'
| 'hls-staking'
| 'open-perp'
| 'close-perp'
lend?: boolean
accountId?: string
changes?: {
@ -118,6 +120,8 @@ interface BroadcastSlice {
execute: (contract: string, msg: ExecuteMsg, funds: Coin[]) => Promise<BroadcastResult>
executeMsg: (options: { messages: MsgExecuteContract[] }) => Promise<BroadcastResult>
lend: (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => Promise<boolean>
closePerpPosition: (options: { accountId: string; denom: string }) => Promise<boolean>
openPerpPosition: (options: { accountId: string; coin: BNCoin }) => Promise<boolean>
reclaim: (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => Promise<boolean>
repay: (options: {
accountId: string

View File

@ -183,8 +183,6 @@ export function convertAccountToPositions(account: Account): PositionsWithoutPer
amount: lend.amount.toString(),
denom: lend.denom,
})),
// TODO: 📈 Add correct type mapping
perps: account.perps,
vaults: account.vaults.map(
(vault) =>
({

View File

@ -1,3 +1,6 @@
import { BN_ZERO } from 'constants/math'
import { BNCoin } from 'types/classes/BNCoin'
import { PnL, Positions } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import {
AssetParamsBaseForAddr as AssetParams,
AssetParamsBaseForAddr,
@ -73,3 +76,29 @@ export function resolveHLSStrategies(
})
return HLSStakingStrategies
}
export function resolvePerpsPositions(perpPositions: Positions['perps']): PerpsPosition[] {
return perpPositions.map((position) => {
return {
denom: position.denom,
baseDenom: position.base_denom,
size: BN(position.size as any),
type: BN(position.size as any).isNegative() ? 'short' : 'long',
closingFee: BNCoin.fromCoin(position.pnl.coins.closing_fee),
pnl: getPnlCoin(position.pnl.coins.pnl, position.base_denom),
entryPrice: BN(position.entry_price),
}
})
}
function getPnlCoin(pnl: PnL, denom: string): BNCoin {
let amount = BN_ZERO
if ('loss' in (pnl as { loss: Coin })) {
amount = BN((pnl as any).loss.amount).times(-1)
} else if ('profit' in (pnl as { profit: Coin })) {
amount = BN((pnl as any).profit.amount)
}
return BNCoin.fromDenomAndBigNumber(denom, amount)
}