mars-v2-frontend/src/components/Trade/TradeModule/SwapForm/index.tsx
Bob van der Helm 2effdfadac
Liq price in balances (#679)
* add liquidation price to balances table trade

* add depositcap to HLS

* fix: add width classes to the balances table, remove abbreviation, remove flicker

* fix: fixed the account selection and added a tooltip

* fix wasm file for debt liquidation price

---------

Co-authored-by: Linkie Link <linkielink.dev@gmail.com>
2023-12-07 14:44:31 +01:00

395 lines
14 KiB
TypeScript

import debounce from 'lodash.debounce'
import { useCallback, useEffect, useMemo, useState } from 'react'
import estimateExactIn from 'api/swap/estimateExactIn'
import AvailableLiquidityMessage from 'components/AvailableLiquidityMessage'
import DepositCapMessage from 'components/DepositCapMessage'
import Divider from 'components/Divider'
import RangeInput from 'components/RangeInput'
import AssetAmountInput from 'components/Trade/TradeModule/SwapForm/AssetAmountInput'
import AutoRepayToggle from 'components/Trade/TradeModule/SwapForm/AutoRepayToggle'
import MarginToggle from 'components/Trade/TradeModule/SwapForm/MarginToggle'
import OrderTypeSelector from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector'
import { AvailableOrderType } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/types'
import TradeSummary from 'components/Trade/TradeModule/SwapForm/TradeSummary'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import useAutoLend from 'hooks/useAutoLend'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useHealthComputer from 'hooks/useHealthComputer'
import useLocalStorage from 'hooks/useLocalStorage'
import useMarketAssets from 'hooks/useMarketAssets'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
import useSwapRoute from 'hooks/useSwapRoute'
import useToggle from 'hooks/useToggle'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { defaultFee, ENABLE_AUTO_REPAY } from 'utils/constants'
import { getCapLeftWithBuffer } from 'utils/generic'
import { asyncThrottle, BN } from 'utils/helpers'
interface Props {
buyAsset: Asset
sellAsset: Asset
}
export default function SwapForm(props: Props) {
const { buyAsset, sellAsset } = props
const useMargin = useStore((s) => s.useMargin)
const useAutoRepay = useStore((s) => s.useAutoRepay)
const account = useCurrentAccount()
const swap = useStore((s) => s.swap)
const [slippage] = useLocalStorage(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
const { computeMaxSwapAmount } = useHealthComputer(account)
const { data: borrowAssets } = useMarketBorrowings()
const { data: marketAssets } = useMarketAssets()
const { data: route, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom)
const isBorrowEnabled = !!marketAssets.find(byDenom(sellAsset.denom))?.borrowEnabled
const isRepayable = !!account?.debts.find(byDenom(buyAsset.denom))
const [isMarginChecked, setMarginChecked] = useToggle(isBorrowEnabled ? useMargin : false)
const [isAutoRepayChecked, setAutoRepayChecked] = useToggle(
isRepayable && ENABLE_AUTO_REPAY ? useAutoRepay : false,
)
const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO)
const [sellAssetAmount, setSellAssetAmount] = useState(BN_ZERO)
const [maxBuyableAmountEstimation, setMaxBuyableAmountEstimation] = useState(BN_ZERO)
const [selectedOrderType, setSelectedOrderType] = useState<AvailableOrderType>('Market')
const [isConfirming, setIsConfirming] = useToggle()
const [estimatedFee, setEstimatedFee] = useState(defaultFee)
const { autoLendEnabledAccountIds } = useAutoLend()
const isAutoLendEnabled = account ? autoLendEnabledAccountIds.includes(account.id) : false
const modal = useStore<string | null>((s) => s.fundAndWithdrawModal)
const { simulateTrade, removedLends, updatedAccount } = useUpdatedAccount(account)
const throttledEstimateExactIn = useMemo(() => asyncThrottle(estimateExactIn, 250), [])
const { computeLiquidationPrice } = useHealthComputer(updatedAccount)
const borrowAsset = useMemo(
() => borrowAssets.find(byDenom(sellAsset.denom)),
[borrowAssets, sellAsset.denom],
)
const depositCapReachedCoins: BNCoin[] = useMemo(() => {
const buyMarketAsset = marketAssets.find(byDenom(buyAsset.denom))
if (!buyMarketAsset) return []
const depositCapLeft = getCapLeftWithBuffer(buyMarketAsset.cap)
if (buyAssetAmount.isGreaterThan(depositCapLeft)) {
return [BNCoin.fromDenomAndBigNumber(buyAsset.denom, depositCapLeft)]
}
return []
}, [marketAssets, buyAsset.denom, buyAssetAmount])
const onChangeSellAmount = useCallback(
(amount: BigNumber) => {
setSellAssetAmount(amount)
throttledEstimateExactIn(
{ denom: sellAsset.denom, amount: amount.toString() },
buyAsset.denom,
).then(setBuyAssetAmount)
},
[sellAsset.denom, throttledEstimateExactIn, buyAsset.denom],
)
const onChangeBuyAmount = useCallback(
(amount: BigNumber) => {
setBuyAssetAmount(amount)
const swapFrom = {
denom: buyAsset.denom,
amount: amount.toString(),
}
throttledEstimateExactIn(swapFrom, sellAsset.denom).then(setSellAssetAmount)
},
[buyAsset.denom, throttledEstimateExactIn, sellAsset.denom],
)
const handleRangeInputChange = useCallback(
(value: number) => {
onChangeBuyAmount(BN(value).shiftedBy(buyAsset.decimals).integerValue())
},
[onChangeBuyAmount, buyAsset.decimals],
)
const [maxSellAmount, sellSideMarginThreshold, marginRatio] = useMemo(() => {
const maxAmount = computeMaxSwapAmount(sellAsset.denom, buyAsset.denom, 'default')
const maxAmountOnMargin = computeMaxSwapAmount(
sellAsset.denom,
buyAsset.denom,
'margin',
).integerValue()
const marginRatio = maxAmount.dividedBy(maxAmountOnMargin)
estimateExactIn(
{
denom: sellAsset.denom,
amount: (isMarginChecked ? maxAmountOnMargin : maxAmount).toString(),
},
buyAsset.denom,
).then(setMaxBuyableAmountEstimation)
if (isMarginChecked) return [maxAmountOnMargin, maxAmount, marginRatio]
if (sellAssetAmount.isGreaterThan(maxAmount)) onChangeSellAmount(maxAmount)
return [maxAmount, maxAmount, marginRatio]
}, [
computeMaxSwapAmount,
sellAsset.denom,
buyAsset.denom,
isMarginChecked,
onChangeSellAmount,
sellAssetAmount,
])
const buySideMarginThreshold = useMemo(() => {
return maxBuyableAmountEstimation.multipliedBy(marginRatio)
}, [marginRatio, maxBuyableAmountEstimation])
const swapTx = useMemo(() => {
const borrowCoin = sellAssetAmount.isGreaterThan(sellSideMarginThreshold)
? BNCoin.fromDenomAndBigNumber(
sellAsset.denom,
sellAssetAmount.minus(sellSideMarginThreshold),
)
: undefined
return swap({
accountId: account?.id || '',
coinIn: BNCoin.fromDenomAndBigNumber(sellAsset.denom, sellAssetAmount.integerValue()),
reclaim: removedLends[0],
borrow: borrowCoin,
denomOut: buyAsset.denom,
slippage,
isMax: sellAssetAmount.isEqualTo(maxSellAmount),
repay: isAutoRepayChecked,
})
}, [
removedLends,
account?.id,
buyAsset.denom,
sellSideMarginThreshold,
sellAsset.denom,
sellAssetAmount,
slippage,
swap,
maxSellAmount,
isAutoRepayChecked,
])
const debouncedUpdateAccount = useMemo(
() =>
debounce((removeCoin: BNCoin, addCoin: BNCoin, debtCoin: BNCoin) => {
simulateTrade(
removeCoin,
addCoin,
debtCoin,
isAutoLendEnabled && !isAutoRepayChecked ? 'lend' : 'deposit',
isAutoRepayChecked,
)
}, 100),
[simulateTrade, isAutoLendEnabled, isAutoRepayChecked],
)
const handleMarginToggleChange = useCallback(
(isMargin: boolean) => {
if (isBorrowEnabled) useStore.setState({ useMargin: isMargin })
setMarginChecked(isBorrowEnabled ? isMargin : false)
},
[isBorrowEnabled, setMarginChecked],
)
const handleAutoRepayToggleChange = useCallback(
(isAutoRepay: boolean) => {
if (isRepayable) useStore.setState({ useAutoRepay: isAutoRepay })
setAutoRepayChecked(isAutoRepay)
},
[isRepayable, setAutoRepayChecked],
)
const liquidationPrice = useMemo(
() => computeLiquidationPrice(props.buyAsset.denom, 'asset'),
[computeLiquidationPrice, props.buyAsset.denom],
)
useEffect(() => {
setBuyAssetAmount(BN_ZERO)
setSellAssetAmount(BN_ZERO)
setMarginChecked(isBorrowEnabled ? useMargin : false)
setAutoRepayChecked(isRepayable ? useAutoRepay : false)
simulateTrade(
BNCoin.fromDenomAndBigNumber(buyAsset.denom, BN_ZERO),
BNCoin.fromDenomAndBigNumber(sellAsset.denom, BN_ZERO),
BNCoin.fromDenomAndBigNumber(sellAsset.denom, BN_ZERO),
isAutoLendEnabled && !isAutoRepayChecked ? 'lend' : 'deposit',
isAutoRepayChecked,
)
}, [
isBorrowEnabled,
isRepayable,
useMargin,
useAutoRepay,
buyAsset.denom,
sellAsset.denom,
isAutoLendEnabled,
isAutoRepayChecked,
simulateTrade,
setMarginChecked,
setAutoRepayChecked,
])
useEffect(() => {
const removeDepositAmount = sellAssetAmount.isGreaterThanOrEqualTo(sellSideMarginThreshold)
? sellSideMarginThreshold
: sellAssetAmount
const addDebtAmount = sellAssetAmount.isGreaterThan(sellSideMarginThreshold)
? sellAssetAmount.minus(sellSideMarginThreshold)
: BN_ZERO
if (removeDepositAmount.isZero() && addDebtAmount.isZero() && buyAssetAmount.isZero() && modal)
return
const removeCoin = BNCoin.fromDenomAndBigNumber(sellAsset.denom, removeDepositAmount)
const debtCoin = BNCoin.fromDenomAndBigNumber(sellAsset.denom, addDebtAmount)
const addCoin = BNCoin.fromDenomAndBigNumber(buyAsset.denom, buyAssetAmount)
debouncedUpdateAccount(removeCoin, addCoin, debtCoin)
}, [
sellAssetAmount,
buyAssetAmount,
sellSideMarginThreshold,
buyAsset.denom,
sellAsset.denom,
debouncedUpdateAccount,
modal,
])
useEffect(() => {
swapTx.estimateFee().then(setEstimatedFee)
}, [swapTx])
const handleBuyClick = useCallback(async () => {
if (account?.id) {
setIsConfirming(true)
const isSucceeded = await swapTx.execute()
if (isSucceeded) {
setSellAssetAmount(BN_ZERO)
setBuyAssetAmount(BN_ZERO)
}
setIsConfirming(false)
}
}, [account?.id, swapTx, setIsConfirming])
useEffect(() => {
if (sellAssetAmount.isEqualTo(maxSellAmount) || buyAssetAmount.isZero()) return
if (buyAssetAmount.isEqualTo(maxBuyableAmountEstimation)) setSellAssetAmount(maxSellAmount)
}, [sellAssetAmount, maxSellAmount, buyAssetAmount, maxBuyableAmountEstimation])
const borrowAmount = useMemo(
() =>
sellAssetAmount.isGreaterThan(sellSideMarginThreshold)
? sellAssetAmount.minus(sellSideMarginThreshold)
: BN_ZERO,
[sellAssetAmount, sellSideMarginThreshold],
)
const availableLiquidity = useMemo(
() => borrowAsset?.liquidity?.amount ?? BN_ZERO,
[borrowAsset?.liquidity?.amount],
)
const isSwapDisabled = useMemo(
() =>
sellAssetAmount.isZero() ||
depositCapReachedCoins.length > 0 ||
borrowAmount.isGreaterThan(availableLiquidity) ||
route.length === 0,
[sellAssetAmount, depositCapReachedCoins, borrowAmount, availableLiquidity, route],
)
return (
<>
<Divider />
<MarginToggle
checked={isMarginChecked}
onChange={handleMarginToggleChange}
disabled={!borrowAsset?.isMarket}
borrowAssetSymbol={sellAsset.symbol}
/>
<Divider />
{isRepayable && ENABLE_AUTO_REPAY && (
<AutoRepayToggle
checked={isAutoRepayChecked}
onChange={handleAutoRepayToggleChange}
buyAssetSymbol={buyAsset.symbol}
/>
)}
<Divider />
<div className='px-3'>
<OrderTypeSelector selected={selectedOrderType} onChange={setSelectedOrderType} />
</div>
<div className='flex flex-col gap-6 px-3 mt-6'>
<AssetAmountInput
label='Buy'
max={maxBuyableAmountEstimation}
amount={buyAssetAmount}
setAmount={onChangeBuyAmount}
asset={buyAsset}
maxButtonLabel='Max Amount:'
disabled={isConfirming}
/>
<RangeInput
disabled={isConfirming || maxBuyableAmountEstimation.isZero()}
onChange={handleRangeInputChange}
value={buyAssetAmount.shiftedBy(-buyAsset.decimals).toNumber()}
max={maxBuyableAmountEstimation.shiftedBy(-buyAsset.decimals).toNumber()}
marginThreshold={
isMarginChecked
? buySideMarginThreshold.shiftedBy(-buyAsset.decimals).toNumber()
: undefined
}
/>
<DepositCapMessage action='buy' coins={depositCapReachedCoins} className='p-4 bg-white/5' />
{borrowAsset && borrowAmount.isGreaterThanOrEqualTo(availableLiquidity) && (
<AvailableLiquidityMessage
availableLiquidity={borrowAsset?.liquidity?.amount ?? BN_ZERO}
asset={borrowAsset}
/>
)}
<AssetAmountInput
label='Sell'
max={maxSellAmount}
amount={sellAssetAmount}
setAmount={onChangeSellAmount}
asset={sellAsset}
maxButtonLabel='Balance:'
disabled={isConfirming}
/>
<TradeSummary
buyAsset={buyAsset}
sellAsset={sellAsset}
borrowRate={borrowAsset?.borrowRate}
buyAction={handleBuyClick}
buyButtonDisabled={isSwapDisabled}
showProgressIndicator={isConfirming || isRouteLoading}
isMargin={isMarginChecked}
borrowAmount={borrowAmount}
estimatedFee={estimatedFee}
route={route}
liquidationPrice={liquidationPrice}
sellAmount={sellAssetAmount}
buyAmount={buyAssetAmount}
/>
</div>
</>
)
}