Mp 2543 farm adv borrow (#247)

* add calculation for max borrowings

* implement borrow side for modal

* add test and resolve comments
This commit is contained in:
Bob van der Helm 2023-06-07 16:22:13 +02:00 committed by GitHub
parent 7daec73bba
commit 0f8e656651
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 311 additions and 9 deletions

View File

@ -0,0 +1,38 @@
import { render } from '@testing-library/react'
import VaultBorrowings from 'components/Modals/vault/VaultBorrowings'
import BigNumber from 'bignumber.js'
jest.mock('hooks/usePrices', () =>
jest.fn(() => ({
data: [],
})),
)
jest.mock('hooks/useMarketAssets', () =>
jest.fn(() => ({
data: [],
})),
)
describe('<VaultBorrowings />', () => {
const defaultProps: {
account: Account
defaultBorrowDenom: string
onChangeBorrowings: (borrowings: Map<string, BigNumber>) => void
} = {
account: {
id: 'test',
deposits: [],
debts: [],
vaults: [],
lends: [],
},
defaultBorrowDenom: 'test-denom',
onChangeBorrowings: jest.fn(),
}
it('should render', () => {
const { container } = render(<VaultBorrowings {...defaultProps} />)
expect(container).toBeInTheDocument()
})
})

View File

@ -6,6 +6,7 @@
"build": "yarn validate-env && next build",
"dev": "next dev",
"test": "jest",
"test:cov": "jest --coverage",
"lint": "eslint ./src/ && yarn prettier-check",
"format": "eslint ./src/ ./__tests__/ --fix && prettier --write ./src/ ./__tests__/",
"prettier-check": "prettier --ignore-path .gitignore --check ./src/",

View File

@ -1,4 +1,3 @@
<svg viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.66667 4.00065V3.46732C9.66667 2.72058 9.66667 2.34721 9.52134 2.062C9.39351 1.81111 9.18954 1.60714 8.93865 1.47931C8.65344 1.33398 8.28007 1.33398 7.53333 1.33398H6.46667C5.71993 1.33398 5.34656 1.33398 5.06135 1.47931C4.81046 1.60714 4.60649 1.81111 4.47866 2.062C4.33333 2.34721 4.33333 2.72058 4.33333 3.46732V4.00065M1 4.00065H13M11.6667 4.00065V11.4673C11.6667 12.5874 11.6667 13.1475 11.4487 13.5753C11.2569 13.9516 10.951 14.2576 10.5746 14.4493C10.1468 14.6673 9.58677 14.6673 8.46667 14.6673H5.53333C4.41323 14.6673 3.85318 14.6673 3.42535 14.4493C3.04903 14.2576 2.74307 13.9516 2.55132 13.5753C2.33333 13.1475 2.33333 12.5874 2.33333 11.4673V4.00065" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<svg viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 4.00033V3.33366C11.3333 2.40024 11.3333 1.93353 11.1517 1.57701C10.9919 1.2634 10.7369 1.00844 10.4233 0.848648C10.0668 0.666992 9.60009 0.666992 8.66667 0.666992H7.33333C6.39991 0.666992 5.9332 0.666992 5.57668 0.848648C5.26308 1.00844 5.00811 1.2634 4.84832 1.57701C4.66667 1.93353 4.66667 2.40024 4.66667 3.33366V4.00033M0.5 4.00033H15.5M13.8333 4.00033V13.3337C13.8333 14.7338 13.8333 15.4339 13.5608 15.9686C13.3212 16.439 12.9387 16.8215 12.4683 17.0612C11.9335 17.3337 11.2335 17.3337 9.83333 17.3337H6.16667C4.76654 17.3337 4.06647 17.3337 3.53169 17.0612C3.06129 16.8215 2.67883 16.439 2.43915 15.9686C2.16667 15.4339 2.16667 14.7338 2.16667 13.3337V4.00033" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 827 B

After

Width:  |  Height:  |  Size: 837 B

View File

@ -1,3 +1,112 @@
export default function VaultBorrowings() {
return null
import { useMemo, useState } from 'react'
import BigNumber from 'bignumber.js'
import { BN } from 'utils/helpers'
import { getAssetByDenom } from 'utils/assets'
import Button from 'components/Button'
import TokenInput from 'components/TokenInput'
import Divider from 'components/Divider'
import Text from 'components/Text'
import { ArrowRight } from 'components/Icons'
import { formatPercent } from 'utils/formatters'
import Slider from 'components/Slider'
import usePrices from 'hooks/usePrices'
import useMarketAssets from 'hooks/useMarketAssets'
import { calculateMaxBorrowAmounts } from 'utils/vaults'
import React from 'react'
interface Props {
account: Account
defaultBorrowDenom: string
onChangeBorrowings: (borrowings: Map<string, BigNumber>) => void
}
export default function VaultBorrowings(props: Props) {
const { data: prices } = usePrices()
const { data: marketAssets } = useMarketAssets()
const [borrowings, setBorrowings] = useState<Map<string, BigNumber>>(
new Map().set(props.defaultBorrowDenom, BN(0)),
)
const maxAmounts: Map<string, BigNumber> = useMemo(
() =>
calculateMaxBorrowAmounts(props.account, marketAssets, prices, Array.from(borrowings.keys())),
[borrowings, marketAssets, prices, props.account],
)
const [percentage, setPercentage] = useState<number>(0)
function onChangeSlider(value: number) {
if (borrowings.size !== 1) return
const denom = Array.from(borrowings.keys())[0]
const newBorrowings = new Map().set(
denom,
maxAmounts.get(denom)?.times(value).div(100).toPrecision(0) || BN(0),
)
setBorrowings(newBorrowings)
props.onChangeBorrowings(newBorrowings)
setPercentage(value)
}
function updateAssets(denom: string, amount: BigNumber) {
const newborrowings = new Map(borrowings)
newborrowings.set(denom, amount)
setBorrowings(newborrowings)
props.onChangeBorrowings(newborrowings)
}
function onDelete(denom: string) {
const newborrowings = new Map(borrowings)
newborrowings.delete(denom)
setBorrowings(newborrowings)
props.onChangeBorrowings(newborrowings)
}
function addAsset() {
const newborrowings = new Map(borrowings)
// Replace with denom parameter from the modal (MP-2546)
newborrowings.set('', BN(0))
setBorrowings(newborrowings)
props.onChangeBorrowings(newborrowings)
}
return (
<div className='flex flex-grow flex-col gap-4 p-4'>
{Array.from(borrowings.entries()).map(([denom, amount]) => {
const asset = getAssetByDenom(denom)
if (!asset) return <React.Fragment key={`input-${denom}`}></React.Fragment>
return (
<TokenInput
key={`input-${denom}`}
amount={amount}
asset={asset}
max={maxAmounts.get(denom)?.plus(amount) || BN(0)}
maxText='Max Borrow'
onChange={(amount) => updateAssets(denom, amount)}
onDelete={() => onDelete(denom)}
/>
)
})}
{borrowings.size === 1 && <Slider onChange={onChangeSlider} value={percentage} />}
<Button text='Select borrow assets +' color='tertiary' onClick={addAsset} />
<Divider />
{Array.from(borrowings.entries()).map(([denom, amount]) => {
const asset = getAssetByDenom(denom)
const borrowRate = marketAssets?.find((market) => market.denom === denom)?.borrowRate
if (!asset || !borrowRate)
return <React.Fragment key={`borrow-rate-${denom}`}></React.Fragment>
return (
<div key={`borrow-rate-${denom}`} className='flex justify-between'>
<Text className='text-white/50'>Borrow APR {asset.symbol}</Text>
<Text>{formatPercent(borrowRate)}</Text>
</div>
)
})}
<Button color='primary' text='Deposit' rightIcon={<ArrowRight />} />
</div>
)
}

View File

@ -82,7 +82,7 @@ export default function VaultDeposit(props: Props) {
)
const [percentage, setPercentage] = useState(
primaryValue.dividedBy(maxAssetValueNonCustom).times(100).decimalPlaces(0).toNumber(),
primaryValue.dividedBy(maxAssetValueNonCustom).times(100).decimalPlaces(0).toNumber() || 0,
)
const disableInput =
(availablePrimaryAmount.isZero() || availableSecondaryAmount.isZero()) && !props.isCustomRatio

View File

@ -8,6 +8,7 @@ import VaultDeposit from 'components/Modals/vault/VaultDeposit'
import VaultDepositSubTitle from 'components/Modals/vault/VaultDepositSubTitle'
import useIsOpenArray from 'hooks/useIsOpenArray'
import { BN } from 'utils/helpers'
import useUpdateAccount from 'hooks/useUpdateAccount'
interface Props {
vault: Vault
@ -17,6 +18,7 @@ interface Props {
}
export default function VaultModalContent(props: Props) {
const { updatedAccount, onChangeBorrowings } = useUpdateAccount(props.account)
const [isOpen, toggleOpen] = useIsOpenArray(2, false)
const [primaryAmount, setPrimaryAmount] = useState<BigNumber>(BN(0))
const [secondaryAmount, setSecondaryAmount] = useState<BigNumber>(BN(0))
@ -68,7 +70,13 @@ export default function VaultModalContent(props: Props) {
toggleOpen: (index: number) => toggleOpen(index),
},
{
renderContent: () => <VaultBorrowings />,
renderContent: () => (
<VaultBorrowings
account={updatedAccount}
defaultBorrowDenom={props.secondaryAsset.denom}
onChangeBorrowings={onChangeBorrowings}
/>
),
title: 'Borrow',
isOpen: isOpen[1],
toggleOpen: (index: number) => toggleOpen(index),

View File

@ -11,7 +11,7 @@ import useStore from 'store'
import { BN } from 'utils/helpers'
import { FormattedNumber } from 'components/FormattedNumber'
import Button from 'components/Button'
import { ExclamationMarkTriangle } from 'components/Icons'
import { ExclamationMarkTriangle, TrashBin } from 'components/Icons'
import { Tooltip } from 'components/Tooltip'
interface Props {
@ -27,6 +27,7 @@ interface Props {
maxText?: string
warning?: string
onChangeAsset?: (asset: Asset) => void
onDelete?: () => void
}
export default function TokenInput(props: Props) {
@ -79,6 +80,11 @@ export default function TokenInput(props: Props) {
max={props.max}
className='border-none p-3'
/>
{props.onDelete && (
<div role='button' className='grid items-center pr-2' onClick={props.onDelete}>
<TrashBin width={16} />
</div>
)}
{props.warning && (
<div className='grid items-center px-2'>
<Tooltip

View File

@ -0,0 +1,10 @@
import useSWR from 'swr'
import getMarkets from 'api/markets/getMarkets'
export default function useMarketAssets() {
return useSWR(`marketAssets`, getMarkets, {
suspense: true,
fallbackData: [],
})
}

View File

@ -0,0 +1,53 @@
import BigNumber from 'bignumber.js'
import { useCallback, useState } from 'react'
import { BN } from 'utils/helpers'
export default function useUpdateAccount(account: Account) {
const [updatedAccount, setUpdatedAccount] = useState<Account>(account)
function getCoin(denom: string, amount: BigNumber): Coin {
return {
denom,
amount: amount.decimalPlaces(0).toString(),
}
}
const onChangeBorrowings = useCallback(
(borrowings: Map<string, BigNumber>) => {
const debts: Coin[] = [...account.debts]
const deposits: Coin[] = [...account.deposits]
const currentDebtDenoms = debts.map((debt) => debt.denom)
const currentDepositDenoms = deposits.map((deposit) => deposit.denom)
borrowings.forEach((amount, denom) => {
if (amount.isZero()) return
if (currentDebtDenoms.includes(denom)) {
const index = currentDebtDenoms.indexOf(denom)
const newAmount = BN(debts[index].amount).plus(amount)
debts[index] = getCoin(denom, newAmount)
} else {
debts.push(getCoin(denom, amount))
}
if (currentDepositDenoms.includes(denom)) {
const index = currentDepositDenoms.indexOf(denom)
const newAmount = BN(deposits[index].amount).plus(amount)
deposits[index] = getCoin(denom, newAmount)
} else {
deposits.push(getCoin(denom, amount))
}
})
setUpdatedAccount({
...account,
debts,
deposits,
})
},
[account],
)
return { updatedAccount, onChangeBorrowings }
}

View File

@ -6,4 +6,5 @@ interface Market {
depositEnabled: boolean
borrowEnabled: boolean
depositCap: string
maxLtv: number
}

View File

@ -1,6 +1,7 @@
import BigNumber from 'bignumber.js'
import { BN } from 'utils/helpers'
import { BN, getApproximateHourlyInterest } from 'utils/helpers'
import { getTokenValue } from 'utils/tokens'
export const calculateAccountBalance = (
account: Account | AccountChange,
@ -59,3 +60,35 @@ export const calculateAccountBorrowRate = (
export function getAmount(denom: string, coins: Coin[]): BigNumber {
return BN(coins.find((asset) => asset.denom === denom)?.amount ?? 0)
}
export function getNetCollateralValue(account: Account, marketAssets: Market[], prices: Coin[]) {
const depositCollateralValue = account.deposits.reduce((acc, coin) => {
const asset = marketAssets.find((asset) => asset.denom === coin.denom)
if (!asset) return acc
const marketValue = BN(getTokenValue(coin, prices))
const collateralValue = marketValue.times(asset.maxLtv)
return collateralValue.plus(acc)
}, BN(0))
// Implement Vault Collateral calculation (MP-2915)
const liabilitiesValue = account.debts.reduce((acc, coin) => {
const asset = marketAssets.find((asset) => asset.denom === coin.denom)
if (!asset) return acc
const estimatedInterestAmount = getApproximateHourlyInterest(coin.amount, asset.borrowRate)
const liability = BN(getTokenValue(coin, prices)).plus(estimatedInterestAmount)
return liability.plus(acc)
}, BN(0))
if (liabilitiesValue.isGreaterThan(depositCollateralValue)) {
return BN(0)
}
return depositCollateralValue.minus(liabilitiesValue)
}

View File

@ -4,3 +4,9 @@ BigNumber.config({ EXPONENTIAL_AT: 1e9 })
export function BN(n: BigNumber.Value) {
return new BigNumber(n)
}
export function getApproximateHourlyInterest(amount: string, borrowRate: number) {
return BigNumber(borrowRate)
.div(24 * 365)
.times(amount)
}

View File

@ -21,5 +21,6 @@ export function resolveMarketResponses(responses: MarketResponse[]): Market[] {
depositEnabled: response.deposit_enabled,
borrowEnabled: response.borrow_enabled,
depositCap: response.deposit_cap,
maxLtv: Number(response.max_loan_to_value),
}))
}

View File

@ -1,4 +1,7 @@
import BigNumber from 'bignumber.js'
import { getBaseAsset } from 'utils/assets'
import { BN } from 'utils/helpers'
export const getTokenSymbol = (denom: string, marketAssets: Asset[]) =>
marketAssets.find((asset) => asset.denom.toLowerCase() === denom.toLowerCase())?.symbol || ''
@ -11,3 +14,8 @@ export const getTokenIcon = (denom: string, marketAssets: Asset[]) =>
export const getTokenInfo = (denom: string, marketAssets: Asset[]) =>
marketAssets.find((asset) => asset.denom.toLowerCase() === denom.toLowerCase()) || getBaseAsset()
export function getTokenValue(coin: Coin, prices: Coin[]): BigNumber {
const price = prices.find((price) => price.denom === coin.denom)?.amount || '0'
return BN(price).times(coin.amount)
}

View File

@ -1,7 +1,36 @@
import BigNumber from 'bignumber.js'
import { IS_TESTNET } from 'constants/env'
import { TESTNET_VAULTS, VAULTS } from 'constants/vaults'
import { BN } from 'utils/helpers'
import { getNetCollateralValue } from 'utils/accounts'
export function getVaultMetaData(address: string) {
const vaults = IS_TESTNET ? TESTNET_VAULTS : VAULTS
return vaults.find((vault) => vault.address === address)
}
// This should be replaced when the calculation is made part of the Health Computer (MP-2877)
export function calculateMaxBorrowAmounts(
account: Account,
marketAssets: Market[],
prices: Coin[],
denoms: string[],
): Map<string, BigNumber> {
const maxAmounts = new Map<string, BigNumber>()
const collateralValue = getNetCollateralValue(account, marketAssets, prices)
for (const denom of denoms) {
const borrowAsset = marketAssets.find((asset) => asset.denom === denom)
const borrowAssetPrice = prices.find((price) => price.denom === denom)?.amount
if (!borrowAssetPrice || !borrowAsset) continue
const borrowValue = BN(1).minus(borrowAsset.maxLtv).times(borrowAssetPrice)
const amount = collateralValue.dividedBy(borrowValue).decimalPlaces(0)
maxAmounts.set(denom, amount)
}
return maxAmounts
}