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 };