From 6b5d0bcd0f069f6c39918dd9e890b79bf43c083a Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 18 Feb 2026 08:28:38 +0800 Subject: [PATCH 1/4] feat: oracle statelness, price and age --- .env.local.example | 2 +- AGENTS.md | 9 + app/api/oracle-metadata/[chainId]/route.ts | 46 ---- src/abis/chainlink-aggregator-v3.ts | 24 ++ src/components/providers/QueryProvider.tsx | 5 +- .../MarketOracle/ChainlinkFeedTooltip.tsx | 9 +- .../MarketOracle/CompoundFeedTooltip.tsx | 10 +- .../oracle/MarketOracle/FeedEntry.tsx | 44 +++- .../MarketOracle/FeedFreshnessSection.tsx | 64 +++++ .../MarketOracle/GeneralFeedTooltip.tsx | 10 +- .../MarketOracle/MarketOracleFeedInfo.tsx | 6 + .../oracle/MarketOracle/MetaOracleInfo.tsx | 18 +- .../oracle/MarketOracle/PendleFeedTooltip.tsx | 10 +- .../MarketOracle/RedstoneFeedTooltip.tsx | 9 +- .../MarketOracle/UnknownFeedTooltip.tsx | 2 +- src/hooks/useFeedLastUpdatedByChain.ts | 235 ++++++++++++++++++ src/hooks/useOracleMetadata.ts | 15 +- src/utils/oracle.ts | 92 ++++++- 18 files changed, 531 insertions(+), 79 deletions(-) delete mode 100644 app/api/oracle-metadata/[chainId]/route.ts create mode 100644 src/abis/chainlink-aggregator-v3.ts create mode 100644 src/features/markets/components/oracle/MarketOracle/FeedFreshnessSection.tsx create mode 100644 src/hooks/useFeedLastUpdatedByChain.ts diff --git a/.env.local.example b/.env.local.example index 944bab15..adff3a6a 100644 --- a/.env.local.example +++ b/.env.local.example @@ -62,4 +62,4 @@ MONARCH_API_KEY= # ==================== Oracle Metadata ==================== # Base URL for oracle metadata Gist (without trailing slash) # Example: https://gist.githubusercontent.com/username/gist-id/raw -ORACLE_GIST_BASE_URL= +NEXT_PUBLIC_ORACLE_GIST_BASE_URL= diff --git a/AGENTS.md b/AGENTS.md index 22ff40e5..8a2dbfef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,15 @@ Always consult these docs for detailed information: 4. Do not claim repo facts without evidence (no invented counts). 5. Prevent double-capture, noisy heuristics, or duplicate logic. +## Post-Implementation Consolidation (Mandatory) + +Before closing any non-trivial change: + +1. Run one consolidation pass and remove duplicated logic across files (especially repeated UI blocks). +2. Prefer one chokepoint fix for layout constraints (container-level width/spacing) over per-component ad hoc truncation. +3. Re-check first principles against the domain model so behavior applies consistently to all valid entities (not vendor-specific shortcuts). +4. Remove transitional code that was useful during debugging but adds long-term complexity. + --- ## 🛠️ Skills System diff --git a/app/api/oracle-metadata/[chainId]/route.ts b/app/api/oracle-metadata/[chainId]/route.ts deleted file mode 100644 index fbc3f4b0..00000000 --- a/app/api/oracle-metadata/[chainId]/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextResponse } from 'next/server'; -import { reportApiRouteError } from '@/utils/sentry-server'; - -const ORACLE_GIST_BASE_URL = process.env.ORACLE_GIST_BASE_URL; - -export async function GET(request: Request, { params }: { params: Promise<{ chainId: string }> }) { - const { chainId } = await params; - - if (!ORACLE_GIST_BASE_URL) { - return NextResponse.json({ error: 'Oracle metadata source not configured' }, { status: 500 }); - } - - const chainIdNum = Number.parseInt(chainId, 10); - if (Number.isNaN(chainIdNum)) { - return NextResponse.json({ error: 'Invalid chainId' }, { status: 400 }); - } - - try { - const url = `${ORACLE_GIST_BASE_URL}/oracles.${chainId}.json`; - const response = await fetch(url, { - cache: 'no-store', - }); - - if (!response.ok) { - if (response.status === 404) { - return NextResponse.json({ error: `No oracle data for chain ${chainId}` }, { status: 404 }); - } - throw new Error(`Failed to fetch oracle data: ${response.status}`); - } - - const data = await response.json(); - - return NextResponse.json(data); - } catch (error) { - reportApiRouteError(error, { - route: '/api/oracle-metadata/[chainId]', - method: 'GET', - status: 500, - extras: { - chainId, - }, - }); - console.error('Failed to fetch oracle metadata:', error); - return NextResponse.json({ error: 'Failed to fetch oracle metadata' }, { status: 500 }); - } -} diff --git a/src/abis/chainlink-aggregator-v3.ts b/src/abis/chainlink-aggregator-v3.ts new file mode 100644 index 00000000..1395c42f --- /dev/null +++ b/src/abis/chainlink-aggregator-v3.ts @@ -0,0 +1,24 @@ +import type { Abi } from 'viem'; + +export const chainlinkAggregatorV3Abi = [ + { + inputs: [], + name: 'latestRoundData', + outputs: [ + { internalType: 'uint80', name: 'roundId', type: 'uint80' }, + { internalType: 'int256', name: 'answer', type: 'int256' }, + { internalType: 'uint256', name: 'startedAt', type: 'uint256' }, + { internalType: 'uint256', name: 'updatedAt', type: 'uint256' }, + { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, +] as const satisfies Abi; diff --git a/src/components/providers/QueryProvider.tsx b/src/components/providers/QueryProvider.tsx index e74601d7..809f3e96 100644 --- a/src/components/providers/QueryProvider.tsx +++ b/src/components/providers/QueryProvider.tsx @@ -12,6 +12,7 @@ type QueryProviderProps = { const ACTIONABLE_QUERY_ROOT_KEYS = new Set([ 'all-position-snapshots', 'enhanced-positions', + 'feed-last-updated', 'fresh-markets-state', 'historicalSupplierPositions', 'marketData', @@ -37,9 +38,7 @@ const ACTIONABLE_QUERY_ROOT_KEYS = new Set([ 'vault-allocations', ]); -const TRANSACTION_MUTATION_ROOT_KEYS = new Set([ - 'sendTransaction', -]); +const TRANSACTION_MUTATION_ROOT_KEYS = new Set(['sendTransaction']); const getQueryRootKey = (queryKey: QueryKey): string => { const root = queryKey[0]; diff --git a/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx index 56db0551..3b181303 100644 --- a/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx @@ -6,14 +6,16 @@ import { Badge } from '@/components/ui/badge'; import { useGlobalModal } from '@/contexts/GlobalModalContext'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { PriceFeedVendors, OracleVendorIcons, getChainlinkFeedUrl, type FeedData } from '@/utils/oracle'; +import { getChainlinkFeedUrl, OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; import type { OracleFeed } from '@/utils/types'; import { ChainlinkRiskTiersModal } from './ChainlinkRiskTiersModal'; +import { FeedFreshnessSection } from './FeedFreshnessSection'; type ChainlinkFeedTooltipProps = { feed: OracleFeed; feedData?: FeedData | null; chainId: number; + feedFreshness?: FeedFreshnessStatus; }; function getRiskTierBadge(category: string) { @@ -36,7 +38,7 @@ function getRiskTierBadge(category: string) { ); } -export function ChainlinkFeedTooltip({ feed, feedData, chainId }: ChainlinkFeedTooltipProps) { +export function ChainlinkFeedTooltip({ feed, feedData, chainId, feedFreshness }: ChainlinkFeedTooltipProps) { const { toggleModal, closeModal } = useGlobalModal(); const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown'; const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown'; @@ -48,7 +50,7 @@ export function ChainlinkFeedTooltip({ feed, feedData, chainId }: ChainlinkFeedT const hasDetails = feedData?.heartbeat != null || feedData?.tier != null || feedData?.deviationThreshold != null; return ( -
+
{/* Header with icon and title */}
{vendorIcon && ( @@ -113,6 +115,7 @@ export function ChainlinkFeedTooltip({ feed, feedData, chainId }: ChainlinkFeedT )}
)} + {/* External Links */}
diff --git a/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx index 8151e7a2..ab872a0f 100644 --- a/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx @@ -3,23 +3,25 @@ import Link from 'next/link'; import type { Address } from 'viem'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { PriceFeedVendors, OracleVendorIcons, type FeedData } from '@/utils/oracle'; +import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; import type { OracleFeed } from '@/utils/types'; +import { FeedFreshnessSection } from './FeedFreshnessSection'; type CompoundFeedTooltipProps = { feed: OracleFeed; feedData?: FeedData | null; chainId: number; + feedFreshness?: FeedFreshnessStatus; }; -export function CompoundFeedTooltip({ feed, feedData, chainId }: CompoundFeedTooltipProps) { +export function CompoundFeedTooltip({ feed, feedData, chainId, feedFreshness }: CompoundFeedTooltipProps) { const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown'; const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown'; const compoundLogo = OracleVendorIcons[PriceFeedVendors.Compound]; return ( -
+
{/* Header with icon and title */}
{compoundLogo && ( @@ -49,6 +51,8 @@ export function CompoundFeedTooltip({ feed, feedData, chainId }: CompoundFeedToo
)} + + {/* External Links */}
View on:
diff --git a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx index 04a75e8d..f06549ca 100644 --- a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx +++ b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import { IoIosSwap } from 'react-icons/io'; import { IoHelpCircleOutline } from 'react-icons/io5'; import type { Address } from 'viem'; +import type { FeedSnapshotByAddress } from '@/hooks/useFeedLastUpdatedByChain'; import { getFeedFromOracleData, getOracleFromMetadata, @@ -11,7 +12,14 @@ import { type EnrichedFeed, type OracleMetadataRecord, } from '@/hooks/useOracleMetadata'; -import { detectFeedVendor, detectFeedVendorFromMetadata, getTruncatedAssetName, PriceFeedVendors, OracleVendorIcons } from '@/utils/oracle'; +import { + detectFeedVendor, + detectFeedVendorFromMetadata, + getFeedFreshnessStatus, + getTruncatedAssetName, + OracleVendorIcons, + PriceFeedVendors, +} from '@/utils/oracle'; import type { OracleFeed } from '@/utils/types'; import { ChainlinkFeedTooltip } from './ChainlinkFeedTooltip'; import { CompoundFeedTooltip } from './CompoundFeedTooltip'; @@ -26,9 +34,17 @@ type FeedEntryProps = { oracleAddress?: string; oracleMetadataMap?: OracleMetadataRecord; enrichedFeed?: EnrichedFeed; + feedSnapshotsByAddress?: FeedSnapshotByAddress; }; -export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enrichedFeed }: FeedEntryProps): JSX.Element | null { +export function FeedEntry({ + feed, + chainId, + oracleAddress, + oracleMetadataMap, + enrichedFeed, + feedSnapshotsByAddress, +}: FeedEntryProps): JSX.Element | null { // Use metadata-based detection when available, fallback to legacy const feedVendorResult = useMemo(() => { if (!feed?.address) return null; @@ -66,10 +82,13 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr const showAssetPair = !(assetPair.baseAsset === 'Unknown' && assetPair.quoteAsset === 'Unknown'); const vendorIcon = OracleVendorIcons[vendor]; - const isChainlink = vendor === PriceFeedVendors.Chainlink; - const isCompound = vendor === PriceFeedVendors.Compound; - const isRedstone = vendor === PriceFeedVendors.Redstone; - const isPendle = vendor === PriceFeedVendors.Pendle; + const hasKnownVendorIcon = vendor !== PriceFeedVendors.Unknown && Boolean(vendorIcon); + const feedAddressKey = feed.address.toLowerCase(); + const snapshot = feedSnapshotsByAddress?.[feedAddressKey]; + const freshness = getFeedFreshnessStatus(snapshot?.updatedAt ?? null, data?.heartbeat, { + updateKind: snapshot?.updateKind, + normalizedPrice: snapshot?.normalizedPrice, + }); const getTooltipContent = () => { switch (vendor) { @@ -79,6 +98,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr feed={feed} feedData={data} chainId={chainId} + feedFreshness={freshness} /> ); @@ -88,6 +108,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr feed={feed} feedData={data} chainId={chainId} + feedFreshness={freshness} /> ); @@ -97,6 +118,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr feed={feed} feedData={data} chainId={chainId} + feedFreshness={freshness} /> ); @@ -106,6 +128,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr feed={feed} feedData={data} chainId={chainId} + feedFreshness={freshness} /> ); @@ -117,6 +140,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr feed={feed} feedData={data} chainId={chainId} + feedFreshness={freshness} /> ); @@ -127,6 +151,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr feed={feed} feedData={data} chainId={chainId} + feedFreshness={freshness} /> ); } @@ -148,7 +173,10 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr }; return ( - +
{showAssetPair ? (
@@ -166,7 +194,7 @@ export function FeedEntry({ feed, chainId, oracleAddress, oracleMetadataMap, enr )}
- {(isChainlink || isCompound || isRedstone || isPendle) && vendorIcon ? ( + {hasKnownVendorIcon ? ( Oracle + {normalizedPrice != null && ( +
+ Price: + {normalizedPrice} +
+ )} + + {updatedAt != null && ( +
+ Last Updated: + {formatOracleTimestamp(updatedAt)} +
+ )} + + {feedFreshness.ageSeconds != null && ( +
+ Age: + {formatOracleDuration(feedFreshness.ageSeconds)} ago +
+ )} + + {isDerived && ( +
+ Mode: + + DERIVED + +
+ )} + + {feedFreshness.isStale && feedFreshness.staleAfterSeconds != null && ( +
+ Status: + Stale +
+ )} +
+ ); +} diff --git a/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx index e8b93645..314db6b1 100644 --- a/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx @@ -3,17 +3,19 @@ import Link from 'next/link'; import type { Address } from 'viem'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { PriceFeedVendors, OracleVendorIcons, mapProviderToVendor, type FeedData } from '@/utils/oracle'; +import { mapProviderToVendor, OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; import type { OracleFeed } from '@/utils/types'; import type { OracleFeedProvider } from '@/hooks/useOracleMetadata'; +import { FeedFreshnessSection } from './FeedFreshnessSection'; type GeneralFeedTooltipProps = { feed: OracleFeed; feedData?: FeedData | null; chainId: number; + feedFreshness?: FeedFreshnessStatus; }; -export function GeneralFeedTooltip({ feed, feedData, chainId }: GeneralFeedTooltipProps) { +export function GeneralFeedTooltip({ feed, feedData, chainId, feedFreshness }: GeneralFeedTooltipProps) { const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown'; const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown'; @@ -21,7 +23,7 @@ export function GeneralFeedTooltip({ feed, feedData, chainId }: GeneralFeedToolt const vendorIcon = OracleVendorIcons[vendor] || OracleVendorIcons[PriceFeedVendors.Unknown]; return ( -
+
{/* Header with icon and title */}
{vendorIcon && ( @@ -51,6 +53,8 @@ export function GeneralFeedTooltip({ feed, feedData, chainId }: GeneralFeedToolt
)} + + {/* External Links */}
View on:
diff --git a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx index 60d1747c..2f858fd1 100644 --- a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx +++ b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useFeedLastUpdatedByChain } from '@/hooks/useFeedLastUpdatedByChain'; import { useOracleMetadata, getOracleFromMetadata, isMetaOracleData } from '@/hooks/useOracleMetadata'; import type { OracleFeed } from '@/utils/types'; import { FeedEntry } from './FeedEntry'; @@ -23,6 +24,7 @@ export function MarketOracleFeedInfo({ oracleAddress, }: MarketOracleFeedInfoProps): JSX.Element { const { data: oracleMetadataMap } = useOracleMetadata(chainId); + const { data: feedSnapshotsByAddress } = useFeedLastUpdatedByChain(chainId); const oracleMetadata = oracleMetadataMap && oracleAddress ? getOracleFromMetadata(oracleMetadataMap, oracleAddress) : undefined; const oracleData = oracleMetadata?.data && !isMetaOracleData(oracleMetadata.data) ? oracleMetadata.data : undefined; @@ -54,6 +56,7 @@ export function MarketOracleFeedInfo({ chainId={chainId} oracleAddress={oracleAddress} oracleMetadataMap={oracleMetadataMap} + feedSnapshotsByAddress={feedSnapshotsByAddress} /> )} {baseFeedTwo && ( @@ -62,6 +65,7 @@ export function MarketOracleFeedInfo({ chainId={chainId} oracleAddress={oracleAddress} oracleMetadataMap={oracleMetadataMap} + feedSnapshotsByAddress={feedSnapshotsByAddress} /> )}
@@ -84,6 +88,7 @@ export function MarketOracleFeedInfo({ chainId={chainId} oracleAddress={oracleAddress} oracleMetadataMap={oracleMetadataMap} + feedSnapshotsByAddress={feedSnapshotsByAddress} /> )} {quoteFeedTwo && ( @@ -92,6 +97,7 @@ export function MarketOracleFeedInfo({ chainId={chainId} oracleAddress={oracleAddress} oracleMetadataMap={oracleMetadataMap} + feedSnapshotsByAddress={feedSnapshotsByAddress} /> )}
diff --git a/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx b/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx index 733a64fc..3aaa3f70 100644 --- a/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx +++ b/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useFeedLastUpdatedByChain, type FeedSnapshotByAddress } from '@/hooks/useFeedLastUpdatedByChain'; import { useOracleMetadata, getOracleFromMetadata, isMetaOracleData, type OracleOutputData } from '@/hooks/useOracleMetadata'; import { AddressIdentity } from '@/components/shared/address-identity'; import type { OracleFeed } from '@/utils/types'; @@ -13,7 +14,17 @@ type MetaOracleInfoProps = { variant?: 'summary' | 'detail'; }; -function OracleFeedSection({ oracleData, chainId, label }: { oracleData: OracleOutputData | null; chainId: number; label: string }) { +function OracleFeedSection({ + oracleData, + chainId, + label, + feedSnapshotsByAddress, +}: { + oracleData: OracleOutputData | null; + chainId: number; + label: string; + feedSnapshotsByAddress: FeedSnapshotByAddress; +}) { if (!oracleData) return null; const feedGroups = [ @@ -53,6 +64,7 @@ function OracleFeedSection({ oracleData, chainId, label }: { oracleData: OracleO feed={oracleFeed} chainId={chainId} enrichedFeed={enrichedFeed} + feedSnapshotsByAddress={feedSnapshotsByAddress} /> ); })} @@ -66,6 +78,7 @@ function OracleFeedSection({ oracleData, chainId, label }: { oracleData: OracleO export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: MetaOracleInfoProps) { const { data: oracleMetadataMap } = useOracleMetadata(chainId); + const { data: feedSnapshotsByAddress } = useFeedLastUpdatedByChain(chainId); const oracleMetadata = getOracleFromMetadata(oracleMetadataMap, oracleAddress); if (!oracleMetadata?.data || !isMetaOracleData(oracleMetadata.data)) return null; @@ -96,6 +109,7 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: oracleData={metaData.oracleSources.primary} chainId={chainId} label="primary" + feedSnapshotsByAddress={feedSnapshotsByAddress} />
@@ -117,6 +131,7 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: oracleData={metaData.oracleSources.backup} chainId={chainId} label="backup" + feedSnapshotsByAddress={feedSnapshotsByAddress} />
@@ -148,6 +163,7 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: oracleData={currentOracleData} chainId={chainId} label="current" + feedSnapshotsByAddress={feedSnapshotsByAddress} /> ); } diff --git a/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx index dafce1b0..b1450816 100644 --- a/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx @@ -4,13 +4,15 @@ import type { Address } from 'viem'; import { formatUnits } from 'viem'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { PriceFeedVendors, OracleVendorIcons, type FeedData } from '@/utils/oracle'; +import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; import type { OracleFeed } from '@/utils/types'; +import { FeedFreshnessSection } from './FeedFreshnessSection'; type PendleFeedTooltipProps = { feed: OracleFeed; feedData?: FeedData | null; chainId: number; + feedFreshness?: FeedFreshnessStatus; }; function formatDiscountPerYear(raw: string): string { @@ -20,7 +22,7 @@ function formatDiscountPerYear(raw: string): string { return `${percent.toFixed(2)}%`; } -export function PendleFeedTooltip({ feed, feedData, chainId }: PendleFeedTooltipProps) { +export function PendleFeedTooltip({ feed, feedData, chainId, feedFreshness }: PendleFeedTooltipProps) { const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown'; const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown'; const pendleFeedKind = feedData?.pendleFeedKind; @@ -30,7 +32,7 @@ export function PendleFeedTooltip({ feed, feedData, chainId }: PendleFeedTooltip const vendorIcon = OracleVendorIcons[PriceFeedVendors.Pendle]; return ( -
+
{/* Header with icon and title */}
{vendorIcon && ( @@ -71,6 +73,8 @@ export function PendleFeedTooltip({ feed, feedData, chainId }: PendleFeedTooltip
)} + + {/* External Links */}
View on:
diff --git a/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx index e552170d..b9949a87 100644 --- a/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx @@ -5,17 +5,19 @@ import type { Address } from 'viem'; import { useGlobalModal } from '@/contexts/GlobalModalContext'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { PriceFeedVendors, OracleVendorIcons, type FeedData } from '@/utils/oracle'; +import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; import type { OracleFeed } from '@/utils/types'; +import { FeedFreshnessSection } from './FeedFreshnessSection'; import { RedstoneTypesModal } from './RedstoneTypesModal'; type RedstoneFeedTooltipProps = { feed: OracleFeed; feedData?: FeedData | null; chainId: number; + feedFreshness?: FeedFreshnessStatus; }; -export function RedstoneFeedTooltip({ feed, feedData, chainId }: RedstoneFeedTooltipProps) { +export function RedstoneFeedTooltip({ feed, feedData, chainId, feedFreshness }: RedstoneFeedTooltipProps) { const { toggleModal, closeModal } = useGlobalModal(); const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown'; const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown'; @@ -25,7 +27,7 @@ export function RedstoneFeedTooltip({ feed, feedData, chainId }: RedstoneFeedToo const hasDetails = feedData?.feedType != null || feedData?.heartbeat != null || feedData?.deviationThreshold != null; return ( -
+
{/* Header with icon and title */}
{vendorIcon && ( @@ -90,6 +92,7 @@ export function RedstoneFeedTooltip({ feed, feedData, chainId }: RedstoneFeedToo )}
)} + {/* External Links */}
diff --git a/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx index 10bd67cb..e00b542b 100644 --- a/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx @@ -13,7 +13,7 @@ type UnknownFeedTooltipProps = { export function UnknownFeedTooltip({ feed, chainId }: UnknownFeedTooltipProps) { return ( -
+
{/* Header with icon and title */}
; +export type FeedLastUpdatedByAddress = FeedSnapshotByAddress; + +function addNormalizedAddress(feedSet: Set, address: string | null | undefined) { + if (!address) return; + const normalized = address.toLowerCase(); + if (normalized === zeroAddress) return; + feedSet.add(normalized); +} + +function isDerivedCandidateFeed(feed: EnrichedFeed): boolean { + if (feed.pendleFeedKind || feed.pendleFeedSubtype) return true; + const provider = feed.provider?.toLowerCase() ?? ''; + return provider.includes('pendle'); +} + +function addFeedAddress(feedSet: Set, hintByAddress: Record, feed: EnrichedFeed | null) { + if (!feed?.address) return; + const normalized = feed.address.toLowerCase(); + addNormalizedAddress(feedSet, normalized); + + const previous = hintByAddress[normalized]; + const nextDerivedCandidate = previous?.derivedCandidate === true || isDerivedCandidateFeed(feed); + + hintByAddress[normalized] = { + derivedCandidate: nextDerivedCandidate, + }; +} + +function addStandardOracleFeeds( + feedSet: Set, + hintByAddress: Record, + oracleData: OracleOutputData | null, +) { + if (!oracleData) return; + + addFeedAddress(feedSet, hintByAddress, oracleData.baseFeedOne); + addFeedAddress(feedSet, hintByAddress, oracleData.baseFeedTwo); + addFeedAddress(feedSet, hintByAddress, oracleData.quoteFeedOne); + addFeedAddress(feedSet, hintByAddress, oracleData.quoteFeedTwo); +} + +function getFeedDataFromMetadata(metadataRecord: OracleMetadataRecord | undefined): { + addresses: string[]; + hintByAddress: Record; +} { + if (!metadataRecord) { + return { + addresses: [], + hintByAddress: {}, + }; + } + + const feedSet = new Set(); + const hintByAddress: Record = {}; + + for (const oracle of Object.values(metadataRecord)) { + if (!oracle?.data) continue; + + if (isMetaOracleData(oracle.data)) { + addStandardOracleFeeds(feedSet, hintByAddress, oracle.data.oracleSources.primary); + addStandardOracleFeeds(feedSet, hintByAddress, oracle.data.oracleSources.backup); + continue; + } + + addStandardOracleFeeds(feedSet, hintByAddress, oracle.data); + } + + return { + addresses: Array.from(feedSet).sort(), + hintByAddress, + }; +} + +function createAddressFingerprint(addresses: string[]): string { + if (addresses.length === 0) return '0'; + + let hash = 2166136261; + for (const address of addresses) { + for (let index = 0; index < address.length; index += 1) { + hash ^= address.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + } + + return `${addresses.length}:${(hash >>> 0).toString(16)}`; +} + +function createHintFingerprint(hintByAddress: Record): string { + const entries = Object.entries(hintByAddress).sort(([left], [right]) => left.localeCompare(right)); + if (entries.length === 0) return '0'; + + let hash = 2166136261; + for (const [address, hints] of entries) { + const encoded = `${address}:${hints.derivedCandidate ? '1' : '0'}`; + for (let index = 0; index < encoded.length; index += 1) { + hash ^= encoded.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + } + + return `${entries.length}:${(hash >>> 0).toString(16)}`; +} + +function chunkAddresses(addresses: string[]): string[][] { + const chunks: string[][] = []; + + for (let index = 0; index < addresses.length; index += MAX_MULTICALL_FEEDS_PER_BATCH) { + chunks.push(addresses.slice(index, index + MAX_MULTICALL_FEEDS_PER_BATCH)); + } + + return chunks; +} + +export function useFeedLastUpdatedByChain(chainId: SupportedNetworks | number | undefined) { + const publicClient = usePublicClient({ chainId }); + const { data: oracleMetadataMap } = useOracleMetadata(chainId); + + const { addresses: feedAddresses, hintByAddress } = useMemo(() => getFeedDataFromMetadata(oracleMetadataMap), [oracleMetadataMap]); + const addressFingerprint = useMemo(() => createAddressFingerprint(feedAddresses), [feedAddresses]); + const hintFingerprint = useMemo(() => createHintFingerprint(hintByAddress), [hintByAddress]); + + const query = useQuery({ + queryKey: ['feed-snapshot', chainId, addressFingerprint, hintFingerprint], + enabled: Boolean(chainId && publicClient && feedAddresses.length > 0), + staleTime: FEED_REFRESH_INTERVAL_MS, + refetchInterval: FEED_REFRESH_INTERVAL_MS, + refetchOnWindowFocus: false, + queryFn: async (): Promise => { + if (!publicClient) return {}; + + const snapshotByAddress: FeedSnapshotByAddress = {}; + const addressChunks = chunkAddresses(feedAddresses); + const blockNumber = await publicClient.getBlockNumber(); + const block = await publicClient.getBlock({ blockNumber }); + const queryBlockTimestamp = Number(block.timestamp); + + for (const addressChunk of addressChunks) { + const latestRoundContracts = addressChunk.map((feedAddress) => ({ + address: feedAddress as Address, + abi: chainlinkAggregatorV3Abi, + functionName: 'latestRoundData' as const, + })); + + const decimalsContracts = addressChunk.map((feedAddress) => ({ + address: feedAddress as Address, + abi: chainlinkAggregatorV3Abi, + functionName: 'decimals' as const, + })); + + const [roundResults, decimalsResults] = await Promise.all([ + publicClient.multicall({ + contracts: latestRoundContracts, + allowFailure: true, + blockNumber, + }), + publicClient.multicall({ + contracts: decimalsContracts, + allowFailure: true, + blockNumber, + }), + ]); + + for (let resultIndex = 0; resultIndex < roundResults.length; resultIndex += 1) { + const result = roundResults[resultIndex]; + const feedAddress = addressChunk[resultIndex]; + if (!result || !feedAddress || result.status !== 'success') continue; + + const [, answer, , updatedAt] = result.result as readonly [bigint, bigint, bigint, bigint, bigint]; + const decimalsResult = decimalsResults[resultIndex]; + const decimals = + decimalsResult?.status === 'success' && Number.isFinite(Number(decimalsResult.result)) + ? Number(decimalsResult.result) + : DEFAULT_FEED_DECIMALS; + + const updatedAtSeconds = updatedAt > 0n ? Number(updatedAt) : null; + const normalizedPrice = formatOraclePrice(answer, decimals); + const isDerivedCandidate = hintByAddress[feedAddress]?.derivedCandidate === true; + const updateKind: FeedUpdateKind = + isDerivedCandidate && updatedAtSeconds != null && updatedAtSeconds === queryBlockTimestamp ? 'derived' : 'reported'; + + snapshotByAddress[feedAddress] = { + updatedAt: updatedAtSeconds, + answerRaw: answer, + decimals, + normalizedPrice, + queryBlockTimestamp, + updateKind, + }; + } + } + + return snapshotByAddress; + }, + }); + + return { + data: query.data ?? {}, + isLoading: query.isLoading, + isFetching: query.isFetching, + error: query.error, + }; +} diff --git a/src/hooks/useOracleMetadata.ts b/src/hooks/useOracleMetadata.ts index 558636d1..aef00076 100644 --- a/src/hooks/useOracleMetadata.ts +++ b/src/hooks/useOracleMetadata.ts @@ -12,7 +12,7 @@ import { ALL_SUPPORTED_NETWORKS, type SupportedNetworks } from '@/utils/networks * Data flow: * 1. Oracles scanner fetches from provider APIs (Chainlink, Redstone, etc.) * 2. Scanner publishes enriched data to GitHub Gist - * 3. This hook fetches from /api/oracle-metadata/{chainId} (proxies Gist) + * 3. This hook fetches directly from the centralized Gist * 4. Components use getOracleFromMetadata() + getFeedFromOracleData() to access data */ @@ -99,12 +99,21 @@ export type OracleMetadataRecord = Record; // Keep Map type for backward compatibility in function signatures export type OracleMetadataMap = Map; +const ORACLE_GIST_BASE_URL = process.env.NEXT_PUBLIC_ORACLE_GIST_BASE_URL?.replace(/\/+$/, ''); + /** - * Fetch oracle metadata from internal API route + * Fetch oracle metadata directly from the centralized Gist. */ async function fetchOracleMetadata(chainId: number): Promise { + if (!ORACLE_GIST_BASE_URL) { + console.warn('[oracle-metadata] NEXT_PUBLIC_ORACLE_GIST_BASE_URL is not configured'); + return null; + } + try { - const response = await fetch(`/api/oracle-metadata/${chainId}`); + const response = await fetch(`${ORACLE_GIST_BASE_URL}/oracles.${chainId}.json`, { + cache: 'no-store', + }); if (!response.ok) { if (response.status === 404) { diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts index 84309c68..b5c38537 100644 --- a/src/utils/oracle.ts +++ b/src/utils/oracle.ts @@ -14,7 +14,7 @@ * https://github.com/monarch-xyz/oracles/blob/master/docs/TYPES.md */ -import { zeroAddress, type Address } from 'viem'; +import { formatUnits, zeroAddress, type Address } from 'viem'; import { getFeedFromOracleData, getOracleFromMetadata, @@ -652,6 +652,96 @@ export function formatOracleDuration(seconds: number): string { return `${Math.floor(seconds / 86400)}d`; } +export type FeedFreshnessStatus = { + updatedAt: number | null; + ageSeconds: number | null; + staleAfterSeconds: number | null; + isStale: boolean; + updateKind: FeedUpdateKind; + normalizedPrice: string | null; +}; + +export type FeedUpdateKind = 'reported' | 'derived'; + +type FeedFreshnessOptions = { + updateKind?: FeedUpdateKind; + normalizedPrice?: string | null; +}; + +/** + * Determine if a feed is stale. + * If no heartbeat is available, we still expose age but avoid stale classification. + */ +export function getFeedFreshnessStatus( + updatedAt: number | null | undefined, + heartbeatSeconds: number | null | undefined, + options?: FeedFreshnessOptions, +): FeedFreshnessStatus { + const updateKind = options?.updateKind ?? 'reported'; + const normalizedPrice = options?.normalizedPrice ?? null; + + if (!updatedAt || updatedAt <= 0) { + return { + updatedAt: null, + ageSeconds: null, + staleAfterSeconds: null, + isStale: false, + updateKind, + normalizedPrice, + }; + } + + const nowSeconds = Math.floor(Date.now() / 1000); + const ageSeconds = Math.max(0, nowSeconds - updatedAt); + const staleAfterSeconds = heartbeatSeconds && heartbeatSeconds > 0 ? heartbeatSeconds : null; + + return { + updatedAt, + ageSeconds, + staleAfterSeconds, + isStale: staleAfterSeconds != null ? ageSeconds > staleAfterSeconds : false, + updateKind, + normalizedPrice, + }; +} + +/** + * Format unix timestamp (seconds) into local date/time. + */ +export function formatOracleTimestamp(seconds: number): string { + return new Date(seconds * 1000).toLocaleString(); +} + +/** + * Compact timestamp for tight UI surfaces: "HH:MM MM/DD" + */ +export function formatOracleTimestampCompact(seconds: number): string { + const date = new Date(seconds * 1000); + const hours = `${date.getHours()}`.padStart(2, '0'); + const minutes = `${date.getMinutes()}`.padStart(2, '0'); + const month = `${date.getMonth() + 1}`.padStart(2, '0'); + const day = `${date.getDate()}`.padStart(2, '0'); + + return `${hours}:${minutes} ${month}/${day}`; +} + +/** + * Format raw feed answer using feed decimals into a compact plain decimal value. + */ +export function formatOraclePrice(answerRaw: bigint, decimals: number, maxFractionDigits = 8): string { + const safeDecimals = Number.isFinite(decimals) ? Math.max(0, Math.min(36, Math.floor(decimals))) : 8; + const raw = formatUnits(answerRaw, safeDecimals); + + if (!raw.includes('.')) return raw; + + const [integerPart, fractionPart = ''] = raw.split('.'); + const trimmedFraction = fractionPart.replace(/0+$/, ''); + if (!trimmedFraction) return integerPart; + + const cappedFraction = trimmedFraction.slice(0, maxFractionDigits).replace(/0+$/, ''); + return cappedFraction ? `${integerPart}.${cappedFraction}` : integerPart; +} + /** * Helper function to get truncated asset names (max 5 chars) */ From 4542a5d66b66d7c66775e6a906b80b6ecfd1340e Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 18 Feb 2026 08:44:59 +0800 Subject: [PATCH 2/4] chore: cleanup --- src/components/providers/QueryProvider.tsx | 2 +- src/hooks/useFeedLastUpdatedByChain.ts | 81 +++++++++------------- 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/src/components/providers/QueryProvider.tsx b/src/components/providers/QueryProvider.tsx index 809f3e96..b9d04222 100644 --- a/src/components/providers/QueryProvider.tsx +++ b/src/components/providers/QueryProvider.tsx @@ -12,7 +12,7 @@ type QueryProviderProps = { const ACTIONABLE_QUERY_ROOT_KEYS = new Set([ 'all-position-snapshots', 'enhanced-positions', - 'feed-last-updated', + 'feed-snapshot', 'fresh-markets-state', 'historicalSupplierPositions', 'marketData', diff --git a/src/hooks/useFeedLastUpdatedByChain.ts b/src/hooks/useFeedLastUpdatedByChain.ts index e7dfc3be..81188ee2 100644 --- a/src/hooks/useFeedLastUpdatedByChain.ts +++ b/src/hooks/useFeedLastUpdatedByChain.ts @@ -3,7 +3,10 @@ import { useQuery } from '@tanstack/react-query'; import { type Address, zeroAddress } from 'viem'; import { usePublicClient } from 'wagmi'; import { chainlinkAggregatorV3Abi } from '@/abis/chainlink-aggregator-v3'; -import { formatOraclePrice, type FeedUpdateKind } from '@/utils/oracle'; +import { + formatOraclePrice, + type FeedUpdateKind, +} from '@/utils/oracle'; import { isMetaOracleData, useOracleMetadata, @@ -23,15 +26,16 @@ type FeedSemanticHints = { export type FeedSnapshot = { updatedAt: number | null; - answerRaw: bigint | null; - decimals: number | null; normalizedPrice: string | null; - queryBlockTimestamp: number | null; updateKind: FeedUpdateKind; }; export type FeedSnapshotByAddress = Record; -export type FeedLastUpdatedByAddress = FeedSnapshotByAddress; + +type FeedMetadataSnapshot = { + addresses: string[]; + hintByAddress: Record; +}; function addNormalizedAddress(feedSet: Set, address: string | null | undefined) { if (!address) return; @@ -48,22 +52,17 @@ function isDerivedCandidateFeed(feed: EnrichedFeed): boolean { function addFeedAddress(feedSet: Set, hintByAddress: Record, feed: EnrichedFeed | null) { if (!feed?.address) return; - const normalized = feed.address.toLowerCase(); - addNormalizedAddress(feedSet, normalized); - const previous = hintByAddress[normalized]; - const nextDerivedCandidate = previous?.derivedCandidate === true || isDerivedCandidateFeed(feed); + const normalizedAddress = feed.address.toLowerCase(); + addNormalizedAddress(feedSet, normalizedAddress); - hintByAddress[normalized] = { - derivedCandidate: nextDerivedCandidate, + const previousHints = hintByAddress[normalizedAddress]; + hintByAddress[normalizedAddress] = { + derivedCandidate: previousHints?.derivedCandidate === true || isDerivedCandidateFeed(feed), }; } -function addStandardOracleFeeds( - feedSet: Set, - hintByAddress: Record, - oracleData: OracleOutputData | null, -) { +function addStandardOracleFeeds(feedSet: Set, hintByAddress: Record, oracleData: OracleOutputData | null) { if (!oracleData) return; addFeedAddress(feedSet, hintByAddress, oracleData.baseFeedOne); @@ -72,10 +71,7 @@ function addStandardOracleFeeds( addFeedAddress(feedSet, hintByAddress, oracleData.quoteFeedTwo); } -function getFeedDataFromMetadata(metadataRecord: OracleMetadataRecord | undefined): { - addresses: string[]; - hintByAddress: Record; -} { +function getFeedMetadataSnapshot(metadataRecord: OracleMetadataRecord | undefined): FeedMetadataSnapshot { if (!metadataRecord) { return { addresses: [], @@ -104,34 +100,26 @@ function getFeedDataFromMetadata(metadataRecord: OracleMetadataRecord | undefine }; } -function createAddressFingerprint(addresses: string[]): string { - if (addresses.length === 0) return '0'; +function createFingerprint(values: readonly string[]): string { + if (values.length === 0) return '0'; let hash = 2166136261; - for (const address of addresses) { - for (let index = 0; index < address.length; index += 1) { - hash ^= address.charCodeAt(index); + for (const value of values) { + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16777619); } } - return `${addresses.length}:${(hash >>> 0).toString(16)}`; + return `${values.length}:${(hash >>> 0).toString(16)}`; } -function createHintFingerprint(hintByAddress: Record): string { - const entries = Object.entries(hintByAddress).sort(([left], [right]) => left.localeCompare(right)); - if (entries.length === 0) return '0'; - - let hash = 2166136261; - for (const [address, hints] of entries) { - const encoded = `${address}:${hints.derivedCandidate ? '1' : '0'}`; - for (let index = 0; index < encoded.length; index += 1) { - hash ^= encoded.charCodeAt(index); - hash = Math.imul(hash, 16777619); - } - } +function createHintsFingerprint(hintByAddress: Record): string { + const encodedHints = Object.entries(hintByAddress) + .sort(([leftAddress], [rightAddress]) => leftAddress.localeCompare(rightAddress)) + .map(([address, hints]) => `${address}:${hints.derivedCandidate ? '1' : '0'}`); - return `${entries.length}:${(hash >>> 0).toString(16)}`; + return createFingerprint(encodedHints); } function chunkAddresses(addresses: string[]): string[][] { @@ -148,9 +136,9 @@ export function useFeedLastUpdatedByChain(chainId: SupportedNetworks | number | const publicClient = usePublicClient({ chainId }); const { data: oracleMetadataMap } = useOracleMetadata(chainId); - const { addresses: feedAddresses, hintByAddress } = useMemo(() => getFeedDataFromMetadata(oracleMetadataMap), [oracleMetadataMap]); - const addressFingerprint = useMemo(() => createAddressFingerprint(feedAddresses), [feedAddresses]); - const hintFingerprint = useMemo(() => createHintFingerprint(hintByAddress), [hintByAddress]); + const { addresses: feedAddresses, hintByAddress } = useMemo(() => getFeedMetadataSnapshot(oracleMetadataMap), [oracleMetadataMap]); + const addressFingerprint = useMemo(() => createFingerprint(feedAddresses), [feedAddresses]); + const hintFingerprint = useMemo(() => createHintsFingerprint(hintByAddress), [hintByAddress]); const query = useQuery({ queryKey: ['feed-snapshot', chainId, addressFingerprint, hintFingerprint], @@ -194,11 +182,11 @@ export function useFeedLastUpdatedByChain(chainId: SupportedNetworks | number | ]); for (let resultIndex = 0; resultIndex < roundResults.length; resultIndex += 1) { - const result = roundResults[resultIndex]; + const roundResult = roundResults[resultIndex]; const feedAddress = addressChunk[resultIndex]; - if (!result || !feedAddress || result.status !== 'success') continue; + if (!roundResult || !feedAddress || roundResult.status !== 'success') continue; - const [, answer, , updatedAt] = result.result as readonly [bigint, bigint, bigint, bigint, bigint]; + const [, answer, , updatedAt] = roundResult.result as readonly [bigint, bigint, bigint, bigint, bigint]; const decimalsResult = decimalsResults[resultIndex]; const decimals = decimalsResult?.status === 'success' && Number.isFinite(Number(decimalsResult.result)) @@ -213,10 +201,7 @@ export function useFeedLastUpdatedByChain(chainId: SupportedNetworks | number | snapshotByAddress[feedAddress] = { updatedAt: updatedAtSeconds, - answerRaw: answer, - decimals, normalizedPrice, - queryBlockTimestamp, updateKind, }; } From d15c905ba90d452a88970ede95c6479cad919702 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 18 Feb 2026 08:45:17 +0800 Subject: [PATCH 3/4] chore: lint --- src/hooks/useFeedLastUpdatedByChain.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hooks/useFeedLastUpdatedByChain.ts b/src/hooks/useFeedLastUpdatedByChain.ts index 81188ee2..5fa4a1fe 100644 --- a/src/hooks/useFeedLastUpdatedByChain.ts +++ b/src/hooks/useFeedLastUpdatedByChain.ts @@ -3,10 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { type Address, zeroAddress } from 'viem'; import { usePublicClient } from 'wagmi'; import { chainlinkAggregatorV3Abi } from '@/abis/chainlink-aggregator-v3'; -import { - formatOraclePrice, - type FeedUpdateKind, -} from '@/utils/oracle'; +import { formatOraclePrice, type FeedUpdateKind } from '@/utils/oracle'; import { isMetaOracleData, useOracleMetadata, @@ -62,7 +59,11 @@ function addFeedAddress(feedSet: Set, hintByAddress: Record, hintByAddress: Record, oracleData: OracleOutputData | null) { +function addStandardOracleFeeds( + feedSet: Set, + hintByAddress: Record, + oracleData: OracleOutputData | null, +) { if (!oracleData) return; addFeedAddress(feedSet, hintByAddress, oracleData.baseFeedOne); From 9aed16204cbc0543c20150c27c229bdf70386d09 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 18 Feb 2026 09:00:23 +0800 Subject: [PATCH 4/4] chore: review fixes --- .../oracle/MarketOracle/FeedFreshnessSection.tsx | 2 +- src/hooks/useOracleMetadata.ts | 2 +- src/utils/oracle.ts | 15 +++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/features/markets/components/oracle/MarketOracle/FeedFreshnessSection.tsx b/src/features/markets/components/oracle/MarketOracle/FeedFreshnessSection.tsx index 290ee86f..e0440000 100644 --- a/src/features/markets/components/oracle/MarketOracle/FeedFreshnessSection.tsx +++ b/src/features/markets/components/oracle/MarketOracle/FeedFreshnessSection.tsx @@ -53,7 +53,7 @@ export function FeedFreshnessSection({ feedFreshness, className }: FeedFreshness
)} - {feedFreshness.isStale && feedFreshness.staleAfterSeconds != null && ( + {feedFreshness.isStale && (
Status: Stale diff --git a/src/hooks/useOracleMetadata.ts b/src/hooks/useOracleMetadata.ts index aef00076..beee2a29 100644 --- a/src/hooks/useOracleMetadata.ts +++ b/src/hooks/useOracleMetadata.ts @@ -123,7 +123,7 @@ async function fetchOracleMetadata(chainId: number): Promise