lev-stake-sol/components/Positions.tsx

448 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import useMangoGroup from 'hooks/useMangoGroup'
import { useMemo, useState } from 'react'
import { SHOW_INACTIVE_POSITIONS_KEY } from 'utils/constants'
import TokenLogo from './shared/TokenLogo'
import Button, { IconButton } from './shared/Button'
import {
formatTokenSymbol,
getStakableTokensDataForTokenName,
} from 'utils/tokens'
import mangoStore, { ActiveTab } from '@store/mangoStore'
import Switch from './forms/Switch'
import useLocalStorageState from 'hooks/useLocalStorageState'
import FormatNumericValue from './shared/FormatNumericValue'
import {
Bank,
MangoAccount,
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
import useBankRates from 'hooks/useBankRates'
import usePositions from 'hooks/usePositions'
import {
AdjustmentsHorizontalIcon,
ArrowLeftIcon,
} from '@heroicons/react/20/solid'
import EditLeverageModal from './modals/EditLeverageModal'
import Tooltip from './shared/Tooltip'
import { useWallet } from '@solana/wallet-adapter-react'
import UnstakeForm from './UnstakeForm'
import StakeForm from './StakeForm'
import DespositForm from './DepositForm'
import { useTheme } from 'next-themes'
const set = mangoStore.getState().set
export type Position = {
borrowBalance: number
stakeBalance: number
pnl: number
solPnl: number | undefined
bank: Bank
acct: MangoAccount | undefined
}
const Positions = ({
setActiveTab,
}: {
setActiveTab: (tab: ActiveTab) => void
}) => {
const selectedToken = mangoStore((s) => s.selectedToken)
const [showInactivePositions, setShowInactivePositions] =
useLocalStorageState(SHOW_INACTIVE_POSITIONS_KEY, false)
const { positions, jlpBorrowBank, lstBorrowBank } = usePositions(
showInactivePositions,
)
const [showAddRemove, setShowAddRemove] = useState('')
const numberOfPositions = useMemo(() => {
if (!positions.length) return 0
return positions.filter((pos) => pos.stakeBalance > 0).length
}, [positions])
return !showAddRemove ? (
<>
<div className="mb-2 flex items-center justify-between rounded-xl border-2 border-th-fgd-1 bg-th-bkg-1 px-6 py-3.5">
<p className="font-medium">{`You have ${numberOfPositions} active position${
numberOfPositions !== 1 ? 's' : ''
}`}</p>
<Switch
checked={showInactivePositions}
onChange={(checked) => setShowInactivePositions(checked)}
>
Show Inactive
</Switch>
</div>
<div className="grid grid-cols-1 gap-2">
{positions.length ? (
positions.map((position) => {
const { bank } = position
const isUsdcBorrow = bank.name === 'JLP' || bank.name === 'USDC'
return position.bank ? (
<PositionItem
key={bank.name}
position={position}
setActiveTab={setActiveTab}
setShowAddRemove={setShowAddRemove}
borrowBank={isUsdcBorrow ? jlpBorrowBank : lstBorrowBank}
/>
) : null
})
) : (
<div className="flex min-h-[336px] items-center justify-center rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 p-6">
<div className="flex flex-col items-center">
<span className="text-xl">😑</span>
<span>Nothing to see here...</span>
</div>
</div>
)}
</div>
</>
) : (
<div
className={`rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 p-6 text-th-fgd-1 md:p-8`}
>
<div className="mb-6 flex items-center space-x-3">
<IconButton onClick={() => setShowAddRemove('')} size="small" isPrimary>
<ArrowLeftIcon className="h-5 w-5" />
</IconButton>
<h2>
{showAddRemove === 'add' ? 'Add' : 'Withdraw'} {selectedToken}
</h2>
</div>
{showAddRemove === 'add' ? (
selectedToken === 'USDC' ? (
<DespositForm
token="USDC"
clientContext={
getStakableTokensDataForTokenName('USDC').clientContext
}
/>
) : (
<StakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)?.clientContext
}
/>
)
) : (
<UnstakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)?.clientContext
}
/>
)}
</div>
)
}
const PositionItem = ({
position,
setActiveTab,
setShowAddRemove,
borrowBank,
}: {
position: Position
setActiveTab: (v: ActiveTab) => void
setShowAddRemove: (v: 'add' | 'remove') => void
borrowBank: Bank | undefined
}) => {
const { connected } = useWallet()
const { theme } = useTheme()
const { jlpGroup, lstGroup } = useMangoGroup()
const { stakeBalance, bank, pnl, acct, solPnl } = position
const [showEditLeverageModal, setShowEditLeverageModal] = useState(false)
const isJlpGroup = bank.name === 'JLP' || bank.name === 'USDC'
const handleAddNoPosition = (token: string) => {
setActiveTab('Earn')
set((state) => {
state.selectedToken = token
})
}
const handleAddPosition = (token: string) => {
setShowAddRemove('add')
set((state) => {
state.selectedToken = token
})
}
const handleRemovePosition = (token: string) => {
setShowAddRemove('remove')
set((state) => {
state.selectedToken = token
})
}
const leverage = useMemo(() => {
if (!acct || !bank) return 1
const isJlpGroup = bank.name === 'JLP' || bank.name === 'USDC'
const group = isJlpGroup ? jlpGroup : lstGroup
if (!group) return 1
const accountValue = toUiDecimalsForQuote(acct.getEquity(group).toNumber())
const assetsValue = toUiDecimalsForQuote(
acct.getAssetsValue(group).toNumber(),
)
if (isNaN(assetsValue / accountValue)) {
return 0
} else {
return Math.abs(1 - assetsValue / accountValue) + 1
}
}, [acct, bank, jlpGroup, lstGroup, isJlpGroup])
const liquidationPrice = useMemo(() => {
let price
if (borrowBank?.name == 'SOL') {
price = Number(bank?.uiPrice) / Number(borrowBank?.uiPrice)
} else {
price = Number(bank?.uiPrice)
}
const borrowMaintLiabWeight = Number(borrowBank?.maintLiabWeight)
const stakeMaintAssetWeight = Number(bank?.maintAssetWeight)
const loanOriginationFee = Number(borrowBank?.loanOriginationFeeRate)
const liqPrice =
price *
((borrowMaintLiabWeight * (1 + loanOriginationFee)) /
stakeMaintAssetWeight) *
(1 - 1 / leverage)
return liqPrice.toFixed(2)
}, [bank, borrowBank, leverage])
const { financialMetrics, stakeBankDepositRate, borrowBankBorrowRate } =
useBankRates(bank.name, leverage)
const APY_Daily_Compound =
Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1
const uiRate =
bank.name == 'USDC' ? APY_Daily_Compound * 100 : financialMetrics.APY
const currentPnl = isJlpGroup ? pnl : solPnl
return (
<div className="rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 p-6">
<div className="flex flex-col pb-6 md:flex-row md:items-center md:justify-between">
<div className="mb-4 flex items-center space-x-3 md:mb-0">
<TokenLogo bank={bank} size={40} />
<div>
<h3>{formatTokenSymbol(bank.name)}</h3>
<p>${bank.uiPrice.toFixed(2)}</p>
</div>
</div>
{stakeBalance ? (
<div className="flex space-x-2">
<Button onClick={() => handleAddPosition(bank.name)}>
<p className="mb-1 text-base tracking-wider text-th-bkg-1">Add</p>
</Button>
<Button onClick={() => handleRemovePosition(bank.name)}>
<p className="mb-1 text-base tracking-wider text-th-bkg-1">
Withdraw
</p>
</Button>
</div>
) : (
<Button onClick={() => handleAddNoPosition(bank.name)}>
<p className="mb-1 text-base tracking-wider text-th-bkg-1">
{`Add`}
</p>
</Button>
)}
</div>
<div
className={`bg-x-repeat h-2 w-full ${
theme === 'Light'
? `bg-[url('/images/zigzag-repeat.svg')]`
: `bg-[url('/images/zigzag-repeat-dark.svg')]`
} bg-contain opacity-20`}
/>
<div className="grid grid-cols-1 gap-4 pt-6 sm:grid-cols-2">
<div>
<p className="mb-1 text-th-fgd-4">Position Size</p>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue
value={stakeBalance * (bank.name != 'USDC' ? bank?.uiPrice : 1)}
decimals={2}
/>{' '}
{'USDC'}
</span>
{bank.name !== 'USDC' ? (
<p className="text-th-fgd-4">
<FormatNumericValue
roundUp={true}
value={stakeBalance}
decimals={3}
/>{' '}
{formatTokenSymbol(bank.name)}
</p>
) : null}
</div>
<div>
<p className="mb-1 text-th-fgd-4">Est. APY</p>
{bank.name !== 'USDC' ? (
<div className="w-max">
<Tooltip
content={
<>
<div className="space-y-2 md:px-3">
<div className="flex justify-between gap-6">
<p className="text-th-fgd-4">
{formatTokenSymbol(bank.name)} Yield APY
</p>
<span className="font-bold text-th-success">
{financialMetrics.collectedReturnsAPY > 0.01
? '+'
: ''}
<FormatNumericValue
value={financialMetrics.collectedReturnsAPY}
decimals={2}
/>
%
</span>
</div>
<div className="flex justify-between gap-6">
<p className="text-th-fgd-4">
{formatTokenSymbol(bank.name)} Collateral Fee APY
</p>
<span
className={`font-bold ${
financialMetrics?.collateralFeeAPY > 0.01
? 'text-th-error'
: 'text-th-bkg-4'
}`}
>
{financialMetrics?.collateralFeeAPY > 0.01 ? '-' : ''}
<FormatNumericValue
value={financialMetrics?.collateralFeeAPY?.toString()}
decimals={2}
/>
%
</span>
</div>
{borrowBank ? (
<>
<div className="flex justify-between gap-6">
<p className="text-th-fgd-4">{`${borrowBank?.name} Borrow APY`}</p>
<span
className={`font-bold ${
borrowBankBorrowRate > 0.01
? 'text-th-error'
: 'text-th-bkg-4'
}`}
>
-
<FormatNumericValue
value={financialMetrics.borrowsAPY}
decimals={2}
/>
%
</span>
</div>
</>
) : null}
</div>
</>
}
>
<span className="tooltip-underline text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={Number(uiRate)} decimals={2} />%
</span>
</Tooltip>
</div>
) : (
<>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={Number(uiRate)} decimals={2} />%
</span>
</>
)}
</div>
<div>
<p className="mb-1 text-th-fgd-4">Total Earned</p>
<span
className={`text-xl font-bold ${
!stakeBalance
? 'text-th-fgd-4'
: currentPnl && currentPnl >= 0
? 'text-th-success'
: 'text-th-error'
}`}
>
{currentPnl ? (
<>
<FormatNumericValue
value={currentPnl}
decimals={2}
isUsd={isJlpGroup}
/>
{!isJlpGroup && ' SOL'}
</>
) : (
''
)}
</span>
</div>
{position.bank.name == 'USDC' ? null : (
<>
<div>
<p className="mb-1 text-th-fgd-4">Leverage</p>
<div className="flex items-center">
<span className="mr-3 text-xl font-bold text-th-fgd-1">
{connected && stakeBalance && leverage
? `${leverage.toFixed(2)}x`
: ''}
</span>
{connected && stakeBalance ? (
<button
onClick={async () => {
await set((state) => {
state.selectedToken = bank.name
})
setShowEditLeverageModal(!showEditLeverageModal)
}}
className="raised-button-neutral group flex h-8 items-center rounded-lg px-3 after:rounded-lg after:border after:border-th-bkg-3 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>
<span className="flex w-full items-center group-hover:mt-1 group-active:mt-2">
<AdjustmentsHorizontalIcon className="mr-1.5 h-4 w-4" />
<span className="font-bold">Edit</span>
</span>
</button>
) : null}
</div>
</div>
<div>
<p className="mb-1 text-th-fgd-4">Est. Liquidation Price</p>
<div className="flex flex-wrap items-end">
<span className="mr-2 whitespace-nowrap text-xl font-bold text-th-fgd-1">
{liquidationPrice}
{borrowBank?.name == ' USDC'
? ' USDC'
: ` ${bank?.name}/${borrowBank?.name}`}
</span>
</div>
{/* {liqPriceChangePercentage ? (
<Tooltip content="Estimated price change required for liquidation.">
<p className="tooltip-underline mb-0.5 text-th-fgd-4">
{liqPriceChangePercentage}%
</p>
</Tooltip>
) : null} */}
</div>
</>
)}
</div>
{showEditLeverageModal ? (
<EditLeverageModal
token={bank.name}
isOpen={showEditLeverageModal}
onClose={() => {
setShowEditLeverageModal(false)
}}
/>
) : null}
</div>
)
}
export default Positions