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