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