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
+
+ )}