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:
Gustavo Mauricio 2022-11-09 12:47:36 +00:00 committed by GitHub
parent 27cdd1c954
commit 56bfea8ed0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 487 additions and 139 deletions

View File

@ -12,12 +12,18 @@ import useCalculateMaxBorrowAmount from 'hooks/useCalculateMaxBorrowAmount'
import useMarkets from 'hooks/useMarkets' import useMarkets from 'hooks/useMarkets'
import useTokenPrices from 'hooks/useTokenPrices' import useTokenPrices from 'hooks/useTokenPrices'
import { formatCurrency } from 'utils/formatters' 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 { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
import Button from './Button' import ProgressBar from './ProgressBar'
import SemiCircleProgress from './SemiCircleProgress'
import ContainerSecondary from './ContainerSecondary' import ContainerSecondary from './ContainerSecondary'
import Spinner from './Spinner' import Spinner from './Spinner'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import Button from './Button'
type Props = { type Props = {
show: boolean show: boolean
@ -29,21 +35,46 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
const [amount, setAmount] = useState(0) const [amount, setAmount] = useState(0)
const [isBorrowToCreditAccount, setIsBorrowToCreditAccount] = useState(false) 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 tokenSymbol = getTokenSymbol(tokenDenom)
const { mutate, isLoading } = useBorrowFunds( const { mutate, isLoading } = useBorrowFunds(borrowAmount, tokenDenom, !isBorrowToCreditAccount, {
BigNumber(amount)
.times(10 ** getTokenDecimals(tokenDenom))
.toNumber(),
tokenDenom,
!isBorrowToCreditAccount,
{
onSuccess: () => { onSuccess: () => {
onClose() onClose()
toast.success(`${amount} ${tokenSymbol} successfully Borrowed`) toast.success(`${amount} ${tokenSymbol} successfully Borrowed`)
}, },
}, })
)
const { data: tokenPrices } = useTokenPrices() const { data: tokenPrices } = useTokenPrices()
const { data: balancesData } = useAllBalances() const { data: balancesData } = useAllBalances()
@ -94,6 +125,17 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
setAmount(0) 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 ( return (
<Transition appear show={show} as={React.Fragment}> <Transition appear show={show} as={React.Fragment}>
<Dialog as='div' className='relative z-10' onClose={onClose}> <Dialog as='div' className='relative z-10' onClose={onClose}>
@ -120,25 +162,13 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
leaveFrom='opacity-100 scale-100' leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95' 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 && ( {isLoading && (
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'> <div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
<Spinner /> <Spinner />
</div> </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'> <div className='flex flex-1 flex-col p-4'>
<Dialog.Title as='h3' className='mb-4 text-center font-medium'> <Dialog.Title as='h3' className='mb-4 text-center font-medium'>
Borrow {tokenSymbol} Borrow {tokenSymbol}
@ -219,6 +249,111 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
Borrow Borrow
</Button> </Button>
</div> </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> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -12,8 +12,8 @@ import useCreditManagerStore from 'stores/useCreditManagerStore'
import useWalletStore from 'stores/useWalletStore' import useWalletStore from 'stores/useWalletStore'
import { formatCurrency } from 'utils/formatters' import { formatCurrency } from 'utils/formatters'
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens' import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
import { chain } from 'utils/chains'
import Button from '../Button' import Button from 'components/Button'
const CreditManager = () => { const CreditManager = () => {
const [showFundWalletModal, setShowFundWalletModal] = useState(false) const [showFundWalletModal, setShowFundWalletModal] = useState(false)
@ -78,11 +78,23 @@ const CreditManager = () => {
<ContainerSecondary className='mb-2 text-sm'> <ContainerSecondary className='mb-2 text-sm'>
<div className='mb-1 flex justify-between'> <div className='mb-1 flex justify-between'>
<div>Total Position:</div> <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>
<div className='flex justify-between'> <div className='flex justify-between'>
<div>Total Liabilities:</div> <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> </div>
</ContainerSecondary> </ContainerSecondary>
<ContainerSecondary> <ContainerSecondary>

View File

@ -4,6 +4,7 @@ import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import BigNumber from 'bignumber.js'
import ArrowRightLine from 'components/Icons/arrow-right-line.svg' import ArrowRightLine from 'components/Icons/arrow-right-line.svg'
import ProgressBar from 'components/ProgressBar' import ProgressBar from 'components/ProgressBar'
@ -17,6 +18,7 @@ import useCreditAccounts from 'hooks/useCreditAccounts'
import useCreditManagerStore from 'stores/useCreditManagerStore' import useCreditManagerStore from 'stores/useCreditManagerStore'
import useWalletStore from 'stores/useWalletStore' import useWalletStore from 'stores/useWalletStore'
import { formatCurrency } from 'utils/formatters' import { formatCurrency } from 'utils/formatters'
import { chain } from 'utils/chains'
import Button from './Button' import Button from './Button'
import SemiCircleProgress from './SemiCircleProgress' import SemiCircleProgress from './SemiCircleProgress'
@ -81,7 +83,13 @@ const Navigation = () => {
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
{accountStats && ( {accountStats && (
<> <>
<p>{formatCurrency(accountStats.netWorth)}</p> <p>
{formatCurrency(
BigNumber(accountStats.netWorth)
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
.toNumber(),
)}
</p>
{/* TOOLTIP */} {/* TOOLTIP */}
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}> <div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
<SemiCircleProgress <SemiCircleProgress

View File

@ -18,6 +18,9 @@ import Button from './Button'
import ContainerSecondary from './ContainerSecondary' import ContainerSecondary from './ContainerSecondary'
import Spinner from './Spinner' 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 = { type Props = {
show: boolean show: boolean
onClose: () => void onClose: () => void
@ -37,6 +40,8 @@ const RepayModal = ({ show, onClose, tokenDenom }: Props) => {
positionsData?.debts.find((coin) => coin.denom === tokenDenom)?.amount ?? 0 positionsData?.debts.find((coin) => coin.denom === tokenDenom)?.amount ?? 0
return BigNumber(tokenDebtAmount) return BigNumber(tokenDebtAmount)
.times(REPAY_BUFFER)
.decimalPlaces(0)
.div(10 ** getTokenDecimals(tokenDenom)) .div(10 ** getTokenDecimals(tokenDenom))
.toNumber() .toNumber()
}, [positionsData, tokenDenom]) }, [positionsData, tokenDenom])

View File

@ -6,7 +6,6 @@ import { toast } from 'react-toastify'
import Slider from 'components/Slider' import Slider from 'components/Slider'
import useWithdrawFunds from 'hooks/mutations/useWithdrawFunds' import useWithdrawFunds from 'hooks/mutations/useWithdrawFunds'
import useAccountStats from 'hooks/useAccountStats'
import useAllBalances from 'hooks/useAllBalances' import useAllBalances from 'hooks/useAllBalances'
import useCalculateMaxWithdrawAmount from 'hooks/useCalculateMaxWithdrawAmount' import useCalculateMaxWithdrawAmount from 'hooks/useCalculateMaxWithdrawAmount'
import useCreditAccountPositions from 'hooks/useCreditAccountPositions' import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
@ -15,12 +14,14 @@ import useTokenPrices from 'hooks/useTokenPrices'
import useCreditManagerStore from 'stores/useCreditManagerStore' import useCreditManagerStore from 'stores/useCreditManagerStore'
import { formatCurrency } from 'utils/formatters' import { formatCurrency } from 'utils/formatters'
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens' 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 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 WithdrawModal = ({ show, onClose }: any) => {
const [amount, setAmount] = useState(0) const [amount, setAmount] = useState(0)
@ -35,7 +36,6 @@ const WithdrawModal = ({ show, onClose }: any) => {
const { data: balancesData } = useAllBalances() const { data: balancesData } = useAllBalances()
const { data: tokenPrices } = useTokenPrices() const { data: tokenPrices } = useTokenPrices()
const { data: marketsData } = useMarkets() const { data: marketsData } = useMarkets()
const accountStats = useAccountStats()
const selectedTokenSymbol = getTokenSymbol(selectedToken) const selectedTokenSymbol = getTokenSymbol(selectedToken)
const selectedTokenDecimals = getTokenDecimals(selectedToken) const selectedTokenDecimals = getTokenDecimals(selectedToken)
@ -46,7 +46,7 @@ const WithdrawModal = ({ show, onClose }: any) => {
.toNumber() .toNumber()
}, [positionsData, selectedTokenDecimals, selectedToken]) }, [positionsData, selectedTokenDecimals, selectedToken])
const { borrowAmount, withdrawAmount } = useMemo(() => { const { actions, borrowAmount, withdrawAmount } = useMemo(() => {
const borrowAmount = const borrowAmount =
amount > tokenAmountInCreditAccount amount > tokenAmountInCreditAccount
? BigNumber(amount) ? BigNumber(amount)
@ -62,8 +62,22 @@ const WithdrawModal = ({ show, onClose }: any) => {
return { return {
borrowAmount, borrowAmount,
withdrawAmount, 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, { const { mutate, isLoading } = useWithdrawFunds(withdrawAmount, borrowAmount, selectedToken, {
onSuccess: () => { onSuccess: () => {
@ -234,7 +248,13 @@ const WithdrawModal = ({ show, onClose }: any) => {
<div className='mb-2 rounded-md border border-white/20 p-3'> <div className='mb-2 rounded-md border border-white/20 p-3'>
{accountStats && ( {accountStats && (
<div className='flex items-center gap-x-3'> <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 */} {/* TOOLTIP */}
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}> <div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
<SemiCircleProgress <SemiCircleProgress
@ -251,13 +271,21 @@ const WithdrawModal = ({ show, onClose }: any) => {
<div className='mb-1 flex justify-between'> <div className='mb-1 flex justify-between'>
<div>Total Position:</div> <div>Total Position:</div>
<div className='font-semibold'> <div className='font-semibold'>
{formatCurrency(accountStats?.totalPosition ?? 0)} {formatCurrency(
BigNumber(accountStats?.totalPosition ?? 0)
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
.toNumber(),
)}
</div> </div>
</div> </div>
<div className='flex justify-between'> <div className='flex justify-between'>
<div>Total Liabilities:</div> <div>Total Liabilities:</div>
<div className='font-semibold'> <div className='font-semibold'>
{formatCurrency(accountStats?.totalDebt ?? 0)} {formatCurrency(
BigNumber(accountStats?.totalDebt ?? 0)
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
.toNumber(),
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,4 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import BigNumber from 'bignumber.js'
import { useMemo } from 'react' import { useMemo } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@ -8,64 +7,51 @@ import useCreditManagerStore from 'stores/useCreditManagerStore'
import useWalletStore from 'stores/useWalletStore' import useWalletStore from 'stores/useWalletStore'
import { queryKeys } from 'types/query-keys-factory' import { queryKeys } from 'types/query-keys-factory'
import { hardcodedFee } from 'utils/contants' 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 = ( const useRepayFunds = (
amount: number, amount: number,
denom: string, denom: string,
options: Omit<UseMutationOptions, 'onError'>, options: Omit<UseMutationOptions, 'onError'>,
) => { ) => {
const signingClient = useWalletStore((s) => s.signingClient) const creditManagerClient = useWalletStore((s) => s.clients.creditManager)
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount) const selectedAccount = useCreditManagerStore((s) => s.selectedAccount ?? '')
const address = useWalletStore((s) => s.address) const address = useWalletStore((s) => s.address)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const adjustedAmount = BigNumber(amount).times(REPAY_BUFFER).decimalPlaces(0).toString() const actions = useMemo(() => {
return [
const executeMsg = useMemo(() => {
return {
update_credit_account: {
account_id: selectedAccount,
actions: [
{ {
deposit: { deposit: {
denom: denom, denom: denom,
amount: adjustedAmount, amount: String(amount),
}, },
}, },
{ {
repay: { repay: {
denom: denom, denom: denom,
amount: adjustedAmount, amount: String(amount),
}, },
}, },
], ]
}, }, [amount, denom])
}
}, [adjustedAmount, denom, selectedAccount])
return useMutation( return useMutation(
async () => async () =>
await signingClient?.execute( await creditManagerClient?.updateCreditAccount(
address, { accountId: selectedAccount, actions },
contractAddresses.creditManager,
executeMsg,
hardcodedFee, hardcodedFee,
undefined, undefined,
[ [
{ {
denom, denom,
amount: adjustedAmount, amount: String(amount),
}, },
], ],
), ),
{ {
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(queryKeys.creditAccountsPositions(selectedAccount ?? '')) queryClient.invalidateQueries(queryKeys.creditAccountsPositions(selectedAccount))
queryClient.invalidateQueries(queryKeys.tokenBalance(address, denom)) queryClient.invalidateQueries(queryKeys.tokenBalance(address, denom))
queryClient.invalidateQueries(queryKeys.allBalances(address)) queryClient.invalidateQueries(queryKeys.allBalances(address))
queryClient.invalidateQueries(queryKeys.redbankBalances()) queryClient.invalidateQueries(queryKeys.redbankBalances())

View File

@ -2,7 +2,6 @@ import BigNumber from 'bignumber.js'
import { useMemo } from 'react' import { useMemo } from 'react'
import useCreditManagerStore from 'stores/useCreditManagerStore' import useCreditManagerStore from 'stores/useCreditManagerStore'
import { getTokenDecimals } from 'utils/tokens'
import useCreditAccountPositions from './useCreditAccountPositions' import useCreditAccountPositions from './useCreditAccountPositions'
import useMarkets from './useMarkets' import useMarkets from './useMarkets'
@ -18,42 +17,38 @@ const getRiskFromAverageLiquidationLTVs = (value: number) => {
return 1 return 1
} }
const useAccountStats = () => { type Asset = {
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount) amount: string
denom: string
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '') liquidationThreshold: string
const { data: marketsData } = useMarkets() basePrice: number
const { data: tokenPrices } = useTokenPrices()
return 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
return BigNumber(amount)
.div(10 ** getTokenDecimals(denom))
.times(tokenPrices[denom])
.toNumber()
} }
const totalPosition = positionsData.coins.reduce((acc, coin) => { type Debt = {
const tokenTotalValue = getTokenTotalUSDValue(coin.amount, coin.denom) 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() return BigNumber(tokenTotalValue).plus(acc).toNumber()
}, 0) }, 0)
const totalDebt = positionsData.debts.reduce((acc, coin) => { const totalDebt = debts.reduce((acc, debt) => {
const tokenTotalValue = getTokenTotalUSDValue(coin.amount, coin.denom) const tokenTotalValue = BigNumber(debt.amount).times(debt.basePrice)
return BigNumber(tokenTotalValue).plus(acc).toNumber() return BigNumber(tokenTotalValue).plus(acc).toNumber()
}, 0) }, 0)
const totalWeightedPositions = positionsData.coins.reduce((acc, coin) => { const totalWeightedPositions = assets.reduce((acc, asset) => {
const tokenWeightedValue = BigNumber(getTokenTotalUSDValue(coin.amount, coin.denom)).times( const assetWeightedValue = BigNumber(asset.amount)
Number(marketsData[coin.denom].liquidation_threshold), .times(asset.basePrice)
) .times(asset.liquidationThreshold)
return tokenWeightedValue.plus(acc).toNumber() return assetWeightedValue.plus(acc).toNumber()
}, 0) }, 0)
const netWorth = BigNumber(totalPosition).minus(totalDebt).toNumber() const netWorth = BigNumber(totalPosition).minus(totalDebt).toNumber()
@ -72,6 +67,55 @@ const useAccountStats = () => {
? getRiskFromAverageLiquidationLTVs(liquidationLTVsWeightedAverage) ? getRiskFromAverageLiquidationLTVs(liquidationLTVsWeightedAverage)
: 0 : 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()
const currentStats = useMemo(() => {
if (!marketsData || !tokenPrices || !positionsData) return null
const assets = positionsData.coins.map((coin) => {
const market = marketsData[coin.denom]
return {
amount: coin.amount,
denom: coin.denom,
liquidationThreshold: market.liquidation_threshold,
basePrice: tokenPrices[coin.denom],
}
})
const debts = positionsData.debts.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 { return {
health, health,
maxLeverage, maxLeverage,
@ -82,6 +126,129 @@ const useAccountStats = () => {
totalDebt, totalDebt,
} }
}, [marketsData, positionsData, tokenPrices]) }, [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 export default useAccountStats

View File

@ -62,32 +62,31 @@ const useCalculateMaxBorrowAmount = (denom: string, isUnderCollateralized: boole
const borrowTokenPrice = tokenPrices[denom] const borrowTokenPrice = tokenPrices[denom]
const tokenDecimals = getTokenDecimals(denom) const tokenDecimals = getTokenDecimals(denom)
let maxValue let maxAmountCapacity
if (isUnderCollateralized) { if (isUnderCollateralized) {
// MAX TO CREDIT ACCOUNT // MAX TO CREDIT ACCOUNT
maxValue = BigNumber(totalLiabilitiesValue) maxAmountCapacity = BigNumber(totalLiabilitiesValue)
.minus(totalWeightedPositions) .minus(totalWeightedPositions)
.div(borrowTokenPrice * Number(marketsData[denom].max_loan_to_value) - borrowTokenPrice) .div(borrowTokenPrice * Number(marketsData[denom].max_loan_to_value) - borrowTokenPrice)
.div(10 ** tokenDecimals)
.decimalPlaces(tokenDecimals)
.toNumber()
} else { } else {
// MAX TO WALLET // MAX TO WALLET
maxValue = BigNumber(totalWeightedPositions) maxAmountCapacity = BigNumber(totalWeightedPositions)
.minus(totalLiabilitiesValue) .minus(totalLiabilitiesValue)
.div(borrowTokenPrice) .div(borrowTokenPrice)
.div(10 ** tokenDecimals)
.decimalPlaces(tokenDecimals)
.toNumber()
} }
const marketLiquidity = BigNumber(redbankBalances?.[denom] ?? 0) const marketLiquidity = redbankBalances?.[denom] ?? 0
.div(10 ** getTokenDecimals(denom))
const maxBorrowAmount = maxAmountCapacity.gt(marketLiquidity)
? marketLiquidity
: maxAmountCapacity.gt(0)
? maxAmountCapacity
: 0
return BigNumber(maxBorrowAmount)
.dividedBy(10 ** tokenDecimals)
.decimalPlaces(tokenDecimals)
.toNumber() .toNumber()
if (marketLiquidity < maxValue) return marketLiquidity
return maxValue > 0 ? maxValue : 0
}, [ }, [
denom, denom,
getTokenValue, getTokenValue,

View File

@ -7,6 +7,7 @@ import { getTokenDecimals } from 'utils/tokens'
import useCreditAccountPositions from './useCreditAccountPositions' import useCreditAccountPositions from './useCreditAccountPositions'
import useMarkets from './useMarkets' import useMarkets from './useMarkets'
import useTokenPrices from './useTokenPrices' import useTokenPrices from './useTokenPrices'
import useRedbankBalances from './useRedbankBalances'
const getApproximateHourlyInterest = (amount: string, borrowAPY: string) => { const getApproximateHourlyInterest = (amount: string, borrowAPY: string) => {
const hourlyAPY = BigNumber(borrowAPY).div(24 * 365) const hourlyAPY = BigNumber(borrowAPY).div(24 * 365)
@ -20,6 +21,7 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '') const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
const { data: marketsData } = useMarkets() const { data: marketsData } = useMarkets()
const { data: tokenPrices } = useTokenPrices() const { data: tokenPrices } = useTokenPrices()
const { data: redbankBalances } = useRedbankBalances()
const tokenDecimals = getTokenDecimals(denom) const tokenDecimals = getTokenDecimals(denom)
@ -37,7 +39,7 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
}, [denom, positionsData]) }, [denom, positionsData])
const maxAmount = useMemo(() => { const maxAmount = useMemo(() => {
if (!marketsData || !tokenPrices || !positionsData || !denom) return 0 if (!marketsData || !tokenPrices || !positionsData || !redbankBalances || !denom) return 0
const hasDebt = positionsData.debts.length > 0 const hasDebt = positionsData.debts.length > 0
@ -98,15 +100,20 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
maxAmountCapacity = maxAmountCapacity < 0 ? 0 : maxAmountCapacity 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) return BigNumber(tokenAmountInCreditAccount)
.div(10 ** tokenDecimals) .div(10 ** tokenDecimals)
.toNumber() .toNumber()
} }
return BigNumber(maxAmountCapacity) return BigNumber(maxWithdrawAmount)
.div(10 ** tokenDecimals) .div(10 ** tokenDecimals)
.decimalPlaces(tokenDecimals) .decimalPlaces(tokenDecimals)
.toNumber() .toNumber()
@ -116,6 +123,7 @@ const useCalculateMaxWithdrawAmount = (denom: string, borrow: boolean) => {
getTokenValue, getTokenValue,
marketsData, marketsData,
positionsData, positionsData,
redbankBalances,
tokenAmountInCreditAccount, tokenAmountInCreditAccount,
tokenDecimals, tokenDecimals,
tokenPrices, tokenPrices,