diff --git a/app/market/[chainId]/[marketid]/components/PositionStats.tsx b/app/market/[chainId]/[marketid]/components/PositionStats.tsx index 22175426..7ae5a41c 100644 --- a/app/market/[chainId]/[marketid]/components/PositionStats.tsx +++ b/app/market/[chainId]/[marketid]/components/PositionStats.tsx @@ -7,6 +7,7 @@ import { HiOutlineGlobeAsiaAustralia } from 'react-icons/hi2'; import { Spinner } from '@/components/common/Spinner'; import { TokenIcon } from '@/components/TokenIcon'; import { formatBalance, formatReadable } from '@/utils/balance'; +import { getTruncatedAssetName } from '@/utils/oracle'; import { Market, MarketPosition } from '@/utils/types'; type PositionStatsProps = { @@ -72,7 +73,7 @@ export function PositionStats({ BigInt(userPosition.state.supplyAssets || 0), market.loanAsset.decimals, ).toString()}{' '} - {market.loanAsset.symbol} + {getTruncatedAssetName(market.loanAsset.symbol)} @@ -91,7 +92,7 @@ export function PositionStats({ BigInt(userPosition.state.borrowAssets || 0), market.loanAsset.decimals, ).toString()}{' '} - {market.loanAsset.symbol} + {getTruncatedAssetName(market.loanAsset.symbol)} diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 44837c8a..eb50dea6 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -13,9 +13,8 @@ import { useAccount } from 'wagmi'; import { BorrowModal } from '@/components/BorrowModal'; import { Button } from '@/components/common'; import { Spinner } from '@/components/common/Spinner'; -import { OracleFeedInfo } from '@/components/FeedInfo/OracleFeedInfo'; import Header from '@/components/layout/header/Header'; -import OracleVendorBadge from '@/components/OracleVendorBadge'; +import { OracleTypeInfo } from '@/components/MarketOracle'; import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { TokenIcon } from '@/components/TokenIcon'; import { useMarketData } from '@/hooks/useMarketData'; @@ -26,6 +25,7 @@ import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; import { getExplorerURL, getMarketURL } from '@/utils/external'; import { getIRMTitle } from '@/utils/morpho'; import { getNetworkImg, getNetworkName, SupportedNetworks } from '@/utils/networks'; +import { getTruncatedAssetName } from '@/utils/oracle'; import { TimeseriesOptions } from '@/utils/types'; import { BorrowsTable } from './components/BorrowsTable'; import { LiquidationsTable } from './components/LiquidationsTable'; @@ -179,12 +179,6 @@ function MarketContent() { // 8. Derived values that depend on market data const cardStyle = 'bg-surface rounded shadow-sm p-4'; - const hasFeed = - market.oracle?.data?.baseFeedOne || - market.oracle?.data?.baseFeedTwo || - market.oracle?.data?.quoteFeedOne || - market.oracle?.data?.quoteFeedTwo; - return ( <>
@@ -300,7 +294,7 @@ function MarketContent() { rel="noopener noreferrer" className="flex items-center no-underline hover:underline" > - {market.collateralAsset.symbol} + {getTruncatedAssetName(market.collateralAsset.symbol)} @@ -324,54 +318,33 @@ function MarketContent() { - Oracle Info + + Oracle Info + + + + + + +
-
- Vendor: - {market.oracle?.data && ( - - {' '} - - - )} -
Live Price: {Number(formattedOraclePrice).toFixed(4)} {market.loanAsset.symbol}
- {hasFeed && ( -
-

Feed Routes:

- {market.oracle?.data && ( -
- - - - -
- )} -
- )} +
diff --git a/app/markets/components/AdvancedSearchBar.tsx b/app/markets/components/AdvancedSearchBar.tsx index 380e8dd2..9335701d 100644 --- a/app/markets/components/AdvancedSearchBar.tsx +++ b/app/markets/components/AdvancedSearchBar.tsx @@ -193,8 +193,8 @@ function AdvancedSearchBar({ onFocus={handleInputFocus} endContent={} classNames={{ - inputWrapper: 'bg-surface rounded-sm w-full lg:w-[600px]', - input: 'bg-surface rounded-sm text-xs', + inputWrapper: 'bg-surface rounded-sm w-full lg:w-[600px] focus-within:outline-none', + input: 'bg-surface rounded-sm text-xs focus:outline-none', }} autoComplete="off" /> diff --git a/app/markets/components/MarketRowDetail.tsx b/app/markets/components/MarketRowDetail.tsx index 2d033589..ea03fbf5 100644 --- a/app/markets/components/MarketRowDetail.tsx +++ b/app/markets/components/MarketRowDetail.tsx @@ -1,64 +1,28 @@ -import { Tooltip } from '@heroui/react'; -import { ExternalLinkIcon, QuestionMarkCircledIcon } from '@radix-ui/react-icons'; -import { OracleFeedInfo } from '@/components/FeedInfo/OracleFeedInfo'; import { Info } from '@/components/Info/info'; -import OracleVendorBadge from '@/components/OracleVendorBadge'; -import { TooltipContent } from '@/components/TooltipContent'; +import { OracleTypeInfo } from '@/components/MarketOracle'; +import { useMarketWarnings } from '@/hooks/useMarketWarnings'; import { formatReadable } from '@/utils/balance'; -import { getExplorerURL } from '@/utils/external'; import { Market } from '@/utils/types'; export function ExpandedMarketDetail({ market }: { market: Market }) { const oracleData = market.oracle ? market.oracle.data : null; - - const hasFeeds = - oracleData && - (oracleData.baseFeedOne || - oracleData.baseFeedTwo || - oracleData.quoteFeedOne || - oracleData.quoteFeedTwo); + const warningsWithDetail = useMarketWarnings(market, true); return (
- {/* Oracle info */}

Oracle Info

-
-

Vendors:

- - - - -
- {hasFeeds && ( -
-
-

Feed Routes:

- } - title="Feed Routes" - detail="Feed routes show how asset prices are derived from different oracle providers" - /> - } - className="max-w-[400px] rounded-sm p-2" - > - - -
- - - - -
- )} + + {/* contains: Oracle Info: Standard (Custom...etc) */} +
{/* market info */} @@ -87,7 +51,7 @@ export function ExpandedMarketDetail({ market }: { market: Market }) {
- {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] ? ( + {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) => ( + + ))}
diff --git a/app/positions/components/onboarding/RiskSelection.tsx b/app/positions/components/onboarding/RiskSelection.tsx index adcc1128..e775e91b 100644 --- a/app/positions/components/onboarding/RiskSelection.tsx +++ b/app/positions/components/onboarding/RiskSelection.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/common/Button'; import { MarketInfoBlock } from '@/components/common/MarketInfoBlock'; import { useTokens } from '@/components/providers/TokenProvider'; import { formatReadable } from '@/utils/balance'; -import { OracleVendors, parseOracleVendors } from '@/utils/oracle'; +import { PriceFeedVendors, parsePriceFeedVendors } from '@/utils/oracle'; import { Market } from '@/utils/types'; import AssetFilter from 'app/markets/components/AssetFilter'; import OracleFilter from 'app/markets/components/OracleFilter'; @@ -27,7 +27,7 @@ export function RiskSelection() { goToPrevStep, } = useOnboarding(); const [selectedCollaterals, setSelectedCollaterals] = useState([]); - const [selectedOracles, setSelectedOracles] = useState([]); + const [selectedOracles, setSelectedOracles] = useState([]); const { findToken, getUniqueTokens } = useTokens(); @@ -63,11 +63,11 @@ export function RiskSelection() { // Check if oracle is selected (if any are selected) if (selectedOracles.length > 0) { - const { vendors } = parseOracleVendors(market.oracle?.data); + const { vendors } = parsePriceFeedVendors(market.oracle?.data, market.morphoBlue.chain.id); // if vendors is empty, push "unknown oracle" into list that needed to be selected if (vendors.length === 0) { - vendors.push(OracleVendors.Unknown); + vendors.push(PriceFeedVendors.Unknown); } // Check if all vendors are selected diff --git a/app/positions/report/components/ReportTable.tsx b/app/positions/report/components/ReportTable.tsx index c6ea4718..d444c936 100644 --- a/app/positions/report/components/ReportTable.tsx +++ b/app/positions/report/components/ReportTable.tsx @@ -94,7 +94,7 @@ function MarketSummaryBlock({
Oracle: - +
diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx index 98c96dc0..88a88490 100644 --- a/app/rewards/components/RewardTable.tsx +++ b/app/rewards/components/RewardTable.tsx @@ -141,7 +141,10 @@ export default function RewardTable({ height={20} /> ) : ( -
+
)}
diff --git a/docs/Styling.md b/docs/Styling.md index c99354f0..f48336cc 100644 --- a/docs/Styling.md +++ b/docs/Styling.md @@ -98,20 +98,22 @@ import { Button } from '@/components/common/Button'; ## Tooltip -Use the nextui tooltip with component for consistnet styling +Use the nextui tooltip with component for consistent styling. Always use the classNames configuration to remove HeroUI's default wrapper styling: -``` +```tsx } - title="Tooltip Title" - detail="Tooltip Detail" - />} - + classNames={{ + base: 'p-0 m-0 bg-transparent shadow-sm border-none', + content: 'p-0 m-0 bg-transparent shadow-sm border-none', + }} + content={} title="Tooltip Title" detail="Tooltip Detail" />} > + {/* Your trigger element */} + ``` +**Important:** The `classNames` configuration removes HeroUI's default padding, background, and borders to prevent double-wrapper styling issues. This ensures only your `TooltipContent` component handles the visual styling. + ## Input Components The codebase uses two different input approaches depending on the use case: diff --git a/src/OnchainProviders.tsx b/src/OnchainProviders.tsx index ae69556a..d6a31f41 100644 --- a/src/OnchainProviders.tsx +++ b/src/OnchainProviders.tsx @@ -9,7 +9,6 @@ import { CustomRpcProvider, useCustomRpcContext } from './components/providers/C type Props = { children: ReactNode }; - const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? ''; if (!projectId) { if (process.env.NODE_ENV !== 'production') { @@ -22,11 +21,11 @@ const staticWagmiConfig = createWagmiConfig(projectId); function WagmiConfigProvider({ children }: Props) { const { customRpcUrls } = useCustomRpcContext(); - + // Only use dynamic config when custom RPCs are explicitly set const hasCustomRpcs = Object.keys(customRpcUrls).length > 0; - const wagmiConfig = hasCustomRpcs - ? createWagmiConfig(projectId, customRpcUrls) + const wagmiConfig = hasCustomRpcs + ? createWagmiConfig(projectId, customRpcUrls) : staticWagmiConfig; return ( @@ -44,9 +43,7 @@ function WagmiConfigProvider({ children }: Props) { }} modalSize="compact" > - - {children} - + {children} ); diff --git a/src/components/FeedInfo/OracleFeedInfo.tsx b/src/components/FeedInfo/OracleFeedInfo.tsx deleted file mode 100644 index 8774a445..00000000 --- a/src/components/FeedInfo/OracleFeedInfo.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Tooltip } from '@heroui/react'; -import { ExternalLinkIcon } from '@radix-ui/react-icons'; -import Image from 'next/image'; -import Link from 'next/link'; -import { IoIosSwap } from 'react-icons/io'; -import { IoWarningOutline } from 'react-icons/io5'; -import { Address } from 'viem'; -import { getSlicedAddress } from '@/utils/address'; -import { getExplorerURL } from '@/utils/external'; -import { OracleVendors, OracleVendorIcons } from '@/utils/oracle'; -import { OracleFeed } from '@/utils/types'; - -export function OracleFeedInfo({ - feed, - chainId, -}: { - feed: OracleFeed | null; - chainId: number; -}): JSX.Element | null { - if (!feed) return null; - - const fromAsset = feed.pair?.[0] ?? 'Unknown'; - const toAsset = feed.pair?.[1] ?? 'Unknown'; - - const vendorIcon = - OracleVendorIcons[feed.vendor as OracleVendors] ?? OracleVendorIcons[OracleVendors.Unknown]; - - const content = ( -
-
- {fromAsset} - - {toAsset} -
- {vendorIcon ? ( - {feed.vendor - ) : ( - - )} -
- ); - - return ( - - - {content} - - - - ); -} diff --git a/src/components/MarketOracle/ChainlinkFeedTooltip.tsx b/src/components/MarketOracle/ChainlinkFeedTooltip.tsx new file mode 100644 index 00000000..d3b6002c --- /dev/null +++ b/src/components/MarketOracle/ChainlinkFeedTooltip.tsx @@ -0,0 +1,145 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { Address } from 'viem'; +import { Badge } from '@/components/common/Badge'; +import { ChainlinkOracleEntry, getChainlinkFeedUrl } from '@/constants/oracle/chainlink-data'; +import etherscanLogo from '@/imgs/etherscan.png'; +import { getExplorerURL } from '@/utils/external'; +import { PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle'; +import { OracleFeed } from '@/utils/types'; + +type ChainlinkFeedTooltipProps = { + feed: OracleFeed; + chainlinkData?: ChainlinkOracleEntry; + chainId: number; +}; + +export function ChainlinkFeedTooltip({ feed, chainlinkData, chainId }: ChainlinkFeedTooltipProps) { + const baseAsset = feed.pair?.[0] ?? chainlinkData?.baseAsset ?? 'Unknown'; + const quoteAsset = feed.pair?.[1] ?? chainlinkData?.quoteAsset ?? 'Unknown'; + + const vendorIcon = OracleVendorIcons[PriceFeedVendors.Chainlink]; + + // Generate Chainlink feed URL if we have the chainlink data + const chainlinkUrl = chainlinkData + ? getChainlinkFeedUrl(chainId, { + ens: chainlinkData.ens, + contractAddress: chainlinkData.contractAddress, + contractVersion: chainlinkData.contractVersion, + heartbeat: chainlinkData.heartbeat, + multiply: chainlinkData.multiply, + name: chainlinkData.name, + path: chainlinkData.path, + proxyAddress: chainlinkData.proxyAddress, + threshold: chainlinkData.threshold, + valuePrefix: chainlinkData.valuePrefix, + assetName: chainlinkData.assetName, + feedCategory: chainlinkData.feedCategory, + feedType: chainlinkData.feedType, + decimals: chainlinkData.decimals, + docs: { + baseAsset: chainlinkData.baseAsset, + quoteAsset: chainlinkData.quoteAsset, + }, + }) + : ''; + + // Risk tier badge using Badge component + const getRiskTierBadge = (category: string) => { + const variantMap = { + low: 'success' as const, + medium: 'warning' as const, + high: 'danger' as const, + custom: 'primary' as const, + }; + + const variant = variantMap[category as keyof typeof variantMap] || 'primary'; + + return ( + + {category.toUpperCase()} RISK + + ); + }; + + return ( +
+
+ {/* Header with icon and title */} +
+ {vendorIcon && ( +
+ Chainlink +
+ )} +
Chainlink Feed Details
+
+ + {/* Feed pair name with SVR badge if applicable */} +
+
+ {baseAsset} / {quoteAsset} +
+ {chainlinkData?.isSVR && ( + + SVR + + )} +
+ + {/* Chainlink Specific Data */} + {chainlinkData && ( +
+
+ Heartbeat: + {chainlinkData.heartbeat}s +
+
+ Risk Tier: + {getRiskTierBadge(chainlinkData.feedCategory)} +
+
+ Deviation Threshold: + {chainlinkData.threshold.toFixed(1)}% +
+
+ )} + + {/* External Links */} +
+
+ View on: +
+
+ + Etherscan + Etherscan + + {chainlinkUrl && ( + + {vendorIcon && Chainlink} + Chainlink + + )} +
+
+
+
+ ); +} diff --git a/src/components/MarketOracle/CompoundFeedTooltip.tsx b/src/components/MarketOracle/CompoundFeedTooltip.tsx new file mode 100644 index 00000000..dfa36ee0 --- /dev/null +++ b/src/components/MarketOracle/CompoundFeedTooltip.tsx @@ -0,0 +1,188 @@ +import { useMemo } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { Address } from 'viem'; +import { Badge } from '@/components/common/Badge'; +import { + getChainlinkFeedUrl, + getChainlinkOracle, +} from '@/constants/oracle/chainlink-data'; +import { CompoundFeedEntry } from '@/constants/oracle/compound'; +import etherscanLogo from '@/imgs/etherscan.png'; +import { getExplorerURL } from '@/utils/external'; +import { PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle'; +import { OracleFeed } from '@/utils/types'; + +type CompoundFeedTooltipProps = { + feed: OracleFeed; + compoundData: CompoundFeedEntry; + chainId: number; +}; + +export function CompoundFeedTooltip({ feed, compoundData, chainId }: CompoundFeedTooltipProps) { + const baseAsset = compoundData.base; + const quoteAsset = compoundData.quote; + + const compoundLogo = OracleVendorIcons[PriceFeedVendors.Compound]; + const chainlinkLogo = OracleVendorIcons[PriceFeedVendors.Chainlink]; + + // Get the underlying Chainlink feed data + const underlyingChainlinkData = useMemo(() => { + return getChainlinkOracle(chainId, compoundData.underlyingChainlinkFeed as Address); + }, [chainId, compoundData.underlyingChainlinkFeed]); + + // Generate Chainlink feed URL if we have the underlying chainlink data + const chainlinkUrl = underlyingChainlinkData + ? getChainlinkFeedUrl(chainId, { + ens: underlyingChainlinkData.ens, + contractAddress: underlyingChainlinkData.contractAddress, + contractVersion: underlyingChainlinkData.contractVersion, + heartbeat: underlyingChainlinkData.heartbeat, + multiply: underlyingChainlinkData.multiply, + name: underlyingChainlinkData.name, + path: underlyingChainlinkData.path, + proxyAddress: underlyingChainlinkData.proxyAddress, + threshold: underlyingChainlinkData.threshold, + valuePrefix: underlyingChainlinkData.valuePrefix, + assetName: underlyingChainlinkData.assetName, + feedCategory: underlyingChainlinkData.feedCategory, + feedType: underlyingChainlinkData.feedType, + decimals: underlyingChainlinkData.decimals, + docs: { + baseAsset: underlyingChainlinkData.baseAsset, + quoteAsset: underlyingChainlinkData.quoteAsset, + }, + }) + : ''; + + // Risk tier badge using Badge component + const getRiskTierBadge = (category: string) => { + const variantMap = { + low: 'success' as const, + medium: 'warning' as const, + high: 'danger' as const, + custom: 'primary' as const, + }; + + const variant = variantMap[category as keyof typeof variantMap] || 'primary'; + + return ( + + {category.toUpperCase()} RISK + + ); + }; + + return ( +
+
+ {/* Header with icon and title */} +
+ {compoundLogo && ( +
+ Compound +
+ )} +
Compound Feed Details
+
+ + {/* Feed pair name */} +
+
+ {baseAsset} / {quoteAsset} +
+
+ + {/* Compound-specific description */} +
+
+ Compound Wrapper Feed +
+
+ This feed converts {underlyingChainlinkData?.baseAsset ?? 'Unknown'} /{' '} + {underlyingChainlinkData?.quoteAsset ?? 'Unknown'} to {baseAsset} / {quoteAsset} using + Compound's conversion logic. +
+
+ + {/* Underlying Chainlink Data */} + {underlyingChainlinkData && ( +
+
+ {chainlinkLogo && ( + Chainlink + )} + + Underlying Chainlink Feed + +
+
+ Heartbeat: + {underlyingChainlinkData.heartbeat}s +
+
+ Risk Tier: + {getRiskTierBadge(underlyingChainlinkData.feedCategory)} +
+
+ Deviation Threshold: + {underlyingChainlinkData.threshold.toFixed(1)}% +
+
+ )} + + {/* External Links */} +
+
+ View on: +
+
+ + Etherscan + Compound Feed + + + Etherscan + Underlying Feed + + {chainlinkUrl && ( + + {chainlinkLogo && ( + Chainlink + )} + Chainlink + + )} +
+
+
+
+ ); +} diff --git a/src/components/MarketOracle/FeedEntry.tsx b/src/components/MarketOracle/FeedEntry.tsx new file mode 100644 index 00000000..fc27cc79 --- /dev/null +++ b/src/components/MarketOracle/FeedEntry.tsx @@ -0,0 +1,115 @@ +import { useMemo } from 'react'; +import { Tooltip } from '@heroui/react'; +import Image from 'next/image'; +import { IoIosSwap } from 'react-icons/io'; +import { IoHelpCircleOutline } from 'react-icons/io5'; +import { Address } from 'viem'; +import { + detectFeedVendor, + getTruncatedAssetName, + PriceFeedVendors, + OracleVendorIcons, +} from '@/utils/oracle'; +import { OracleFeed } from '@/utils/types'; +import { ChainlinkFeedTooltip } from './ChainlinkFeedTooltip'; +import { CompoundFeedTooltip } from './CompoundFeedTooltip'; +import { GeneralFeedTooltip } from './GeneralFeedTooltip'; +import { UnknownFeedTooltip } from './UnknownFeedTooltip'; + +type FeedEntryProps = { + feed: OracleFeed | null; + chainId: number; +}; + +export function FeedEntry({ feed, chainId }: FeedEntryProps): JSX.Element | null { + // Use centralized feed detection - moved before early return to avoid conditional hook calls + const feedVendorResult = useMemo(() => { + if (!feed?.address) return null; + return detectFeedVendor(feed.address as Address, chainId); + }, [feed?.address, chainId, feed?.pair]); + + if (!feed) return null; + + if (!feedVendorResult) return null; + + + const { vendor, data, assetPair } = feedVendorResult; + const { baseAsset, quoteAsset } = { + baseAsset: getTruncatedAssetName(assetPair.baseAsset), + quoteAsset: getTruncatedAssetName(assetPair.quoteAsset), + }; + + // Don't show asset pair if it's unknown + const showAssetPair = !(assetPair.baseAsset === 'Unknown' && assetPair.quoteAsset === 'Unknown'); + + const vendorIcon = OracleVendorIcons[vendor]; + const isChainlink = vendor === PriceFeedVendors.Chainlink; + const isCompound = vendor === PriceFeedVendors.Compound; + // Type-safe SVR check using discriminated union + const isSVR = vendor === PriceFeedVendors.Chainlink && data?.isSVR; + + const getTooltipContent = () => { + // Use discriminated union for type-safe tooltip selection + switch (vendor) { + case PriceFeedVendors.Chainlink: + return ; + + case PriceFeedVendors.Compound: + return ; + + case PriceFeedVendors.Redstone: + case PriceFeedVendors.PythNetwork: + case PriceFeedVendors.Oval: + case PriceFeedVendors.Lido: + return ; + + case PriceFeedVendors.Unknown: + // For unknown feeds, check if we have general feed data or fallback to unknown + if (data) { + return ; + } + return ; + + default: + return ; + } + }; + + return ( + +
+ {showAssetPair ? ( +
+ {baseAsset} + + {quoteAsset} +
+ ) : ( +
+ Unknown Feed +
+ )} + +
+ {isSVR && ( + + SVR + + )} + + {(isChainlink || isCompound) && vendorIcon ? ( + Oracle + ) : ( + + )} +
+
+
+ ); +} diff --git a/src/components/MarketOracle/GeneralFeedTooltip.tsx b/src/components/MarketOracle/GeneralFeedTooltip.tsx new file mode 100644 index 00000000..8daa7fec --- /dev/null +++ b/src/components/MarketOracle/GeneralFeedTooltip.tsx @@ -0,0 +1,89 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { Address } from 'viem'; +import { GeneralPriceFeed } from '@/constants/oracle/general-feeds'; +import etherscanLogo from '@/imgs/etherscan.png'; +import { getExplorerURL } from '@/utils/external'; +import { PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle'; +import { OracleFeed } from '@/utils/types'; + +type GeneralFeedTooltipProps = { + feed: OracleFeed; + feedData: GeneralPriceFeed; + chainId: number; +}; + +export function GeneralFeedTooltip({ feed, feedData, chainId }: GeneralFeedTooltipProps) { + const [baseAsset, quoteAsset] = feedData.pair; + + const vendorIcon = + OracleVendorIcons[feedData.vendor as PriceFeedVendors] || + OracleVendorIcons[PriceFeedVendors.Unknown]; + + return ( +
+
+ {/* Header with icon and title */} +
+ {vendorIcon && ( +
+ {feedData.vendor} +
+ )} +
Price Feed Details
+
+ + {/* Feed pair name with vendor badge */} +
+
+ {baseAsset} / {quoteAsset} +
+
+ + {/* Feed Details */} +
+
+ Vendor: + {feedData.vendor} +
+
+ + {/* Description */} + {feedData.description && ( +
+
+ Description: +
+
+ {feedData.description} +
+
+ )} + + {/* External Links */} +
+
+ View on: +
+
+ + Etherscan + Etherscan + +
+
+
+
+ ); +} diff --git a/src/components/MarketOracle/MarketOracleFeedInfo.tsx b/src/components/MarketOracle/MarketOracleFeedInfo.tsx new file mode 100644 index 00000000..b03f6184 --- /dev/null +++ b/src/components/MarketOracle/MarketOracleFeedInfo.tsx @@ -0,0 +1,67 @@ +import { OracleFeed } from '@/utils/types'; +import { FeedEntry } from './FeedEntry'; + +type MarketOracleFeedInfoProps = { + baseFeedOne: OracleFeed | null | undefined; + baseFeedTwo: OracleFeed | null | undefined; + quoteFeedOne: OracleFeed | null | undefined; + quoteFeedTwo: OracleFeed | null | undefined; + chainId: number; +}; + +function EmptyFeedSlot() { + return
+ -- +
+} + +export function MarketOracleFeedInfo({ + baseFeedOne, + baseFeedTwo, + quoteFeedOne, + quoteFeedTwo, + chainId, +}: MarketOracleFeedInfoProps): JSX.Element { + const hasAnyFeed = baseFeedOne || baseFeedTwo || quoteFeedOne || quoteFeedTwo; + + if (!hasAnyFeed) { + return ( +
+ No feed routes available +
+ ); + } + + const renderFeed = (feed: OracleFeed | null | undefined) => + feed ? ( +
+ +
+ ) : ( + + ); + + return ( +
+ {(baseFeedOne || baseFeedTwo) && ( +
+ Base: +
+
{renderFeed(baseFeedOne)}
+
{renderFeed(baseFeedTwo)}
+
+
+ )} + + {(quoteFeedOne || quoteFeedTwo) && ( +
+ Quote: +
+
{renderFeed(quoteFeedOne)}
+
{renderFeed(quoteFeedTwo)}
+
+
+ )} +
+ ); +} diff --git a/src/components/MarketOracle/OracleTypeInfo.tsx b/src/components/MarketOracle/OracleTypeInfo.tsx new file mode 100644 index 00000000..d70b3ac6 --- /dev/null +++ b/src/components/MarketOracle/OracleTypeInfo.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link'; +import { FiExternalLink } from 'react-icons/fi'; +import { MarketOracleFeedInfo } from '@/components/MarketOracle'; +import { getExplorerURL } from '@/utils/external'; +import { getOracleType, getOracleTypeDescription, OracleType } from '@/utils/oracle'; +import { MorphoChainlinkOracleData } from '@/utils/types'; + +type OracleTypeInfoProps = { + oracleData: MorphoChainlinkOracleData | null | undefined; + oracleAddress: string; + chainId: number; + showLink?: boolean; + showCustom?: boolean +}; + +export function OracleTypeInfo({ oracleData, oracleAddress, chainId, showLink, showCustom }: OracleTypeInfoProps) { + const oracleType = getOracleType(oracleData, oracleAddress, chainId); + const typeDescription = getOracleTypeDescription(oracleType); + + return ( + <> +
+ Oracle Type: + { + showLink ? ( + ( + {typeDescription} + + ) + ): + ({typeDescription}) + } +
+ + {oracleType === OracleType.Standard ? ( + + ) : showCustom ? ( +
+
{typeDescription}
+
+ This market uses a custom oracle implementation that doesn't follow the standard + feed structure. +
+
+ ) : null} + + ); +} diff --git a/src/components/MarketOracle/UnknownFeedTooltip.tsx b/src/components/MarketOracle/UnknownFeedTooltip.tsx new file mode 100644 index 00000000..fd581e43 --- /dev/null +++ b/src/components/MarketOracle/UnknownFeedTooltip.tsx @@ -0,0 +1,55 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { IoHelpCircleOutline } from 'react-icons/io5'; +import { Address } from 'viem'; +import etherscanLogo from '@/imgs/etherscan.png'; +import { getExplorerURL } from '@/utils/external'; +import { OracleFeed } from '@/utils/types'; + +type UnknownFeedTooltipProps = { + feed: OracleFeed; + chainId: number; +}; + +export function UnknownFeedTooltip({ feed, chainId }: UnknownFeedTooltipProps) { + return ( +
+
+ {/* Header with icon and title */} +
+ +
Unknown Price Feed
+
+ + {/* Description */} +
+ This oracle uses an unrecognized price feed contract. +
+ + {/* External Links */} +
+
+ View contract: +
+
+ + Etherscan + Etherscan + +
+
+
+
+ ); +} diff --git a/src/components/MarketOracle/index.ts b/src/components/MarketOracle/index.ts new file mode 100644 index 00000000..acf67ca1 --- /dev/null +++ b/src/components/MarketOracle/index.ts @@ -0,0 +1,4 @@ +export { FeedEntry } from './FeedEntry'; +export { MarketOracleFeedInfo } from './MarketOracleFeedInfo'; +export { ChainlinkFeedTooltip } from './ChainlinkFeedTooltip'; +export { OracleTypeInfo } from './OracleTypeInfo'; diff --git a/src/components/OracleVendorBadge.tsx b/src/components/OracleVendorBadge.tsx index ad38a9b4..cfbf3e97 100644 --- a/src/components/OracleVendorBadge.tsx +++ b/src/components/OracleVendorBadge.tsx @@ -1,41 +1,62 @@ import React from 'react'; import { Tooltip } from '@heroui/react'; import Image from 'next/image'; -import { IoWarningOutline } from 'react-icons/io5'; -import { OracleVendorIcons, OracleVendors, parseOracleVendors } from '@/utils/oracle'; +import { IoWarningOutline, IoHelpCircleOutline } from 'react-icons/io5'; +import { OracleType, OracleVendorIcons, PriceFeedVendors, getOracleType, parsePriceFeedVendors } from '@/utils/oracle'; import { MorphoChainlinkOracleData } from '@/utils/types'; type OracleVendorBadgeProps = { oracleData: MorphoChainlinkOracleData | null | undefined; + chainId: number; useTooltip?: boolean; showText?: boolean; }; -const renderVendorIcon = (vendor: OracleVendors) => +const renderVendorIcon = (vendor: PriceFeedVendors) => OracleVendorIcons[vendor] ? ( {vendor} ) : ( - + ); +/** + * IoWarningOutline: Unknown Oracles + * IoHelpCircleOutline: For unknown feeds + */ + function OracleVendorBadge({ oracleData, + chainId, showText = false, useTooltip = true, }: OracleVendorBadgeProps) { - const { vendors, isUnknown } = parseOracleVendors(oracleData); + + // check whether it's standard oracle or not. + const isCustom = getOracleType(oracleData) === OracleType.Custom + + const vendorInfo = parsePriceFeedVendors(oracleData, chainId); + const { coreVendors, taggedVendors, hasCompletelyUnknown, hasTaggedUnknown, vendors, hasUnknown } = vendorInfo; const content = (
{showText && ( - {isUnknown ? 'Unknown' : vendors.join(', ')} + {hasUnknown ? 'Unknown' : vendors.join(', ')} )} - {isUnknown ? ( + {isCustom ? ( + ) : hasCompletelyUnknown || hasTaggedUnknown ? ( + // Show core vendor icons plus question mark for any unknown types + <> + {coreVendors.map((vendor, index) => ( + {renderVendorIcon(vendor)} + ))} + + ) : ( - vendors.map((vendor, index) => ( + // Only core vendors, show their icons + coreVendors.map((vendor, index) => ( {renderVendorIcon(vendor)} )) )} @@ -43,22 +64,56 @@ function OracleVendorBadge({ ); if (useTooltip) { - return ( - { + if (isCustom) { + return (
-

- {isUnknown ? 'Unknown Oracle' : 'Oracle Vendors:'} -

-
    - {vendors.map((vendor, index) => ( -
  • - {vendor} -
  • - ))} -
+

Custom Oracle

+

Uses an unrecognized oracle contract.

+ ); + } + + if (hasCompletelyUnknown || hasTaggedUnknown) { + let description = ''; + const parts = []; + + if (coreVendors.length > 0) { + parts.push(`${coreVendors.join(', ')}`); + } + + if (taggedVendors.length > 0) { + parts.push(`${taggedVendors.join(', ')} (third-party)`); } + + if (hasCompletelyUnknown) { + const unknownCount = 1; // Simplified for now + parts.push(`${unknownCount} unrecognized feed${unknownCount > 1 ? 's' : ''}`); + } + + description = `Uses feeds from: ${parts.join(', ')}.`; + + return ( +
+

Standard Oracle

+

{description}

+
+ ); + } + + // All core vendors - clean case + const allVendors = [...coreVendors, ...taggedVendors]; + return ( +
+

Standard Oracle

+

Uses feeds from {allVendors.join(', ')}.

+
+ ); + }; + + return ( + {content} diff --git a/src/components/TooltipContent.tsx b/src/components/TooltipContent.tsx index 6e8a5d7d..869dd2f6 100644 --- a/src/components/TooltipContent.tsx +++ b/src/components/TooltipContent.tsx @@ -13,21 +13,21 @@ export function TooltipContent({ icon, title, detail, className = '' }: TooltipC // Simple tooltip with just an icon and title if (!detail) { return ( -
+
{icon &&
{icon}
} - {title} + {title}
); } // Complex tooltip with additional details return ( -
+
{icon &&
{icon}
}
- {title &&
{title}
} -
{detail}
+ {title &&
{title}
} +
{detail}
diff --git a/src/components/common/MarketDetailsBlock.tsx b/src/components/common/MarketDetailsBlock.tsx index 37dcde08..c2f4636f 100644 --- a/src/components/common/MarketDetailsBlock.tsx +++ b/src/components/common/MarketDetailsBlock.tsx @@ -85,7 +85,7 @@ export function MarketDetailsBlock({ {!isExpanded && (
· - + · {getAPY()}% APY · @@ -116,6 +116,7 @@ export function MarketDetailsBlock({ oracleData={market.oracle?.data} showText useTooltip={false} + chainId={market.morphoBlue.chain.id} /> · {getIRMTitle(market.irmAddress)} diff --git a/src/components/common/MarketInfoBlock.tsx b/src/components/common/MarketInfoBlock.tsx index 7f162585..37dd408d 100644 --- a/src/components/common/MarketInfoBlock.tsx +++ b/src/components/common/MarketInfoBlock.tsx @@ -34,12 +34,12 @@ export function MarketInfoBlock({ market, amount, className }: MarketInfoBlockPr {formatUnits(BigInt(market.lltv), 16)}% LTV
- {amount && amount !== maxUint256 ? ( + {amount !== undefined && amount !== maxUint256 ? ( {formatBalance(amount, market.loanAsset.decimals)} {market.loanAsset.symbol} ) : ( - + )}
@@ -125,12 +125,12 @@ export function MarketInfoBlockCompact({
- {amount && amount !== maxUint256 ? ( + {amount !== undefined && amount !== maxUint256 ? ( {formatBalance(amount, market.loanAsset.decimals)} {market.loanAsset.symbol} ) : ( - + )}
); diff --git a/src/components/layout/header/Navbar.tsx b/src/components/layout/header/Navbar.tsx index 85e7db22..c96edd20 100644 --- a/src/components/layout/header/Navbar.tsx +++ b/src/components/layout/header/Navbar.tsx @@ -102,11 +102,7 @@ export function Navbar() { )} - +