diff --git a/src/components/shared/kleros-tag-badge.tsx b/src/components/shared/kleros-tag-badge.tsx index f5358f0d..ef6dbb9e 100644 --- a/src/components/shared/kleros-tag-badge.tsx +++ b/src/components/shared/kleros-tag-badge.tsx @@ -13,6 +13,7 @@ type KlerosTagBadgeProps = { publicNote?: string; className?: string; labelClassName?: string; + showTooltip?: boolean; }; function KlerosBadgeLogo({ size = 14 }: { size?: number }) { @@ -33,7 +34,22 @@ function KlerosBadgeLogo({ size = 14 }: { size?: number }) { ); } -export function KlerosTagBadge({ label, publicNote, className, labelClassName }: KlerosTagBadgeProps) { +export function KlerosTagBadge({ label, publicNote, className, labelClassName, showTooltip = true }: KlerosTagBadgeProps) { + const badge = ( + + + {label} + + ); + + if (!showTooltip) { + return badge; + } + return ( } > - - - {label} - + {badge} ); } diff --git a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx index 3280da8f..db11c63d 100644 --- a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx +++ b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx @@ -3,7 +3,9 @@ import Link from 'next/link'; import type { Address } from 'viem'; import { useReadContracts } from 'wagmi'; import { chainlinkAggregatorV3Abi } from '@/abis/chainlink-aggregator-v3'; +import { KlerosTagBadge } from '@/components/shared/kleros-tag-badge'; import { Tooltip } from '@/components/ui/tooltip'; +import { formatKlerosAddressTagLabel, type KlerosAddressTag } from '@/data-sources/kleros/address-tags'; import Image from 'next/image'; import { IoIosSwap } from 'react-icons/io'; import { IoHelpCircleOutline } from 'react-icons/io5'; @@ -29,9 +31,10 @@ type FeedEntryProps = { feed: EnrichedFeed | null; chainId: number; feedSnapshotsByAddress?: FeedSnapshotByAddress; + klerosTag?: KlerosAddressTag | null; }; -export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryProps): JSX.Element | null { +export function FeedEntry({ feed, chainId, feedSnapshotsByAddress, klerosTag }: FeedEntryProps): JSX.Element | null { const feedVendorResult = useMemo(() => { return detectFeedVendorFromMetadata(feed); }, [feed]); @@ -84,6 +87,8 @@ export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryPr // Don't show asset pair if it's unknown const showAssetPair = !(assetPair.baseAsset === 'Unknown' && assetPair.quoteAsset === 'Unknown'); + const klerosLabel = formatKlerosAddressTagLabel(klerosTag); + const klerosFallbackLabel = showAssetPair ? undefined : klerosLabel; const vendorIcon = OracleVendorIcons[vendor]; const hasKnownVendorIcon = vendor !== PriceFeedVendors.Unknown && Boolean(vendorIcon); @@ -164,6 +169,7 @@ export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryPr feed={feed} chainId={chainId} feedFreshness={freshness} + klerosTag={klerosTag} /> ); @@ -174,6 +180,7 @@ export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryPr feed={feed} chainId={chainId} feedFreshness={freshness} + klerosTag={klerosTag} /> ); } @@ -182,6 +189,7 @@ export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryPr feed={feed} chainId={chainId} feedFreshness={freshness} + klerosTag={klerosTag} /> ); @@ -191,6 +199,7 @@ export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryPr feed={feed} chainId={chainId} feedFreshness={freshness} + klerosTag={klerosTag} /> ); } @@ -217,7 +226,17 @@ export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryPr ) : (
- Unknown Feed + {klerosFallbackLabel ? ( + + ) : ( + Unknown Feed + )}
)} diff --git a/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx index 1051370f..69376463 100644 --- a/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx @@ -1,6 +1,7 @@ import Image from 'next/image'; import Link from 'next/link'; import type { Address } from 'viem'; +import { formatKlerosAddressTagLabel, type KlerosAddressTag } from '@/data-sources/kleros/address-tags'; import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; @@ -12,14 +13,18 @@ type GeneralFeedTooltipProps = { feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; + klerosTag?: KlerosAddressTag | null; }; -export function GeneralFeedTooltip({ feed, chainId, feedFreshness }: GeneralFeedTooltipProps) { +export function GeneralFeedTooltip({ feed, chainId, feedFreshness, klerosTag }: GeneralFeedTooltipProps) { const baseAsset = feed.pair[0] ?? 'Unknown'; const quoteAsset = feed.pair[1] ?? 'Unknown'; + const showAssetPair = !(baseAsset === 'Unknown' && quoteAsset === 'Unknown'); const vendor = feed.provider ? mapProviderToVendor(feed.provider) : PriceFeedVendors.Unknown; const vendorIcon = OracleVendorIcons[vendor] || OracleVendorIcons[PriceFeedVendors.Unknown]; + const klerosLabel = formatKlerosAddressTagLabel(klerosTag); + const showKlerosFallback = vendor === PriceFeedVendors.Unknown && Boolean(klerosLabel); return (
@@ -35,15 +40,20 @@ export function GeneralFeedTooltip({ feed, chainId, feedFreshness }: GeneralFeed />
)} -
{feed.provider ?? 'Price'} Feed
+
+
{showKlerosFallback ? klerosLabel : `${feed.provider ?? 'Price'} Feed`}
+ {showKlerosFallback &&
Name tag from Kleros Scout
} +
{/* Feed pair name */} -
-
- {baseAsset} / {quoteAsset} + {showAssetPair && ( +
+
+ {baseAsset} / {quoteAsset} +
-
+ )} @@ -75,6 +85,16 @@ export function GeneralFeedTooltip({ feed, chainId, feedFreshness }: GeneralFeed /> Explorer + {showKlerosFallback && klerosTag?.dataOriginLink && ( + + View Tag Source + + )}
diff --git a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx index a7f29dc4..d8211043 100644 --- a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx +++ b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx @@ -1,5 +1,8 @@ 'use client'; +import { useMemo } from 'react'; +import { getKlerosAddressTagKey } from '@/data-sources/kleros/address-tags'; +import { useKlerosAddressTagsQuery } from '@/hooks/queries/useKlerosAddressTagsQuery'; import { useFeedLastUpdatedByChain } from '@/hooks/useFeedLastUpdatedByChain'; import { getStandardOracleDataFromMetadata, useOracleMetadata } from '@/hooks/useOracleMetadata'; import { FeedEntry } from './FeedEntry'; @@ -21,6 +24,15 @@ export function MarketOracleFeedInfo({ chainId, oracleAddress }: MarketOracleFee const baseFeedTwo = oracleData?.baseFeedTwo ?? null; const quoteFeedOne = oracleData?.quoteFeedOne ?? null; const quoteFeedTwo = oracleData?.quoteFeedTwo ?? null; + const feedAddresses = useMemo( + () => + [baseFeedOne?.address, baseFeedTwo?.address, quoteFeedOne?.address, quoteFeedTwo?.address].filter( + (address): address is string => Boolean(address), + ), + [baseFeedOne?.address, baseFeedTwo?.address, quoteFeedOne?.address, quoteFeedTwo?.address], + ); + // Batch Kleros tags for the visible oracle feeds; FeedEntry only renders them as fallback identity for unclassified feeds. + const { data: klerosAddressTags } = useKlerosAddressTagsQuery(chainId, feedAddresses); const hasAnyFeed = baseFeedOne || baseFeedTwo || quoteFeedOne || quoteFeedTwo; const hasAnyVault = baseVault || quoteVault; @@ -46,6 +58,7 @@ export function MarketOracleFeedInfo({ chainId, oracleAddress }: MarketOracleFee feed={baseFeedOne} chainId={chainId} feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosTag={klerosAddressTags?.[getKlerosAddressTagKey(chainId, baseFeedOne.address)]} /> )} {baseFeedTwo && ( @@ -53,6 +66,7 @@ export function MarketOracleFeedInfo({ chainId, oracleAddress }: MarketOracleFee feed={baseFeedTwo} chainId={chainId} feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosTag={klerosAddressTags?.[getKlerosAddressTagKey(chainId, baseFeedTwo.address)]} /> )} @@ -74,6 +88,7 @@ export function MarketOracleFeedInfo({ chainId, oracleAddress }: MarketOracleFee feed={quoteFeedOne} chainId={chainId} feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosTag={klerosAddressTags?.[getKlerosAddressTagKey(chainId, quoteFeedOne.address)]} /> )} {quoteFeedTwo && ( @@ -81,6 +96,7 @@ export function MarketOracleFeedInfo({ chainId, oracleAddress }: MarketOracleFee feed={quoteFeedTwo} chainId={chainId} feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosTag={klerosAddressTags?.[getKlerosAddressTagKey(chainId, quoteFeedTwo.address)]} /> )} diff --git a/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx b/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx index a70be0b7..c369e2a2 100644 --- a/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx +++ b/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx @@ -1,9 +1,13 @@ 'use client'; +import { useMemo } from 'react'; import { useFeedLastUpdatedByChain, type FeedSnapshotByAddress } from '@/hooks/useFeedLastUpdatedByChain'; import { getMetaOracleDataFromMetadata, type OracleOutputData, useOracleMetadata } from '@/hooks/useOracleMetadata'; import { AddressIdentity } from '@/components/shared/address-identity'; +import { KlerosTagBadge } from '@/components/shared/kleros-tag-badge'; +import { formatKlerosAddressTagLabel, getKlerosAddressTagKey, type KlerosAddressTagsByKey } from '@/data-sources/kleros/address-tags'; import { formatOracleDuration } from '@/utils/oracle'; +import { useKlerosAddressTagsQuery } from '@/hooks/queries/useKlerosAddressTagsQuery'; import { FeedEntry } from './FeedEntry'; import { VaultEntry } from './VaultEntry'; @@ -13,16 +17,24 @@ type MetaOracleInfoProps = { variant?: 'summary' | 'detail'; }; +function getOracleFeedAddresses(oracleData: OracleOutputData | null): string[] { + return [oracleData?.baseFeedOne?.address, oracleData?.baseFeedTwo?.address, oracleData?.quoteFeedOne?.address, oracleData?.quoteFeedTwo?.address].filter( + (address): address is string => Boolean(address), + ); +} + function OracleFeedSection({ oracleData, chainId, label, feedSnapshotsByAddress, + klerosAddressTags, }: { oracleData: OracleOutputData | null; chainId: number; label: string; feedSnapshotsByAddress: FeedSnapshotByAddress; + klerosAddressTags?: KlerosAddressTagsByKey; }) { if (!oracleData) return null; @@ -57,6 +69,7 @@ function OracleFeedSection({ feed={enrichedFeed} chainId={chainId} feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosTag={klerosAddressTags?.[getKlerosAddressTagKey(chainId, enrichedFeed.address)]} /> ); })} @@ -73,8 +86,24 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: const { data: feedSnapshotsByAddress } = useFeedLastUpdatedByChain(chainId); const metaData = getMetaOracleDataFromMetadata(oracleMetadataMap, oracleAddress, chainId); + const isPrimaryActive = Boolean(metaData?.currentOracle?.toLowerCase() === metaData?.primaryOracle?.toLowerCase()); + const feedAddresses = useMemo(() => { + if (!metaData) return []; + + const activeOracleData = isPrimaryActive ? metaData.oracleSources.primary : metaData.oracleSources.backup; + const oracleSources = variant === 'detail' ? [metaData.oracleSources.primary, metaData.oracleSources.backup] : [activeOracleData]; + const oracleContractAddresses = variant === 'detail' ? [metaData.primaryOracle, metaData.backupOracle] : []; + + return [...oracleContractAddresses, ...oracleSources.flatMap(getOracleFeedAddresses)]; + }, [isPrimaryActive, metaData, variant]); + // Batch Kleros tags for the meta oracle contracts and feeds currently rendered in this view. + const { data: klerosAddressTags } = useKlerosAddressTagsQuery(chainId, feedAddresses); + if (!metaData) return null; - const isPrimaryActive = metaData.currentOracle?.toLowerCase() === metaData.primaryOracle?.toLowerCase(); + const primaryOracleTag = klerosAddressTags?.[getKlerosAddressTagKey(chainId, metaData.primaryOracle)]; + const primaryOracleLabel = formatKlerosAddressTagLabel(primaryOracleTag); + const backupOracleTag = klerosAddressTags?.[getKlerosAddressTagKey(chainId, metaData.backupOracle)]; + const backupOracleLabel = formatKlerosAddressTagLabel(backupOracleTag); if (variant === 'detail') { const deviationPct = (Number(metaData.deviationThreshold) / 1e18) * 100; @@ -89,6 +118,14 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: address={metaData.primaryOracle} chainId={chainId} /> + {primaryOracleLabel && ( + + + + )} {isPrimaryActive && ( Active @@ -100,6 +137,7 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: chainId={chainId} label="primary" feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosAddressTags={klerosAddressTags} /> @@ -111,6 +149,14 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: address={metaData.backupOracle} chainId={chainId} /> + {backupOracleLabel && ( + + + + )} {!isPrimaryActive && ( Active @@ -122,6 +168,7 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: chainId={chainId} label="backup" feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosAddressTags={klerosAddressTags} /> @@ -154,6 +201,7 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: chainId={chainId} label="current" feedSnapshotsByAddress={feedSnapshotsByAddress} + klerosAddressTags={klerosAddressTags} /> ); } diff --git a/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx index 8014e7b3..9ff9c88b 100644 --- a/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx @@ -2,6 +2,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { IoHelpCircleOutline } from 'react-icons/io5'; import type { Address } from 'viem'; +import { formatKlerosAddressTagLabel, type KlerosAddressTag } from '@/data-sources/kleros/address-tags'; import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; @@ -13,22 +14,32 @@ type UnknownFeedTooltipProps = { feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; + klerosTag?: KlerosAddressTag | null; }; -export function UnknownFeedTooltip({ feed, chainId, feedFreshness }: UnknownFeedTooltipProps) { +export function UnknownFeedTooltip({ feed, chainId, feedFreshness, klerosTag }: UnknownFeedTooltipProps) { + const klerosLabel = formatKlerosAddressTagLabel(klerosTag); + return (
{/* Header with icon and title */} -
+
-
Unknown Price Feed
+
+
{klerosLabel ?? 'Unknown Price Feed'}
+ {klerosLabel &&
Name tag from Kleros Scout
} +
{/* Description */} -
This oracle uses an unrecognized price feed contract.
+
+ {klerosLabel + ? 'Scanner metadata does not classify this feed, so Monarch shows the Kleros tag as a fallback label.' + : 'This oracle uses an unrecognized price feed contract.'} +
@@ -36,7 +47,7 @@ export function UnknownFeedTooltip({ feed, chainId, feedFreshness }: UnknownFeed {/* External Links */}
-
View contract:
+
Links:
Etherscan + {klerosTag?.dataOriginLink && ( + + View Tag Source + + )}