- {market.warningsWithDetail.map((warning) => {
+ {warningsWithDetail.map((warning) => {
return (
{
// if no warning
- market.warnings.length === 0 && (
+ warningsWithDetail.length === 0 && (
)
}
diff --git a/app/markets/components/MarketTableBody.tsx b/app/markets/components/MarketTableBody.tsx
index f60b679c..b7926507 100644
--- a/app/markets/components/MarketTableBody.tsx
+++ b/app/markets/components/MarketTableBody.tsx
@@ -103,7 +103,7 @@ export function MarketTableBody({
/>
-
+
|
diff --git a/app/markets/components/OracleFilter.tsx b/app/markets/components/OracleFilter.tsx
index beef1019..561c7cc8 100644
--- a/app/markets/components/OracleFilter.tsx
+++ b/app/markets/components/OracleFilter.tsx
@@ -1,14 +1,12 @@
import { useState, useRef, useEffect } from 'react';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import Image from 'next/image';
-import { FaQuestionCircle } from 'react-icons/fa';
-import OracleVendorBadge from '@/components/OracleVendorBadge';
-import { OracleVendors, OracleVendorIcons } from '@/utils/oracle';
-import { MorphoChainlinkOracleData } from '@/utils/types';
+import { IoHelpCircleOutline } from 'react-icons/io5';
+import { PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle';
type OracleFilterProps = {
- selectedOracles: OracleVendors[];
- setSelectedOracles: (oracles: OracleVendors[]) => void;
+ selectedOracles: PriceFeedVendors[];
+ setSelectedOracles: (oracles: PriceFeedVendors[]) => void;
};
export default function OracleFilter({ selectedOracles, setSelectedOracles }: OracleFilterProps) {
@@ -30,7 +28,7 @@ export default function OracleFilter({ selectedOracles, setSelectedOracles }: Or
const toggleDropdown = () => setIsOpen(!isOpen);
- const toggleOracle = (oracle: OracleVendors) => {
+ const toggleOracle = (oracle: PriceFeedVendors) => {
if (selectedOracles.includes(oracle)) {
setSelectedOracles(selectedOracles.filter((o) => o !== oracle));
} else {
@@ -58,16 +56,20 @@ export default function OracleFilter({ selectedOracles, setSelectedOracles }: Or
Oracle
{selectedOracles.length > 0 ? (
-
- {selectedOracles.map((oracle) => (
-
+
+ {selectedOracles.map((oracle, index) => (
+
+ {OracleVendorIcons[oracle] ? (
+
+ ) : (
+
+ )}
+
))}
) : (
@@ -84,7 +86,7 @@ export default function OracleFilter({ selectedOracles, setSelectedOracles }: Or
}`}
>
- {Object.values(OracleVendors).map((oracle) => (
+ {Object.values(PriceFeedVendors).map((oracle) => (
) : (
-
+
)}
- {oracle}
+ {oracle === PriceFeedVendors.Unknown ? 'Unknown Feed' : oracle}
))}
diff --git a/app/markets/components/RiskIndicator.tsx b/app/markets/components/RiskIndicator.tsx
index 6f354391..9db8ec37 100644
--- a/app/markets/components/RiskIndicator.tsx
+++ b/app/markets/components/RiskIndicator.tsx
@@ -2,7 +2,8 @@ import { Tooltip } from '@heroui/react';
import { GrStatusGood } from 'react-icons/gr';
import { MdWarning, MdError } from 'react-icons/md';
import { TooltipContent } from '@/components/TooltipContent';
-import { WarningWithDetail } from '@/utils/types';
+import { useMarketWarnings } from '@/hooks/useMarketWarnings';
+import { WarningWithDetail, Market } from '@/utils/types';
import { WarningCategory } from '@/utils/types';
type RiskFlagProps = {
@@ -62,7 +63,14 @@ export function RiskIndicator({
);
return (
-
+
@@ -79,7 +87,7 @@ export function RiskIndicatorFromWarning({
isBatched = false,
mode = 'simple',
}: {
- market: { warningsWithDetail: WarningWithDetail[] };
+ market: Market;
category: WarningCategory;
greenDescription: string;
yellowDescription: string;
@@ -87,7 +95,8 @@ export function RiskIndicatorFromWarning({
isBatched?: boolean;
mode?: 'simple' | 'complex';
}) {
- const warnings = market.warningsWithDetail.filter((w) => w.category === category);
+ const warningsWithDetail = useMarketWarnings(market, true);
+ const warnings = warningsWithDetail.filter((w) => w.category === category);
if (warnings.length === 0) {
return ;
@@ -120,7 +129,7 @@ export function MarketAssetIndicator({
isBatched = false,
mode = 'simple',
}: {
- market: { warningsWithDetail: WarningWithDetail[] };
+ market: Market;
isBatched?: boolean;
mode?: 'simple' | 'complex';
}) {
@@ -142,7 +151,7 @@ export function MarketOracleIndicator({
isBatched = false,
mode = 'simple',
}: {
- market: { warningsWithDetail: WarningWithDetail[] };
+ market: Market;
isBatched?: boolean;
mode?: 'simple' | 'complex';
}) {
@@ -164,7 +173,7 @@ export function MarketDebtIndicator({
isBatched = false,
mode = 'simple',
}: {
- market: { warningsWithDetail: WarningWithDetail[] };
+ market: Market;
isBatched?: boolean;
mode?: 'simple' | 'complex';
}) {
diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx
index 9b1201b3..fe1c6707 100644
--- a/app/markets/components/markets.tsx
+++ b/app/markets/components/markets.tsx
@@ -17,7 +17,7 @@ import { usePagination } from '@/hooks/usePagination';
import { useStaredMarkets } from '@/hooks/useStaredMarkets';
import { useStyledToast } from '@/hooks/useStyledToast';
import { SupportedNetworks } from '@/utils/networks';
-import { OracleVendors, parseOracleVendors } from '@/utils/oracle';
+import { PriceFeedVendors, parsePriceFeedVendors } from '@/utils/oracle';
import * as keys from '@/utils/storageKeys';
import { ERC20Token, UnknownERC20Token } from '@/utils/tokens';
import { Market } from '@/utils/types';
@@ -75,7 +75,7 @@ export default function Markets({
const [searchQuery, setSearchQuery] = useState('');
- const [selectedOracles, setSelectedOracles] = useState([]);
+ const [selectedOracles, setSelectedOracles] = useState([]);
const { currentPage, setCurrentPage, entriesPerPage, handleEntriesPerPageChange, resetPage } =
usePagination();
@@ -208,7 +208,7 @@ export default function Markets({
).filter((market) => {
if (!searchQuery) return true; // If no search query, show all markets
const lowercaseQuery = searchQuery.toLowerCase();
- const { vendors } = parseOracleVendors(market.oracle?.data);
+ const { vendors } = parsePriceFeedVendors(market.oracle?.data, market.morphoBlue.chain.id);
const vendorsName = vendors.join(',');
return (
market.uniqueKey.toLowerCase().includes(lowercaseQuery) ||
diff --git a/app/markets/components/utils.ts b/app/markets/components/utils.ts
index b55606c4..5d214b4a 100644
--- a/app/markets/components/utils.ts
+++ b/app/markets/components/utils.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { SupportedNetworks } from '@/utils/networks';
-import { parseOracleVendors, OracleVendors } from '@/utils/oracle';
+import { parsePriceFeedVendors, PriceFeedVendors, getOracleType, OracleType } from '@/utils/oracle';
import { ERC20Token } from '@/utils/tokens';
import { Market } from '@/utils/types';
import { SortColumn } from './constants';
@@ -61,7 +61,7 @@ export function applyFilterAndSort(
showUnknownOracle: boolean,
selectedCollaterals: string[],
selectedLoanAssets: string[],
- selectedOracles: OracleVendors[],
+ selectedOracles: PriceFeedVendors[],
staredIds: string[],
findToken: (address: string, chainId: number) => ERC20Token | undefined,
usdFilters: UsdFilters,
@@ -89,11 +89,15 @@ export function applyFilterAndSort(
return false;
}
- if (
- !showUnknownOracle &&
- (!market.oracle || parseOracleVendors(market.oracle.data).isUnknown)
- ) {
- return false;
+ if (!showUnknownOracle) {
+ const info = market.oracle
+ ? parsePriceFeedVendors(market.oracle.data, market.morphoBlue.chain.id)
+ : null;
+ const isCustom =
+ getOracleType(market.oracle?.data, market.oracleAddress, market.morphoBlue.chain.id) ===
+ OracleType.Custom;
+ const isUnknown = isCustom || (info?.hasUnknown ?? false);
+ if (!market.oracle || isUnknown) return false;
}
if (
@@ -105,7 +109,7 @@ export function applyFilterAndSort(
}
if (selectedOracles.length > 0 && !!market.oracle) {
- const marketOracles = parseOracleVendors(market.oracle.data).vendors;
+ const marketOracles = parsePriceFeedVendors(market.oracle.data, market.morphoBlue.chain.id).vendors;
if (!marketOracles.some((oracle) => selectedOracles.includes(oracle))) {
return false;
}
diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx
index 9bd5b3e3..fa13541c 100644
--- a/app/positions/components/PositionsSummaryTable.tsx
+++ b/app/positions/components/PositionsSummaryTable.tsx
@@ -22,6 +22,7 @@ import { Button } from '@/components/common/Button';
import { TokenIcon } from '@/components/TokenIcon';
import { TooltipContent } from '@/components/TooltipContent';
import { useLocalStorage } from '@/hooks/useLocalStorage';
+import { computeMarketWarnings } from '@/hooks/useMarketWarnings';
import { useStyledToast } from '@/hooks/useStyledToast';
import { formatReadable, formatBalance } from '@/utils/balance';
import { getNetworkImg } from '@/utils/networks';
@@ -37,15 +38,87 @@ import {
GroupedPosition,
MarketPositionWithEarnings,
UserRebalancerInfo,
+ WarningWithDetail,
+ WarningCategory,
} from '@/utils/types';
-import {
- MarketAssetIndicator,
- MarketOracleIndicator,
- MarketDebtIndicator,
-} from 'app/markets/components/RiskIndicator';
+import { RiskIndicator } from 'app/markets/components/RiskIndicator';
import { RebalanceModal } from './RebalanceModal';
import { SuppliedMarketsDetail } from './SuppliedMarketsDetail';
+// Component to compute and display aggregated risk indicators for a group of positions
+function AggregatedRiskIndicators({ groupedPosition }: { groupedPosition: GroupedPosition }) {
+ // Compute warnings for all markets in the group
+ const allWarnings: WarningWithDetail[] = [];
+
+ for (const position of groupedPosition.markets) {
+ const marketWarnings = computeMarketWarnings(position.market, true);
+ allWarnings.push(...marketWarnings);
+ }
+
+ // Remove duplicates based on warning code
+ const uniqueWarnings = allWarnings.filter(
+ (warning, index, array) => array.findIndex((w) => w.code === warning.code) === index,
+ );
+
+ // Helper to get warnings by category and determine risk level
+ const getWarningIndicator = (
+ category: WarningCategory,
+ greenDesc: string,
+ yellowDesc: string,
+ redDesc: string,
+ ) => {
+ const categoryWarnings = uniqueWarnings.filter((w) => w.category === category);
+
+ if (categoryWarnings.length === 0) {
+ return ;
+ }
+
+ if (categoryWarnings.some((w) => w.level === 'alert')) {
+ const alertWarning = categoryWarnings.find((w) => w.level === 'alert');
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ return (
+ <>
+ {getWarningIndicator(
+ WarningCategory.asset,
+ 'Recognized asset',
+ 'Asset with warning',
+ 'High-risk asset',
+ )}
+ {getWarningIndicator(
+ WarningCategory.oracle,
+ 'Recognized oracles',
+ 'Oracle warning',
+ 'Oracle warning',
+ )}
+ {getWarningIndicator(
+ WarningCategory.debt,
+ 'No bad debt',
+ 'Bad debt has occurred',
+ 'Bad debt higher than 1% of supply',
+ )}
+ >
+ );
+}
+
type PositionsSummaryTableProps = {
account: string;
marketPositions: MarketPositionWithEarnings[];
@@ -344,21 +417,7 @@ export function PositionsSummaryTable({
|
|
diff --git a/app/positions/components/SuppliedMarketsDetail.tsx b/app/positions/components/SuppliedMarketsDetail.tsx
index f381dc02..9190a4e2 100644
--- a/app/positions/components/SuppliedMarketsDetail.tsx
+++ b/app/positions/components/SuppliedMarketsDetail.tsx
@@ -6,6 +6,7 @@ import { IoWarningOutline } from 'react-icons/io5';
import { Button } from '@/components/common';
import OracleVendorBadge from '@/components/OracleVendorBadge';
import { TokenIcon } from '@/components/TokenIcon';
+import { useMarketWarnings } from '@/hooks/useMarketWarnings';
import { formatReadable, formatBalance } from '@/utils/balance';
import { MarketPosition, GroupedPosition, WarningWithDetail, WarningCategory } from '@/utils/types';
import { getCollateralColor } from '../utils/colors';
@@ -39,6 +40,133 @@ function WarningTooltip({ warnings }: { warnings: WarningWithDetail[] }) {
);
}
+function MarketRow({
+ position,
+ totalSupply,
+ setShowWithdrawModal,
+ setShowSupplyModal,
+ setSelectedPosition,
+}: {
+ position: MarketPosition;
+ totalSupply: number;
+ setShowWithdrawModal: (show: boolean) => void;
+ setShowSupplyModal: (show: boolean) => void;
+ setSelectedPosition: (position: MarketPosition) => void;
+}) {
+ const warningsWithDetail = useMarketWarnings(position.market, true);
+
+ const getWarningColor = (warnings: WarningWithDetail[]) => {
+ if (warnings.some((w) => w.level === 'alert')) return 'text-red-500';
+ if (warnings.some((w) => w.level === 'warning')) return 'text-yellow-500';
+ return '';
+ };
+
+ const suppliedAmount = Number(
+ formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals),
+ );
+ const percentageOfPortfolio = totalSupply > 0 ? (suppliedAmount / totalSupply) * 100 : 0;
+ const warningColor = getWarningColor(warningsWithDetail);
+
+ return (
+ |
+
+
+
+ {warningsWithDetail.length > 0 ? (
+ }
+ placement="top"
+ >
+
+
+
+
+ ) : (
+
+ )}
+
+
+ {position.market.uniqueKey.slice(2, 8)}
+
+
+ |
+
+ {position.market.collateralAsset ? (
+
+
+ {position.market.collateralAsset.symbol}
+
+ ) : (
+ 'N/A'
+ )}
+ |
+
+
+
+
+ |
+
+ {formatBalance(position.market.lltv, 16)}%
+ |
+
+ {formatReadable(position.market.state.supplyApy * 100)}%
+ |
+
+ {formatReadable(suppliedAmount)} {position.market.loanAsset.symbol}
+ |
+
+
+
+ {formatReadable(percentageOfPortfolio)}%
+
+ |
+
+
+
+
+
+ |
+
+ );
+}
+
export function SuppliedMarketsDetail({
groupedPosition,
setShowWithdrawModal,
@@ -65,12 +193,6 @@ export function SuppliedMarketsDetail({
const totalSupply = groupedPosition.totalSupply;
- const getWarningColor = (warnings: WarningWithDetail[]) => {
- if (warnings.some((w) => w.level === 'alert')) return 'text-red-500';
- if (warnings.some((w) => w.level === 'warning')) return 'text-yellow-500';
- return '';
- };
-
return (
- {filteredMarkets.map((position) => {
- const suppliedAmount = Number(
- formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals),
- );
- const percentageOfPortfolio =
- totalSupply > 0 ? (suppliedAmount / totalSupply) * 100 : 0;
- const warningColor = getWarningColor(position.market.warningsWithDetail);
-
- return (
-
-
-
-
- {position.market.warningsWithDetail.length > 0 ? (
-
- }
- placement="top"
- >
-
-
-
-
- ) : (
-
- )}
-
-
- {position.market.uniqueKey.slice(2, 8)}
-
-
- |
-
- {position.market.collateralAsset ? (
-
-
- {position.market.collateralAsset.symbol}
-
- ) : (
- 'N/A'
- )}
- |
-
-
-
-
- |
-
- {formatBalance(position.market.lltv, 16)}%
- |
-
- {formatReadable(position.market.state.supplyApy * 100)}%
- |
-
- {formatReadable(suppliedAmount)} {position.market.loanAsset.symbol}
- |
-
-
-
-
- {formatReadable(percentageOfPortfolio)}%
-
-
- |
-
-
-
-
-
- |
-
- );
- })}
+ {filteredMarkets.map((position) => (
+
+ ))}