Update account stats on pending action (#51)
* feat: update account stats on pending action * lift repay amount with buffer calculation to repay modal * refactor navigation component to properly format account stats * update repay funds hook using credit manager client * consider market liquidity when calculating max withdraw amount * refactor useMaxBorrowAmount hook to be consistent with withdraw * format * remove duplicated import statements
This commit is contained in:
parent
27cdd1c954
commit
56bfea8ed0
@ -12,12 +12,18 @@ import useCalculateMaxBorrowAmount from 'hooks/useCalculateMaxBorrowAmount'
|
||||
import useMarkets from 'hooks/useMarkets'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useAccountStats, { AccountStatsAction } from 'hooks/useAccountStats'
|
||||
import { chain } from 'utils/chains'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
|
||||
import Button from './Button'
|
||||
import ProgressBar from './ProgressBar'
|
||||
import SemiCircleProgress from './SemiCircleProgress'
|
||||
import ContainerSecondary from './ContainerSecondary'
|
||||
import Spinner from './Spinner'
|
||||
import Tooltip from './Tooltip'
|
||||
import Button from './Button'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
@ -29,21 +35,46 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
|
||||
const [amount, setAmount] = useState(0)
|
||||
const [isBorrowToCreditAccount, setIsBorrowToCreditAccount] = useState(false)
|
||||
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
|
||||
selectedAccount ?? '',
|
||||
)
|
||||
|
||||
const { actions, borrowAmount } = useMemo(() => {
|
||||
const borrowAmount = BigNumber(amount)
|
||||
.times(10 ** getTokenDecimals(tokenDenom))
|
||||
.toNumber()
|
||||
|
||||
const withdrawAmount = isBorrowToCreditAccount ? 0 : borrowAmount
|
||||
|
||||
return {
|
||||
borrowAmount,
|
||||
withdrawAmount,
|
||||
actions: [
|
||||
{
|
||||
type: 'borrow',
|
||||
amount: borrowAmount,
|
||||
denom: tokenDenom,
|
||||
},
|
||||
{
|
||||
type: 'withdraw',
|
||||
amount: withdrawAmount,
|
||||
denom: tokenDenom,
|
||||
},
|
||||
] as AccountStatsAction[],
|
||||
}
|
||||
}, [amount, isBorrowToCreditAccount, tokenDenom])
|
||||
|
||||
const accountStats = useAccountStats(actions)
|
||||
|
||||
const tokenSymbol = getTokenSymbol(tokenDenom)
|
||||
|
||||
const { mutate, isLoading } = useBorrowFunds(
|
||||
BigNumber(amount)
|
||||
.times(10 ** getTokenDecimals(tokenDenom))
|
||||
.toNumber(),
|
||||
tokenDenom,
|
||||
!isBorrowToCreditAccount,
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
toast.success(`${amount} ${tokenSymbol} successfully Borrowed`)
|
||||
},
|
||||
const { mutate, isLoading } = useBorrowFunds(borrowAmount, tokenDenom, !isBorrowToCreditAccount, {
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
toast.success(`${amount} ${tokenSymbol} successfully Borrowed`)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
const { data: balancesData } = useAllBalances()
|
||||
@ -94,6 +125,17 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
|
||||
setAmount(0)
|
||||
}
|
||||
|
||||
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||
// early return if prices are not fetched yet
|
||||
if (!tokenPrices) return 0
|
||||
|
||||
return (
|
||||
BigNumber(amount)
|
||||
.div(10 ** getTokenDecimals(denom))
|
||||
.toNumber() * tokenPrices[denom]
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition appear show={show} as={React.Fragment}>
|
||||
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
||||
@ -120,25 +162,13 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
<Dialog.Panel className='flex min-h-[520px] w-full max-w-3xl transform overflow-hidden rounded-2xl bg-[#585A74] align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className='flex w-full max-w-3xl transform overflow-hidden rounded-2xl bg-[#585A74] align-middle shadow-xl transition-all'>
|
||||
{isLoading && (
|
||||
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col items-start justify-between bg-[#4A4C60] p-6'>
|
||||
<div>
|
||||
<p className='text-bold mb-3 text-xs uppercase text-white/50'>About</p>
|
||||
<h4 className='mb-4 text-xl leading-8'>
|
||||
Bringing the next generation of video creation to the Metaverse.
|
||||
<br />
|
||||
Powered by deep-learning.
|
||||
</h4>
|
||||
</div>
|
||||
<Image src='/logo.svg' alt='mars' width={150} height={50} />
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<Dialog.Title as='h3' className='mb-4 text-center font-medium'>
|
||||
Borrow {tokenSymbol}
|
||||
@ -219,6 +249,111 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
|
||||
Borrow
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex w-1/2 flex-col justify-center bg-[#4A4C60] p-4'>
|
||||
<p className='text-bold mb-3 text-xs uppercase text-white/50'>About</p>
|
||||
<h4 className='mb-4 text-xl'>Subaccount {selectedAccount}</h4>
|
||||
<div className='mb-2 rounded-md border border-white/20 p-3'>
|
||||
{accountStats && (
|
||||
<div className='flex items-center gap-x-3'>
|
||||
<p className='flex-1 text-xs'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats.netWorth)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</p>
|
||||
{/* TOOLTIP */}
|
||||
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
|
||||
<SemiCircleProgress
|
||||
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
||||
label='Lvg'
|
||||
/>
|
||||
</div>
|
||||
<SemiCircleProgress value={accountStats.risk} label='Risk' />
|
||||
<ProgressBar value={accountStats.health} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='mb-2 rounded-md border border-white/20 p-3 text-sm'>
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div>Total Position:</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalPosition ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<div>Total Liabilities:</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalDebt ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-md border border-white/20 p-3'>
|
||||
<h4 className='mb-2 font-bold'>Balances</h4>
|
||||
{isLoadingPositions ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<table className='w-full border-separate border-spacing-1'>
|
||||
<thead className='text-left text-xs font-semibold'>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th>Value</th>
|
||||
<th>Size</th>
|
||||
<th className='text-right'>APY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{positionsData?.coins.map((coin) => (
|
||||
<tr key={coin.denom} className='text-xs text-white/50'>
|
||||
<td>{getTokenSymbol(coin.denom)}</td>
|
||||
<td>
|
||||
{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||
</td>
|
||||
<td>
|
||||
{BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()
|
||||
.toLocaleString(undefined, {
|
||||
maximumFractionDigits: getTokenDecimals(coin.denom),
|
||||
})}
|
||||
</td>
|
||||
<td className='text-right'>-</td>
|
||||
</tr>
|
||||
))}
|
||||
{positionsData?.debts.map((coin) => (
|
||||
<tr key={coin.denom} className='text-xs text-red-500'>
|
||||
<td className='text-white/50'>{getTokenSymbol(coin.denom)}</td>
|
||||
<td>
|
||||
-{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||
</td>
|
||||
<td>
|
||||
-
|
||||
{BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()
|
||||
.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 6,
|
||||
})}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
-{(Number(marketsData?.[coin.denom].borrow_rate) * 100).toFixed(1)}%
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
|
@ -12,8 +12,8 @@ import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
|
||||
import Button from '../Button'
|
||||
import { chain } from 'utils/chains'
|
||||
import Button from 'components/Button'
|
||||
|
||||
const CreditManager = () => {
|
||||
const [showFundWalletModal, setShowFundWalletModal] = useState(false)
|
||||
@ -78,11 +78,23 @@ const CreditManager = () => {
|
||||
<ContainerSecondary className='mb-2 text-sm'>
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div>Total Position:</div>
|
||||
<div className='font-semibold'>{formatCurrency(accountStats?.totalPosition ?? 0)}</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalPosition ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<div>Total Liabilities:</div>
|
||||
<div className='font-semibold'>{formatCurrency(accountStats?.totalDebt ?? 0)}</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalDebt ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ContainerSecondary>
|
||||
<ContainerSecondary>
|
||||
|
@ -4,6 +4,7 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo } from 'react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
import ArrowRightLine from 'components/Icons/arrow-right-line.svg'
|
||||
import ProgressBar from 'components/ProgressBar'
|
||||
@ -17,6 +18,7 @@ import useCreditAccounts from 'hooks/useCreditAccounts'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
import { chain } from 'utils/chains'
|
||||
|
||||
import Button from './Button'
|
||||
import SemiCircleProgress from './SemiCircleProgress'
|
||||
@ -81,7 +83,13 @@ const Navigation = () => {
|
||||
<div className='flex items-center gap-4'>
|
||||
{accountStats && (
|
||||
<>
|
||||
<p>{formatCurrency(accountStats.netWorth)}</p>
|
||||
<p>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats.netWorth)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</p>
|
||||
{/* TOOLTIP */}
|
||||
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
|
||||
<SemiCircleProgress
|
||||
|
@ -18,6 +18,9 @@ import Button from './Button'
|
||||
import ContainerSecondary from './ContainerSecondary'
|
||||
import Spinner from './Spinner'
|
||||
|
||||
// 0.001% buffer / slippage to avoid repay action from not fully repaying the debt amount
|
||||
const REPAY_BUFFER = 1.00001
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
@ -37,6 +40,8 @@ const RepayModal = ({ show, onClose, tokenDenom }: Props) => {
|
||||
positionsData?.debts.find((coin) => coin.denom === tokenDenom)?.amount ?? 0
|
||||
|
||||
return BigNumber(tokenDebtAmount)
|
||||
.times(REPAY_BUFFER)
|
||||
.decimalPlaces(0)
|
||||
.div(10 ** getTokenDecimals(tokenDenom))
|
||||
.toNumber()
|
||||
}, [positionsData, tokenDenom])
|
||||
|
@ -6,7 +6,6 @@ import { toast } from 'react-toastify'
|
||||
|
||||
import Slider from 'components/Slider'
|
||||
import useWithdrawFunds from 'hooks/mutations/useWithdrawFunds'
|
||||
import useAccountStats from 'hooks/useAccountStats'
|
||||
import useAllBalances from 'hooks/useAllBalances'
|
||||
import useCalculateMaxWithdrawAmount from 'hooks/useCalculateMaxWithdrawAmount'
|
||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||
@ -15,12 +14,14 @@ import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
import useAccountStats, { AccountStatsAction } from 'hooks/useAccountStats'
|
||||
import { chain } from 'utils/chains'
|
||||
|
||||
import Button from './Button'
|
||||
import ContainerSecondary from './ContainerSecondary'
|
||||
import ProgressBar from './ProgressBar'
|
||||
import SemiCircleProgress from './SemiCircleProgress'
|
||||
import Spinner from './Spinner'
|
||||
import SemiCircleProgress from './SemiCircleProgress'
|
||||
import ProgressBar from './ProgressBar'
|
||||
import ContainerSecondary from './ContainerSecondary'
|
||||
import Button from './Button'
|
||||
|
||||
const WithdrawModal = ({ show, onClose }: any) => {
|
||||
const [amount, setAmount] = useState(0)
|
||||
@ -35,7 +36,6 @@ const WithdrawModal = ({ show, onClose }: any) => {
|
||||
const { data: balancesData } = useAllBalances()
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
const { data: marketsData } = useMarkets()
|
||||
const accountStats = useAccountStats()
|
||||
|
||||
const selectedTokenSymbol = getTokenSymbol(selectedToken)
|
||||
const selectedTokenDecimals = getTokenDecimals(selectedToken)
|
||||
@ -46,7 +46,7 @@ const WithdrawModal = ({ show, onClose }: any) => {
|
||||
.toNumber()
|
||||
}, [positionsData, selectedTokenDecimals, selectedToken])
|
||||
|
||||
const { borrowAmount, withdrawAmount } = useMemo(() => {
|
||||
const { actions, borrowAmount, withdrawAmount } = useMemo(() => {
|
||||
const borrowAmount =
|
||||
amount > tokenAmountInCreditAccount
|
||||
? BigNumber(amount)
|
||||
@ -62,8 +62,22 @@ const WithdrawModal = ({ show, onClose }: any) => {
|
||||
return {
|
||||
borrowAmount,
|
||||
withdrawAmount,
|
||||
actions: [
|
||||
{
|
||||
type: 'borrow',
|
||||
amount: borrowAmount,
|
||||
denom: selectedToken,
|
||||
},
|
||||
{
|
||||
type: 'withdraw',
|
||||
amount: withdrawAmount,
|
||||
denom: selectedToken,
|
||||
},
|
||||
] as AccountStatsAction[],
|
||||
}
|
||||
}, [amount, selectedTokenDecimals, tokenAmountInCreditAccount])
|
||||
}, [amount, selectedToken, selectedTokenDecimals, tokenAmountInCreditAccount])
|
||||
|
||||
const accountStats = useAccountStats(actions)
|
||||
|
||||
const { mutate, isLoading } = useWithdrawFunds(withdrawAmount, borrowAmount, selectedToken, {
|
||||
onSuccess: () => {
|
||||
@ -234,7 +248,13 @@ const WithdrawModal = ({ show, onClose }: any) => {
|
||||
<div className='mb-2 rounded-md border border-white/20 p-3'>
|
||||
{accountStats && (
|
||||
<div className='flex items-center gap-x-3'>
|
||||
<p>{formatCurrency(accountStats.netWorth)}</p>
|
||||
<p className='flex-1 text-xs'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats.netWorth)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</p>
|
||||
{/* TOOLTIP */}
|
||||
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
|
||||
<SemiCircleProgress
|
||||
@ -251,13 +271,21 @@ const WithdrawModal = ({ show, onClose }: any) => {
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div>Total Position:</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(accountStats?.totalPosition ?? 0)}
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalPosition ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<div>Total Liabilities:</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(accountStats?.totalDebt ?? 0)}
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalDebt ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { useMemo } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
@ -8,64 +7,51 @@ import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { queryKeys } from 'types/query-keys-factory'
|
||||
import { hardcodedFee } from 'utils/contants'
|
||||
import { getTokenDecimals } from 'utils/tokens'
|
||||
|
||||
// 0.001% buffer / slippage to avoid repay action from not fully repaying the debt amount
|
||||
const REPAY_BUFFER = 1.00001
|
||||
|
||||
const useRepayFunds = (
|
||||
amount: number,
|
||||
denom: string,
|
||||
options: Omit<UseMutationOptions, 'onError'>,
|
||||
) => {
|
||||
const signingClient = useWalletStore((s) => s.signingClient)
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const creditManagerClient = useWalletStore((s) => s.clients.creditManager)
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount ?? '')
|
||||
const address = useWalletStore((s) => s.address)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const adjustedAmount = BigNumber(amount).times(REPAY_BUFFER).decimalPlaces(0).toString()
|
||||
|
||||
const executeMsg = useMemo(() => {
|
||||
return {
|
||||
update_credit_account: {
|
||||
account_id: selectedAccount,
|
||||
actions: [
|
||||
{
|
||||
deposit: {
|
||||
denom: denom,
|
||||
amount: adjustedAmount,
|
||||
},
|
||||
},
|
||||
{
|
||||
repay: {
|
||||
denom: denom,
|
||||
amount: adjustedAmount,
|
||||
},
|
||||
},
|
||||
],
|
||||
const actions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
deposit: {
|
||||
denom: denom,
|
||||
amount: String(amount),
|
||||
},
|
||||
},
|
||||
}
|
||||
}, [adjustedAmount, denom, selectedAccount])
|
||||
{
|
||||
repay: {
|
||||
denom: denom,
|
||||
amount: String(amount),
|
||||
},
|
||||
},
|
||||
]
|
||||
}, [amount, denom])
|
||||
|
||||
return useMutation(
|
||||
async () =>
|
||||
await signingClient?.execute(
|
||||
address,
|
||||
contractAddresses.creditManager,
|
||||
executeMsg,
|
||||
await creditManagerClient?.updateCreditAccount(
|
||||
{ accountId: selectedAccount, actions },
|
||||
hardcodedFee,
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
denom,
|
||||
amount: adjustedAmount,
|
||||
amount: String(amount),
|
||||
},
|
||||
],
|
||||
),
|
||||
{
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(queryKeys.creditAccountsPositions(selectedAccount ?? ''))
|
||||
queryClient.invalidateQueries(queryKeys.creditAccountsPositions(selectedAccount))
|
||||
queryClient.invalidateQueries(queryKeys.tokenBalance(address, denom))
|
||||
queryClient.invalidateQueries(queryKeys.allBalances(address))
|
||||
queryClient.invalidateQueries(queryKeys.redbankBalances())
|
||||
|
@ -2,7 +2,6 @@ import BigNumber from 'bignumber.js'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import { getTokenDecimals } from 'utils/tokens'
|
||||
|
||||
import useCreditAccountPositions from './useCreditAccountPositions'
|
||||
import useMarkets from './useMarkets'
|
||||
@ -18,59 +17,104 @@ const getRiskFromAverageLiquidationLTVs = (value: number) => {
|
||||
return 1
|
||||
}
|
||||
|
||||
const useAccountStats = () => {
|
||||
type Asset = {
|
||||
amount: string
|
||||
denom: string
|
||||
liquidationThreshold: string
|
||||
basePrice: number
|
||||
}
|
||||
|
||||
type Debt = {
|
||||
amount: string
|
||||
denom: string
|
||||
basePrice: number
|
||||
}
|
||||
|
||||
const calculateStatsFromAccountPositions = (assets: Asset[], debts: Debt[]) => {
|
||||
const totalPosition = assets.reduce((acc, asset) => {
|
||||
const tokenTotalValue = BigNumber(asset.amount).times(asset.basePrice)
|
||||
|
||||
return BigNumber(tokenTotalValue).plus(acc).toNumber()
|
||||
}, 0)
|
||||
|
||||
const totalDebt = debts.reduce((acc, debt) => {
|
||||
const tokenTotalValue = BigNumber(debt.amount).times(debt.basePrice)
|
||||
|
||||
return BigNumber(tokenTotalValue).plus(acc).toNumber()
|
||||
}, 0)
|
||||
|
||||
const totalWeightedPositions = assets.reduce((acc, asset) => {
|
||||
const assetWeightedValue = BigNumber(asset.amount)
|
||||
.times(asset.basePrice)
|
||||
.times(asset.liquidationThreshold)
|
||||
|
||||
return assetWeightedValue.plus(acc).toNumber()
|
||||
}, 0)
|
||||
|
||||
const netWorth = BigNumber(totalPosition).minus(totalDebt).toNumber()
|
||||
|
||||
const liquidationLTVsWeightedAverage = BigNumber(totalWeightedPositions)
|
||||
.div(totalPosition)
|
||||
.toNumber()
|
||||
|
||||
const maxLeverage = BigNumber(1)
|
||||
.div(BigNumber(1).minus(liquidationLTVsWeightedAverage))
|
||||
.toNumber()
|
||||
const currentLeverage = BigNumber(totalPosition).div(netWorth).toNumber()
|
||||
const health = BigNumber(1).minus(BigNumber(currentLeverage).div(maxLeverage)).toNumber() || 1
|
||||
|
||||
const risk = liquidationLTVsWeightedAverage
|
||||
? getRiskFromAverageLiquidationLTVs(liquidationLTVsWeightedAverage)
|
||||
: 0
|
||||
|
||||
return {
|
||||
health,
|
||||
maxLeverage,
|
||||
currentLeverage,
|
||||
netWorth,
|
||||
risk,
|
||||
totalPosition,
|
||||
totalDebt,
|
||||
}
|
||||
}
|
||||
|
||||
export type AccountStatsAction = {
|
||||
type: 'borrow' | 'repay' | 'deposit' | 'withdraw'
|
||||
amount: number
|
||||
denom: string
|
||||
}
|
||||
|
||||
const useAccountStats = (actions?: AccountStatsAction[]) => {
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
|
||||
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
|
||||
const { data: marketsData } = useMarkets()
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
|
||||
return useMemo(() => {
|
||||
const currentStats = useMemo(() => {
|
||||
if (!marketsData || !tokenPrices || !positionsData) return null
|
||||
|
||||
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||
// early return if prices are not fetched yet
|
||||
if (!tokenPrices) return 0
|
||||
const assets = positionsData.coins.map((coin) => {
|
||||
const market = marketsData[coin.denom]
|
||||
|
||||
return BigNumber(amount)
|
||||
.div(10 ** getTokenDecimals(denom))
|
||||
.times(tokenPrices[denom])
|
||||
.toNumber()
|
||||
}
|
||||
return {
|
||||
amount: coin.amount,
|
||||
denom: coin.denom,
|
||||
liquidationThreshold: market.liquidation_threshold,
|
||||
basePrice: tokenPrices[coin.denom],
|
||||
}
|
||||
})
|
||||
|
||||
const totalPosition = positionsData.coins.reduce((acc, coin) => {
|
||||
const tokenTotalValue = getTokenTotalUSDValue(coin.amount, coin.denom)
|
||||
return BigNumber(tokenTotalValue).plus(acc).toNumber()
|
||||
}, 0)
|
||||
const debts = positionsData.debts.map((debt) => {
|
||||
return {
|
||||
amount: debt.amount,
|
||||
denom: debt.denom,
|
||||
basePrice: tokenPrices[debt.denom],
|
||||
}
|
||||
})
|
||||
|
||||
const totalDebt = positionsData.debts.reduce((acc, coin) => {
|
||||
const tokenTotalValue = getTokenTotalUSDValue(coin.amount, coin.denom)
|
||||
return BigNumber(tokenTotalValue).plus(acc).toNumber()
|
||||
}, 0)
|
||||
|
||||
const totalWeightedPositions = positionsData.coins.reduce((acc, coin) => {
|
||||
const tokenWeightedValue = BigNumber(getTokenTotalUSDValue(coin.amount, coin.denom)).times(
|
||||
Number(marketsData[coin.denom].liquidation_threshold),
|
||||
)
|
||||
|
||||
return tokenWeightedValue.plus(acc).toNumber()
|
||||
}, 0)
|
||||
|
||||
const netWorth = BigNumber(totalPosition).minus(totalDebt).toNumber()
|
||||
|
||||
const liquidationLTVsWeightedAverage = BigNumber(totalWeightedPositions)
|
||||
.div(totalPosition)
|
||||
.toNumber()
|
||||
|
||||
const maxLeverage = BigNumber(1)
|
||||
.div(BigNumber(1).minus(liquidationLTVsWeightedAverage))
|
||||
.toNumber()
|
||||
const currentLeverage = BigNumber(totalPosition).div(netWorth).toNumber()
|
||||
const health = BigNumber(1).minus(BigNumber(currentLeverage).div(maxLeverage)).toNumber() || 1
|
||||
|
||||
const risk = liquidationLTVsWeightedAverage
|
||||
? getRiskFromAverageLiquidationLTVs(liquidationLTVsWeightedAverage)
|
||||
: 0
|
||||
const { health, maxLeverage, currentLeverage, netWorth, risk, totalPosition, totalDebt } =
|
||||
calculateStatsFromAccountPositions(assets, debts)
|
||||
|
||||
return {
|
||||
health,
|
||||
@ -82,6 +126,129 @@ const useAccountStats = () => {
|
||||
totalDebt,
|
||||
}
|
||||
}, [marketsData, positionsData, tokenPrices])
|
||||
|
||||
const actionResultStats = useMemo(() => {
|
||||
if (!actions) return null
|
||||
if (!tokenPrices || !marketsData || !positionsData) return null
|
||||
|
||||
let resultPositionsCoins = positionsData.coins.map((coin) => ({ ...coin }))
|
||||
let resultPositionsDebts = positionsData.debts.map((debt) => {
|
||||
const { shares, ...otherProps } = debt
|
||||
return { ...otherProps }
|
||||
})
|
||||
|
||||
actions.forEach((action) => {
|
||||
if (action.amount === 0) return
|
||||
|
||||
if (action.type === 'deposit') {
|
||||
const index = resultPositionsCoins.findIndex((coin) => coin.denom === action.denom)
|
||||
|
||||
if (index !== -1) {
|
||||
resultPositionsCoins[index].amount = BigNumber(resultPositionsCoins[index].amount)
|
||||
.plus(action.amount)
|
||||
.toFixed()
|
||||
} else {
|
||||
resultPositionsCoins.push({
|
||||
amount: action.amount.toString(),
|
||||
denom: action.denom,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (action.type === 'withdraw') {
|
||||
const index = resultPositionsCoins.findIndex((coin) => coin.denom === action.denom)
|
||||
|
||||
if (index !== -1) {
|
||||
resultPositionsCoins[index].amount = BigNumber(resultPositionsCoins[index].amount)
|
||||
.minus(action.amount)
|
||||
.toFixed()
|
||||
} else {
|
||||
throw new Error('Cannot withdraw more than you have')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (action.type === 'borrow') {
|
||||
const indexDebts = resultPositionsDebts.findIndex((coin) => coin.denom === action.denom)
|
||||
const indexPositions = resultPositionsCoins.findIndex((coin) => coin.denom === action.denom)
|
||||
|
||||
if (indexDebts !== -1) {
|
||||
resultPositionsDebts[indexDebts].amount = BigNumber(
|
||||
resultPositionsDebts[indexDebts].amount,
|
||||
)
|
||||
.plus(action.amount)
|
||||
.toFixed()
|
||||
} else {
|
||||
resultPositionsDebts.push({
|
||||
amount: action.amount.toString(),
|
||||
denom: action.denom,
|
||||
})
|
||||
}
|
||||
|
||||
if (indexPositions !== -1) {
|
||||
resultPositionsCoins[indexPositions].amount = BigNumber(
|
||||
resultPositionsCoins[indexPositions].amount,
|
||||
)
|
||||
.plus(action.amount)
|
||||
.toFixed()
|
||||
} else {
|
||||
resultPositionsCoins.push({
|
||||
amount: action.amount.toString(),
|
||||
denom: action.denom,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: repay
|
||||
if (action.type === 'repay') {
|
||||
const index = resultPositionsDebts.findIndex((coin) => coin.denom === action.denom)
|
||||
resultPositionsDebts[index].amount = BigNumber(resultPositionsDebts[index].amount)
|
||||
.minus(action.amount)
|
||||
.toFixed()
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const assets = resultPositionsCoins.map((coin) => {
|
||||
const market = marketsData[coin.denom]
|
||||
|
||||
return {
|
||||
amount: coin.amount,
|
||||
denom: coin.denom,
|
||||
liquidationThreshold: market.liquidation_threshold,
|
||||
basePrice: tokenPrices[coin.denom],
|
||||
}
|
||||
})
|
||||
|
||||
const debts = resultPositionsDebts.map((debt) => {
|
||||
return {
|
||||
amount: debt.amount,
|
||||
denom: debt.denom,
|
||||
basePrice: tokenPrices[debt.denom],
|
||||
}
|
||||
})
|
||||
|
||||
const { health, maxLeverage, currentLeverage, netWorth, risk, totalPosition, totalDebt } =
|
||||
calculateStatsFromAccountPositions(assets, debts)
|
||||
|
||||
return {
|
||||
health,
|
||||
maxLeverage,
|
||||
currentLeverage,
|
||||
netWorth,
|
||||
risk,
|
||||
totalPosition,
|
||||
totalDebt,
|
||||
}
|
||||
}, [actions, marketsData, positionsData, tokenPrices])
|
||||
|
||||
return actionResultStats || currentStats
|
||||
}
|
||||
|
||||
export default useAccountStats
|
||||
|
@ -62,32 +62,31 @@ const useCalculateMaxBorrowAmount = (denom: string, isUnderCollateralized: boole
|
||||
const borrowTokenPrice = tokenPrices[denom]
|
||||
const tokenDecimals = getTokenDecimals(denom)
|
||||
|
||||
let maxValue
|
||||
let maxAmountCapacity
|
||||
if (isUnderCollateralized) {
|
||||
// MAX TO CREDIT ACCOUNT
|
||||
maxValue = BigNumber(totalLiabilitiesValue)
|
||||
maxAmountCapacity = BigNumber(totalLiabilitiesValue)
|
||||
.minus(totalWeightedPositions)
|
||||
.div(borrowTokenPrice * Number(marketsData[denom].max_loan_to_value) - borrowTokenPrice)
|
||||
.div(10 ** tokenDecimals)
|
||||
.decimalPlaces(tokenDecimals)
|
||||
.toNumber()
|
||||
} else {
|
||||
// MAX TO WALLET
|
||||
maxValue = BigNumber(totalWeightedPositions)
|
||||
maxAmountCapacity = BigNumber(totalWeightedPositions)
|
||||
.minus(totalLiabilitiesValue)
|
||||
.div(borrowTokenPrice)
|
||||
.div(10 ** tokenDecimals)
|
||||
.decimalPlaces(tokenDecimals)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
const marketLiquidity = BigNumber(redbankBalances?.[denom] ?? 0)
|
||||
.div(10 ** getTokenDecimals(denom))
|
||||
const marketLiquidity = redbankBalances?.[denom] ?? 0
|
||||
|
||||
const maxBorrowAmount = maxAmountCapacity.gt(marketLiquidity)
|
||||
? marketLiquidity
|
||||
: maxAmountCapacity.gt(0)
|
||||
? maxAmountCapacity
|
||||
: 0
|
||||
|
||||
return BigNumber(maxBorrowAmount)
|
||||
.dividedBy(10 ** tokenDecimals)
|
||||
.decimalPlaces(tokenDecimals)
|
||||
.toNumber()
|
||||
|
||||
if (marketLiquidity < maxValue) return marketLiquidity
|
||||
|
||||
return maxValue > 0 ? maxValue : 0
|
||||
}, [
|
||||
denom,
|
||||
getTokenValue,
|
||||
|
@ -7,6 +7,7 @@ import { getTokenDecimals } from 'utils/tokens'
|
||||
import useCreditAccountPositions from './useCreditAccountPositions'
|
||||
import useMarkets from './useMarkets'
|
||||
import useTokenPrices from './useTokenPrices'
|
||||
import useRedbankBalances from './useRedbankBalances'
|
||||
|
||||
const getApproximateHourlyInterest = (amount: string, borrowAPY: string) => {
|
||||
const hourlyAPY = BigNumber(borrowAPY).div(24 * 365)
|
||||
@ -20,6 +21,7 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
|
||||
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
|
||||
const { data: marketsData } = useMarkets()
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
const { data: redbankBalances } = useRedbankBalances()
|
||||
|
||||
const tokenDecimals = getTokenDecimals(denom)
|
||||
|
||||
@ -37,7 +39,7 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
|
||||
}, [denom, positionsData])
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
if (!marketsData || !tokenPrices || !positionsData || !denom) return 0
|
||||
if (!marketsData || !tokenPrices || !positionsData || !redbankBalances || !denom) return 0
|
||||
|
||||
const hasDebt = positionsData.debts.length > 0
|
||||
|
||||
@ -98,15 +100,20 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
|
||||
maxAmountCapacity = maxAmountCapacity < 0 ? 0 : maxAmountCapacity
|
||||
}
|
||||
|
||||
const isCapacityHigherThanBalance = BigNumber(maxAmountCapacity).gt(tokenAmountInCreditAccount)
|
||||
const marketLiquidity = redbankBalances?.[denom] ?? 0
|
||||
|
||||
if (!borrow && isCapacityHigherThanBalance) {
|
||||
const maxWithdrawAmount = BigNumber(maxAmountCapacity).gt(marketLiquidity)
|
||||
? marketLiquidity
|
||||
: maxAmountCapacity
|
||||
const isMaxAmountHigherThanBalance = BigNumber(maxWithdrawAmount).gt(tokenAmountInCreditAccount)
|
||||
|
||||
if (!borrow && isMaxAmountHigherThanBalance) {
|
||||
return BigNumber(tokenAmountInCreditAccount)
|
||||
.div(10 ** tokenDecimals)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
return BigNumber(maxAmountCapacity)
|
||||
return BigNumber(maxWithdrawAmount)
|
||||
.div(10 ** tokenDecimals)
|
||||
.decimalPlaces(tokenDecimals)
|
||||
.toNumber()
|
||||
@ -116,6 +123,7 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
|
||||
getTokenValue,
|
||||
marketsData,
|
||||
positionsData,
|
||||
redbankBalances,
|
||||
tokenAmountInCreditAccount,
|
||||
tokenDecimals,
|
||||
tokenPrices,
|
||||
|
Loading…
Reference in New Issue
Block a user