mars-v2-frontend/src/components/Trade/TradeModule/SwapForm/index.tsx
2024-01-15 16:29:46 +01:00

471 lines
17 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 MarginSlider from 'components/MarginSlider'
import Text from 'components/Text'
import AssetSelectorPair from 'components/Trade/TradeModule/AssetSelector/AssetSelectorPair'
import AssetSelectorSingle from 'components/Trade/TradeModule/AssetSelector/AssetSelectorSingle'
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 { TradeDirectionSelector } from 'components/TradeDirectionSelector'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import useMarketEnabledAssets from 'hooks/assets/useMarketEnabledAssets'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useMarketAssets from 'hooks/markets/useMarketAssets'
import useMarketBorrowings from 'hooks/markets/useMarketBorrowings'
import useAutoLend from 'hooks/useAutoLend'
import useChainConfig from 'hooks/useChainConfig'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useHealthComputer from 'hooks/useHealthComputer'
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 { formatValue } from 'utils/formatters'
import { getCapLeftWithBuffer } from 'utils/generic'
import { asyncThrottle, BN } from 'utils/helpers'
interface Props {
buyAsset: Asset
sellAsset: Asset
isAdvanced: boolean
}
export default function SwapForm(props: Props) {
const { buyAsset, sellAsset, isAdvanced } = 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 [tradeDirection, setTradeDirection] = useState<TradeDirection>('long')
const { data: borrowAssets } = useMarketBorrowings()
const { data: marketAssets } = useMarketAssets()
const [inputAsset, outputAsset] = useMemo(() => {
if (isAdvanced) return [sellAsset, buyAsset]
if (tradeDirection === 'long') return [sellAsset, buyAsset]
return [buyAsset, sellAsset]
}, [buyAsset, sellAsset, tradeDirection, isAdvanced])
const { data: route, isLoading: isRouteLoading } = useSwapRoute(
inputAsset.denom,
outputAsset.denom,
)
const isBorrowEnabled = !!marketAssets.find(byDenom(inputAsset.denom))?.borrowEnabled
const isRepayable = !!account?.debts.find(byDenom(outputAsset.denom))
const [isMarginChecked, setMarginChecked] = useToggle(isBorrowEnabled ? useMargin : false)
const [isAutoRepayChecked, setAutoRepayChecked] = useToggle(
isRepayable && ENABLE_AUTO_REPAY ? useAutoRepay : false,
)
const [outputAssetAmount, setOutputAssetAmount] = useState(BN_ZERO)
const [inputAssetAmount, setInputAssetAmount] = useState(BN_ZERO)
const [maxOutputAmountEstimation, 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 chainConfig = useChainConfig()
const assets = useMarketEnabledAssets()
const depositCapReachedCoins: BNCoin[] = useMemo(() => {
const outputMarketAsset = marketAssets.find(byDenom(outputAsset.denom))
if (!outputMarketAsset) return []
const depositCapLeft = getCapLeftWithBuffer(outputMarketAsset.cap)
if (outputAssetAmount.isGreaterThan(depositCapLeft)) {
return [BNCoin.fromDenomAndBigNumber(outputAsset.denom, depositCapLeft)]
}
return []
}, [marketAssets, outputAsset.denom, outputAssetAmount])
const onChangeInputAmount = useCallback(
(amount: BigNumber) => {
setInputAssetAmount(amount)
const swapTo = { denom: inputAsset.denom, amount: amount.toString() }
throttledEstimateExactIn(chainConfig, swapTo, outputAsset.denom).then(setOutputAssetAmount)
},
[inputAsset.denom, throttledEstimateExactIn, chainConfig, outputAsset.denom],
)
const onChangeOutputAmount = useCallback(
(amount: BigNumber) => {
setOutputAssetAmount(amount)
const swapFrom = {
denom: outputAsset.denom,
amount: amount.toString(),
}
throttledEstimateExactIn(chainConfig, swapFrom, inputAsset.denom).then(setInputAssetAmount)
},
[outputAsset.denom, throttledEstimateExactIn, chainConfig, inputAsset.denom],
)
const handleRangeInputChange = useCallback(
(value: number) => {
onChangeOutputAmount(BN(value).shiftedBy(outputAsset.decimals).integerValue())
},
[onChangeOutputAmount, outputAsset.decimals],
)
const [maxInputAmount, imputMarginThreshold, marginRatio] = useMemo(() => {
const maxAmount = computeMaxSwapAmount(inputAsset.denom, outputAsset.denom, 'default')
const maxAmountOnMargin = computeMaxSwapAmount(
inputAsset.denom,
outputAsset.denom,
'margin',
).integerValue()
const marginRatio = maxAmount.dividedBy(maxAmountOnMargin)
estimateExactIn(
chainConfig,
{
denom: inputAsset.denom,
amount: (isMarginChecked ? maxAmountOnMargin : maxAmount).toString(),
},
outputAsset.denom,
).then(setMaxBuyableAmountEstimation)
if (isMarginChecked) return [maxAmountOnMargin, maxAmount, marginRatio]
if (inputAssetAmount.isGreaterThan(maxAmount)) onChangeInputAmount(maxAmount)
return [maxAmount, maxAmount, marginRatio]
}, [
computeMaxSwapAmount,
inputAsset.denom,
outputAsset.denom,
chainConfig,
isMarginChecked,
inputAssetAmount,
onChangeInputAmount,
])
const outputSideMarginThreshold = useMemo(() => {
return maxOutputAmountEstimation.multipliedBy(marginRatio)
}, [marginRatio, maxOutputAmountEstimation])
const swapTx = useMemo(() => {
const borrowCoin = inputAssetAmount.isGreaterThan(imputMarginThreshold)
? BNCoin.fromDenomAndBigNumber(inputAsset.denom, inputAssetAmount.minus(imputMarginThreshold))
: undefined
return swap({
accountId: account?.id || '',
coinIn: BNCoin.fromDenomAndBigNumber(inputAsset.denom, inputAssetAmount.integerValue()),
reclaim: removedLends[0],
borrow: borrowCoin,
denomOut: outputAsset.denom,
slippage,
isMax: inputAssetAmount.isEqualTo(maxInputAmount),
repay: isAutoRepayChecked,
})
}, [
removedLends,
account?.id,
outputAsset.denom,
imputMarginThreshold,
inputAsset.denom,
inputAssetAmount,
slippage,
swap,
maxInputAmount,
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(outputAsset.denom, 'asset'),
[computeLiquidationPrice, outputAsset.denom],
)
useEffect(() => {
swapTx.estimateFee().then(setEstimatedFee)
}, [swapTx])
const handleBuyClick = useCallback(async () => {
if (account?.id) {
setIsConfirming(true)
const isSucceeded = await swapTx.execute()
if (isSucceeded) {
setInputAssetAmount(BN_ZERO)
setOutputAssetAmount(BN_ZERO)
}
setIsConfirming(false)
}
}, [account?.id, swapTx, setIsConfirming])
useEffect(() => {
onChangeOutputAmount(BN_ZERO)
onChangeInputAmount(BN_ZERO)
}, [tradeDirection, onChangeOutputAmount, onChangeInputAmount])
useEffect(() => {
setOutputAssetAmount(BN_ZERO)
setInputAssetAmount(BN_ZERO)
setMarginChecked(isBorrowEnabled ? useMargin : false)
setAutoRepayChecked(isRepayable ? useAutoRepay : false)
simulateTrade(
BNCoin.fromDenomAndBigNumber(inputAsset.denom, BN_ZERO),
BNCoin.fromDenomAndBigNumber(outputAsset.denom, BN_ZERO),
BNCoin.fromDenomAndBigNumber(inputAsset.denom, BN_ZERO),
isAutoLendEnabled && !isAutoRepayChecked ? 'lend' : 'deposit',
isAutoRepayChecked,
)
}, [
isBorrowEnabled,
isRepayable,
useMargin,
useAutoRepay,
outputAsset.denom,
inputAsset.denom,
isAutoLendEnabled,
isAutoRepayChecked,
simulateTrade,
setMarginChecked,
setAutoRepayChecked,
])
useEffect(() => {
const removeDepositAmount = inputAssetAmount.isGreaterThanOrEqualTo(imputMarginThreshold)
? imputMarginThreshold
: inputAssetAmount
const addDebtAmount = inputAssetAmount.isGreaterThan(imputMarginThreshold)
? inputAssetAmount.minus(imputMarginThreshold)
: BN_ZERO
if (
removeDepositAmount.isZero() &&
addDebtAmount.isZero() &&
outputAssetAmount.isZero() &&
modal
)
return
const removeCoin = BNCoin.fromDenomAndBigNumber(inputAsset.denom, removeDepositAmount)
const addCoin = BNCoin.fromDenomAndBigNumber(outputAsset.denom, outputAssetAmount)
const debtCoin = BNCoin.fromDenomAndBigNumber(inputAsset.denom, addDebtAmount)
debouncedUpdateAccount(removeCoin, addCoin, debtCoin)
}, [
inputAssetAmount,
outputAssetAmount,
imputMarginThreshold,
outputAsset.denom,
inputAsset.denom,
debouncedUpdateAccount,
modal,
])
const borrowAsset = useMemo(
() => borrowAssets.find(byDenom(inputAsset.denom)),
[borrowAssets, inputAsset.denom],
)
useEffect(() => {
if (inputAssetAmount.isEqualTo(maxInputAmount) || outputAssetAmount.isZero()) return
if (outputAssetAmount.isEqualTo(maxOutputAmountEstimation)) setInputAssetAmount(maxInputAmount)
}, [inputAssetAmount, maxInputAmount, outputAssetAmount, maxOutputAmountEstimation])
const borrowAmount = useMemo(
() =>
inputAssetAmount.isGreaterThan(imputMarginThreshold)
? inputAssetAmount.minus(imputMarginThreshold)
: BN_ZERO,
[inputAssetAmount, imputMarginThreshold],
)
const availableLiquidity = useMemo(
() => borrowAsset?.liquidity?.amount ?? BN_ZERO,
[borrowAsset?.liquidity?.amount],
)
const isSwapDisabled = useMemo(
() =>
inputAssetAmount.isZero() ||
depositCapReachedCoins.length > 0 ||
borrowAmount.isGreaterThan(availableLiquidity) ||
route.length === 0,
[inputAssetAmount, depositCapReachedCoins, borrowAmount, availableLiquidity, route],
)
return (
<>
<div className='flex flex-wrap w-full'>
{isAdvanced ? (
<AssetSelectorSingle buyAsset={outputAsset} sellAsset={inputAsset} />
) : (
<AssetSelectorPair buyAsset={buyAsset} sellAsset={sellAsset} assets={assets} />
)}
<Divider />
<MarginToggle
checked={isMarginChecked}
onChange={handleMarginToggleChange}
disabled={!borrowAsset?.isMarket}
borrowAssetSymbol={inputAsset.symbol}
/>
<Divider />
{isRepayable && ENABLE_AUTO_REPAY && (
<AutoRepayToggle
checked={isAutoRepayChecked}
onChange={handleAutoRepayToggleChange}
buyAssetSymbol={outputAsset.symbol}
/>
)}
<div className='px-3'>
<OrderTypeSelector selected={selectedOrderType} onChange={setSelectedOrderType} />
</div>
<div className='flex flex-col w-full gap-6 px-3 mt-6'>
{isAdvanced ? (
<AssetAmountInput
label='Buy'
max={maxOutputAmountEstimation}
amount={outputAssetAmount}
setAmount={onChangeOutputAmount}
asset={outputAsset}
maxButtonLabel='Max Amount:'
disabled={isConfirming}
/>
) : (
<>
<TradeDirectionSelector
direction={tradeDirection}
onChangeDirection={setTradeDirection}
asset={buyAsset}
/>
<AssetAmountInput
max={maxInputAmount}
amount={inputAssetAmount}
setAmount={onChangeInputAmount}
asset={inputAsset}
maxButtonLabel='Balance:'
disabled={isConfirming}
/>
</>
)}
{!isAdvanced && <Divider />}
<MarginSlider
disabled={isConfirming || maxOutputAmountEstimation.isZero()}
onChange={handleRangeInputChange}
value={outputAssetAmount.shiftedBy(-outputAsset.decimals).toNumber()}
max={maxOutputAmountEstimation.shiftedBy(-outputAsset.decimals).toNumber()}
marginThreshold={
isMarginChecked
? outputSideMarginThreshold.shiftedBy(-outputAsset.decimals).toNumber()
: undefined
}
type='trade'
/>
<DepositCapMessage
action='buy'
coins={depositCapReachedCoins}
className='p-4 bg-white/5'
/>
{borrowAsset && borrowAmount.isGreaterThanOrEqualTo(availableLiquidity) && (
<AvailableLiquidityMessage
availableLiquidity={borrowAsset?.liquidity?.amount ?? BN_ZERO}
asset={borrowAsset}
/>
)}
{isAdvanced ? (
<AssetAmountInput
label='Sell'
max={maxInputAmount}
amount={inputAssetAmount}
setAmount={onChangeInputAmount}
asset={inputAsset}
maxButtonLabel='Balance:'
disabled={isConfirming}
/>
) : (
<>
<Divider />
<div className='flex justify-between w-full'>
<Text size='sm'>You receive</Text>
<Text size='sm'>
{formatValue(outputAssetAmount.toNumber(), {
decimals: outputAsset.decimals,
abbreviated: false,
suffix: ` ${outputAsset.symbol}`,
minDecimals: 0,
maxDecimals: outputAsset.decimals,
})}
</Text>
</div>
</>
)}
</div>
</div>
<div className='flex w-full px-3 pt-6'>
<TradeSummary
sellAsset={inputAsset}
buyAsset={outputAsset}
borrowRate={borrowAsset?.borrowRate}
buyAction={handleBuyClick}
buyButtonDisabled={isSwapDisabled}
showProgressIndicator={isConfirming || isRouteLoading}
isMargin={isMarginChecked}
borrowAmount={borrowAmount}
estimatedFee={estimatedFee}
route={route}
liquidationPrice={liquidationPrice}
sellAmount={inputAssetAmount}
buyAmount={outputAssetAmount}
isAdvanced={isAdvanced}
direction={tradeDirection}
/>
</div>
</>
)
}