/ src / components / transactions / Swap / SwapModalContent.tsx
SwapModalContent.tsx
  1  import { SwitchVerticalIcon } from '@heroicons/react/outline';
  2  import { Trans } from '@lingui/macro';
  3  import { Box, Stack, SvgIcon, Typography } from '@mui/material';
  4  import { BigNumber } from 'bignumber.js';
  5  import React, { useRef, useState } from 'react';
  6  import { PriceImpactTooltip } from 'src/components/infoTooltips/PriceImpactTooltip';
  7  import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
  8  import { TokenIcon } from 'src/components/primitives/TokenIcon';
  9  import { Warning } from 'src/components/primitives/Warning';
 10  import { Asset, AssetInput } from 'src/components/transactions/AssetInput';
 11  import { TxModalDetails } from 'src/components/transactions/FlowCommons/TxModalDetails';
 12  import { StETHCollateralWarning } from 'src/components/Warnings/StETHCollateralWarning';
 13  import { CollateralType } from 'src/helpers/types';
 14  import { minimumReceivedAfterSlippage } from 'src/hooks/paraswap/common';
 15  import { useCollateralSwap } from 'src/hooks/paraswap/useCollateralSwap';
 16  import { getDebtCeilingData } from 'src/hooks/useAssetCaps';
 17  import { useModalContext } from 'src/hooks/useModal';
 18  import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw';
 19  import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
 20  import { ListSlippageButton } from 'src/modules/dashboard/lists/SlippageList';
 21  import { useRootStore } from 'src/store/root';
 22  import { remainingCap } from 'src/utils/getMaxAmountAvailableToSupply';
 23  import { displayGhoForMintableMarket } from 'src/utils/ghoUtilities';
 24  import { calculateHFAfterSwap } from 'src/utils/hfUtils';
 25  import { amountToUsd } from 'src/utils/utils';
 26  import { useShallow } from 'zustand/shallow';
 27  
 28  import {
 29    ComputedUserReserveData,
 30    ExtendedFormattedUser,
 31    useAppDataContext,
 32  } from '../../../hooks/app-data-provider/useAppDataProvider';
 33  import { ModalWrapperProps } from '../FlowCommons/ModalWrapper';
 34  import { TxSuccessView } from '../FlowCommons/Success';
 35  import { ErrorType, getAssetCollateralType, useFlashloan } from '../utils';
 36  import { ParaswapErrorDisplay } from '../Warnings/ParaswapErrorDisplay';
 37  import { SwapActions } from './SwapActions';
 38  import { SwapModalDetails } from './SwapModalDetails';
 39  
 40  export type SupplyProps = {
 41    underlyingAsset: string;
 42  };
 43  
 44  export const SwapModalContent = ({
 45    poolReserve,
 46    userReserve,
 47    isWrongNetwork,
 48    user,
 49  }: ModalWrapperProps & { user: ExtendedFormattedUser }) => {
 50    const { reserves, marketReferencePriceInUsd } = useAppDataContext();
 51    const [currentChainId, currentMarket, currentNetworkConfig] = useRootStore(
 52      useShallow((store) => [store.currentChainId, store.currentMarket, store.currentNetworkConfig])
 53    );
 54    const { currentAccount } = useWeb3Context();
 55    const { gasLimit, mainTxState: supplyTxState, txError } = useModalContext();
 56  
 57    const swapTargets = reserves
 58      .filter((r) => !displayGhoForMintableMarket({ symbol: r.symbol, currentMarket }))
 59      .filter((r) => r.underlyingAsset !== poolReserve.underlyingAsset && !r.isFrozen)
 60      .map((reserve) => ({
 61        address: reserve.underlyingAsset,
 62        symbol: reserve.symbol,
 63        iconSymbol: reserve.iconSymbol,
 64      }));
 65  
 66    // states
 67    const [_amount, setAmount] = useState('');
 68    const amountRef = useRef<string>('');
 69    const [targetReserve, setTargetReserve] = useState<Asset>(swapTargets[0]);
 70    const [maxSlippage, setMaxSlippage] = useState('0.1');
 71  
 72    const swapTarget = user.userReservesData.find(
 73      (r) => r.underlyingAsset === targetReserve.address
 74    ) as ComputedUserReserveData;
 75  
 76    // a user can never swap more then 100% of available as the txn would fail on withdraw step
 77    const maxAmountToSwap = BigNumber.min(
 78      userReserve.underlyingBalance,
 79      new BigNumber(poolReserve.availableLiquidity).multipliedBy(0.99)
 80    ).toString(10);
 81  
 82    const isMaxSelected = _amount === '-1';
 83    const amount = isMaxSelected ? maxAmountToSwap : _amount;
 84  
 85    const {
 86      inputAmountUSD,
 87      inputAmount,
 88      outputAmount,
 89      outputAmountUSD,
 90      error,
 91      loading: routeLoading,
 92      buildTxFn,
 93    } = useCollateralSwap({
 94      chainId: currentNetworkConfig.underlyingChainId || currentChainId,
 95      userAddress: currentAccount,
 96      swapIn: { ...poolReserve, amount: amountRef.current },
 97      swapOut: { ...swapTarget.reserve, amount: '0' },
 98      max: isMaxSelected,
 99      skip: supplyTxState.loading || false,
100      maxSlippage: Number(maxSlippage),
101    });
102  
103    const loadingSkeleton = routeLoading && outputAmountUSD === '0';
104  
105    const handleChange = (value: string) => {
106      const maxSelected = value === '-1';
107      amountRef.current = maxSelected ? maxAmountToSwap : value;
108      setAmount(value);
109    };
110  
111    const { hfAfterSwap, hfEffectOfFromAmount } = calculateHFAfterSwap({
112      fromAmount: amount,
113      fromAssetData: poolReserve,
114      fromAssetUserData: userReserve,
115      user,
116      toAmountAfterSlippage: outputAmount,
117      toAssetData: swapTarget.reserve,
118    });
119  
120    // if the hf would drop below 1 from the hf effect a flashloan should be used to mitigate liquidation
121    const shouldUseFlashloan = useFlashloan(user.healthFactor, hfEffectOfFromAmount);
122  
123    // consider caps
124    // we cannot check this in advance as it's based on the swap result
125    const remainingSupplyCap = remainingCap(
126      swapTarget.reserve.supplyCap,
127      swapTarget.reserve.totalLiquidity
128    );
129    const remainingCapUsd = amountToUsd(
130      remainingSupplyCap,
131      swapTarget.reserve.formattedPriceInMarketReferenceCurrency,
132      marketReferencePriceInUsd
133    );
134  
135    const assetsBlockingWithdraw = useZeroLTVBlockingWithdraw();
136  
137    let blockingError: ErrorType | undefined = undefined;
138    if (assetsBlockingWithdraw.length > 0 && !assetsBlockingWithdraw.includes(poolReserve.symbol)) {
139      blockingError = ErrorType.ZERO_LTV_WITHDRAW_BLOCKED;
140    } else if (!remainingSupplyCap.eq('-1') && remainingCapUsd.lt(outputAmountUSD)) {
141      blockingError = ErrorType.SUPPLY_CAP_REACHED;
142    } else if (shouldUseFlashloan && !poolReserve.flashLoanEnabled) {
143      blockingError = ErrorType.FLASH_LOAN_NOT_AVAILABLE;
144    }
145  
146    const BlockingError: React.FC = () => {
147      switch (blockingError) {
148        case ErrorType.SUPPLY_CAP_REACHED:
149          return <Trans>Supply cap on target reserve reached. Try lowering the amount.</Trans>;
150        case ErrorType.ZERO_LTV_WITHDRAW_BLOCKED:
151          return (
152            <Trans>
153              Assets with zero LTV ({assetsBlockingWithdraw.join(', ')}) must be withdrawn or disabled
154              as collateral to perform this action
155            </Trans>
156          );
157        case ErrorType.FLASH_LOAN_NOT_AVAILABLE:
158          return (
159            <Trans>
160              Due to health factor impact, a flashloan is required to perform this transaction, but
161              Aave Governance has disabled flashloan availability for this asset. Try lowering the
162              amount or supplying additional collateral.
163            </Trans>
164          );
165        default:
166          return null;
167      }
168    };
169  
170    if (supplyTxState.success)
171      return (
172        <TxSuccessView
173          action={<Trans>Switched</Trans>}
174          amount={amountRef.current}
175          symbol={poolReserve.symbol}
176        />
177      );
178  
179    // hf is only relevant when there are borrows
180    const showHealthFactor =
181      user &&
182      user.totalBorrowsMarketReferenceCurrency !== '0' &&
183      poolReserve.reserveLiquidationThreshold !== '0';
184  
185    const { debtCeilingReached: sourceDebtCeiling } = getDebtCeilingData(swapTarget.reserve);
186    const swapSourceCollateralType = getAssetCollateralType(
187      userReserve,
188      user.totalCollateralUSD,
189      user.isInIsolationMode,
190      sourceDebtCeiling
191    );
192  
193    const { debtCeilingReached: targetDebtCeiling } = getDebtCeilingData(swapTarget.reserve);
194    let swapTargetCollateralType = getAssetCollateralType(
195      swapTarget,
196      user.totalCollateralUSD,
197      user.isInIsolationMode,
198      targetDebtCeiling
199    );
200  
201    // If the user is swapping all of their isolated asset to an asset that is not supplied,
202    // then the swap target will be enabled as collateral as part of the swap.
203    if (
204      isMaxSelected &&
205      swapSourceCollateralType === CollateralType.ISOLATED_ENABLED &&
206      swapTarget.underlyingBalance === '0'
207    ) {
208      if (swapTarget.reserve.isIsolated) {
209        swapTargetCollateralType = CollateralType.ISOLATED_ENABLED;
210      } else {
211        swapTargetCollateralType = CollateralType.ENABLED;
212      }
213    }
214  
215    // If the user is swapping all of their enabled asset to an isolated asset that is not supplied,
216    // and no other supplied assets are being used as collateral,
217    // then the swap target will be enabled as collateral and the user will be in isolation mode.
218    if (
219      isMaxSelected &&
220      swapSourceCollateralType === CollateralType.ENABLED &&
221      swapTarget.underlyingBalance === '0' &&
222      swapTarget.reserve.isIsolated
223    ) {
224      const reservesAsCollateral = user.userReservesData.filter(
225        (r) => r.usageAsCollateralEnabledOnUser
226      );
227  
228      if (
229        reservesAsCollateral.length === 1 &&
230        reservesAsCollateral[0].underlyingAsset === userReserve.underlyingAsset
231      ) {
232        swapTargetCollateralType = CollateralType.ISOLATED_ENABLED;
233      }
234    }
235  
236    const minimumReceived = minimumReceivedAfterSlippage(
237      outputAmount,
238      maxSlippage,
239      swapTarget.reserve.decimals
240    );
241  
242    return (
243      <>
244        {/* {showIsolationWarning && (
245              <Typography>You are about to enter into isolation. FAQ link</Typography>
246            )} */}
247        <AssetInput
248          value={amount}
249          onChange={handleChange}
250          usdValue={inputAmountUSD}
251          symbol={poolReserve.iconSymbol}
252          assets={[
253            {
254              balance: maxAmountToSwap,
255              address: poolReserve.underlyingAsset,
256              symbol: poolReserve.symbol,
257              iconSymbol: poolReserve.iconSymbol,
258            },
259          ]}
260          maxValue={maxAmountToSwap}
261          inputTitle={<Trans>Supplied asset amount</Trans>}
262          balanceText={<Trans>Supply balance</Trans>}
263          isMaxSelected={isMaxSelected}
264        />
265        <Box sx={{ padding: '18px', pt: '14px', display: 'flex', justifyContent: 'space-between' }}>
266          <SvgIcon sx={{ fontSize: '18px !important' }}>
267            <SwitchVerticalIcon />
268          </SvgIcon>
269  
270          <PriceImpactTooltip
271            loading={loadingSkeleton}
272            outputAmountUSD={outputAmountUSD}
273            inputAmountUSD={inputAmountUSD}
274          />
275        </Box>
276        <AssetInput
277          value={outputAmount}
278          onSelect={setTargetReserve}
279          usdValue={outputAmountUSD}
280          symbol={targetReserve.symbol}
281          assets={swapTargets}
282          inputTitle={<Trans>Switch to</Trans>}
283          balanceText={<Trans>Supply balance</Trans>}
284          disableInput
285          loading={loadingSkeleton}
286        />
287        {error && !loadingSkeleton && (
288          <Typography variant="helperText" color="error.main">
289            {error}
290          </Typography>
291        )}
292        {!error && blockingError !== undefined && (
293          <Typography variant="helperText" color="error.main">
294            <BlockingError />
295          </Typography>
296        )}
297  
298        {swapTarget.reserve.symbol === 'stETH' && (
299          <Warning severity="warning" sx={{ mt: 2, mb: 0 }}>
300            <StETHCollateralWarning />
301          </Warning>
302        )}
303  
304        <TxModalDetails
305          gasLimit={gasLimit}
306          slippageSelector={
307            <ListSlippageButton
308              selectedSlippage={maxSlippage}
309              setSlippage={setMaxSlippage}
310              slippageTooltipHeader={
311                <Stack direction="row" gap={2} alignItems="center" justifyContent="space-between">
312                  <Trans>Minimum amount received</Trans>
313                  <Stack alignItems="end">
314                    <Stack direction="row">
315                      <TokenIcon
316                        symbol={swapTarget.reserve.iconSymbol}
317                        sx={{ mr: 1, fontSize: '14px' }}
318                      />
319                      <FormattedNumber value={minimumReceived} variant="secondary12" />
320                    </Stack>
321                  </Stack>
322                </Stack>
323              }
324            />
325          }
326        >
327          <SwapModalDetails
328            showHealthFactor={showHealthFactor}
329            healthFactor={user?.healthFactor}
330            healthFactorAfterSwap={hfAfterSwap.toString(10)}
331            swapSource={{ ...userReserve, collateralType: swapSourceCollateralType }}
332            swapTarget={{ ...swapTarget, collateralType: swapTargetCollateralType }}
333            toAmount={outputAmount}
334            fromAmount={amount === '' ? '0' : amount}
335            loading={loadingSkeleton}
336          />
337        </TxModalDetails>
338  
339        {txError && <ParaswapErrorDisplay txError={txError} />}
340  
341        <SwapActions
342          isMaxSelected={isMaxSelected}
343          poolReserve={poolReserve}
344          amountToSwap={inputAmount}
345          amountToReceive={minimumReceived}
346          isWrongNetwork={isWrongNetwork}
347          targetReserve={swapTarget.reserve}
348          symbol={poolReserve.symbol}
349          blocked={
350            blockingError !== undefined || error !== '' || swapTarget.reserve.symbol === 'stETH'
351          }
352          useFlashLoan={shouldUseFlashloan}
353          loading={routeLoading}
354          buildTxFn={buildTxFn}
355        />
356      </>
357    );
358  };