/ src / components / transactions / Emode / EmodeModalContent.tsx
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  };