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:
parent
7daec73bba
commit
0f8e656651
38
__tests__/components/Modals/vault/VaultBorrowings.test.tsx
Normal file
38
__tests__/components/Modals/vault/VaultBorrowings.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
@ -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/",
|
||||
|
@ -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 |
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
10
src/hooks/useMarketAssets.tsx
Normal file
10
src/hooks/useMarketAssets.tsx
Normal 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: [],
|
||||
})
|
||||
}
|
53
src/hooks/useUpdateAccount.tsx
Normal file
53
src/hooks/useUpdateAccount.tsx
Normal 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 }
|
||||
}
|
1
src/types/interfaces/market.d.ts
vendored
1
src/types/interfaces/market.d.ts
vendored
@ -6,4 +6,5 @@ interface Market {
|
||||
depositEnabled: boolean
|
||||
borrowEnabled: boolean
|
||||
depositCap: string
|
||||
maxLtv: number
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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),
|
||||
}))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user