/ src / components / transactions / Repay / RepayModalContent.tsx
RepayModalContent.tsx
  1  import { API_ETH_MOCK_ADDRESS, synthetixProxyByChainId } from '@aave/contract-helpers';
  2  import {
  3    BigNumberValue,
  4    calculateHealthFactorFromBalancesBigUnits,
  5    USD_DECIMALS,
  6    valueToBigNumber,
  7  } from '@aave/math-utils';
  8  import { Trans } from '@lingui/macro';
  9  import Typography from '@mui/material/Typography';
 10  import { BigNumber } from 'bignumber.js';
 11  import React, { useEffect, useRef, useState } from 'react';
 12  import {
 13    ExtendedFormattedUser,
 14    useAppDataContext,
 15  } from 'src/hooks/app-data-provider/useAppDataProvider';
 16  import { useModalContext } from 'src/hooks/useModal';
 17  import { useRootStore } from 'src/store/root';
 18  import { displayGhoForMintableMarket } from 'src/utils/ghoUtilities';
 19  import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
 20  import { useShallow } from 'zustand/shallow';
 21  
 22  import { Asset, AssetInput } from '../AssetInput';
 23  import { GasEstimationError } from '../FlowCommons/GasEstimationError';
 24  import { ModalWrapperProps } from '../FlowCommons/ModalWrapper';
 25  import { TxSuccessView } from '../FlowCommons/Success';
 26  import {
 27    DetailsHFLine,
 28    DetailsNumberLineWithSub,
 29    TxModalDetails,
 30  } from '../FlowCommons/TxModalDetails';
 31  import { RepayActions } from './RepayActions';
 32  
 33  interface RepayAsset extends Asset {
 34    balance: string;
 35  }
 36  
 37  export const RepayModalContent = ({
 38    poolReserve,
 39    userReserve,
 40    symbol: modalSymbol,
 41    tokenBalance,
 42    nativeBalance,
 43    isWrongNetwork,
 44    user,
 45  }: ModalWrapperProps & { user: ExtendedFormattedUser }) => {
 46    const { gasLimit, mainTxState: repayTxState, txError } = useModalContext();
 47    const { marketReferencePriceInUsd } = useAppDataContext();
 48  
 49    const [minRemainingBaseTokenBalance, currentChainId, currentMarketData, currentMarket] =
 50      useRootStore(
 51        useShallow((store) => [
 52          store.poolComputed.minRemainingBaseTokenBalance,
 53          store.currentChainId,
 54          store.currentMarketData,
 55          store.currentMarket,
 56        ])
 57      );
 58  
 59    // states
 60    const [tokenToRepayWith, setTokenToRepayWith] = useState<RepayAsset>({
 61      address: poolReserve.underlyingAsset,
 62      symbol: poolReserve.symbol,
 63      iconSymbol: poolReserve.iconSymbol,
 64      balance: tokenBalance,
 65    });
 66    const [assets, setAssets] = useState<RepayAsset[]>([tokenToRepayWith]);
 67    const [repayMax, setRepayMax] = useState('');
 68    const [_amount, setAmount] = useState('');
 69    const amountRef = useRef<string>();
 70  
 71    const networkConfig = getNetworkConfig(currentChainId);
 72  
 73    const { underlyingBalance, usageAsCollateralEnabledOnUser, reserve } = userReserve;
 74  
 75    const repayWithATokens = tokenToRepayWith.address === poolReserve.aTokenAddress;
 76  
 77    const debt = userReserve?.variableBorrows || '0';
 78    const debtUSD = new BigNumber(debt)
 79      .multipliedBy(poolReserve.formattedPriceInMarketReferenceCurrency)
 80      .multipliedBy(marketReferencePriceInUsd)
 81      .shiftedBy(-USD_DECIMALS);
 82  
 83    const safeAmountToRepayAll = valueToBigNumber(debt)
 84      .multipliedBy('1.0025')
 85      .decimalPlaces(poolReserve.decimals, BigNumber.ROUND_UP);
 86  
 87    // calculate max amount abailable to repay
 88    let maxAmountToRepay: BigNumber;
 89    let balance: string;
 90    if (repayWithATokens) {
 91      maxAmountToRepay = BigNumber.min(underlyingBalance, debt);
 92      balance = underlyingBalance;
 93    } else {
 94      const normalizedWalletBalance = valueToBigNumber(tokenToRepayWith.balance).minus(
 95        userReserve?.reserve.symbol.toUpperCase() === networkConfig.baseAssetSymbol
 96          ? minRemainingBaseTokenBalance
 97          : '0'
 98      );
 99      balance = normalizedWalletBalance.toString(10);
100      maxAmountToRepay = BigNumber.min(normalizedWalletBalance, debt);
101    }
102  
103    const isMaxSelected = _amount === '-1';
104    const amount = isMaxSelected ? maxAmountToRepay.toString(10) : _amount;
105  
106    const handleChange = (value: string) => {
107      const maxSelected = value === '-1';
108      amountRef.current = maxSelected ? maxAmountToRepay.toString(10) : value;
109      setAmount(value);
110      if (maxSelected && (repayWithATokens || maxAmountToRepay.eq(debt))) {
111        if (
112          tokenToRepayWith.address === API_ETH_MOCK_ADDRESS.toLowerCase() ||
113          (synthetixProxyByChainId[currentChainId] &&
114            synthetixProxyByChainId[currentChainId].toLowerCase() ===
115              reserve.underlyingAsset.toLowerCase())
116        ) {
117          // for native token and synthetix (only mainnet) we can't send -1 as
118          // contract does not accept max unit256
119          setRepayMax(safeAmountToRepayAll.toString(10));
120        } else {
121          // -1 can always be used for v3 otherwise
122          // for v2 we can onl use -1 when user has more balance than max debt to repay
123          // this is accounted for when maxAmountToRepay.eq(debt) as maxAmountToRepay is
124          // min between debt and walletbalance, so if it enters here for v2 it means
125          // balance is bigger and will be able to transact with -1
126          setRepayMax('-1');
127        }
128      } else {
129        setRepayMax(
130          safeAmountToRepayAll.lt(balance)
131            ? safeAmountToRepayAll.toString(10)
132            : maxAmountToRepay.toString(10)
133        );
134      }
135    };
136  
137    // token info
138    useEffect(() => {
139      const repayTokens: RepayAsset[] = [];
140      // set possible repay tokens
141      // if wrapped reserve push both wrapped / native
142      if (poolReserve.symbol === networkConfig.wrappedBaseAssetSymbol) {
143        const nativeTokenWalletBalance = valueToBigNumber(nativeBalance);
144        const maxNativeToken = BigNumber.max(
145          nativeTokenWalletBalance,
146          BigNumber.min(nativeTokenWalletBalance, debt)
147        );
148        repayTokens.push({
149          address: API_ETH_MOCK_ADDRESS.toLowerCase(),
150          symbol: networkConfig.baseAssetSymbol,
151          balance: maxNativeToken.toString(10),
152        });
153      }
154      // push reserve asset
155      const minReserveTokenRepay = BigNumber.min(valueToBigNumber(tokenBalance), debt);
156      const maxReserveTokenForRepay = BigNumber.max(minReserveTokenRepay, tokenBalance);
157      repayTokens.push({
158        address: poolReserve.underlyingAsset,
159        symbol: poolReserve.symbol,
160        iconSymbol: poolReserve.iconSymbol,
161        balance: maxReserveTokenForRepay.toString(10),
162      });
163      // push reserve aToken
164      if (
165        currentMarketData.v3 &&
166        !displayGhoForMintableMarket({ symbol: poolReserve.symbol, currentMarket })
167      ) {
168        const aTokenBalance = valueToBigNumber(underlyingBalance);
169        const maxBalance = BigNumber.max(
170          aTokenBalance,
171          BigNumber.min(aTokenBalance, debt).toString(10)
172        );
173        repayTokens.push({
174          address: poolReserve.aTokenAddress,
175          symbol: `a${poolReserve.symbol}`,
176          iconSymbol: poolReserve.iconSymbol,
177          aToken: true,
178          balance: maxBalance.toString(10),
179        });
180      }
181      setAssets(repayTokens);
182      setTokenToRepayWith(repayTokens[0]);
183    }, []);
184  
185    // debt remaining after repay
186    const amountAfterRepay = valueToBigNumber(debt)
187      .minus(amount || '0')
188      .toString(10);
189    const amountAfterRepayInUsd = new BigNumber(amountAfterRepay)
190      .multipliedBy(poolReserve.formattedPriceInMarketReferenceCurrency)
191      .multipliedBy(marketReferencePriceInUsd)
192      .shiftedBy(-USD_DECIMALS);
193  
194    const maxRepayWithDustRemaining = isMaxSelected && amountAfterRepayInUsd.toNumber() > 0;
195  
196    // health factor calculations
197    // we use usd values instead of MarketreferenceCurrency so it has same precision
198    let newHF = user?.healthFactor;
199    if (amount) {
200      let collateralBalanceMarketReferenceCurrency: BigNumberValue = user?.totalCollateralUSD || '0';
201      if (repayWithATokens && usageAsCollateralEnabledOnUser) {
202        collateralBalanceMarketReferenceCurrency = valueToBigNumber(
203          user?.totalCollateralUSD || '0'
204        ).minus(valueToBigNumber(reserve.priceInUSD).multipliedBy(amount));
205      }
206  
207      const remainingBorrowBalance = valueToBigNumber(user?.totalBorrowsUSD || '0').minus(
208        valueToBigNumber(reserve.priceInUSD).multipliedBy(amount)
209      );
210      const borrowBalanceMarketReferenceCurrency = BigNumber.max(remainingBorrowBalance, 0);
211  
212      const calculatedHealthFactor = calculateHealthFactorFromBalancesBigUnits({
213        collateralBalanceMarketReferenceCurrency,
214        borrowBalanceMarketReferenceCurrency,
215        currentLiquidationThreshold: user?.currentLiquidationThreshold || '0',
216      });
217  
218      newHF =
219        calculatedHealthFactor.isLessThan(0) && !calculatedHealthFactor.eq(-1)
220          ? '0'
221          : calculatedHealthFactor.toString(10);
222    }
223  
224    // calculating input usd value
225    const usdValue = valueToBigNumber(amount).multipliedBy(reserve.priceInUSD);
226  
227    if (repayTxState.success)
228      return (
229        <TxSuccessView
230          action={<Trans>repaid</Trans>}
231          amount={amountRef.current}
232          symbol={repayWithATokens ? poolReserve.symbol : tokenToRepayWith.symbol}
233        />
234      );
235  
236    return (
237      <>
238        <AssetInput
239          value={amount}
240          onChange={handleChange}
241          usdValue={usdValue.toString(10)}
242          symbol={tokenToRepayWith.symbol}
243          assets={assets}
244          onSelect={setTokenToRepayWith}
245          isMaxSelected={isMaxSelected}
246          maxValue={maxAmountToRepay.toString(10)}
247          balanceText={<Trans>Wallet balance</Trans>}
248        />
249  
250        {maxRepayWithDustRemaining && (
251          <Typography color="warning.main" variant="helperText">
252            <Trans>
253              You don’t have enough funds in your wallet to repay the full amount. If you proceed to
254              repay with your current amount of funds, you will still have a small borrowing position
255              in your dashboard.
256            </Trans>
257          </Typography>
258        )}
259  
260        <TxModalDetails gasLimit={gasLimit}>
261          <DetailsNumberLineWithSub
262            description={<Trans>Remaining debt</Trans>}
263            futureValue={amountAfterRepay}
264            futureValueUSD={amountAfterRepayInUsd.toString(10)}
265            value={debt}
266            valueUSD={debtUSD.toString()}
267            symbol={
268              poolReserve.iconSymbol === networkConfig.wrappedBaseAssetSymbol
269                ? networkConfig.baseAssetSymbol
270                : poolReserve.iconSymbol
271            }
272          />
273          <DetailsHFLine
274            visibleHfChange={!!_amount}
275            healthFactor={user?.healthFactor}
276            futureHealthFactor={newHF}
277          />
278        </TxModalDetails>
279  
280        {txError && <GasEstimationError txError={txError} />}
281  
282        <RepayActions
283          maxApproveNeeded={safeAmountToRepayAll.toString()}
284          poolReserve={poolReserve}
285          amountToRepay={isMaxSelected ? repayMax : amount}
286          poolAddress={
287            repayWithATokens ? poolReserve.underlyingAsset : tokenToRepayWith.address ?? ''
288          }
289          isWrongNetwork={isWrongNetwork}
290          symbol={modalSymbol}
291          repayWithATokens={repayWithATokens}
292        />
293      </>
294    );
295  };