mars-v2-frontend/src/components/Modals/BorrowModal.tsx
Linkie Link dc6a2a6b16
repay from wallet hotfix (#691)
* fix: removed overpay from assets that are close to their cap or reached it

* fix: calculate the amount of overpay

* fix: added repay_from_wallet
2023-12-12 12:34:12 +01:00

353 lines
12 KiB
TypeScript

import BigNumber from 'bignumber.js'
import { useCallback, useEffect, useMemo, useState } from 'react'
import AccountSummary from 'components/Account/AccountSummary'
import AssetImage from 'components/Asset/AssetImage'
import Button from 'components/Button'
import Card from 'components/Card'
import DisplayCurrency from 'components/DisplayCurrency'
import Divider from 'components/Divider'
import { FormattedNumber } from 'components/FormattedNumber'
import { ArrowRight, InfoCircle } from 'components/Icons'
import Modal from 'components/Modal'
import Switch from 'components/Switch'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
import { BN_ZERO } from 'constants/math'
import useAutoLend from 'hooks/useAutoLend'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useHealthComputer from 'hooks/useHealthComputer'
import useMarketAssets from 'hooks/useMarketAssets'
import useToggle from 'hooks/useToggle'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import { getDepositAndLendCoinsToSpend } from 'hooks/useUpdatedAccount/functions'
import useWalletBalances from 'hooks/useWalletBalances'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { formatPercent } from 'utils/formatters'
import { BN } from 'utils/helpers'
import { getDebtAmountWithInterest } from 'utils/tokens'
interface Props {
account: Account
modal: BorrowModal
}
function getDebtAmount(modal: BorrowModal) {
return BN((modal.marketData as BorrowMarketTableData)?.debt ?? 0).toString()
}
function getAssetLogo(modal: BorrowModal) {
if (!modal.asset) return null
return <AssetImage asset={modal.asset} size={24} />
}
function RepayNotAvailable(props: { asset: Asset; repayFromWallet: boolean }) {
return (
<Card className='mt-6'>
<div className='flex items-start p-4'>
<InfoCircle className='w-6 mr-2 flex-0' />
<div className='flex flex-col flex-1 gap-1'>
<Text size='sm'>No funds for repay</Text>
<Text size='xs' className='text-white/40'>{`Unfortunately you don't have any ${
props.asset.symbol
} in your ${
props.repayFromWallet ? 'Wallet' : 'Credit Account'
} to repay the debt.`}</Text>
</div>
</div>
</Card>
)
}
export default function BorrowModalController() {
const account = useCurrentAccount()
const modal = useStore((s) => s.borrowModal)
if (account && modal) {
return <BorrowModal account={account} modal={modal} />
}
return null
}
function BorrowModal(props: Props) {
const { modal, account } = props
const [amount, setAmount] = useState(BN_ZERO)
const [borrowToWallet, setBorrowToWallet] = useToggle()
const [repayFromWallet, setRepayFromWallet] = useToggle()
const walletAddress = useStore((s) => s.address)
const { data: walletBalances } = useWalletBalances(walletAddress)
const borrow = useStore((s) => s.borrow)
const repay = useStore((s) => s.repay)
const asset = modal.asset
const isRepay = modal.isRepay ?? false
const [max, setMax] = useState(BN_ZERO)
const { simulateBorrow, simulateRepay } = useUpdatedAccount(account)
const { autoLendEnabledAccountIds } = useAutoLend()
const apy = modal.marketData.apy.borrow
const isAutoLendEnabled = autoLendEnabledAccountIds.includes(account.id)
const { computeMaxBorrowAmount } = useHealthComputer(account)
const totalDebt = BN(getDebtAmount(modal))
const { data: marketAssets } = useMarketAssets()
const [depositBalance, lendBalance] = useMemo(
() => [
account.deposits.find(byDenom(asset.denom))?.amount ?? BN_ZERO,
account.lends.find(byDenom(asset.denom))?.amount ?? BN_ZERO,
],
[account, asset.denom],
)
const totalDebtRepayAmount = useMemo(
() => getDebtAmountWithInterest(totalDebt, apy),
[totalDebt, apy],
)
const overpayExeedsCap = useMemo(() => {
const marketAsset = marketAssets.find(byDenom(asset.denom))
if (!marketAsset) return
const overpayAmount = totalDebtRepayAmount.minus(totalDebt)
const marketCapAfterOverpay = marketAsset.cap.used.plus(overpayAmount)
return marketAsset.cap.max.isLessThanOrEqualTo(marketCapAfterOverpay)
}, [marketAssets, asset.denom])
const maxRepayAmount = useMemo(() => {
const maxBalance = repayFromWallet
? BN(walletBalances.find(byDenom(asset.denom))?.amount ?? 0)
: depositBalance.plus(lendBalance)
return isRepay
? BigNumber.min(maxBalance, overpayExeedsCap ? totalDebt : totalDebtRepayAmount)
: BN_ZERO
}, [
depositBalance,
lendBalance,
isRepay,
totalDebtRepayAmount,
walletBalances,
asset.denom,
repayFromWallet,
])
function resetState() {
setAmount(BN_ZERO)
}
function onConfirmClick() {
if (!asset) return
const { lend } = getDepositAndLendCoinsToSpend(
BNCoin.fromDenomAndBigNumber(asset.denom, amount),
account,
)
if (isRepay) {
repay({
accountId: account.id,
coin: BNCoin.fromDenomAndBigNumber(asset.denom, amount),
accountBalance: amount.isEqualTo(totalDebtRepayAmount),
lend: repayFromWallet ? BNCoin.fromDenomAndBigNumber(asset.denom, BN_ZERO) : lend,
fromWallet: repayFromWallet,
})
} else {
borrow({
accountId: account.id,
coin: BNCoin.fromDenomAndBigNumber(asset.denom, amount),
borrowToWallet,
})
}
resetState()
useStore.setState({ borrowModal: null })
}
function onClose() {
resetState()
useStore.setState({ borrowModal: null })
}
const handleChange = useCallback(
(newAmount: BigNumber) => {
const coin = BNCoin.fromDenomAndBigNumber(asset.denom, newAmount)
if (!amount.isEqualTo(newAmount)) setAmount(newAmount)
if (!isRepay) return
const repayCoin = coin.amount.isGreaterThan(totalDebt)
? BNCoin.fromDenomAndBigNumber(asset.denom, totalDebt)
: coin
simulateRepay(repayCoin, repayFromWallet)
},
[amount, asset.denom, isRepay, simulateRepay, totalDebt, repayFromWallet],
)
const maxBorrow = useMemo(() => {
const maxBorrowAmount = isRepay
? BN_ZERO
: computeMaxBorrowAmount(asset.denom, borrowToWallet ? 'wallet' : 'deposit')
return BigNumber.min(maxBorrowAmount, modal.marketData?.liquidity?.amount || 0)
}, [asset.denom, borrowToWallet, computeMaxBorrowAmount, isRepay, modal.marketData])
useEffect(() => {
if (!account || isRepay) return
if (maxBorrow.isEqualTo(max)) return
setMax(maxBorrow)
}, [account, isRepay, maxBorrow, max])
useEffect(() => {
if (!isRepay) return
if (maxRepayAmount.isEqualTo(max)) return
setMax(maxRepayAmount)
}, [isRepay, max, maxRepayAmount])
useEffect(() => {
if (amount.isLessThanOrEqualTo(max)) return
handleChange(max)
setAmount(max)
}, [amount, max, handleChange])
useEffect(() => {
if (isRepay) return
const coin = BNCoin.fromDenomAndBigNumber(asset.denom, amount.isGreaterThan(max) ? max : amount)
const target = borrowToWallet ? 'wallet' : isAutoLendEnabled ? 'lend' : 'deposit'
simulateBorrow(target, coin)
}, [isRepay, borrowToWallet, isAutoLendEnabled, simulateBorrow, asset, amount, max])
if (!modal || !asset) return null
return (
<Modal
onClose={onClose}
header={
<span className='flex items-center gap-4 px-4'>
{getAssetLogo(modal)}
<Text>
{isRepay ? 'Repay' : 'Borrow'} {asset.symbol}
</Text>
</span>
}
headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b'
contentClassName='flex flex-col'
>
<div className='flex gap-3 px-6 py-4 border-b border-white/5 gradient-header'>
<TitleAndSubCell
title={formatPercent(modal.marketData.apy.borrow)}
sub={'Borrow Rate APY'}
/>
{totalDebt.isGreaterThan(0) && (
<>
<div className='h-100 w-[1px] bg-white/10' />
<div className='flex flex-col gap-0.5'>
<div className='flex gap-2'>
<FormattedNumber
className='text-xs'
amount={totalDebt.toNumber()}
options={{
decimals: asset.decimals,
abbreviated: false,
suffix: ` ${asset.symbol}`,
}}
/>
<DisplayCurrency
className='text-xs'
coin={BNCoin.fromDenomAndBigNumber(asset.denom, totalDebt)}
parentheses
/>
</div>
<Text size='xs' className='text-white/50' tag='span'>
Borrowed
</Text>
</div>
</>
)}
<div className='h-100 w-[1px] bg-white/10' />
<div className='flex flex-col gap-0.5'>
<div className='flex gap-2'>
<FormattedNumber
className='text-xs'
amount={modal.marketData?.liquidity?.amount.toNumber() ?? 0}
options={{ decimals: asset.decimals, abbreviated: true, suffix: ` ${asset.symbol}` }}
animate
/>
<DisplayCurrency
className='text-xs'
coin={BNCoin.fromDenomAndBigNumber(
asset.denom,
modal.marketData?.liquidity?.amount ?? BN_ZERO,
)}
parentheses
/>
</div>
<Text size='xs' className='text-white/50' tag='span'>
Liquidity available
</Text>
</div>
</div>
<div className='flex items-start flex-1 gap-6 p-6'>
<Card
className='flex flex-1 p-4 bg-white/5'
contentClassName='gap-6 flex flex-col justify-between h-full min-h-[380px]'
>
<div className='flex flex-wrap w-full'>
<TokenInputWithSlider
asset={asset}
onChange={handleChange}
amount={amount}
max={max}
disabled={max.isZero()}
className='w-full'
maxText='Max'
warningMessages={[]}
/>
{isRepay && maxRepayAmount.isZero() && (
<RepayNotAvailable asset={asset} repayFromWallet={repayFromWallet} />
)}
{isRepay ? (
<>
<Divider className='my-6' />
<div className='flex flex-wrap flex-1'>
<Text className='w-full mb-1'>Repay from Wallet</Text>
<Text size='xs' className='text-white/50'>
Repay your debt directly from your wallet
</Text>
</div>
<div className='flex flex-wrap items-center justify-end'>
<Switch
name='borrow-to-wallet'
checked={repayFromWallet}
onChange={setRepayFromWallet}
/>
</div>
</>
) : (
<>
<Divider className='my-6' />
<div className='flex flex-wrap flex-1'>
<Text className='w-full mb-1'>Receive funds to Wallet</Text>
<Text size='xs' className='text-white/50'>
Your borrowed funds will directly go to your wallet
</Text>
</div>
<div className='flex flex-wrap items-center justify-end'>
<Switch
name='borrow-to-wallet'
checked={borrowToWallet}
onChange={setBorrowToWallet}
/>
</div>
</>
)}
</div>
<Button
onClick={onConfirmClick}
className='w-full'
disabled={amount.isZero()}
text={isRepay ? 'Repay' : 'Borrow'}
rightIcon={<ArrowRight />}
/>
</Card>
<AccountSummary account={account} />
</div>
</Modal>
)
}