/ src / modules / reserve-overview / ReserveActions.tsx
ReserveActions.tsx
  1  import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers';
  2  import { BigNumberValue, USD_DECIMALS, valueToBigNumber } from '@aave/math-utils';
  3  import { Trans } from '@lingui/macro';
  4  import { Box, Button, Divider, Paper, Skeleton, Stack, Typography, useTheme } from '@mui/material';
  5  import { BigNumber } from 'bignumber.js';
  6  import React, { ReactNode, useState } from 'react';
  7  import { WalletIcon } from 'src/components/icons/WalletIcon';
  8  import { getMarketInfoById } from 'src/components/MarketSwitcher';
  9  import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
 10  import { Warning } from 'src/components/primitives/Warning';
 11  import { StyledTxModalToggleButton } from 'src/components/StyledToggleButton';
 12  import { StyledTxModalToggleGroup } from 'src/components/StyledToggleButtonGroup';
 13  import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
 14  import {
 15    ComputedReserveData,
 16    useAppDataContext,
 17  } from 'src/hooks/app-data-provider/useAppDataProvider';
 18  import { useWalletBalances } from 'src/hooks/app-data-provider/useWalletBalances';
 19  import { useModalContext } from 'src/hooks/useModal';
 20  import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
 21  import { BuyWithFiat } from 'src/modules/staking/BuyWithFiat';
 22  import { useRootStore } from 'src/store/root';
 23  import {
 24    getMaxAmountAvailableToBorrow,
 25    getMaxGhoMintAmount,
 26  } from 'src/utils/getMaxAmountAvailableToBorrow';
 27  import { getMaxAmountAvailableToSupply } from 'src/utils/getMaxAmountAvailableToSupply';
 28  import { displayGhoForMintableMarket } from 'src/utils/ghoUtilities';
 29  import { GENERAL } from 'src/utils/mixPanelEvents';
 30  import { amountToUsd } from 'src/utils/utils';
 31  import { useShallow } from 'zustand/shallow';
 32  
 33  import { CapType } from '../../components/caps/helper';
 34  import { AvailableTooltip } from '../../components/infoTooltips/AvailableTooltip';
 35  import { Link, ROUTES } from '../../components/primitives/Link';
 36  import { useReserveActionState } from '../../hooks/useReserveActionState';
 37  
 38  const amountToUSD = (
 39    amount: BigNumberValue,
 40    formattedPriceInMarketReferenceCurrency: string,
 41    marketReferencePriceInUsd: string
 42  ) => {
 43    return valueToBigNumber(amount)
 44      .multipliedBy(formattedPriceInMarketReferenceCurrency)
 45      .multipliedBy(marketReferencePriceInUsd)
 46      .shiftedBy(-USD_DECIMALS)
 47      .toString();
 48  };
 49  
 50  interface ReserveActionsProps {
 51    reserve: ComputedReserveData;
 52  }
 53  
 54  export const ReserveActions = ({ reserve }: ReserveActionsProps) => {
 55    const [selectedAsset, setSelectedAsset] = useState<string>(reserve.symbol);
 56  
 57    const { currentAccount } = useWeb3Context();
 58    const { openBorrow, openSupply } = useModalContext();
 59    const [currentMarket, currentNetworkConfig, currentMarketData, minRemainingBaseTokenBalance] =
 60      useRootStore(
 61        useShallow((store) => [
 62          store.currentMarket,
 63          store.currentNetworkConfig,
 64          store.currentMarketData,
 65          store.poolComputed.minRemainingBaseTokenBalance,
 66        ])
 67      );
 68    const {
 69      ghoReserveData,
 70      user,
 71      loading: loadingReserves,
 72      marketReferencePriceInUsd,
 73    } = useAppDataContext();
 74    const { walletBalances, loading: loadingWalletBalance } = useWalletBalances(currentMarketData);
 75    const { baseAssetSymbol } = currentNetworkConfig;
 76    let balance = walletBalances[reserve.underlyingAsset];
 77    if (reserve.isWrappedBaseAsset && selectedAsset === baseAssetSymbol) {
 78      balance = walletBalances[API_ETH_MOCK_ADDRESS.toLowerCase()];
 79    }
 80  
 81    let maxAmountToBorrow = '0';
 82    let maxAmountToSupply = '0';
 83    const isGho = displayGhoForMintableMarket({ symbol: reserve.symbol, currentMarket });
 84  
 85    if (isGho && user) {
 86      const maxMintAmount = getMaxGhoMintAmount(user, reserve);
 87      maxAmountToBorrow = BigNumber.min(
 88        maxMintAmount,
 89        valueToBigNumber(ghoReserveData.aaveFacilitatorRemainingCapacity)
 90      ).toString();
 91      maxAmountToSupply = '0';
 92    } else if (user) {
 93      maxAmountToBorrow = getMaxAmountAvailableToBorrow(reserve, user).toString();
 94  
 95      maxAmountToSupply = getMaxAmountAvailableToSupply(
 96        balance?.amount || '0',
 97        reserve,
 98        reserve.underlyingAsset,
 99        minRemainingBaseTokenBalance
100      ).toString();
101    }
102  
103    const maxAmountToBorrowUsd = amountToUsd(
104      maxAmountToBorrow,
105      reserve.formattedPriceInMarketReferenceCurrency,
106      marketReferencePriceInUsd
107    ).toString();
108  
109    const maxAmountToSupplyUsd = amountToUSD(
110      maxAmountToSupply,
111      reserve.formattedPriceInMarketReferenceCurrency,
112      marketReferencePriceInUsd
113    ).toString();
114  
115    const { disableSupplyButton, disableBorrowButton, alerts } = useReserveActionState({
116      balance: balance?.amount || '0',
117      maxAmountToSupply: maxAmountToSupply.toString(),
118      maxAmountToBorrow: maxAmountToBorrow.toString(),
119      reserve,
120    });
121  
122    if (!currentAccount) {
123      return <ConnectWallet />;
124    }
125  
126    if (loadingReserves || loadingWalletBalance) {
127      return <ActionsSkeleton />;
128    }
129  
130    const onSupplyClicked = () => {
131      if (reserve.isWrappedBaseAsset && selectedAsset === baseAssetSymbol) {
132        openSupply(API_ETH_MOCK_ADDRESS.toLowerCase(), currentMarket, reserve.name, 'reserve', true);
133      } else {
134        openSupply(reserve.underlyingAsset, currentMarket, reserve.name, 'reserve', true);
135      }
136    };
137  
138    const { market } = getMarketInfoById(currentMarket);
139  
140    return (
141      <PaperWrapper>
142        {reserve.isWrappedBaseAsset && (
143          <Box>
144            <WrappedBaseAssetSelector
145              assetSymbol={reserve.symbol}
146              baseAssetSymbol={baseAssetSymbol}
147              selectedAsset={selectedAsset}
148              setSelectedAsset={setSelectedAsset}
149            />
150          </Box>
151        )}
152        <WalletBalance
153          balance={balance.amount}
154          symbol={selectedAsset}
155          marketTitle={market.marketTitle}
156        />
157        {reserve.isFrozen || reserve.isPaused ? (
158          <Box sx={{ mt: 3 }}>{reserve.isPaused ? <PauseWarning /> : <FrozenWarning />}</Box>
159        ) : (
160          <>
161            <Divider sx={{ my: 6 }} />
162            <Stack gap={3}>
163              {!isGho && (
164                <SupplyAction
165                  reserve={reserve}
166                  value={maxAmountToSupply.toString()}
167                  usdValue={maxAmountToSupplyUsd}
168                  symbol={selectedAsset}
169                  disable={disableSupplyButton}
170                  onActionClicked={onSupplyClicked}
171                />
172              )}
173              {reserve.borrowingEnabled && (
174                <BorrowAction
175                  reserve={reserve}
176                  value={maxAmountToBorrow.toString()}
177                  usdValue={maxAmountToBorrowUsd}
178                  symbol={selectedAsset}
179                  disable={disableBorrowButton}
180                  onActionClicked={() => {
181                    openBorrow(reserve.underlyingAsset, currentMarket, reserve.name, 'reserve', true);
182                  }}
183                />
184              )}
185              {alerts}
186            </Stack>
187          </>
188        )}
189      </PaperWrapper>
190    );
191  };
192  
193  const PauseWarning = () => {
194    return (
195      <Warning sx={{ mb: 0 }} severity="error" icon={true}>
196        <Trans>Because this asset is paused, no actions can be taken until further notice</Trans>
197      </Warning>
198    );
199  };
200  
201  const FrozenWarning = () => {
202    return (
203      <Warning sx={{ mb: 0 }} severity="error" icon={true}>
204        <Trans>
205          Since this asset is frozen, the only available actions are withdraw and repay which can be
206          accessed from the <Link href={ROUTES.dashboard}>Dashboard</Link>
207        </Trans>
208      </Warning>
209    );
210  };
211  
212  const ActionsSkeleton = () => {
213    const RowSkeleton = (
214      <Stack>
215        <Skeleton width={150} height={14} />
216        <Stack
217          sx={{ height: '44px' }}
218          direction="row"
219          justifyContent="space-between"
220          alignItems="center"
221        >
222          <Box>
223            <Skeleton width={100} height={14} sx={{ mt: 1, mb: 2 }} />
224            <Skeleton width={75} height={12} />
225          </Box>
226          <Skeleton height={36} width={96} />
227        </Stack>
228      </Stack>
229    );
230  
231    return (
232      <PaperWrapper>
233        <Stack direction="row" gap={3}>
234          <Skeleton width={42} height={42} sx={{ borderRadius: '12px' }} />
235          <Box>
236            <Skeleton width={100} height={12} sx={{ mt: 1, mb: 2 }} />
237            <Skeleton width={100} height={14} />
238          </Box>
239        </Stack>
240        <Divider sx={{ my: 6 }} />
241        <Box>
242          <Stack gap={3}>
243            {RowSkeleton}
244            {RowSkeleton}
245          </Stack>
246        </Box>
247      </PaperWrapper>
248    );
249  };
250  
251  const PaperWrapper = ({ children }: { children: ReactNode }) => {
252    return (
253      <Paper sx={{ pt: 4, pb: { xs: 4, xsm: 6 }, px: { xs: 4, xsm: 6 } }}>
254        <Typography variant="h3" sx={{ mb: 6 }}>
255          <Trans>Your info</Trans>
256        </Typography>
257  
258        {children}
259      </Paper>
260    );
261  };
262  
263  const ConnectWallet = () => {
264    return (
265      <Paper sx={{ pt: 4, pb: { xs: 4, xsm: 6 }, px: { xs: 4, xsm: 6 } }}>
266        <>
267          <Typography variant="h3" sx={{ mb: { xs: 6, xsm: 10 } }}>
268            <Trans>Your info</Trans>
269          </Typography>
270          <Typography sx={{ mb: 6 }} color="text.secondary">
271            <Trans>Please connect a wallet to view your personal information here.</Trans>
272          </Typography>
273          <ConnectWalletButton />
274        </>
275      </Paper>
276    );
277  };
278  
279  interface ActionProps {
280    value: string;
281    usdValue: string;
282    symbol: string;
283    disable: boolean;
284    onActionClicked: () => void;
285    reserve: ComputedReserveData;
286  }
287  
288  const SupplyAction = ({
289    reserve,
290    value,
291    usdValue,
292    symbol,
293    disable,
294    onActionClicked,
295  }: ActionProps) => {
296    return (
297      <Stack>
298        <AvailableTooltip
299          variant="description"
300          text={<Trans>Available to supply</Trans>}
301          capType={CapType.supplyCap}
302          event={{
303            eventName: GENERAL.TOOL_TIP,
304            eventParams: {
305              tooltip: 'Available to supply: your info',
306              asset: reserve.underlyingAsset,
307              assetName: reserve.name,
308            },
309          }}
310        />
311        <Stack
312          sx={{ height: '44px' }}
313          direction="row"
314          justifyContent="space-between"
315          alignItems="center"
316        >
317          <Box>
318            <ValueWithSymbol value={value} symbol={symbol} />
319            <FormattedNumber
320              value={usdValue}
321              variant="subheader2"
322              color="text.muted"
323              symbolsColor="text.muted"
324              symbol="USD"
325            />
326          </Box>
327          <Button
328            sx={{ height: '36px', width: '96px' }}
329            onClick={onActionClicked}
330            disabled={disable}
331            fullWidth={false}
332            variant="contained"
333            data-cy="supplyButton"
334          >
335            <Trans>Supply</Trans>
336          </Button>
337        </Stack>
338      </Stack>
339    );
340  };
341  
342  const BorrowAction = ({
343    reserve,
344    value,
345    usdValue,
346    symbol,
347    disable,
348    onActionClicked,
349  }: ActionProps) => {
350    return (
351      <Stack>
352        <AvailableTooltip
353          variant="description"
354          text={<Trans>Available to borrow</Trans>}
355          capType={CapType.borrowCap}
356          event={{
357            eventName: GENERAL.TOOL_TIP,
358            eventParams: {
359              tooltip: 'Available to borrow: your info',
360              asset: reserve.underlyingAsset,
361              assetName: reserve.name,
362            },
363          }}
364        />
365        <Stack
366          sx={{ height: '44px' }}
367          direction="row"
368          justifyContent="space-between"
369          alignItems="center"
370        >
371          <Box>
372            <ValueWithSymbol value={value} symbol={symbol} />
373            <FormattedNumber
374              value={usdValue}
375              variant="subheader2"
376              color="text.muted"
377              symbolsColor="text.muted"
378              symbol="USD"
379            />
380          </Box>
381          <Button
382            sx={{ height: '36px', width: '96px' }}
383            onClick={onActionClicked}
384            disabled={disable}
385            fullWidth={false}
386            variant="contained"
387            data-cy="borrowButton"
388          >
389            <Trans>Borrow </Trans>
390          </Button>
391        </Stack>
392      </Stack>
393    );
394  };
395  
396  const WrappedBaseAssetSelector = ({
397    assetSymbol,
398    baseAssetSymbol,
399    selectedAsset,
400    setSelectedAsset,
401  }: {
402    assetSymbol: string;
403    baseAssetSymbol: string;
404    selectedAsset: string;
405    setSelectedAsset: (value: string) => void;
406  }) => {
407    return (
408      <StyledTxModalToggleGroup
409        color="primary"
410        value={selectedAsset}
411        exclusive
412        onChange={(_, value) => setSelectedAsset(value)}
413        sx={{ mb: 4 }}
414      >
415        <StyledTxModalToggleButton value={assetSymbol}>
416          <Typography variant="buttonM">{assetSymbol}</Typography>
417        </StyledTxModalToggleButton>
418  
419        <StyledTxModalToggleButton value={baseAssetSymbol}>
420          <Typography variant="buttonM">{baseAssetSymbol}</Typography>
421        </StyledTxModalToggleButton>
422      </StyledTxModalToggleGroup>
423    );
424  };
425  
426  interface ValueWithSymbolProps {
427    value: string;
428    symbol: string;
429    children?: ReactNode;
430  }
431  
432  const ValueWithSymbol = ({ value, symbol, children }: ValueWithSymbolProps) => {
433    return (
434      <Stack direction="row" alignItems="center" gap={1}>
435        <FormattedNumber value={value} variant="h4" color="text.primary" />
436        <Typography variant="buttonL" color="text.secondary">
437          {symbol}
438        </Typography>
439        {children}
440      </Stack>
441    );
442  };
443  
444  interface WalletBalanceProps {
445    balance: string;
446    symbol: string;
447    marketTitle: string;
448  }
449  const WalletBalance = ({ balance, symbol, marketTitle }: WalletBalanceProps) => {
450    const theme = useTheme();
451  
452    return (
453      <Stack direction="row" gap={3}>
454        <Box
455          sx={(theme) => ({
456            width: '42px',
457            height: '42px',
458            background: theme.palette.background.surface,
459            border: `0.5px solid ${theme.palette.background.disabled}`,
460            borderRadius: '12px',
461            display: 'flex',
462            alignItems: 'center',
463            justifyContent: 'center',
464          })}
465        >
466          <WalletIcon sx={{ stroke: `${theme.palette.text.secondary}` }} />
467        </Box>
468        <Box>
469          <Typography variant="description" color="text.secondary">
470            Wallet balance
471          </Typography>
472          <ValueWithSymbol value={balance} symbol={symbol}>
473            <Box sx={{ ml: 2 }}>
474              <BuyWithFiat cryptoSymbol={symbol} networkMarketName={marketTitle} />
475            </Box>
476          </ValueWithSymbol>
477        </Box>
478      </Stack>
479    );
480  };