EmodeModalContent.tsx
1 import { formatUserSummary } from '@aave/math-utils'; 2 import { ArrowNarrowRightIcon } from '@heroicons/react/solid'; 3 import { Trans } from '@lingui/macro'; 4 import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; 5 import CloseIcon from '@mui/icons-material/Close'; 6 import { 7 Box, 8 Collapse, 9 Divider, 10 MenuItem, 11 Select, 12 Stack, 13 SvgIcon, 14 Switch, 15 Table, 16 TableBody, 17 TableCell, 18 tableCellClasses, 19 TableContainer, 20 TableHead, 21 TableRow, 22 Typography, 23 } from '@mui/material'; 24 import { useState } from 'react'; 25 import { MaxLTVTooltip } from 'src/components/infoTooltips/MaxLTVTooltip'; 26 import { FormattedNumber } from 'src/components/primitives/FormattedNumber'; 27 import { Link } from 'src/components/primitives/Link'; 28 import { Row } from 'src/components/primitives/Row'; 29 import { TokenIcon } from 'src/components/primitives/TokenIcon'; 30 import { Warning } from 'src/components/primitives/Warning'; 31 import { EmodeCategory } from 'src/helpers/types'; 32 import { 33 ExtendedFormattedUser, 34 useAppDataContext, 35 } from 'src/hooks/app-data-provider/useAppDataProvider'; 36 import { useCurrentTimestamp } from 'src/hooks/useCurrentTimestamp'; 37 import { useModalContext } from 'src/hooks/useModal'; 38 import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; 39 import { useRootStore } from 'src/store/root'; 40 import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig'; 41 42 import { TxErrorView } from '../FlowCommons/Error'; 43 import { GasEstimationError } from '../FlowCommons/GasEstimationError'; 44 import { TxSuccessView } from '../FlowCommons/Success'; 45 import { DetailsHFLine, TxModalDetails } from '../FlowCommons/TxModalDetails'; 46 import { TxModalTitle } from '../FlowCommons/TxModalTitle'; 47 import { ChangeNetworkWarning } from '../Warnings/ChangeNetworkWarning'; 48 import { EmodeActions } from './EmodeActions'; 49 50 export enum ErrorType { 51 EMODE_DISABLED_LIQUIDATION, 52 CLOSE_POSITIONS_BEFORE_SWITCHING, 53 } 54 55 export type EModeCategoryDisplay = EmodeCategory & { 56 available: boolean; // indicates if the user can enter this category 57 }; 58 59 // An E-Mode category is available if the user has no borrow positions outside of the category 60 function isEModeCategoryAvailable(user: ExtendedFormattedUser, eMode: EmodeCategory): boolean { 61 const borrowableReserves = eMode.assets 62 .filter((asset) => asset.borrowable) 63 .map((asset) => asset.underlyingAsset); 64 65 const hasIncompatiblePositions = user.userReservesData.some( 66 (userReserve) => 67 Number(userReserve.scaledVariableDebt) > 0 && 68 !borrowableReserves.includes(userReserve.reserve.underlyingAsset) 69 ); 70 71 return !hasIncompatiblePositions; 72 } 73 74 export const EmodeModalContent = ({ user }: { user: ExtendedFormattedUser }) => { 75 const { 76 reserves, 77 eModes, 78 marketReferenceCurrencyDecimals, 79 marketReferencePriceInUsd, 80 userReserves, 81 } = useAppDataContext(); 82 const currentChainId = useRootStore((store) => store.currentChainId); 83 const { chainId: connectedChainId, readOnlyModeAddress } = useWeb3Context(); 84 const currentTimestamp = useCurrentTimestamp(1); 85 const { gasLimit, mainTxState: emodeTxState, txError } = useModalContext(); 86 const [disableEmode, setDisableEmode] = useState(false); 87 88 const eModeCategories: Record<number, EModeCategoryDisplay> = Object.fromEntries( 89 Object.entries(eModes).map(([key, value]) => [ 90 key, 91 { 92 ...value, 93 available: isEModeCategoryAvailable(user, value), 94 }, 95 ]) 96 ); 97 98 const [selectedEmode, setSelectedEmode] = useState<EModeCategoryDisplay>( 99 user.userEmodeCategoryId === 0 ? eModeCategories[1] : eModeCategories[user.userEmodeCategoryId] 100 ); 101 const networkConfig = getNetworkConfig(currentChainId); 102 103 // calcs 104 const newSummary = formatUserSummary({ 105 currentTimestamp, 106 userReserves: userReserves, 107 formattedReserves: reserves, 108 userEmodeCategoryId: disableEmode ? 0 : selectedEmode.id, 109 marketReferenceCurrencyDecimals, 110 marketReferencePriceInUsd, 111 }); 112 113 // error handling 114 let blockingError: ErrorType | undefined = undefined; 115 // if user is disabling eMode 116 if (user.isInEmode && disableEmode) { 117 if (Number(newSummary.healthFactor) < 1.01 && newSummary.healthFactor !== '-1') { 118 blockingError = ErrorType.EMODE_DISABLED_LIQUIDATION; // intl.formatMessage(messages.eModeDisabledLiquidation); 119 } 120 } 121 122 const Blocked: React.FC = () => { 123 switch (blockingError) { 124 case ErrorType.EMODE_DISABLED_LIQUIDATION: 125 return ( 126 <Warning severity="error" sx={{ mt: 6, alignItems: 'center' }}> 127 <Typography variant="subheader1" color="#4F1919"> 128 <Trans>Cannot disable E-Mode</Trans> 129 </Typography> 130 <Typography variant="caption"> 131 <Trans> 132 You can not disable E-Mode because that could cause liquidation. To exit E-Mode 133 supply or repay borrowed positions. 134 </Trans> 135 </Typography> 136 </Warning> 137 ); 138 default: 139 return null; 140 } 141 }; 142 143 // is Network mismatched 144 const isWrongNetwork: boolean = currentChainId !== connectedChainId; 145 146 const ArrowRight: React.FC = () => ( 147 <SvgIcon color="primary" sx={{ fontSize: '14px', mx: 1 }}> 148 <ArrowNarrowRightIcon /> 149 </SvgIcon> 150 ); 151 152 // Shown only if the user is disabling eMode, is not blocked from disabling, and has a health factor that is decreasing 153 // HF will never decrease on enable or switch because all borrow positions must initially be in the eMode category 154 const showLiquidationRiskWarning: boolean = 155 user.userEmodeCategoryId !== 0 && 156 disableEmode && 157 blockingError === undefined && 158 Number(newSummary.healthFactor).toFixed(3) < Number(user.healthFactor).toFixed(3); // Comparing without rounding causes stuttering, HFs update asyncronously 159 160 // Shown only if the user has a collateral asset which is changing in LTV 161 const showLTVChange = 162 user.currentLoanToValue !== '0' && 163 Number(newSummary.currentLoanToValue).toFixed(3) !== Number(user.currentLoanToValue).toFixed(3); // Comparing without rounding causes stuttering, LTVs update asyncronously 164 165 if (txError && txError.blocking) { 166 return <TxErrorView txError={txError} />; 167 } 168 169 if (emodeTxState.success) return <TxSuccessView action={<Trans>Emode</Trans>} />; 170 171 function selectEMode(id: number) { 172 const emode = eModeCategories[id]; 173 if (!emode) { 174 throw new Error(`EMode with id ${id} not found`); 175 } 176 177 setSelectedEmode(emode); 178 } 179 180 return ( 181 <> 182 <TxModalTitle title={<Trans>Manage E-Mode</Trans>} /> 183 {isWrongNetwork && !readOnlyModeAddress && ( 184 <ChangeNetworkWarning networkName={networkConfig.name} chainId={currentChainId} /> 185 )} 186 187 <Typography variant="caption"> 188 <Trans> 189 Enabling E-Mode allows you to maximize your borrowing power, however, borrowing is 190 restricted to assets within the selected category.{' '} 191 <Link 192 sx={{ textDecoration: 'underline' }} 193 variant="caption" 194 href="https://aave.com/help/borrowing/e-mode" 195 target="_blank" 196 rel="noopener" 197 > 198 Learn more 199 </Link>{' '} 200 about how it works and the applied restrictions. 201 </Trans> 202 </Typography> 203 204 {blockingError === ErrorType.EMODE_DISABLED_LIQUIDATION && <Blocked />} 205 {showLiquidationRiskWarning && ( 206 <Warning severity="error" sx={{ mt: 6, alignItems: 'center' }}> 207 <Typography variant="subheader1" color="#4F1919"> 208 <Trans>Liquidation risk</Trans> 209 </Typography> 210 <Typography variant="caption"> 211 <Trans> 212 This action will reduce your health factor. Please be mindful of the increased risk of 213 collateral liquidation.{' '} 214 </Trans> 215 </Typography> 216 </Warning> 217 )} 218 219 <TxModalDetails gasLimit={gasLimit}> 220 {user.userEmodeCategoryId !== 0 && ( 221 <Row caption={<Trans>Disable E-Mode</Trans>} captionVariant="description" mb={4}> 222 <Switch 223 disableRipple 224 checked={disableEmode} 225 onClick={() => setDisableEmode(!disableEmode)} 226 /> 227 </Row> 228 )} 229 <Collapse in={disableEmode}> 230 <Row 231 captionVariant="description" 232 my={2} 233 caption={<MaxLTVTooltip variant="description" text={<Trans>Max LTV</Trans>} />} 234 > 235 <Stack direction="row"> 236 {showLTVChange && ( 237 <> 238 <FormattedNumber 239 percent 240 visibleDecimals={2} 241 value={user.currentLoanToValue} 242 variant="secondary12" 243 /> 244 <ArrowRight /> 245 </> 246 )} 247 <FormattedNumber 248 percent 249 visibleDecimals={2} 250 value={newSummary.currentLoanToValue} 251 variant="secondary12" 252 /> 253 </Stack> 254 </Row> 255 <DetailsHFLine 256 visibleHfChange={!!selectedEmode} 257 healthFactor={user.healthFactor} 258 futureHealthFactor={newSummary.healthFactor} 259 /> 260 </Collapse> 261 262 <Collapse in={!disableEmode}> 263 <Box> 264 <Stack direction="column"> 265 <Typography mb={1} variant="caption" color="text.secondary"> 266 <Trans>Asset category</Trans> 267 </Typography> 268 <Select 269 sx={{ 270 mb: 3, 271 width: '100%', 272 height: '44px', 273 borderRadius: '6px', 274 borderColor: 'divider', 275 outline: 'none !important', 276 color: 'text.primary', 277 '.MuiOutlinedInput-input': { 278 backgroundColor: 'transparent', 279 }, 280 '&:hover .MuiOutlinedInput-notchedOutline, .MuiOutlinedInput-notchedOutline': { 281 borderColor: 'divider', 282 outline: 'none !important', 283 borderWidth: '1px', 284 }, 285 '&.Mui-focused .MuiOutlinedInput-notchedOutline': { 286 borderColor: 'divider', 287 borderWidth: '1px', 288 }, 289 '.MuiSelect-icon': { color: 'text.primary' }, 290 }} 291 value={selectedEmode.id} 292 onChange={(e) => selectEMode(Number(e.target.value))} 293 > 294 {Object.values(eModeCategories) 295 .filter((emode) => emode.id !== 0) 296 .sort((a, b) => { 297 if (a.available !== b.available) { 298 return a.available ? -1 : 1; 299 } 300 301 return a.id - b.id; 302 }) 303 .map((emode) => ( 304 <MenuItem key={emode.id} value={emode.id}> 305 <Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between"> 306 <Typography 307 sx={{ opacity: emode.available ? 1 : 0.5 }} 308 fontStyle={emode.available ? 'normal' : 'italic'} 309 > 310 {emode.label} 311 </Typography> 312 {emode.id === user.userEmodeCategoryId && ( 313 <Box sx={{ display: 'inline-flex', alignItems: 'center' }}> 314 <Box 315 sx={{ 316 width: '6px', 317 height: '6px', 318 borderRadius: '50%', 319 bgcolor: 'success.main', 320 boxShadow: 321 '0px 2px 1px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.25)', 322 mr: '5px', 323 }} 324 /> 325 <Typography variant="subheader2" color="success.main"> 326 <Trans>Enabled</Trans> 327 </Typography> 328 </Box> 329 )} 330 {!emode.available && ( 331 <Typography variant="caption" color="text.secondary" fontStyle="italic"> 332 <Trans>Unavailable</Trans> 333 </Typography> 334 )} 335 </Stack> 336 </MenuItem> 337 ))} 338 </Select> 339 </Stack> 340 {!selectedEmode.available && ( 341 <Typography variant="caption" color="text.secondary" sx={{ mb: 3 }}> 342 <Trans> 343 All borrow positions outside of this category must be closed to enable this 344 category. 345 </Trans> 346 </Typography> 347 )} 348 <Divider /> 349 <Row 350 captionVariant="description" 351 my={2} 352 caption={<MaxLTVTooltip variant="description" text={<Trans>Max LTV</Trans>} />} 353 > 354 <Stack direction="row"> 355 {showLTVChange && ( 356 <> 357 <FormattedNumber 358 percent 359 visibleDecimals={2} 360 value={user.currentLoanToValue} 361 variant="secondary12" 362 /> 363 <ArrowRight /> 364 </> 365 )} 366 <FormattedNumber 367 percent 368 visibleDecimals={2} 369 value={Number(selectedEmode.ltv) / 10000} 370 variant="secondary12" 371 /> 372 </Stack> 373 </Row> 374 375 <DetailsHFLine 376 visibleHfChange={selectedEmode.id !== user.userEmodeCategoryId} 377 healthFactor={user.healthFactor} 378 futureHealthFactor={newSummary.healthFactor} 379 /> 380 381 <TableContainer sx={{ maxHeight: '270px' }}> 382 <Table size="small" stickyHeader> 383 <TableHead> 384 <TableRow 385 sx={{ 386 [`& .${tableCellClasses.root}`]: { 387 py: 2, 388 lineHeight: 0, 389 }, 390 }} 391 > 392 <TableCell align="center" sx={{ pl: 0, width: '120px' }}> 393 <Typography variant="helperText"> 394 <Trans>Asset</Trans> 395 </Typography> 396 </TableCell> 397 <TableCell align="center"> 398 <Typography variant="helperText"> 399 <Trans>Collateral</Trans> 400 </Typography> 401 </TableCell> 402 <TableCell align="center"> 403 <Typography variant="helperText"> 404 <Trans>Borrowable</Trans> 405 </Typography> 406 </TableCell> 407 </TableRow> 408 </TableHead> 409 <TableBody sx={{ width: '100%' }}> 410 {selectedEmode.assets.map((asset, index) => ( 411 <TableRow 412 key={index} 413 sx={{ 414 pt: 8, 415 [`& .${tableCellClasses.root}`]: { 416 borderBottom: 'none', 417 pt: 3, 418 pb: 2, 419 }, 420 }} 421 > 422 <TableCell align="center" sx={{ py: 1 }}> 423 <Stack direction="row" gap={1} alignItems="center"> 424 <TokenIcon symbol={asset.iconSymbol} sx={{ fontSize: '16px' }} /> 425 <Typography variant="secondary12">{asset.symbol}</Typography> 426 </Stack> 427 </TableCell> 428 <TableCell align="center"> 429 {asset.collateral ? ( 430 <CheckRoundedIcon fontSize="small" color="success" /> 431 ) : ( 432 <CloseIcon fontSize="small" color="error" /> 433 )} 434 </TableCell> 435 <TableCell align="center"> 436 {asset.borrowable ? ( 437 <CheckRoundedIcon fontSize="small" color="success" /> 438 ) : ( 439 <CloseIcon fontSize="small" color="error" /> 440 )} 441 </TableCell> 442 </TableRow> 443 ))} 444 </TableBody> 445 </Table> 446 </TableContainer> 447 </Box> 448 </Collapse> 449 </TxModalDetails> 450 451 {txError && <GasEstimationError txError={txError} />} 452 453 {disableEmode ? ( 454 <EmodeActions 455 isWrongNetwork={isWrongNetwork} 456 blocked={blockingError !== undefined} 457 selectedEmode={0} 458 activeEmode={user.userEmodeCategoryId} 459 eModes={eModeCategories} 460 /> 461 ) : ( 462 <EmodeActions 463 isWrongNetwork={isWrongNetwork} 464 blocked={ 465 blockingError !== undefined || 466 !selectedEmode.available || 467 selectedEmode.id === user.userEmodeCategoryId 468 } 469 selectedEmode={disableEmode ? 0 : selectedEmode.id} 470 activeEmode={user.userEmodeCategoryId} 471 eModes={eModeCategories} 472 /> 473 )} 474 </> 475 ); 476 };