-
-
-
Current Volumes
- {['supply', 'borrow', 'liquidity'].map((type) => {
- const stats = getCurrentVolumeStats(type as 'supply' | 'borrow' | 'liquidity');
- return (
-
- {type}:
-
- {formatValue(stats.current)}
- 0 ? 'ml-2 text-green-500' : 'ml-2 text-red-500'}>
- ({stats.netChangePercentage > 0 ? '+' : ''}
- {stats.netChangePercentage.toFixed(2)}%)
-
-
-
- );
- })}
-
- {/* Delta to target Utilization */}
-
-
IRM Targets
-
-
- Supply Δ:
-
- }
- >
-
-
-
-
-
-
- {formatValue(Number(formatUnits(targetUtilizationData.supplyDelta, market.loanAsset.decimals)))}
-
-
+
-
-
- Borrow Δ:
-
- }
- >
-
-
-
-
-
-
- {formatValue(Number(formatUnits(targetUtilizationData.borrowDelta, market.loanAsset.decimals)))}
-
-
-
+ {/* Historical Averages */}
+
+
{TIMEFRAME_LABELS[selectedTimeframe]} Averages
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+ Supply
+ {formatValue(supplyStats.average)}
-
-
-
- Historical Averages ({selectedTimeframe})
-
- {isLoading ? (
-
-
-
- ) : (
- ['supply', 'borrow', 'liquidity'].map((type) => (
-
- {type}:
-
- {formatValue(getAverageVolumeStats(type as 'supply' | 'borrow' | 'liquidity'))}
-
-
- ))
- )}
+
+ Borrow
+ {formatValue(borrowStats.average)}
+
+
+ Liquidity
+ {formatValue(liquidityStats.average)}
-
+ )}
-
+
);
}
diff --git a/src/features/market-detail/components/market-header.tsx b/src/features/market-detail/components/market-header.tsx
new file mode 100644
index 00000000..847763ea
--- /dev/null
+++ b/src/features/market-detail/components/market-header.tsx
@@ -0,0 +1,458 @@
+'use client';
+
+import Image from 'next/image';
+import { formatUnits } from 'viem';
+import { ChevronDownIcon } from '@radix-ui/react-icons';
+import { GrStatusGood } from 'react-icons/gr';
+import { IoWarningOutline, IoEllipsisVertical } from 'react-icons/io5';
+import { MdError } from 'react-icons/md';
+import { BsArrowUpCircle, BsArrowDownLeftCircle } from 'react-icons/bs';
+import { FiExternalLink } from 'react-icons/fi';
+import { Button } from '@/components/ui/button';
+import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
+import { TokenIcon } from '@/components/shared/token-icon';
+import { Tooltip } from '@/components/ui/tooltip';
+import { TooltipContent } from '@/components/shared/tooltip-content';
+import { AddressIdentity } from '@/components/shared/address-identity';
+import { CampaignBadge } from '@/features/market-detail/components/campaign-badge';
+import { PositionPill } from '@/features/market-detail/components/position-pill';
+import { OracleTypeInfo } from '@/features/markets/components/oracle/MarketOracle/OracleTypeInfo';
+import { useRateLabel } from '@/hooks/useRateLabel';
+import { useAppSettings } from '@/stores/useAppSettings';
+import { convertApyToApr } from '@/utils/rateMath';
+import { getIRMTitle } from '@/utils/morpho';
+import { getNetworkImg, getNetworkName, type SupportedNetworks } from '@/utils/networks';
+import { getMarketURL } from '@/utils/external';
+import type { Market, MarketPosition, WarningWithDetail } from '@/utils/types';
+import { WarningCategory } from '@/utils/types';
+import { getRiskLevel, countWarningsByLevel, type RiskLevel } from '@/utils/warnings';
+
+// Reusable component for rendering a block of warnings with consistent styling
+type WarningBlockProps = {
+ warnings: WarningWithDetail[];
+ riskLevel: RiskLevel;
+};
+
+function WarningBlock({ warnings, riskLevel }: WarningBlockProps): React.ReactNode {
+ if (warnings.length === 0) return null;
+
+ const isAlert = riskLevel === 'red';
+ const containerClass = isAlert
+ ? 'border-red-200 bg-red-50 dark:border-red-400/20 dark:bg-red-400/10'
+ : 'border-yellow-200 bg-yellow-50 dark:border-yellow-400/20 dark:bg-yellow-400/10';
+
+ return (
+
+
+ {isAlert ? (
+
+ ) : (
+
+ )}
+
+ {warnings.map((w) => (
+
+ {w.description}
+
+ ))}
+
+
+
+ );
+}
+
+// Reusable badge component for warning/alert counts
+type StatusBadgeProps = {
+ variant: 'success' | 'warning' | 'alert';
+ count?: number;
+ label: string;
+};
+
+function StatusBadge({ variant, count, label }: StatusBadgeProps): React.ReactNode {
+ const styles = {
+ success: 'bg-green-100 text-green-800 dark:bg-green-400/10 dark:text-green-300',
+ warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-400/10 dark:text-yellow-300',
+ alert: 'bg-red-100 text-red-800 dark:bg-red-400/10 dark:text-red-300',
+ };
+
+ const icons = {
+ success:
,
+ warning:
,
+ alert:
,
+ };
+
+ const displayLabel = count !== undefined ? `${count} ${label}${count > 1 ? 's' : ''}` : label;
+
+ return (
+
+ {icons[variant]}
+ {displayLabel}
+
+ );
+}
+
+// Risk indicator icon component
+function RiskIcon({ level }: { level: RiskLevel }): React.ReactNode {
+ switch (level) {
+ case 'green':
+ return
;
+ case 'yellow':
+ return
;
+ case 'red':
+ return
;
+ default:
+ return null;
+ }
+}
+
+type MarketHeaderProps = {
+ market: Market;
+ marketId: string;
+ network: SupportedNetworks;
+ userPosition: MarketPosition | null;
+ oraclePrice: string;
+ allWarnings: WarningWithDetail[];
+ onSupplyClick: () => void;
+ onBorrowClick: () => void;
+};
+
+export function MarketHeader({
+ market,
+ marketId,
+ network,
+ userPosition,
+ oraclePrice,
+ allWarnings,
+ onSupplyClick,
+ onBorrowClick,
+}: MarketHeaderProps) {
+ const { short: rateLabel } = useRateLabel();
+ const { isAprDisplay } = useAppSettings();
+ const networkImg = getNetworkImg(network);
+
+ const formatRate = (rate: number) => {
+ const displayRate = isAprDisplay ? convertApyToApr(rate) : rate;
+ return `${(displayRate * 100).toFixed(2)}%`;
+ };
+
+ const formattedLltv = `${formatUnits(BigInt(market.lltv), 16)}%`;
+
+ const campaignBadgeProps = {
+ marketId,
+ loanTokenAddress: market.loanAsset.address,
+ chainId: market.morphoBlue.chain.id,
+ whitelisted: market.whitelisted,
+ };
+
+ // Filter warnings by category
+ const assetWarnings = allWarnings.filter((w) => w.category === WarningCategory.asset);
+ const oracleWarnings = allWarnings.filter((w) => w.category === WarningCategory.oracle);
+ const globalWarnings = allWarnings.filter((w) => w.category === WarningCategory.debt || w.category === WarningCategory.general);
+
+ // Compute risk levels for each category
+ const assetRiskLevel = getRiskLevel(assetWarnings);
+ const oracleRiskLevel = getRiskLevel(oracleWarnings);
+ const globalRiskLevel = getRiskLevel(globalWarnings);
+ const { alertCount, warningCount } = countWarningsByLevel(allWarnings);
+
+ // Render summary badges based on warning counts
+ const renderSummaryBadges = (): React.ReactNode => {
+ if (allWarnings.length === 0) {
+ return (
+
+ );
+ }
+
+ if (alertCount > 0 && warningCount > 0) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (alertCount > 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ return (
+
+ {/* Main Header */}
+
+
+ {/* LEFT: Market Identity */}
+
+ {/* Overlapping token icons */}
+
+
+
+
+
+
+
+ {market.loanAsset.symbol}/{market.collateralAsset.symbol}
+
+
+ {networkImg && (
+
+
+ {getNetworkName(network)}
+
+ )}
+
·
+
LLTV {formattedLltv}
+
·
+
{getIRMTitle(market.irmAddress)}
+
+
+
+
+ {/* RIGHT: Stats + Actions */}
+
+ {/* Key Stats - Hidden on small screens */}
+
+
+
Supply {rateLabel}
+
+
{formatRate(market.state.supplyApy)}
+
+
+
+
+
Borrow {rateLabel}
+
+
{formatRate(market.state.borrowApy)}
+
+
+
+
+
+ }
+ >
+
+
Oracle
+
{Number(oraclePrice).toFixed(2)}
+
+
+
+
+
+ {/* Position Pill + Actions Dropdown */}
+
+ {userPosition && (
+
+ )}
+
+
+
+
+
+ }
+ >
+ Supply
+
+ }
+ >
+ Borrow
+
+ window.open(getMarketURL(marketId, network), '_blank')}
+ startContent={}
+ >
+ View on Morpho
+
+
+
+
+
+
+
+ {/* Mobile Stats Row - Visible only on small screens */}
+
+
+
Supply {rateLabel}
+
+
{formatRate(market.state.supplyApy)}
+
+
+
+
+
Borrow {rateLabel}
+
+
{formatRate(market.state.borrowApy)}
+
+
+
+
+
Oracle
+
{Number(oraclePrice).toFixed(2)}
+
+
+
+ {/* Advanced Details - Expandable */}
+
+
+
+ Advanced Details
+ {renderSummaryBadges()}
+
+
+
+ {/* Global Warnings (debt + general) at top */}
+
+
+ {/* Two-column grid */}
+
+ {/* LEFT: Market Configuration */}
+
+
+
Market Configuration
+
+
+
+
+
+ {/* Asset warnings */}
+
+
+
+ {/* RIGHT: Oracle */}
+
+
+
Oracle
+
+
+
+
+
+ {/* Oracle warnings */}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/market-detail/components/market-warning-banner.tsx b/src/features/market-detail/components/market-warning-banner.tsx
deleted file mode 100644
index 259b91ea..00000000
--- a/src/features/market-detail/components/market-warning-banner.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-'use client';
-
-import { motion } from 'framer-motion';
-import { MdError } from 'react-icons/md';
-import { IoWarningOutline } from 'react-icons/io5';
-import type { WarningWithDetail } from '@/utils/types';
-
-type MarketWarningBannerProps = {
- warnings: WarningWithDetail[];
-};
-
-export function MarketWarningBanner({ warnings }: MarketWarningBannerProps) {
- if (warnings.length === 0) return null;
-
- const hasAlert = warnings.some((w) => w.level === 'alert');
- const Icon = hasAlert ? MdError : IoWarningOutline;
-
- const colorClasses = hasAlert ? 'border-red-500/20 bg-red-500/10 text-red-500' : 'border-yellow-500/20 bg-yellow-500/10 text-yellow-500';
-
- return (
-
-
-
-
- {warnings.map((warning) => (
-
{warning.description}
- ))}
-
-
-
- );
-}
diff --git a/src/features/market-detail/components/position-pill.tsx b/src/features/market-detail/components/position-pill.tsx
new file mode 100644
index 00000000..452dedaa
--- /dev/null
+++ b/src/features/market-detail/components/position-pill.tsx
@@ -0,0 +1,127 @@
+'use client';
+
+import { formatUnits } from 'viem';
+import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
+import { TokenIcon } from '@/components/shared/token-icon';
+import { formatReadable } from '@/utils/balance';
+import type { MarketPosition } from '@/utils/types';
+
+type PositionRowProps = {
+ tokenAddress: string;
+ chainId: number;
+ symbol: string;
+ label: string;
+ amount: number;
+ textColor?: string;
+};
+
+function PositionRow({ tokenAddress, chainId, symbol, label, amount, textColor }: PositionRowProps) {
+ if (amount <= 0) return null;
+ return (
+
+
+
+ {label}
+
+
+ {formatReadable(amount)} {symbol}
+
+
+ );
+}
+
+type PositionPillProps = {
+ position: MarketPosition;
+ onSupplyClick?: () => void;
+ onBorrowClick?: () => void;
+};
+
+export function PositionPill({ position, onSupplyClick, onBorrowClick }: PositionPillProps) {
+ const { market, state } = position;
+
+ const supplyAmount = Number(formatUnits(BigInt(state.supplyAssets), market.loanAsset.decimals));
+ const borrowAmount = Number(formatUnits(BigInt(state.borrowAssets), market.loanAsset.decimals));
+ const collateralAmount = Number(formatUnits(BigInt(state.collateral), market.collateralAsset.decimals));
+
+ // Check if user has any position
+ const hasPosition = supplyAmount > 0 || borrowAmount > 0 || collateralAmount > 0;
+
+ if (!hasPosition) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
Your Position
+
+
+
+
+
+ {/* Action buttons */}
+ {(onSupplyClick ?? onBorrowClick) && (
+
+ {onSupplyClick && (
+
+ )}
+ {onBorrowClick && (
+
+ )}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/features/market-detail/market-view.tsx b/src/features/market-detail/market-view.tsx
index 8233594d..31ff4d15 100644
--- a/src/features/market-detail/market-view.tsx
+++ b/src/features/market-detail/market-view.tsx
@@ -3,45 +3,30 @@
'use client';
import { useState, useCallback, useMemo } from 'react';
-import { Card, CardHeader, CardBody } from '@/components/ui/card';
-import { ExternalLinkIcon } from '@radix-ui/react-icons';
-import Image from 'next/image';
-import Link from 'next/link';
import { useParams } from 'next/navigation';
-import { formatUnits, parseUnits } from 'viem';
+import { parseUnits, formatUnits } from 'viem';
import { useConnection } from 'wagmi';
import { BorrowModal } from '@/modals/borrow/borrow-modal';
-import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Spinner } from '@/components/ui/spinner';
import Header from '@/components/layout/header/Header';
-import { OracleTypeInfo } from '@/features/markets/components/oracle';
-import { TokenIcon } from '@/components/shared/token-icon';
import { useModal } from '@/hooks/useModal';
import { useMarketData } from '@/hooks/useMarketData';
import { useOraclePrice } from '@/hooks/useOraclePrice';
import { useTransactionFilters } from '@/stores/useTransactionFilters';
import useUserPosition from '@/hooks/useUserPosition';
-import MORPHO_LOGO from '@/imgs/tokens/morpho.svg';
-import { getExplorerURL, getMarketURL } from '@/utils/external';
-import { getIRMTitle } from '@/utils/morpho';
-import { getNetworkImg, getNetworkName, type SupportedNetworks } from '@/utils/networks';
-import { getTruncatedAssetName } from '@/utils/oracle';
+import type { SupportedNetworks } from '@/utils/networks';
import { BorrowersTable } from '@/features/market-detail/components/borrowers-table';
import { BorrowsTable } from '@/features/market-detail/components/borrows-table';
import BorrowerFiltersModal from '@/features/market-detail/components/filters/borrower-filters-modal';
-import { CampaignBadge } from '@/features/market-detail/components/campaign-badge';
import { LiquidationsTable } from '@/features/market-detail/components/liquidations-table';
-import { PositionStats } from '@/features/market-detail/components/position-stats';
import { SuppliesTable } from '@/features/market-detail/components/supplies-table';
import { SuppliersTable } from '@/features/market-detail/components/suppliers-table';
import SupplierFiltersModal from '@/features/market-detail/components/filters/supplier-filters-modal';
import TransactionFiltersModal from '@/features/market-detail/components/filters/transaction-filters-modal';
import { useMarketWarnings } from '@/hooks/useMarketWarnings';
-import { WarningCategory } from '@/utils/types';
-import { CardWarningIndicator } from './components/card-warning-indicator';
+import { MarketHeader } from './components/market-header';
import RateChart from './components/charts/rate-chart';
-import { MarketWarningBanner } from './components/market-warning-banner';
import VolumeChart from './components/charts/volume-chart';
function MarketContent() {
@@ -50,7 +35,6 @@ function MarketContent() {
// 2. Network setup
const network = Number(chainId as string) as SupportedNetworks;
- const networkImg = getNetworkImg(network);
// 3. Consolidated state
const { open: openModal } = useModal();
@@ -83,92 +67,79 @@ function MarketContent() {
const { address } = useConnection();
- const {
- position: userPosition,
- loading: positionLoading,
- refetch: refetchUserPosition,
- } = useUserPosition(address, network, marketId as string);
+ const { position: userPosition, refetch: refetchUserPosition } = useUserPosition(address, network, marketId as string);
// Get all warnings for this market (hook handles undefined market)
const allWarnings = useMarketWarnings(market);
// 6. All memoized values and callbacks
- const formattedOraclePrice = useMemo(() => {
- if (!market) return '0';
- const adjusted = (oraclePrice * BigInt(10 ** market.collateralAsset.decimals)) / BigInt(10 ** market.loanAsset.decimals);
- return formatUnits(adjusted, 36);
- }, [oraclePrice, market]);
- // convert to token amounts
- const scaledMinSupplyAmount = useMemo(() => {
- if (!market || !minSupplyAmount || minSupplyAmount === '0' || minSupplyAmount === '') {
- return '0';
- }
+ // Helper to scale user input to token amount
+ const scaleToTokenAmount = (value: string, decimals: number): string => {
+ if (!value || value === '0' || value === '') return '0';
try {
- return parseUnits(minSupplyAmount, market.loanAsset.decimals).toString();
+ return parseUnits(value, decimals).toString();
} catch {
return '0';
}
- }, [minSupplyAmount, market]);
+ };
- const scaledMinBorrowAmount = useMemo(() => {
- if (!market || !minBorrowAmount || minBorrowAmount === '0' || minBorrowAmount === '') {
- return '0';
- }
+ // Helper to convert asset amount to shares: (amount × totalShares) / totalAssets
+ const convertAssetToShares = (amount: string, totalAssets: bigint, totalShares: bigint, decimals: number): string => {
+ if (!amount || amount === '0' || amount === '' || totalAssets === 0n) return '0';
try {
- return parseUnits(minBorrowAmount, market.loanAsset.decimals).toString();
+ const assetAmount = parseUnits(amount, decimals);
+ return ((assetAmount * totalShares) / totalAssets).toString();
} catch {
return '0';
}
- }, [minBorrowAmount, market]);
+ };
- // Convert user-specified asset amount to shares for filtering suppliers
- // Formula: effectiveMinShares = (minAssetAmount × totalSupplyShares) / totalSupplyAssets
- const scaledMinSupplierShares = useMemo(() => {
- if (!market || !minSupplierShares || minSupplierShares === '0' || minSupplierShares === '') {
- return '0';
- }
- try {
- const minAssetAmount = parseUnits(minSupplierShares, market.loanAsset.decimals);
- const totalSupplyAssets = BigInt(market.state.supplyAssets);
- const totalSupplyShares = BigInt(market.state.supplyShares);
-
- // If no supply yet, return 0
- if (totalSupplyAssets === 0n) {
- return '0';
- }
-
- // Convert asset amount to shares
- const effectiveMinShares = (minAssetAmount * totalSupplyShares) / totalSupplyAssets;
- return effectiveMinShares.toString();
- } catch {
- return '0';
- }
- }, [minSupplierShares, market]);
+ // Oracle price scaled for display (36 decimals is the Morpho oracle price scale)
+ const ORACLE_PRICE_SCALE = 36;
+ const formattedOraclePrice = useMemo(() => {
+ if (!market) return '0';
+ const adjusted = (oraclePrice * BigInt(10 ** market.collateralAsset.decimals)) / BigInt(10 ** market.loanAsset.decimals);
+ return formatUnits(adjusted, ORACLE_PRICE_SCALE);
+ }, [oraclePrice, market]);
- // Convert user-specified asset amount to shares for filtering borrowers
- // Formula: effectiveMinShares = (minAssetAmount × totalBorrowShares) / totalBorrowAssets
- const scaledMinBorrowerShares = useMemo(() => {
- if (!market || !minBorrowerShares || minBorrowerShares === '0' || minBorrowerShares === '') {
- return '0';
- }
- try {
- const minAssetAmount = parseUnits(minBorrowerShares, market.loanAsset.decimals);
- const totalBorrowAssets = BigInt(market.state.borrowAssets);
- const totalBorrowShares = BigInt(market.state.borrowShares);
-
- // If no borrows yet, return 0
- if (totalBorrowAssets === 0n) {
- return '0';
- }
-
- // Convert asset amount to shares
- const effectiveMinShares = (minAssetAmount * totalBorrowShares) / totalBorrowAssets;
- return effectiveMinShares.toString();
- } catch {
- return '0';
- }
- }, [minBorrowerShares, market]);
+ // Convert filter amounts to token amounts
+ const scaledMinSupplyAmount = useMemo(
+ () => (market ? scaleToTokenAmount(minSupplyAmount, market.loanAsset.decimals) : '0'),
+ [minSupplyAmount, market],
+ );
+
+ const scaledMinBorrowAmount = useMemo(
+ () => (market ? scaleToTokenAmount(minBorrowAmount, market.loanAsset.decimals) : '0'),
+ [minBorrowAmount, market],
+ );
+
+ // Convert user-specified asset amounts to shares for filtering suppliers/borrowers
+ const scaledMinSupplierShares = useMemo(
+ () =>
+ market
+ ? convertAssetToShares(
+ minSupplierShares,
+ BigInt(market.state.supplyAssets),
+ BigInt(market.state.supplyShares),
+ market.loanAsset.decimals,
+ )
+ : '0',
+ [minSupplierShares, market],
+ );
+
+ const scaledMinBorrowerShares = useMemo(
+ () =>
+ market
+ ? convertAssetToShares(
+ minBorrowerShares,
+ BigInt(market.state.borrowAssets),
+ BigInt(market.state.borrowShares),
+ market.loanAsset.decimals,
+ )
+ : '0',
+ [minBorrowerShares, market],
+ );
// Unified refetch function for both market and user position
const handleRefreshAll = useCallback(async () => {
@@ -215,67 +186,30 @@ function MarketContent() {
);
}
- // 8. Derived values that depend on market data
- const cardStyle = 'bg-surface rounded shadow-sm p-4';
+ // Handlers for supply/borrow actions
+ const handleSupplyClick = () => {
+ openModal('supply', { market, position: userPosition, isMarketPage: true, refetch: handleRefreshAllSync });
+ };
- // 9. Warning filtering by category
- const pageWarnings = allWarnings.filter((w) => w.category === WarningCategory.debt || w.category === WarningCategory.general);
- const assetWarnings = allWarnings.filter((w) => w.category === WarningCategory.asset);
- const oracleWarnings = allWarnings.filter((w) => w.category === WarningCategory.oracle);
+ const handleBorrowClick = () => {
+ setShowBorrowModal(true);
+ };
return (
<>
- {/* Market title and actions */}
-
-
-
- {market.loanAsset.symbol}/{market.collateralAsset.symbol} Market
-
-
-
-
-
-
-
-
-
-
-
- {/* Page-level warnings (debt + general) */}
-
+ {/* Unified Market Header */}
+
{showBorrowModal && (
)}
-
-
-
- Basic Info
-
-
- {networkImg && (
-
- )}
- {getNetworkName(network)}
-
-
-
-
-
-
Loan Asset:
-
-
-
- {getTruncatedAssetName(market.loanAsset.symbol)}
-
-
-
-
-
Collateral Asset:
-
-
-
- {getTruncatedAssetName(market.collateralAsset.symbol)}
-
-
-
-
- IRM:
-
- {getIRMTitle(market.irmAddress)}
-
-
-
- LLTV:
- {formatUnits(BigInt(market.lltv), 16)}%
-
-
-
-
-
-
-
- Oracle Info
-
-
-
-
-
-
-
-
-
-
- Live Price:
-
- {Number(formattedOraclePrice).toFixed(4)} {market.loanAsset.symbol}
-
-
-
-
-
-
-
-
-
-
{/* Tabs Section */}
- Volume
- Rates
-
+
+
+
diff --git a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx
index f4644d34..b8b35f17 100644
--- a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx
+++ b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx
@@ -115,7 +115,7 @@ export function FeedEntry({ feed, chainId }: FeedEntryProps): JSX.Element | null
return (
-
+
{showAssetPair ? (
{baseAsset}
diff --git a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx
index 735391f1..e70eec58 100644
--- a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx
+++ b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx
@@ -9,14 +9,6 @@ type MarketOracleFeedInfoProps = {
chainId: number;
};
-function EmptyFeedSlot() {
- return (
-
- --
-
- );
-}
-
export function MarketOracleFeedInfo({
baseFeedOne,
baseFeedTwo,
@@ -30,26 +22,24 @@ export function MarketOracleFeedInfo({
return
No feed routes available
;
}
- const renderFeed = (feed: OracleFeed | null | undefined) =>
- feed ? (
-
-
-
- ) : (
-
- );
-
return (
{(baseFeedOne || baseFeedTwo) && (
Base:
-
-
{renderFeed(baseFeedOne)}
-
{renderFeed(baseFeedTwo)}
+
+ {baseFeedOne && (
+
+ )}
+ {baseFeedTwo && (
+
+ )}
)}
@@ -57,9 +47,19 @@ export function MarketOracleFeedInfo({
{(quoteFeedOne || quoteFeedTwo) && (
Quote:
-
-
{renderFeed(quoteFeedOne)}
-
{renderFeed(quoteFeedTwo)}
+
+ {quoteFeedOne && (
+
+ )}
+ {quoteFeedTwo && (
+
+ )}
)}
diff --git a/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx b/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx
index 748fabec..39ec2264 100644
--- a/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx
+++ b/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx
@@ -1,5 +1,6 @@
import Link from 'next/link';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
+import { AddressIdentity } from '@/components/shared/address-identity';
import { MarketOracleFeedInfo } from '@/features/markets/components/oracle';
import { getExplorerURL } from '@/utils/external';
import { getOracleType, getOracleTypeDescription, OracleType } from '@/utils/oracle';
@@ -11,30 +12,42 @@ type OracleTypeInfoProps = {
chainId: number;
showLink?: boolean;
showCustom?: boolean;
+ useBadge?: boolean;
};
-export function OracleTypeInfo({ oracleData, oracleAddress, chainId, showLink, showCustom }: OracleTypeInfoProps) {
+export function OracleTypeInfo({ oracleData, oracleAddress, chainId, showLink, showCustom, useBadge }: OracleTypeInfoProps) {
const oracleType = getOracleType(oracleData, oracleAddress, chainId);
const typeDescription = getOracleTypeDescription(oracleType);
return (
<>
-
- Oracle Type:
- {showLink ? (
-
- {typeDescription}
-
-
- ) : (
- {typeDescription}
- )}
-
+ {useBadge ? (
+
+ ) : (
+
+ Oracle Type:
+ {showLink ? (
+
+ {typeDescription}
+
+
+ ) : (
+ {typeDescription}
+ )}
+
+ )}
{oracleType === OracleType.Standard ? (
w === UNRECOGNIZED_FEEDS) === undefined) {
+ if (!result.includes(UNRECOGNIZED_FEEDS)) {
result.push(UNKNOWN_FEED_FOR_PAIR_MATCHING);
}
} else if (!feedsPathResult.isValid) {
- // Create a dynamic warning with the specific error message
- const incompatibleFeedsWarning: WarningWithDetail = {
+ result.push({
...INCOMPATIBLE_ORACLE_FEEDS,
description: feedsPathResult.missingPath ?? INCOMPATIBLE_ORACLE_FEEDS.description,
- };
- result.push(incompatibleFeedsWarning);
+ });
}
}
}
@@ -231,3 +229,27 @@ export const getMarketWarningsWithDetail = (market: Market, considerWhitelist =
return result;
};
+
+// Risk level type for UI components
+export type RiskLevel = 'green' | 'yellow' | 'red';
+
+/**
+ * Determine risk level for a set of warnings
+ * - green: no warnings
+ * - yellow: has warnings but no alerts
+ * - red: has at least one alert-level warning
+ */
+export const getRiskLevel = (warnings: WarningWithDetail[]): RiskLevel => {
+ if (warnings.length === 0) return 'green';
+ if (warnings.some((w) => w.level === 'alert')) return 'red';
+ return 'yellow';
+};
+
+/**
+ * Count warnings by level
+ */
+export const countWarningsByLevel = (warnings: WarningWithDetail[]) => {
+ const alertCount = warnings.filter((w) => w.level === 'alert').length;
+ const warningCount = warnings.filter((w) => w.level === 'warning').length;
+ return { alertCount, warningCount };
+};