diff --git a/src/modules/reserve-overview/ReserveTopDetails.tsx b/src/modules/reserve-overview/ReserveTopDetails.tsx index 044dbf1cfb..7c724f7e5d 100644 --- a/src/modules/reserve-overview/ReserveTopDetails.tsx +++ b/src/modules/reserve-overview/ReserveTopDetails.tsx @@ -1,34 +1,29 @@ import { ExternalLinkIcon } from '@heroicons/react/outline'; import { Trans } from '@lingui/macro'; -import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackOutlined'; -import { Box, Button, Skeleton, SvgIcon, Typography, useMediaQuery, useTheme } from '@mui/material'; -import { useRouter } from 'next/router'; -import { getMarketInfoById, MarketLogo } from 'src/components/MarketSwitcher'; +import { Box, Skeleton, SvgIcon, useMediaQuery, useTheme } from '@mui/material'; +import { CircleIcon } from 'src/components/CircleIcon'; import { FormattedNumber } from 'src/components/primitives/FormattedNumber'; import { Link } from 'src/components/primitives/Link'; -import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; +import { useRootStore } from 'src/store/root'; +import { assetIsBorrowableOnMarket } from 'src/utils/getMaxAmountAvailableToBorrow'; +import { GENERAL } from 'src/utils/events'; +import { useShallow } from 'zustand/shallow'; -import { TopInfoPanel } from '../../components/TopInfoPanel/TopInfoPanel'; import { TopInfoPanelItem } from '../../components/TopInfoPanel/TopInfoPanelItem'; import { ComputedReserveData, useAppDataContext, } from '../../hooks/app-data-provider/useAppDataProvider'; -import CubeIcon from '../../../public/icons/markets/cube-icon.svg'; -import PieIcon from '../../../public/icons/markets/pie-icon.svg'; -import UptrendIcon from '../../../public/icons/markets/uptrend-icon.svg'; -import DollarIcon from '../../../public/icons/markets/dollar-icon.svg'; - interface ReserveTopDetailsProps { underlyingAsset: string; } export const ReserveTopDetails = ({ underlyingAsset }: ReserveTopDetailsProps) => { - const router = useRouter(); const { reserves, loading } = useAppDataContext(); - const { currentMarket, currentNetworkConfig } = useProtocolDataContext(); - const { market, network } = getMarketInfoById(currentMarket); + const [trackEvent, currentNetworkConfig] = useRootStore( + useShallow((store) => [store.trackEvent, store.currentNetworkConfig]) + ); const theme = useTheme(); const downToSM = useMediaQuery(theme.breakpoints.down('sm')); @@ -40,107 +35,19 @@ export const ReserveTopDetails = ({ underlyingAsset }: ReserveTopDetailsProps) = const valueTypographyVariant = downToSM ? 'main16' : 'main21'; const symbolsTypographyVariant = downToSM ? 'secondary16' : 'secondary21'; - const ReserveIcon = () => { - return ( - - {loading ? ( - - ) : ( - - )} - - ); - }; - - const ReserveName = () => { - return loading ? ( - - ) : ( - {poolReserve.name} - ); + const iconStyling = { + display: 'inline-flex', + alignItems: 'center', + color: '#A5A8B6', + '&:hover': { color: '#F1F1F3' }, + cursor: 'pointer', }; return ( - - - - - - - - {market.marketTitle} Market - - {market.v3 && ( - theme.palette.gradients.aaveGradient, - }} - > - Version 3 - - )} - - - - {downToSM && ( - - - - {!loading && ( - - {poolReserve.symbol} - - )} - - - - - )} - - } - > - {!downToSM && ( - {poolReserve.symbol}} - withoutIconWrapper - icon={} - loading={loading} - > - - - )} - - } title={Reserve Size} loading={loading}> + <> + Reserve Size} loading={loading} hideIcon> - } - title={Available liquidity} - loading={loading} - > + Available liquidity} loading={loading} hideIcon> - } - title={Utilization Rate} - loading={loading} - > + Utilization Rate} loading={loading} hideIcon> - } - title={Oracle price} - titleIcon={ - loading ? ( + Oracle price} loading={loading} hideIcon> + + + {loading ? ( ) : ( - - - - - - ) - } - loading={loading} - > - + + + trackEvent(GENERAL.EXTERNAL_LINK, { + Link: 'Oracle Price', + oracle: poolReserve?.priceOracle, + assetName: poolReserve.name, + asset: poolReserve.underlyingAsset, + }) + } + href={currentNetworkConfig.explorerLinkBuilder({ + address: poolReserve?.priceOracle, + })} + sx={iconStyling} + > + + + + + + )} + - + ); }; diff --git a/src/utils/__tests__/getMaxAmountAvailableToBorrow.spec.ts b/src/utils/__tests__/getMaxAmountAvailableToBorrow.spec.ts new file mode 100644 index 0000000000..b2efccefdb --- /dev/null +++ b/src/utils/__tests__/getMaxAmountAvailableToBorrow.spec.ts @@ -0,0 +1,50 @@ +import { + assetCanBeBorrowedByUser, + assetIsBorrowableOnMarket, +} from '../getMaxAmountAvailableToBorrow'; + +const baseReserve = { + borrowingEnabled: false, + isActive: true, + borrowableInIsolation: false, + isFrozen: false, + isPaused: false, + eModes: [{ id: 1, borrowingEnabled: true }], +}; + +describe('assetIsBorrowableOnMarket', () => { + it('returns true when borrowingEnabled is true', () => { + expect( + assetIsBorrowableOnMarket({ borrowingEnabled: true, eModes: [] }) + ).toBe(true); + }); + + it('returns true when borrowable in any e-mode', () => { + expect( + assetIsBorrowableOnMarket({ + borrowingEnabled: false, + eModes: [{ id: 1, borrowingEnabled: true }], + }) + ).toBe(true); + }); + + it('returns false when not borrowable in normal mode or e-mode', () => { + expect( + assetIsBorrowableOnMarket({ + borrowingEnabled: false, + eModes: [{ id: 1, borrowingEnabled: false }], + }) + ).toBe(false); + }); +}); + +describe('assetCanBeBorrowedByUser', () => { + it('allows e-mode users to borrow when their category permits it', () => { + expect( + assetCanBeBorrowedByUser(baseReserve as any, { + isInEmode: true, + userEmodeCategoryId: 1, + } as any) + ).toBe(true); + }); +}); diff --git a/src/utils/getMaxAmountAvailableToBorrow.ts b/src/utils/getMaxAmountAvailableToBorrow.ts index b49584031c..aa99ccc532 100644 --- a/src/utils/getMaxAmountAvailableToBorrow.ts +++ b/src/utils/getMaxAmountAvailableToBorrow.ts @@ -1,42 +1,73 @@ -import { InterestRate } from '@aave/contract-helpers'; import { FormatUserSummaryAndIncentivesResponse, valueToBigNumber } from '@aave/math-utils'; -import BigNumber from 'bignumber.js'; +import { BigNumber } from 'bignumber.js'; +import { ethers } from 'ethers'; import { ComputedReserveData, ExtendedFormattedUser, } from '../hooks/app-data-provider/useAppDataProvider'; +import { roundToTokenDecimals } from './utils'; + +// Subset of ComputedReserveData +interface PoolReserveBorrowSubset { + borrowCap: string; + availableLiquidityUSD: string; + totalDebt: string; + isFrozen: boolean; + decimals: number; + formattedAvailableLiquidity: string; + formattedPriceInMarketReferenceCurrency: string; + borrowCapUSD: string; +} + +type MarketBorrowabilityReserve = Pick; + +/** + * Whether a reserve has any borrow path on the market. + * Mirrors on-chain ValidationLogic: outside e-mode uses borrowingEnabled; + * inside e-mode uses the category borrowableBitmap (exposed as eMode.borrowingEnabled). + */ +export function assetIsBorrowableOnMarket(reserve: MarketBorrowabilityReserve): boolean { + return reserve.borrowingEnabled || reserve.eModes.some((eMode) => eMode.borrowingEnabled); +} /** * Calculates the maximum amount a user can borrow. * @param poolReserve - * @param userReserve * @param user */ export function getMaxAmountAvailableToBorrow( - poolReserve: ComputedReserveData, - user: FormatUserSummaryAndIncentivesResponse, - rateMode: InterestRate -) { + poolReserve: PoolReserveBorrowSubset, + user: FormatUserSummaryAndIncentivesResponse +): string { const availableInPoolUSD = poolReserve.availableLiquidityUSD; const availableForUserUSD = BigNumber.min(user.availableBorrowsUSD, availableInPoolUSD); - let maxUserAmountToBorrow = BigNumber.min( - valueToBigNumber(user?.availableBorrowsMarketReferenceCurrency || 0).div( - poolReserve.formattedPriceInMarketReferenceCurrency - ), - poolReserve.formattedAvailableLiquidity + const availableBorrowCap = + poolReserve.borrowCap === '0' + ? valueToBigNumber(ethers.constants.MaxUint256.toString()) + : valueToBigNumber(Number(poolReserve.borrowCap)).minus( + valueToBigNumber(poolReserve.totalDebt) + ); + const availableLiquidity = BigNumber.max( + BigNumber.min(poolReserve.formattedAvailableLiquidity, availableBorrowCap), + 0 ); - if (rateMode === InterestRate.Stable) { - maxUserAmountToBorrow = BigNumber.min( - maxUserAmountToBorrow, - // TODO: put MAX_STABLE_RATE_BORROW_SIZE_PERCENT on uipooldataprovider instead of using the static value here - valueToBigNumber(poolReserve.formattedAvailableLiquidity).multipliedBy(0.25) - ); - } + const availableForUserMarketReferenceCurrency = valueToBigNumber( + user?.availableBorrowsMarketReferenceCurrency || 0 + ).div(poolReserve.formattedPriceInMarketReferenceCurrency); + + const maxUserAmountToBorrow = BigNumber.min( + availableForUserMarketReferenceCurrency, + availableLiquidity + ); const shouldAddMargin = + /** + * When the user is trying to do a max borrow + */ + maxUserAmountToBorrow.gte(availableForUserMarketReferenceCurrency) || /** * When a user has borrows we assume the debt is increasing faster then the supply. * That's a simplification that might not be true, but doesn't matter in most cases. @@ -65,15 +96,29 @@ export function getMaxAmountAvailableToBorrow( .multipliedBy('0.99') .lt(user.availableBorrowsUSD)); - return shouldAddMargin ? maxUserAmountToBorrow.multipliedBy('0.99') : maxUserAmountToBorrow; + const amountWithMargin = shouldAddMargin + ? maxUserAmountToBorrow.multipliedBy('0.99') + : maxUserAmountToBorrow; + return roundToTokenDecimals(amountWithMargin.toString(10), poolReserve.decimals); } export function assetCanBeBorrowedByUser( - { borrowingEnabled, isActive, borrowableInIsolation, eModeCategoryId }: ComputedReserveData, - user: ExtendedFormattedUser + { + borrowingEnabled, + isActive, + borrowableInIsolation, + eModes, + isFrozen, + isPaused, + }: ComputedReserveData, + user?: ExtendedFormattedUser ) { - if (!borrowingEnabled || !isActive) return false; - if (user?.isInEmode && eModeCategoryId !== user.userEmodeCategoryId) return false; + if (!isActive || isFrozen || isPaused) return false; + if (user?.isInEmode) { + const reserveEmode = eModes.find((emode) => emode.id === user.userEmodeCategoryId); + if (!reserveEmode) return false; + return reserveEmode.borrowingEnabled; + } if (user?.isInIsolationMode && !borrowableInIsolation) return false; - return true; + return borrowingEnabled; }