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