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('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('Market') const [isConfirming, setIsConfirming] = useToggle() const [estimatedFee, setEstimatedFee] = useState(defaultFee) const { autoLendEnabledAccountIds } = useAutoLend() const isAutoLendEnabled = account ? autoLendEnabledAccountIds.includes(account.id) : false const modal = useStore((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 ( <>
{isAdvanced ? ( ) : ( )} {isRepayable && ENABLE_AUTO_REPAY && ( )}
{isAdvanced ? ( ) : ( <> )} {!isAdvanced && } {borrowAsset && borrowAmount.isGreaterThanOrEqualTo(availableLiquidity) && ( )} {isAdvanced ? ( ) : ( <>
You receive {formatValue(outputAssetAmount.toNumber(), { decimals: outputAsset.decimals, abbreviated: false, suffix: ` ${outputAsset.symbol}`, minDecimals: 0, maxDecimals: outputAsset.decimals, })}
)}
) }