1deba2059e
* update generated types * added CRO to token info * update contract addresses to match latest deployment * feat: token prices fetched from oracle contract * trade page initial commit * trade asset action hook * extract max swap amount logic into custom hook * trade component ui adjustments * trade container min-width and some styling improvements * trade success message and loading indicator * normalize naming conventions on trading * max swap amount formula adjustments * trade execute msg with borrow. code cleanup * fix: click max update tokenOut amount. remove wallet from fund denom * delay token amount decimal conversion. input formatting * increase hardcoded gas * renamed max swappable amount hook * display token prices and market information on landing page * reset trade amounts when selected account change * max trade amount cleanup and minor performance optimizations * fix: liabilities value with 1 hour interest buffer for trade action * add token symbol to wallet and account labels * swap trade pairs icon and basic functionality * remove unnecessary optional chaining. comment adjusted * refactor useTokenPrices to build query dynamically on tokens data * extracted trade container and respective functionality into separate file * fix: properly calculate positions after full swap * mp-1218: trading using wallet
145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
import { useCallback, useMemo } from 'react'
|
|
import BigNumber from 'bignumber.js'
|
|
|
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
|
|
|
import useCreditAccountPositions from './useCreditAccountPositions'
|
|
import useMarkets from './useMarkets'
|
|
import useRedbankBalances from './useRedbankBalances'
|
|
import useTokenPrices from './useTokenPrices'
|
|
|
|
const getApproximateHourlyInterest = (amount: string, borrowAPY: string) => {
|
|
const hourlyAPY = BigNumber(borrowAPY).div(24 * 365)
|
|
|
|
return hourlyAPY.times(amount).toNumber()
|
|
}
|
|
|
|
// max trade amount doesnt consider wallet balance as its not relevant
|
|
// the entire token balance within the wallet will always be able to be fully swapped
|
|
const useCalculateMaxTradeAmount = (tokenIn: string, tokenOut: string, isMargin: boolean) => {
|
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
|
|
|
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
|
|
const { data: marketsData } = useMarkets()
|
|
const { data: tokenPrices } = useTokenPrices()
|
|
const { data: redbankBalances } = useRedbankBalances()
|
|
|
|
const accountAmount = useMemo(() => {
|
|
return BigNumber(
|
|
positionsData?.coins?.find((coin) => coin.denom === tokenIn)?.amount ?? 0,
|
|
).toNumber()
|
|
}, [positionsData, tokenIn])
|
|
|
|
const getTokenValue = useCallback(
|
|
(amount: string, denom: string) => {
|
|
if (!tokenPrices) return 0
|
|
|
|
return BigNumber(amount).times(tokenPrices[denom]).toNumber()
|
|
},
|
|
[tokenPrices],
|
|
)
|
|
|
|
return useMemo(() => {
|
|
if (!marketsData || !tokenPrices || !positionsData || !redbankBalances || !tokenIn || !tokenOut)
|
|
return 0
|
|
|
|
const totalWeightedPositions = positionsData.coins.reduce((acc, coin) => {
|
|
const tokenWeightedValue = BigNumber(getTokenValue(coin.amount, coin.denom)).times(
|
|
Number(marketsData[coin.denom].max_loan_to_value),
|
|
)
|
|
|
|
return tokenWeightedValue.plus(acc).toNumber()
|
|
}, 0)
|
|
|
|
// approximate debt value in an hour timespan to avoid throwing on smart contract level
|
|
// due to debt interest being applied
|
|
const totalLiabilitiesValue = positionsData.debts.reduce((acc, coin) => {
|
|
const estimatedInterestAmount = getApproximateHourlyInterest(
|
|
coin.amount,
|
|
marketsData[coin.denom].borrow_rate,
|
|
)
|
|
|
|
const tokenDebtValue = BigNumber(getTokenValue(coin.amount, coin.denom)).plus(
|
|
estimatedInterestAmount,
|
|
)
|
|
|
|
return tokenDebtValue.plus(acc).toNumber()
|
|
}, 0)
|
|
|
|
const tokenOutLTV = Number(marketsData[tokenOut].max_loan_to_value)
|
|
const tokenInLTV = Number(marketsData[tokenIn].max_loan_to_value)
|
|
|
|
// if the target token ltv higher, the full amount will always be able to be swapped
|
|
if (tokenOutLTV < tokenInLTV) {
|
|
// in theory, the most you can swap from x to y while keeping an health factor of 1
|
|
const maxSwapValue = BigNumber(totalLiabilitiesValue)
|
|
.minus(totalWeightedPositions)
|
|
.dividedBy(tokenOutLTV - tokenInLTV)
|
|
|
|
const maxSwapAmount = maxSwapValue.div(tokenPrices[tokenIn]).decimalPlaces(0)
|
|
|
|
// if the swappable amount is lower than the account amount, any further calculations are irrelevant
|
|
if (maxSwapAmount.isLessThanOrEqualTo(accountAmount)) return maxSwapAmount.toNumber()
|
|
}
|
|
|
|
// if margin is disabled, the max swap amount is capped at the account amount
|
|
if (!isMargin) {
|
|
return accountAmount
|
|
}
|
|
|
|
const estimatedTokenOutAmount = BigNumber(accountAmount).times(
|
|
tokenPrices[tokenIn] / tokenPrices[tokenOut],
|
|
)
|
|
|
|
let positionsCoins = [...positionsData.coins]
|
|
|
|
// if the target token is not in the account, add it to the positions
|
|
if (!positionsCoins.find((coin) => coin.denom === tokenOut)) {
|
|
positionsCoins.push({
|
|
amount: '0',
|
|
denom: tokenOut,
|
|
})
|
|
}
|
|
|
|
// calculate weighted positions assuming the initial swap is made
|
|
const totalWeightedPositionsAfterSwap = positionsCoins
|
|
.filter((coin) => coin.denom !== tokenIn)
|
|
.reduce((acc, coin) => {
|
|
const coinAmount =
|
|
coin.denom === tokenOut
|
|
? BigNumber(coin.amount).plus(estimatedTokenOutAmount).toString()
|
|
: coin.amount
|
|
|
|
const tokenWeightedValue = BigNumber(getTokenValue(coinAmount, coin.denom)).times(
|
|
Number(marketsData[coin.denom].max_loan_to_value),
|
|
)
|
|
|
|
return tokenWeightedValue.plus(acc).toNumber()
|
|
}, 0)
|
|
|
|
const maxBorrowValue =
|
|
totalWeightedPositionsAfterSwap === 0
|
|
? 0
|
|
: BigNumber(totalWeightedPositionsAfterSwap)
|
|
.minus(totalLiabilitiesValue)
|
|
.dividedBy(1 - tokenOutLTV)
|
|
.toNumber()
|
|
|
|
const maxBorrowAmount = BigNumber(maxBorrowValue).dividedBy(tokenPrices[tokenIn]).toNumber()
|
|
|
|
return BigNumber(accountAmount).plus(maxBorrowAmount).decimalPlaces(0).toNumber()
|
|
}, [
|
|
accountAmount,
|
|
getTokenValue,
|
|
isMargin,
|
|
marketsData,
|
|
positionsData,
|
|
redbankBalances,
|
|
tokenIn,
|
|
tokenOut,
|
|
tokenPrices,
|
|
])
|
|
}
|
|
|
|
export default useCalculateMaxTradeAmount
|