From a72f9677645b7974b9accbb266272ec517c39989 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 21 Mar 2026 00:57:49 +0800 Subject: [PATCH 1/2] refacotr: remove old oracle data dependencies --- AGENTS.md | 2 + docs/TECHNICAL_OVERVIEW.md | 39 ++- src/components/DataPrefetcher.tsx | 2 - .../morpho-api/vault-allocations.ts | 47 --- .../components/market-header.tsx | 1 - .../components/market-details-block.tsx | 2 - .../markets/components/market-identity.tsx | 6 - .../markets/components/market-info-block.tsx | 2 - .../markets/components/market-selector.tsx | 1 - .../components/markets-table-same-loan.tsx | 9 +- .../components/oracle-vendor-badge.tsx | 96 +++---- .../oracle/MarketOracle/API3FeedTooltip.tsx | 17 +- .../MarketOracle/ChainlinkFeedTooltip.tsx | 29 +- .../MarketOracle/CompoundFeedTooltip.tsx | 17 +- .../oracle/MarketOracle/FeedEntry.tsx | 63 +--- .../MarketOracle/GeneralFeedTooltip.tsx | 24 +- .../MarketOracle/MarketOracleFeedInfo.tsx | 31 +- .../oracle/MarketOracle/MetaOracleInfo.tsx | 18 +- .../oracle/MarketOracle/OracleTypeInfo.tsx | 10 +- .../oracle/MarketOracle/PendleFeedTooltip.tsx | 21 +- .../MarketOracle/RedstoneFeedTooltip.tsx | 27 +- .../MarketOracle/UnknownFeedTooltip.tsx | 4 +- .../markets/components/pending-market-cap.tsx | 1 - .../components/table/market-row-detail.tsx | 2 - .../components/table/market-table-body.tsx | 1 - src/graphql/morpho-api-queries.ts | 121 -------- src/graphql/morpho-subgraph-queries.ts | 40 --- src/graphql/vault-allocation-query.ts | 49 ---- src/hooks/queries/useOracleDataQuery.ts | 88 ------ src/hooks/useFeedLastUpdatedByChain.ts | 11 +- src/hooks/useMarketData.ts | 24 +- src/hooks/useOracleMetadata.ts | 70 +++-- src/hooks/useProcessedMarkets.ts | 12 +- src/utils/marketFilters.ts | 34 +-- src/utils/oracle.ts | 270 ++---------------- src/utils/types.ts | 62 ---- src/utils/warnings.ts | 34 +-- 37 files changed, 258 insertions(+), 1029 deletions(-) delete mode 100644 src/graphql/vault-allocation-query.ts delete mode 100644 src/hooks/queries/useOracleDataQuery.ts diff --git a/AGENTS.md b/AGENTS.md index 2178274c..acb48c02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -176,6 +176,8 @@ When touching transaction and position flows, validation MUST include all releva 38. **Morpho vault query schema integrity**: shared Morpho vault metadata/rate queries must only request fields confirmed on the live `Vault`/`VaultState` schema. Do not add speculative top-level fields to the registry query, and do not swallow schema errors in a way that turns the global vault registry into an empty success state. 39. **User position discovery integrity**: when a shared upstream supports chain-scoped bulk position discovery (`userAddress_in` plus `chainId_in` or equivalent), use that batched chokepoint to collect position market keys before falling back to per-chain queries. Do not force one `userByAddress` request per chain when the backend can already return mixed-chain positions in one response. 40. **Source-discovery failure integrity**: market/position source-discovery hooks must fail closed when both primary and fallback providers fail for a chain. Do not convert dual-source fetch failures into empty success states; surface typed errors with source and network metadata so callers can fall back explicitly or show the failure. +41. **Oracle metadata source integrity**: oracle vendor/type/feed classification must resolve from the scanner metadata source keyed by `chainId + oracleAddress`. Do not reintroduce Morpho API `oracles` feed enrichment into market objects or UI/filter/warning logic as a fallback source for oracle structure. +42. **Mixed oracle badge signal integrity**: when a standard or meta oracle contains both classified feeds and unknown/unverified feeds, vendor badges and their tooltips must preserve both signals together (known vendor icon(s) plus unknown indicator/text) instead of collapsing to only the recognized vendor. ### REQUIRED: Regression Rule Capture diff --git a/docs/TECHNICAL_OVERVIEW.md b/docs/TECHNICAL_OVERVIEW.md index ffa86c72..52c38ad0 100644 --- a/docs/TECHNICAL_OVERVIEW.md +++ b/docs/TECHNICAL_OVERVIEW.md @@ -81,7 +81,6 @@ RootLayout liquidityAssets, liquidityUsd; utilization; }; - oracle?: { data: MorphoChainlinkOracleData }; } ``` @@ -118,9 +117,25 @@ GroupedPosition { // Grouped by loan asset ### Oracle ```typescript -MorphoChainlinkOracleData { - baseFeedOne, baseFeedTwo: OracleFeed; // Base token feeds - quoteFeedOne, quoteFeedTwo: OracleFeed; // Quote token feeds +StandardOracleOutput { + address: string; + chainId: number; + type: 'standard'; + data: OracleOutputData; // { baseFeedOne, baseFeedTwo, quoteFeedOne, quoteFeedTwo, baseVault, quoteVault } +} + +MetaOracleOutput { + address: string; + chainId: number; + type: 'meta'; + data: MetaOracleOutputData; // { primaryOracle, backupOracle, currentOracle, oracleSources, ... } +} + +NonStandardOracleOutput { + address: string; + chainId: number; + type: 'custom' | 'unknown'; + data: { reason: string }; } ``` @@ -167,9 +182,6 @@ Market metrics: Monarch metrics API via `/api/monarch/metrics` ### Static Data (Build-time or cached) | Data Type | Source | Location | |-----------|--------|----------| -| Oracle definitions | Pre-generated | `/src/constants/oracle/oracle-cache.json` | -| Chainlink feeds | Pre-generated | `/src/constants/oracle/chainlink/` | -| Redstone feeds | Pre-generated | `/src/constants/oracle/redstone/` | | Network configs | Hardcoded | `/src/utils/networks.ts` | | Default blacklist | Hardcoded | `/src/constants/markets/blacklisted.ts` | @@ -185,7 +197,7 @@ Market metrics: Monarch metrics API via `/api/monarch/metrics` | Vault detail/settings metadata | Monarch GraphQL + narrow RPC fallback | 30s | `useVaultV2Data` | | Vault allocations | On-chain multicall | 30s | `useAllocationsQuery` | | Token balances | On-chain multicall | 5 min | `useUserBalancesQuery` | -| Oracle prices | Morpho API | 5 min | `useOracleDataQuery` | +| Oracle metadata | Scanner Gist | 30 min | `useOracleMetadata` / `useAllOracleMetadata` | | Merkl rewards | Merkl API | On demand | `useMerklCampaignsQuery` | | Market liquidations | Morpho API/Subgraph | 5 min stale | `useMarketLiquidations` | @@ -193,7 +205,7 @@ Market metrics: Monarch metrics API via `/api/monarch/metrics` **Market Data Flow:** ``` -Raw API fetch → Blacklist filtering → Oracle enrichment → +Raw API fetch → Blacklist filtering → Split: allMarkets vs whitelistedMarkets ``` @@ -263,7 +275,6 @@ All hooks in `/src/hooks/queries/` follow React Query patterns: | `useMarketsQuery` | `['markets']` | 5 min | 5 min | Yes | | `useMarketMetricsQuery` | `['market-metrics', ...]` | 5 min | 5 min | No | | `useTokensQuery` | `['tokens']` | 5 min | 5 min | Yes | -| `useOracleDataQuery` | `['oracle-data']` | 5 min | 5 min | Yes | | `useUserBalancesQuery` | `['user-balances', addr, networks]` | 30s | - | Yes | | `useUserVaultsV2Query` | `['user-vaults-v2', addr]` | 60s | - | Yes | | `useVaultV2Data` | `['vault-v2-data', addr, chainId]` | 30s | - | No | @@ -309,7 +320,7 @@ Fallback Strategy: ↓ 2. Parallel queries start: - usePublicClient() for on-chain reads - - useOracleDataQuery() for oracle enrichment + - useOracleMetadata() for oracle classification and feed details ↓ 3. Market fetch: a. Try on-chain snapshot (viem multicall) @@ -317,7 +328,9 @@ Fallback Strategy: c. Fallback to Subgraph d. Merge snapshot with API state ↓ -4. Oracle enrichment via useMemo() +4. Oracle metadata resolves separately by `chainId + oracleAddress` + - Standard/meta oracle UI reads scanner-native `OracleOutputData` / `MetaOracleOutputData` + - No Morpho API oracle feed enrichment or local feed-shape conversion ↓ 5. Return { data: enrichedMarket, isLoading, error } ``` @@ -328,7 +341,7 @@ Fallback Strategy: 2. **Parallel Execution**: `Promise.all()` for multi-network 3. **Graceful Degradation**: Partial data > Error 4. **Two-Phase Market**: On-chain snapshot + API state -5. **Hybrid Caching**: Static JSON + dynamic API (oracles) +5. **Hybrid Reads**: Scanner metadata for oracle structure + live RPC/API for market state --- diff --git a/src/components/DataPrefetcher.tsx b/src/components/DataPrefetcher.tsx index 60ab6eca..8be4a2ec 100644 --- a/src/components/DataPrefetcher.tsx +++ b/src/components/DataPrefetcher.tsx @@ -4,13 +4,11 @@ import { usePathname } from 'next/navigation'; import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery'; import { useTokensQuery } from '@/hooks/queries/useTokensQuery'; import { useMerklCampaignsQuery } from '@/hooks/queries/useMerklCampaignsQuery'; -import { useOracleDataQuery } from '@/hooks/queries/useOracleDataQuery'; function DataPrefetcherContent() { useMarketsQuery(); useTokensQuery(); useMerklCampaignsQuery(); - useOracleDataQuery(); return null; } diff --git a/src/data-sources/morpho-api/vault-allocations.ts b/src/data-sources/morpho-api/vault-allocations.ts index 1ec97cb7..00df3923 100644 --- a/src/data-sources/morpho-api/vault-allocations.ts +++ b/src/data-sources/morpho-api/vault-allocations.ts @@ -1,7 +1,3 @@ -import { vaultAllocationQuery } from '@/graphql/vault-allocation-query'; -import type { SupportedNetworks } from '@/utils/networks'; -import { morphoGraphqlFetcher } from './fetchers'; - export type VaultAllocationMarket = { uniqueKey: string; loanAsset: { @@ -31,46 +27,3 @@ export type VaultAllocation = { supplyAssets: string; supplyCap: string; }; - -export type VaultAllocationData = { - address: string; - name: string; - symbol: string; - asset: { - address: string; - symbol: string; - decimals: number; - }; - state: { - totalAssets: string; - allocation: VaultAllocation[]; - }; -}; - -type VaultAllocationApiResponse = { - data?: { - vaultByAddress?: VaultAllocationData | null; - }; - errors?: { message: string }[]; -}; - -/** - * Fetches a MetaMorpho vault's allocation data from the Morpho Blue API. - * Returns the vault's markets with their supply amounts and liquidity. - */ -export const fetchVaultAllocations = async (vaultAddress: string, chainId: SupportedNetworks): Promise => { - const response = await morphoGraphqlFetcher(vaultAllocationQuery, { - address: vaultAddress, - chainId, - }); - - if (response?.errors?.length) { - console.warn('fetchVaultAllocations errors:', response.errors); - } - - if (!response?.data?.vaultByAddress) { - return null; - } - - return response.data.vaultByAddress; -}; diff --git a/src/features/market-detail/components/market-header.tsx b/src/features/market-detail/components/market-header.tsx index b413cd22..870b608e 100644 --- a/src/features/market-detail/components/market-header.tsx +++ b/src/features/market-detail/components/market-header.tsx @@ -756,7 +756,6 @@ export function MarketHeader({ ·
{lltv}%} {showOracle && ( {lltv}% LLTV} {showOracle && ( {lltv}% LLTV} {showOracle && ( {lltv}% LLTV} {showOracle && ( {lltv}% LLTV} {showOracle && ( {lltv}% LLTV} {showOracle && (
{ if (!m?.market?.morphoBlue?.chain?.id) return; - const vendorInfo = parsePriceFeedVendors(m.market.oracle?.data, m.market.morphoBlue.chain.id, { - metadataMap: oracleMetadataMap, - oracleAddress: m.market.oracleAddress, - }); + const vendorInfo = parsePriceFeedVendors( + getStandardOracleDataFromMetadata(oracleMetadataMap, m.market.oracleAddress, m.market.morphoBlue.chain.id), + ); if (vendorInfo?.coreVendors) { vendorInfo.coreVendors.forEach((vendor) => oracleSet.add(vendor)); } diff --git a/src/features/markets/components/oracle-vendor-badge.tsx b/src/features/markets/components/oracle-vendor-badge.tsx index c1127fee..6fbe181d 100644 --- a/src/features/markets/components/oracle-vendor-badge.tsx +++ b/src/features/markets/components/oracle-vendor-badge.tsx @@ -12,11 +12,9 @@ import { parseMetaOracleVendors, parsePriceFeedVendors, } from '@/utils/oracle'; -import { getOracleFromMetadata, isMetaOracleData, useOracleMetadata } from '@/hooks/useOracleMetadata'; -import type { MorphoChainlinkOracleData } from '@/utils/types'; +import { getMetaOracleDataFromMetadata, getStandardOracleDataFromMetadata, useOracleMetadata } from '@/hooks/useOracleMetadata'; type OracleVendorBadgeProps = { - oracleData: MorphoChainlinkOracleData | null | undefined; chainId: number; oracleAddress?: string; useTooltip?: boolean; @@ -38,59 +36,49 @@ const renderVendorIcon = (vendor: PriceFeedVendors) => /> ); -/** - * IoWarningOutline: Unknown Oracles - * IoHelpCircleOutline: For unknown feeds - */ - -function OracleVendorBadge({ oracleData, chainId, oracleAddress, showText = false, useTooltip = true }: OracleVendorBadgeProps) { +function OracleVendorBadge({ chainId, oracleAddress, showText = false, useTooltip = true }: OracleVendorBadgeProps) { const { data: oracleMetadataMap } = useOracleMetadata(chainId); + const standardOracleData = getStandardOracleDataFromMetadata(oracleMetadataMap, oracleAddress, chainId); + const metaOracleData = getMetaOracleDataFromMetadata(oracleMetadataMap, oracleAddress, chainId); - const oracleType = getOracleType(oracleData, oracleAddress, chainId, oracleMetadataMap); + const oracleType = getOracleType(oracleAddress, chainId, oracleMetadataMap); const isCustom = oracleType === OracleType.Custom; const isMeta = oracleType === OracleType.Meta; - // Check if this is a vault-only oracle (no feeds, only vault conversion) - const oracleMetadata = oracleMetadataMap && oracleAddress ? getOracleFromMetadata(oracleMetadataMap, oracleAddress) : undefined; - const oracleMetadataData = oracleMetadata?.data && !isMetaOracleData(oracleMetadata.data) ? oracleMetadata.data : undefined; const isVaultOnly = oracleType === OracleType.Standard && - !oracleMetadataData?.baseFeedOne && - !oracleMetadataData?.baseFeedTwo && - !oracleMetadataData?.quoteFeedOne && - !oracleMetadataData?.quoteFeedTwo && - (oracleMetadataData?.baseVault || oracleMetadataData?.quoteVault); - - const vendorInfo = (() => { - if (isMeta && oracleMetadataMap && oracleAddress) { - const metadata = getOracleFromMetadata(oracleMetadataMap, oracleAddress); - if (metadata?.data && isMetaOracleData(metadata.data)) { - return parseMetaOracleVendors(metadata.data); - } - } - return parsePriceFeedVendors(oracleData, chainId, { - metadataMap: oracleMetadataMap, - oracleAddress, - }); - })(); - const { coreVendors, taggedVendors, hasCompletelyUnknown, hasTaggedUnknown, vendors, hasUnknown } = vendorInfo; + !standardOracleData?.baseFeedOne && + !standardOracleData?.baseFeedTwo && + !standardOracleData?.quoteFeedOne && + !standardOracleData?.quoteFeedTwo && + (standardOracleData?.baseVault || standardOracleData?.quoteVault); + + const vendorInfo = isMeta && metaOracleData ? parseMetaOracleVendors(metaOracleData) : parsePriceFeedVendors(standardOracleData); + const { coreVendors, taggedVendors, hasCompletelyUnknown, hasTaggedUnknown } = vendorInfo; + const hasUnknownFeed = hasCompletelyUnknown || hasTaggedUnknown; + const displayNames = hasUnknownFeed ? [...coreVendors, ...taggedVendors, 'Unverified'] : [...coreVendors, ...taggedVendors]; + const showTaggedFallbackIcon = !isCustom && !isVaultOnly && coreVendors.length === 0 && taggedVendors.length > 0; + const showGenericFallbackIcon = !isCustom && !isVaultOnly && coreVendors.length === 0 && taggedVendors.length === 0; const content = (
- {showText && {hasUnknown ? 'Unknown' : vendors.join(', ')}} + {showText && {displayNames.join(', ') || 'Oracle'}} {isCustom ? ( ) : isVaultOnly ? ( - // Vault-only oracle - show checkmark icon - ) : hasCompletelyUnknown || hasTaggedUnknown ? ( - // Show core vendor icons plus question mark for any unknown types + ) : showTaggedFallbackIcon || showGenericFallbackIcon ? ( + + ) : hasUnknownFeed ? ( <> {coreVendors.map((vendor, index) => ( {renderVendorIcon(vendor)} @@ -101,7 +89,6 @@ function OracleVendorBadge({ oracleData, chainId, oracleAddress, showText = fals /> ) : ( - // Only core vendors, show their icons coreVendors.map((vendor, index) => {renderVendorIcon(vendor)}) )}
@@ -118,7 +105,6 @@ function OracleVendorBadge({ oracleData, chainId, oracleAddress, showText = fals ); } - // Vault-only oracle - special case if (isVaultOnly) { return (
@@ -129,40 +115,28 @@ function OracleVendorBadge({ oracleData, chainId, oracleAddress, showText = fals } const oracleLabel = isMeta ? 'Meta Oracle' : 'Standard Oracle'; + const allKnownVendors = [...coreVendors, ...taggedVendors]; - 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(', ')}.`; - + if (showGenericFallbackIcon && !hasUnknownFeed) { return (

{oracleLabel}

-

{description}

+

Vendor classification is not available in oracle metadata.

); } - // All core vendors - clean case - const allVendors = [...coreVendors, ...taggedVendors]; + const feedSummary = + allKnownVendors.length > 0 + ? hasUnknownFeed + ? `${allKnownVendors.join(', ')} and unverified feeds` + : allKnownVendors.join(', ') + : 'unverified or unclassified feeds'; + return (

{oracleLabel}

-

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

+

Uses feeds from {feedSummary}.

); }; diff --git a/src/features/markets/components/oracle/MarketOracle/API3FeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/API3FeedTooltip.tsx index 2c65d1b1..12a258b9 100644 --- a/src/features/markets/components/oracle/MarketOracle/API3FeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/API3FeedTooltip.tsx @@ -1,22 +1,21 @@ import Image from 'next/image'; import Link from 'next/link'; import type { Address } from 'viem'; +import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; -import type { OracleFeed } from '@/utils/types'; +import { OracleVendorIcons, PriceFeedVendors, type FeedFreshnessStatus } from '@/utils/oracle'; import { FeedFreshnessSection } from './FeedFreshnessSection'; type API3FeedTooltipProps = { - feed: OracleFeed; - feedData?: FeedData | null; + feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; }; -export function API3FeedTooltip({ feed, feedData, chainId, feedFreshness }: API3FeedTooltipProps) { - const baseAsset = feed.pair?.[0] ?? feedData?.pair[0] ?? 'Unknown'; - const quoteAsset = feed.pair?.[1] ?? feedData?.pair[1] ?? 'Unknown'; +export function API3FeedTooltip({ feed, chainId, feedFreshness }: API3FeedTooltipProps) { + const baseAsset = feed.pair[0] ?? 'Unknown'; + const quoteAsset = feed.pair[1] ?? 'Unknown'; const vendorIcon = OracleVendorIcons[PriceFeedVendors.API3]; @@ -45,9 +44,9 @@ export function API3FeedTooltip({ feed, feedData, chainId, feedFreshness }: API3
{/* Description */} - {feedData?.description && ( + {feed.description && (
-
{feedData.description}
+
{feed.description}
)} diff --git a/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx index 3b181303..a66943fe 100644 --- a/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/ChainlinkFeedTooltip.tsx @@ -4,16 +4,15 @@ import { IoHelpCircleOutline } from 'react-icons/io5'; import type { Address } from 'viem'; import { Badge } from '@/components/ui/badge'; import { useGlobalModal } from '@/contexts/GlobalModalContext'; +import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { getChainlinkFeedUrl, OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; -import type { OracleFeed } from '@/utils/types'; +import { getChainlinkFeedUrl, OracleVendorIcons, PriceFeedVendors, type FeedFreshnessStatus } from '@/utils/oracle'; import { ChainlinkRiskTiersModal } from './ChainlinkRiskTiersModal'; import { FeedFreshnessSection } from './FeedFreshnessSection'; type ChainlinkFeedTooltipProps = { - feed: OracleFeed; - feedData?: FeedData | null; + feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; }; @@ -38,16 +37,16 @@ function getRiskTierBadge(category: string) { ); } -export function ChainlinkFeedTooltip({ feed, feedData, chainId, feedFreshness }: ChainlinkFeedTooltipProps) { +export function ChainlinkFeedTooltip({ feed, 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'; + const baseAsset = feed.pair[0] ?? 'Unknown'; + const quoteAsset = feed.pair[1] ?? 'Unknown'; const vendorIcon = OracleVendorIcons[PriceFeedVendors.Chainlink]; - const chainlinkUrl = feedData?.ens ? getChainlinkFeedUrl(chainId, feedData.ens) : ''; + const chainlinkUrl = feed.ens ? getChainlinkFeedUrl(chainId, feed.ens) : ''; - const hasDetails = feedData?.heartbeat != null || feedData?.tier != null || feedData?.deviationThreshold != null; + const hasDetails = feed.heartbeat != null || feed.tier != null || feed.deviationThreshold != null; return (
@@ -76,17 +75,17 @@ export function ChainlinkFeedTooltip({ feed, feedData, chainId, feedFreshness }: {/* Chainlink Specific Data */} {hasDetails && (
- {feedData?.heartbeat != null && ( + {feed.heartbeat != null && (
Heartbeat: - {feedData.heartbeat}s + {feed.heartbeat}s
)} - {feedData?.tier != null && ( + {feed.tier != null && (
Risk Tier:
- {getRiskTierBadge(feedData.tier)} + {getRiskTierBadge(feed.tier)}
)} - {feedData?.deviationThreshold != null && ( + {feed.deviationThreshold != null && (
Deviation Threshold: - {feedData.deviationThreshold}% + {feed.deviationThreshold}%
)}
diff --git a/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx index ab872a0f..a5b7e909 100644 --- a/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/CompoundFeedTooltip.tsx @@ -1,22 +1,21 @@ import Image from 'next/image'; import Link from 'next/link'; import type { Address } from 'viem'; +import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; -import type { OracleFeed } from '@/utils/types'; +import { OracleVendorIcons, PriceFeedVendors, type FeedFreshnessStatus } from '@/utils/oracle'; import { FeedFreshnessSection } from './FeedFreshnessSection'; type CompoundFeedTooltipProps = { - feed: OracleFeed; - feedData?: FeedData | null; + feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; }; -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'; +export function CompoundFeedTooltip({ feed, chainId, feedFreshness }: CompoundFeedTooltipProps) { + const baseAsset = feed.pair[0] ?? 'Unknown'; + const quoteAsset = feed.pair[1] ?? 'Unknown'; const compoundLogo = OracleVendorIcons[PriceFeedVendors.Compound]; @@ -45,9 +44,9 @@ export function CompoundFeedTooltip({ feed, feedData, chainId, feedFreshness }:
{/* Feed description if available */} - {feedData?.description && ( + {feed.description && (
-
{feedData.description}
+
{feed.description}
)} diff --git a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx index 27f0d6c1..188f1dee 100644 --- a/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx +++ b/src/features/markets/components/oracle/MarketOracle/FeedEntry.tsx @@ -3,24 +3,15 @@ import { Tooltip } from '@/components/ui/tooltip'; 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 type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import { - getFeedFromOracleData, - getOracleFromMetadata, - isMetaOracleData, - type EnrichedFeed, - type OracleMetadataRecord, -} from '@/hooks/useOracleMetadata'; -import { - detectFeedVendor, detectFeedVendorFromMetadata, getFeedFreshnessStatus, getTruncatedAssetName, OracleVendorIcons, PriceFeedVendors, } from '@/utils/oracle'; -import type { OracleFeed } from '@/utils/types'; import { ChainlinkFeedTooltip } from './ChainlinkFeedTooltip'; import { CompoundFeedTooltip } from './CompoundFeedTooltip'; import { GeneralFeedTooltip } from './GeneralFeedTooltip'; @@ -30,50 +21,19 @@ import { API3FeedTooltip } from './API3FeedTooltip'; import { UnknownFeedTooltip } from './UnknownFeedTooltip'; type FeedEntryProps = { - feed: OracleFeed | null; + feed: EnrichedFeed | null; chainId: number; - oracleAddress?: string; - oracleMetadataMap?: OracleMetadataRecord; - enrichedFeed?: EnrichedFeed; feedSnapshotsByAddress?: FeedSnapshotByAddress; }; -export function FeedEntry({ - feed, - chainId, - oracleAddress, - oracleMetadataMap, - enrichedFeed, - feedSnapshotsByAddress, -}: FeedEntryProps): JSX.Element | null { - // Use metadata-based detection when available, fallback to legacy +export function FeedEntry({ feed, chainId, feedSnapshotsByAddress }: FeedEntryProps): JSX.Element | null { const feedVendorResult = useMemo(() => { - if (!feed?.address) return null; - - // Use pre-enriched feed directly when provided (e.g. from meta oracle scanner data) - if (enrichedFeed) { - return detectFeedVendorFromMetadata(enrichedFeed); - } - - // Try metadata-based detection first - if (oracleMetadataMap && oracleAddress) { - const oracleMetadata = getOracleFromMetadata(oracleMetadataMap, oracleAddress); - if (oracleMetadata?.data && !isMetaOracleData(oracleMetadata.data)) { - const found = getFeedFromOracleData(oracleMetadata.data, feed.address); - if (found) { - return detectFeedVendorFromMetadata(found); - } - } - } - - // Fallback to legacy detection (will return Unknown without static data) - return detectFeedVendor(feed.address as Address, chainId); - }, [feed?.address, chainId, oracleAddress, oracleMetadataMap, enrichedFeed]); + return detectFeedVendorFromMetadata(feed); + }, [feed]); if (!feed) return null; - if (!feedVendorResult) return null; - const { vendor, data, assetPair } = feedVendorResult; + const { vendor, assetPair } = feedVendorResult; const { baseAsset, quoteAsset } = { baseAsset: getTruncatedAssetName(assetPair.baseAsset), quoteAsset: getTruncatedAssetName(assetPair.quoteAsset), @@ -86,7 +46,7 @@ export function FeedEntry({ const hasKnownVendorIcon = vendor !== PriceFeedVendors.Unknown && Boolean(vendorIcon); const feedAddressKey = feed.address.toLowerCase(); const snapshot = feedSnapshotsByAddress?.[feedAddressKey]; - const freshness = getFeedFreshnessStatus(snapshot?.updatedAt ?? null, data?.heartbeat, { + const freshness = getFeedFreshnessStatus(snapshot?.updatedAt ?? null, feed.heartbeat, { updateKind: snapshot?.updateKind, normalizedPrice: snapshot?.normalizedPrice, }); @@ -97,7 +57,6 @@ export function FeedEntry({ return ( @@ -107,7 +66,6 @@ export function FeedEntry({ return ( @@ -117,7 +75,6 @@ export function FeedEntry({ return ( @@ -127,7 +84,6 @@ export function FeedEntry({ return ( @@ -137,7 +93,6 @@ export function FeedEntry({ return ( @@ -149,18 +104,16 @@ export function FeedEntry({ return ( ); case PriceFeedVendors.Unknown: - if (data) { + if (feed.provider || feed.description || feed.pair.length === 2) { return ( diff --git a/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx index 314db6b1..ad94acc8 100644 --- a/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/GeneralFeedTooltip.tsx @@ -1,25 +1,23 @@ import Image from 'next/image'; import Link from 'next/link'; import type { Address } from 'viem'; +import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { mapProviderToVendor, OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; -import type { OracleFeed } from '@/utils/types'; -import type { OracleFeedProvider } from '@/hooks/useOracleMetadata'; +import { mapProviderToVendor, OracleVendorIcons, PriceFeedVendors, type FeedFreshnessStatus } from '@/utils/oracle'; import { FeedFreshnessSection } from './FeedFreshnessSection'; type GeneralFeedTooltipProps = { - feed: OracleFeed; - feedData?: FeedData | null; + feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; }; -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'; +export function GeneralFeedTooltip({ feed, chainId, feedFreshness }: GeneralFeedTooltipProps) { + const baseAsset = feed.pair[0] ?? 'Unknown'; + const quoteAsset = feed.pair[1] ?? 'Unknown'; - const vendor = feedData?.vendor ? mapProviderToVendor(feedData.vendor as OracleFeedProvider) : PriceFeedVendors.Unknown; + const vendor = feed.provider ? mapProviderToVendor(feed.provider) : PriceFeedVendors.Unknown; const vendorIcon = OracleVendorIcons[vendor] || OracleVendorIcons[PriceFeedVendors.Unknown]; return ( @@ -30,13 +28,13 @@ export function GeneralFeedTooltip({ feed, feedData, chainId, feedFreshness }: G
{feedData?.vendor
)} -
{feedData?.vendor ?? 'Price'} Feed
+
{feed.provider ?? 'Price'} Feed
{/* Feed pair name */} @@ -47,9 +45,9 @@ export function GeneralFeedTooltip({ feed, feedData, chainId, feedFreshness }: G
{/* Description */} - {feedData?.description && ( + {feed.description && (
-
{feedData.description}
+
{feed.description}
)} diff --git a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx index 2f858fd1..a7f29dc4 100644 --- a/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx +++ b/src/features/markets/components/oracle/MarketOracle/MarketOracleFeedInfo.tsx @@ -1,35 +1,26 @@ 'use client'; import { useFeedLastUpdatedByChain } from '@/hooks/useFeedLastUpdatedByChain'; -import { useOracleMetadata, getOracleFromMetadata, isMetaOracleData } from '@/hooks/useOracleMetadata'; -import type { OracleFeed } from '@/utils/types'; +import { getStandardOracleDataFromMetadata, useOracleMetadata } from '@/hooks/useOracleMetadata'; import { FeedEntry } from './FeedEntry'; import { VaultEntry } from './VaultEntry'; type MarketOracleFeedInfoProps = { - baseFeedOne: OracleFeed | null | undefined; - baseFeedTwo: OracleFeed | null | undefined; - quoteFeedOne: OracleFeed | null | undefined; - quoteFeedTwo: OracleFeed | null | undefined; chainId: number; oracleAddress?: string; }; -export function MarketOracleFeedInfo({ - baseFeedOne, - baseFeedTwo, - quoteFeedOne, - quoteFeedTwo, - chainId, - oracleAddress, -}: MarketOracleFeedInfoProps): JSX.Element { +export function MarketOracleFeedInfo({ chainId, 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; + const oracleData = getStandardOracleDataFromMetadata(oracleMetadataMap, oracleAddress, chainId); const baseVault = oracleData?.baseVault ?? null; const quoteVault = oracleData?.quoteVault ?? null; + const baseFeedOne = oracleData?.baseFeedOne ?? null; + const baseFeedTwo = oracleData?.baseFeedTwo ?? null; + const quoteFeedOne = oracleData?.quoteFeedOne ?? null; + const quoteFeedTwo = oracleData?.quoteFeedTwo ?? null; const hasAnyFeed = baseFeedOne || baseFeedTwo || quoteFeedOne || quoteFeedTwo; const hasAnyVault = baseVault || quoteVault; @@ -54,8 +45,6 @@ export function MarketOracleFeedInfo({ )} @@ -63,8 +52,6 @@ export function MarketOracleFeedInfo({ )} @@ -86,8 +73,6 @@ export function MarketOracleFeedInfo({ )} @@ -95,8 +80,6 @@ export function MarketOracleFeedInfo({ )} diff --git a/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx b/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx index 3aaa3f70..a70be0b7 100644 --- a/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx +++ b/src/features/markets/components/oracle/MarketOracle/MetaOracleInfo.tsx @@ -1,9 +1,8 @@ 'use client'; import { useFeedLastUpdatedByChain, type FeedSnapshotByAddress } from '@/hooks/useFeedLastUpdatedByChain'; -import { useOracleMetadata, getOracleFromMetadata, isMetaOracleData, type OracleOutputData } from '@/hooks/useOracleMetadata'; +import { getMetaOracleDataFromMetadata, type OracleOutputData, useOracleMetadata } from '@/hooks/useOracleMetadata'; import { AddressIdentity } from '@/components/shared/address-identity'; -import type { OracleFeed } from '@/utils/types'; import { formatOracleDuration } from '@/utils/oracle'; import { FeedEntry } from './FeedEntry'; import { VaultEntry } from './VaultEntry'; @@ -52,18 +51,11 @@ function OracleFeedSection({ )} {activeFeeds.map((enrichedFeed) => { if (!enrichedFeed) return null; - const oracleFeed: OracleFeed = { - address: enrichedFeed.address, - chain: { id: chainId }, - id: enrichedFeed.address, - pair: enrichedFeed.pair.length === 2 ? [enrichedFeed.pair[0], enrichedFeed.pair[1]] : null, - }; return ( ); @@ -80,10 +72,8 @@ export function MetaOracleInfo({ oracleAddress, chainId, variant = 'summary' }: const { data: oracleMetadataMap } = useOracleMetadata(chainId); const { data: feedSnapshotsByAddress } = useFeedLastUpdatedByChain(chainId); - const oracleMetadata = getOracleFromMetadata(oracleMetadataMap, oracleAddress); - if (!oracleMetadata?.data || !isMetaOracleData(oracleMetadata.data)) return null; - - const metaData = oracleMetadata.data; + const metaData = getMetaOracleDataFromMetadata(oracleMetadataMap, oracleAddress, chainId); + if (!metaData) return null; const isPrimaryActive = metaData.currentOracle?.toLowerCase() === metaData.primaryOracle?.toLowerCase(); if (variant === 'detail') { diff --git a/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx b/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx index 08241aac..354e09e4 100644 --- a/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx +++ b/src/features/markets/components/oracle/MarketOracle/OracleTypeInfo.tsx @@ -5,11 +5,9 @@ import { MarketOracleFeedInfo } from '@/features/markets/components/oracle'; import { Tooltip } from '@/components/ui/tooltip'; import { useOracleMetadata } from '@/hooks/useOracleMetadata'; import { getOracleType, getOracleTypeDescription, OracleType } from '@/utils/oracle'; -import type { MorphoChainlinkOracleData } from '@/utils/types'; import { MetaOracleInfo } from './MetaOracleInfo'; type OracleTypeInfoProps = { - oracleData: MorphoChainlinkOracleData | null | undefined; oracleAddress: string; chainId: number; showCustom?: boolean; @@ -17,9 +15,9 @@ type OracleTypeInfoProps = { variant?: 'summary' | 'detail'; }; -export function OracleTypeInfo({ oracleData, oracleAddress, chainId, showCustom, useBadge, variant }: OracleTypeInfoProps) { +export function OracleTypeInfo({ oracleAddress, chainId, showCustom, useBadge, variant }: OracleTypeInfoProps) { const { data: oracleMetadataMap } = useOracleMetadata(chainId); - const oracleType = getOracleType(oracleData, oracleAddress, chainId, oracleMetadataMap); + const oracleType = getOracleType(oracleAddress, chainId, oracleMetadataMap); const typeDescription = getOracleTypeDescription(oracleType); return ( @@ -49,10 +47,6 @@ export function OracleTypeInfo({ oracleData, oracleAddress, chainId, showCustom, {oracleType === OracleType.Standard ? ( diff --git a/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx index b1450816..f4e9a40e 100644 --- a/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/PendleFeedTooltip.tsx @@ -2,15 +2,14 @@ import Image from 'next/image'; import Link from 'next/link'; import type { Address } from 'viem'; import { formatUnits } from 'viem'; +import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; -import type { OracleFeed } from '@/utils/types'; +import { OracleVendorIcons, PriceFeedVendors, type FeedFreshnessStatus } from '@/utils/oracle'; import { FeedFreshnessSection } from './FeedFreshnessSection'; type PendleFeedTooltipProps = { - feed: OracleFeed; - feedData?: FeedData | null; + feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; }; @@ -22,10 +21,10 @@ function formatDiscountPerYear(raw: string): string { return `${percent.toFixed(2)}%`; } -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; +export function PendleFeedTooltip({ feed, chainId, feedFreshness }: PendleFeedTooltipProps) { + const baseAsset = feed.pair[0] ?? 'Unknown'; + const quoteAsset = feed.pair[1] ?? 'Unknown'; + const pendleFeedKind = feed.pendleFeedKind; const isLinearDiscount = pendleFeedKind?.toLowerCase() === 'lineardiscount'; const typeLabel = isLinearDiscount ? 'Linear Discount' : pendleFeedKind; @@ -56,7 +55,7 @@ export function PendleFeedTooltip({ feed, feedData, chainId, feedFreshness }: Pe {/* Pendle Specific Data */} - {(typeLabel != null || feedData?.baseDiscountPerYear != null) && ( + {(typeLabel != null || feed.baseDiscountPerYear != null) && (
{typeLabel != null && (
@@ -64,10 +63,10 @@ export function PendleFeedTooltip({ feed, feedData, chainId, feedFreshness }: Pe {typeLabel}
)} - {feedData?.baseDiscountPerYear != null && ( + {feed.baseDiscountPerYear != null && (
Base Discount: - {formatDiscountPerYear(feedData.baseDiscountPerYear)} + {formatDiscountPerYear(feed.baseDiscountPerYear)}
)}
diff --git a/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx index b9949a87..9551ae46 100644 --- a/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/RedstoneFeedTooltip.tsx @@ -3,28 +3,27 @@ import Link from 'next/link'; import { IoHelpCircleOutline } from 'react-icons/io5'; import type { Address } from 'viem'; import { useGlobalModal } from '@/contexts/GlobalModalContext'; +import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import { OracleVendorIcons, PriceFeedVendors, type FeedData, type FeedFreshnessStatus } from '@/utils/oracle'; -import type { OracleFeed } from '@/utils/types'; +import { OracleVendorIcons, PriceFeedVendors, type FeedFreshnessStatus } from '@/utils/oracle'; import { FeedFreshnessSection } from './FeedFreshnessSection'; import { RedstoneTypesModal } from './RedstoneTypesModal'; type RedstoneFeedTooltipProps = { - feed: OracleFeed; - feedData?: FeedData | null; + feed: EnrichedFeed; chainId: number; feedFreshness?: FeedFreshnessStatus; }; -export function RedstoneFeedTooltip({ feed, feedData, chainId, feedFreshness }: RedstoneFeedTooltipProps) { +export function RedstoneFeedTooltip({ feed, 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'; + const baseAsset = feed.pair[0] ?? 'Unknown'; + const quoteAsset = feed.pair[1] ?? 'Unknown'; const vendorIcon = OracleVendorIcons[PriceFeedVendors.Redstone]; - const hasDetails = feedData?.feedType != null || feedData?.heartbeat != null || feedData?.deviationThreshold != null; + const hasDetails = feed.feedType != null || feed.heartbeat != null || feed.deviationThreshold != null; return (
@@ -53,11 +52,11 @@ export function RedstoneFeedTooltip({ feed, feedData, chainId, feedFreshness }: {/* Redstone Specific Data */} {hasDetails && (
- {feedData?.feedType != null && ( + {feed.feedType != null && (
Type:
- {feedData.feedType === 'fundamental' ? 'Fundamental' : 'Standard'} + {feed.feedType === 'fundamental' ? 'Fundamental' : 'Standard'}
)} - {feedData?.heartbeat != null && ( + {feed.heartbeat != null && (
Heartbeat: - {feedData.heartbeat}s + {feed.heartbeat}s
)} - {feedData?.deviationThreshold != null && ( + {feed.deviationThreshold != null && (
Deviation Threshold: - {feedData.deviationThreshold}% + {feed.deviationThreshold}%
)}
diff --git a/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx b/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx index e00b542b..e60793a0 100644 --- a/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx +++ b/src/features/markets/components/oracle/MarketOracle/UnknownFeedTooltip.tsx @@ -2,12 +2,12 @@ import Image from 'next/image'; import Link from 'next/link'; import { IoHelpCircleOutline } from 'react-icons/io5'; import type { Address } from 'viem'; +import type { EnrichedFeed } from '@/hooks/useOracleMetadata'; import etherscanLogo from '@/imgs/etherscan.png'; import { getExplorerURL } from '@/utils/external'; -import type { OracleFeed } from '@/utils/types'; type UnknownFeedTooltipProps = { - feed: OracleFeed; + feed: EnrichedFeed; chainId: number; }; diff --git a/src/features/markets/components/pending-market-cap.tsx b/src/features/markets/components/pending-market-cap.tsx index 64fa0ce9..a3ba0759 100644 --- a/src/features/markets/components/pending-market-cap.tsx +++ b/src/features/markets/components/pending-market-cap.tsx @@ -83,7 +83,6 @@ export function PendingMarketCap({
diff --git a/src/features/markets/components/table/market-table-body.tsx b/src/features/markets/components/table/market-table-body.tsx index 18b33647..5dadcfd2 100644 --- a/src/features/markets/components/table/market-table-body.tsx +++ b/src/features/markets/components/table/market-table-body.tsx @@ -148,7 +148,6 @@ export function MarketTableBody({ currentEntries, expandedRowId, setExpandedRowI >
diff --git a/src/graphql/morpho-api-queries.ts b/src/graphql/morpho-api-queries.ts index c4eaeec7..23a906b1 100644 --- a/src/graphql/morpho-api-queries.ts +++ b/src/graphql/morpho-api-queries.ts @@ -1,66 +1,3 @@ -export const feedFieldsFragment = ` - fragment FeedFields on OracleFeed { - address - chain { - id - } - description - id - pair - vendor - } -`; - -export const oraclesQuery = ` - query getOracles($first: Int, $skip: Int, $where: OraclesFilters) { - oracles(first: $first, skip: $skip, where: $where) { - items { - address - chain { - id - } - data { - ... on MorphoChainlinkOracleData { - baseFeedOne { - ...FeedFields - } - baseFeedTwo { - ...FeedFields - } - quoteFeedOne { - ...FeedFields - } - quoteFeedTwo { - ...FeedFields - } - } - ... on MorphoChainlinkOracleV2Data { - baseFeedOne { - ...FeedFields - } - baseFeedTwo { - ...FeedFields - } - quoteFeedOne { - ...FeedFields - } - quoteFeedTwo { - ...FeedFields - } - } - } - } - pageInfo { - countTotal - count - limit - skip - } - } - } - ${feedFieldsFragment} -`; - const commonMarketFields = ` lltv uniqueKey @@ -358,21 +295,6 @@ export const marketHistoricalDataQuery = ` } `; -export const userRebalancerInfoQuery = ` - query UserRebalancerInfo($id: String!) { - user(id: $id) { - rebalancer - marketCaps (where: {cap_gt: 0}) { - marketId - cap - } - transactions { - transactionHash - } - } - } -`; - export const userTransactionsQuery = ` query getUserTransactions($where: TransactionFilters, $first: Int, $skip: Int) { transactions(where: $where, first: $first, skip: $skip) { @@ -561,49 +483,6 @@ export const marketBorrowersQuery = ` } `; -export const vaultV2Query = ` - query VaultV2($address: String!, $chainId: Int!) { - vaultV2ByAddress(address: $address, chainId: $chainId) { - id - address - name - symbol - avgApy - asset { - id - address - symbol - name - decimals - } - curator { - address - } - owner { - address - } - allocators { - allocator { - address - } - } - caps { - items { - id - idData - absoluteCap - relativeCap - } - } - adapters { - items { - address - } - } - } - } -`; - export const assetPricesQuery = ` query getAssetPrices($where: AssetsFilters) { assets(where: $where) { diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts index c9697de9..707bab88 100644 --- a/src/graphql/morpho-subgraph-queries.ts +++ b/src/graphql/morpho-subgraph-queries.ts @@ -242,26 +242,6 @@ export const marketLiquidationsAndBadDebtQuery = ` `; // --- End Query --- -// --- Query to check which markets have had at least one liquidation --- -export const subgraphMarketsWithLiquidationCheckQuery = ` - query getSubgraphMarketsWithLiquidationCheck( - $first: Int, - $where: Market_filter, - ) { - markets( - first: $first, - where: $where, - orderBy: totalValueLockedUSD, - orderDirection: desc, - ) { - id # Market ID (uniqueKey) - liquidates(first: 1) { # Fetch only one to check existence - id - } - } - } -`; - // --- Query for User Position Market IDs --- export const subgraphUserPositionMarketsQuery = ` query GetUserPositionMarkets($userId: ID!) { @@ -414,26 +394,6 @@ export const getSubgraphUserTransactionsQuery = (useMarketFilter: boolean) => { `; }; -export const marketPositionsQuery = ` - query getMarketPositions($market: String!, $minShares: BigInt!, $first: Int!, $skip: Int!) { - positions( - where: { - shares_gt: $minShares - market: $market - } - orderBy: shares - orderDirection: desc - first: $first - skip: $skip - ) { - shares - account { - id - } - } - } -`; - // Query for market suppliers (positions with side: SUPPLIER, isCollateral: false) export const marketSuppliersQuery = ` query getMarketSuppliers($market: String!, $minShares: BigInt!, $first: Int!, $skip: Int!) { diff --git a/src/graphql/vault-allocation-query.ts b/src/graphql/vault-allocation-query.ts deleted file mode 100644 index 0385ac38..00000000 --- a/src/graphql/vault-allocation-query.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Query for fetching a MetaMorpho vault's allocation details, - * including which markets the vault supplies to and their current state. - * Used by the Public Allocator reallocate feature. - */ -export const vaultAllocationQuery = ` - query getVaultAllocation($address: String!, $chainId: Int!) { - vaultByAddress(address: $address, chainId: $chainId) { - address - name - symbol - asset { - address - symbol - decimals - } - state { - totalAssets - allocation { - market { - uniqueKey - loanAsset { - address - symbol - decimals - } - collateralAsset { - address - symbol - decimals - } - oracle { - address - } - irmAddress - lltv - state { - supplyAssets - borrowAssets - liquidityAssets - } - } - supplyAssets - supplyCap - } - } - } - } -`; diff --git a/src/hooks/queries/useOracleDataQuery.ts b/src/hooks/queries/useOracleDataQuery.ts deleted file mode 100644 index fa91428b..00000000 --- a/src/hooks/queries/useOracleDataQuery.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; -import { oraclesQuery } from '@/graphql/morpho-api-queries'; -import { ALL_SUPPORTED_NETWORKS } from '@/utils/networks'; -import type { MorphoChainlinkOracleData, OraclesQueryResponse } from '@/utils/types'; -import { URLS } from '@/utils/urls'; - -const createKey = (address: string, chainId: number): string => { - return `${address.toLowerCase()}-${chainId}`; -}; - -async function fetchOracleData(): Promise> { - const fetchedMap = new Map(); - - await Promise.all( - ALL_SUPPORTED_NETWORKS.map(async (network) => { - let skip = 0; - const pageSize = 1000; - - try { - while (true) { - const variables = { - first: pageSize, - skip, - where: { chainId_in: [network] }, - }; - - const response = await fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: oraclesQuery, variables }), - }); - - if (!response.ok) break; - - const result = (await response.json()) as OraclesQueryResponse; - if (result.errors) break; - - const items = result.data?.oracles?.items; - if (!items || items.length === 0) break; - - for (const oracle of items) { - if (oracle.data) { - const key = createKey(oracle.address, oracle.chain.id); - fetchedMap.set(key, oracle.data); - } - } - - if (items.length < pageSize) break; - skip += pageSize; - } - } catch (networkError) { - console.error(`Error fetching oracles for network ${network}:`, networkError); - } - }), - ); - - return fetchedMap; -} - -export const useOracleDataQuery = () => { - const query = useQuery({ - queryKey: ['oracle-data'], - queryFn: fetchOracleData, - staleTime: 5 * 60 * 1000, - refetchInterval: 5 * 60 * 1000, - refetchOnWindowFocus: true, - }); - - const dataMap = useMemo(() => { - return query.data ?? new Map(); - }, [query.data]); - - const getOracleData = useCallback( - (oracleAddress: string, chainId: number): MorphoChainlinkOracleData | null => { - const key = createKey(oracleAddress, chainId); - return dataMap.get(key) ?? null; - }, - [dataMap], - ); - - return { - getOracleData, - loading: query.isLoading, - error: query.error, - refetch: query.refetch, - }; -}; diff --git a/src/hooks/useFeedLastUpdatedByChain.ts b/src/hooks/useFeedLastUpdatedByChain.ts index 5fa4a1fe..a43406d0 100644 --- a/src/hooks/useFeedLastUpdatedByChain.ts +++ b/src/hooks/useFeedLastUpdatedByChain.ts @@ -5,7 +5,6 @@ import { usePublicClient } from 'wagmi'; import { chainlinkAggregatorV3Abi } from '@/abis/chainlink-aggregator-v3'; import { formatOraclePrice, type FeedUpdateKind } from '@/utils/oracle'; import { - isMetaOracleData, useOracleMetadata, type EnrichedFeed, type OracleMetadataRecord, @@ -84,15 +83,15 @@ function getFeedMetadataSnapshot(metadataRecord: OracleMetadataRecord | undefine const hintByAddress: Record = {}; for (const oracle of Object.values(metadataRecord)) { - if (!oracle?.data) continue; - - if (isMetaOracleData(oracle.data)) { + if (oracle?.type === 'meta') { addStandardOracleFeeds(feedSet, hintByAddress, oracle.data.oracleSources.primary); addStandardOracleFeeds(feedSet, hintByAddress, oracle.data.oracleSources.backup); continue; } - addStandardOracleFeeds(feedSet, hintByAddress, oracle.data); + if (oracle?.type === 'standard') { + addStandardOracleFeeds(feedSet, hintByAddress, oracle.data); + } } return { @@ -196,7 +195,7 @@ export function useFeedLastUpdatedByChain(chainId: SupportedNetworks | number | const updatedAtSeconds = updatedAt > 0n ? Number(updatedAt) : null; const normalizedPrice = formatOraclePrice(answer, decimals); - const isDerivedCandidate = hintByAddress[feedAddress]?.derivedCandidate === true; + const isDerivedCandidate = hintByAddress[feedAddress.toLowerCase()]?.derivedCandidate === true; const updateKind: FeedUpdateKind = isDerivedCandidate && updatedAtSeconds != null && updatedAtSeconds === queryBlockTimestamp ? 'derived' : 'reported'; diff --git a/src/hooks/useMarketData.ts b/src/hooks/useMarketData.ts index fd6f10ca..7c74812a 100644 --- a/src/hooks/useMarketData.ts +++ b/src/hooks/useMarketData.ts @@ -1,8 +1,6 @@ -import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { usePublicClient } from 'wagmi'; import { supportsMorphoApi } from '@/config/dataSources'; -import { useOracleDataQuery } from '@/hooks/queries/useOracleDataQuery'; import { fetchMorphoMarket } from '@/data-sources/morpho-api/market'; import { fetchSubgraphMarket } from '@/data-sources/subgraph/market'; import type { SupportedNetworks } from '@/utils/networks'; @@ -12,7 +10,6 @@ import type { Market } from '@/utils/types'; export const useMarketData = (uniqueKey: string | undefined, network: SupportedNetworks | undefined) => { const queryKey = ['marketData', uniqueKey, network]; const publicClient = usePublicClient({ chainId: network }); - const { getOracleData } = useOracleDataQuery(); const { data, isLoading, error, refetch } = useQuery({ queryKey: queryKey, @@ -98,27 +95,8 @@ export const useMarketData = (uniqueKey: string | undefined, network: SupportedN retry: 1, }); - // Enrich with oracle data OUTSIDE the query to avoid re-triggering the entire fetch - // when oracle data context updates - const enrichedMarket = useMemo(() => { - if (!data || !network) return data; - - const oracleData = getOracleData(data.oracleAddress, network); - - if (oracleData) { - return { - ...data, - oracle: { - data: oracleData, - }, - }; - } - - return data; - }, [data, network, getOracleData]); - return { - data: enrichedMarket, + data, isLoading: isLoading, error: error, refetch: refetch, diff --git a/src/hooks/useOracleMetadata.ts b/src/hooks/useOracleMetadata.ts index 28528aa0..02119ef4 100644 --- a/src/hooks/useOracleMetadata.ts +++ b/src/hooks/useOracleMetadata.ts @@ -13,7 +13,9 @@ import { ALL_SUPPORTED_NETWORKS, type SupportedNetworks } from '@/utils/networks * 1. Oracles scanner fetches from provider APIs (Chainlink, Redstone, etc.) * 2. Scanner publishes enriched data to GitHub Gist * 3. This hook fetches directly from the centralized Gist - * 4. Components use getOracleFromMetadata() + getFeedFromOracleData() to access data + * 4. Components read the scanner-native shapes directly via + * getOracleFromMetadata(), getStandardOracleDataFromMetadata(), and + * getMetaOracleDataFromMetadata() */ export type OracleFeedProvider = string | null; @@ -68,24 +70,40 @@ export type MetaOracleOutputData = { }; }; -export type OracleOutput = { +export type NonStandardOracleOutputData = { + reason: string; +}; + +type OracleOutputBase = { address: string; chainId: number; - type: 'standard' | 'custom' | 'unknown' | 'meta'; verifiedByFactory: boolean; isUpgradable: boolean; + lastUpdated: string; + lastScannedAt: string; proxy: { isProxy: boolean; proxyType?: string; implementation?: string; }; - data: OracleOutputData | MetaOracleOutputData; - lastScannedAt: string; }; -export function isMetaOracleData(data: OracleOutputData | MetaOracleOutputData): data is MetaOracleOutputData { - return 'oracleSources' in data; -} +export type StandardOracleOutput = OracleOutputBase & { + type: 'standard'; + data: OracleOutputData; +}; + +export type MetaOracleOutput = OracleOutputBase & { + type: 'meta'; + data: MetaOracleOutputData; +}; + +export type NonStandardOracleOutput = OracleOutputBase & { + type: 'custom' | 'unknown'; + data: NonStandardOracleOutputData; +}; + +export type OracleOutput = StandardOracleOutput | MetaOracleOutput | NonStandardOracleOutput; export type OracleMetadataFile = { version: string; @@ -193,24 +211,32 @@ export function getOracleFromMetadata( return metadataRecord[key]; } -/** - * Get feed info by address from an oracle's data - * Includes null guards for malformed external data - */ -export function getFeedFromOracleData(oracleData: OracleOutputData | undefined, feedAddress: string): EnrichedFeed | null { - if (!oracleData || !feedAddress) return null; +export function getStandardOracleDataFromMetadata( + metadataRecord: OracleMetadataRecord | OracleMetadataMap | undefined, + oracleAddress: string | undefined, + chainId?: number, +): OracleOutputData | undefined { + const oracle = getOracleFromMetadata(metadataRecord, oracleAddress, chainId); - const lowerFeed = feedAddress.toLowerCase(); - const feeds = [oracleData.baseFeedOne, oracleData.baseFeedTwo, oracleData.quoteFeedOne, oracleData.quoteFeedTwo]; + if (!oracle || oracle.type !== 'standard') { + return undefined; + } - for (const feed of feeds) { - // Null guard: ensure feed exists and has a valid address string - if (feed && typeof feed.address === 'string' && feed.address && feed.address.toLowerCase() === lowerFeed) { - return feed; - } + return oracle.data; +} + +export function getMetaOracleDataFromMetadata( + metadataRecord: OracleMetadataRecord | OracleMetadataMap | undefined, + oracleAddress: string | undefined, + chainId?: number, +): MetaOracleOutputData | undefined { + const oracle = getOracleFromMetadata(metadataRecord, oracleAddress, chainId); + + if (!oracle || oracle.type !== 'meta') { + return undefined; } - return null; + return oracle.data; } /** diff --git a/src/hooks/useProcessedMarkets.ts b/src/hooks/useProcessedMarkets.ts index 2093409c..591d56bb 100644 --- a/src/hooks/useProcessedMarkets.ts +++ b/src/hooks/useProcessedMarkets.ts @@ -1,6 +1,5 @@ import { useMemo } from 'react'; import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery'; -import { useOracleDataQuery } from '@/hooks/queries/useOracleDataQuery'; import { useTokenPrices } from '@/hooks/useTokenPrices'; import { useBlacklistedMarkets } from '@/stores/useBlacklistedMarkets'; import { useAppSettings } from '@/stores/useAppSettings'; @@ -34,7 +33,7 @@ const computeUsdValue = (assets: string, decimals: number, price: number): numbe }; /** - * Processes raw markets data with blacklist filtering and oracle enrichment. + * Processes raw markets data with blacklist filtering. * * It provides the foundation data that other hooks can build upon. * @@ -56,13 +55,12 @@ const computeUsdValue = (assets: string, decimals: number, price: number): numbe export const useProcessedMarkets = () => { const { data: rawMarketsFromQuery, isLoading, isRefetching, error, refetch } = useMarketsQuery(); const { getAllBlacklistedKeys, customBlacklistedMarkets } = useBlacklistedMarkets(); - const { getOracleData } = useOracleDataQuery(); const { showUnwhitelistedMarkets } = useAppSettings(); // Get blacklisted keys (memoized to prevent infinite loops) const allBlacklistedMarketKeys = useMemo(() => getAllBlacklistedKeys(), [customBlacklistedMarkets, getAllBlacklistedKeys]); - // Process markets: blacklist filter + oracle enrichment + // Process markets: blacklist filter + force-unwhitelisted overrides const processedData = useMemo(() => { if (!rawMarketsFromQuery) { return { @@ -78,14 +76,12 @@ export const useProcessedMarkets = () => { // Apply blacklist filter const blacklistFiltered = rawMarketsUnfiltered.filter((market) => !allBlacklistedMarketKeys.has(market.uniqueKey)); - // Enrich with oracle data and apply force-unwhitelisted overrides + // Apply force-unwhitelisted overrides after blacklist filtering const enriched = blacklistFiltered.map((market) => { - const oracleData = getOracleData(market.oracleAddress, market.morphoBlue.chain.id); const shouldForceUnwhitelist = isForceUnwhitelisted(market.uniqueKey); return { ...market, - ...(oracleData && { oracle: { data: oracleData } }), ...(shouldForceUnwhitelist && { whitelisted: false }), }; }); @@ -101,7 +97,7 @@ export const useProcessedMarkets = () => { allMarkets, whitelistedMarkets, }; - }, [rawMarketsFromQuery, allBlacklistedMarketKeys, getOracleData]); + }, [rawMarketsFromQuery, allBlacklistedMarketKeys]); // Build token list for USD fallbacks only when needed const tokensForUsdFallback = useMemo(() => { diff --git a/src/utils/marketFilters.ts b/src/utils/marketFilters.ts index f11959a8..544221e9 100644 --- a/src/utils/marketFilters.ts +++ b/src/utils/marketFilters.ts @@ -6,7 +6,7 @@ */ import { LOCKED_MARKET_APY_THRESHOLD } from '@/constants/markets'; -import { type OracleMetadataRecord, getOracleFromMetadata, isMetaOracleData } from '@/hooks/useOracleMetadata'; +import { type OracleMetadataRecord, getMetaOracleDataFromMetadata, getStandardOracleDataFromMetadata } from '@/hooks/useOracleMetadata'; import { parseNumericThreshold } from '@/utils/markets'; import type { SupportedNetworks } from '@/utils/networks'; import { parsePriceFeedVendors, parseMetaOracleVendors, type PriceFeedVendors, getOracleType, OracleType } from '@/utils/oracle'; @@ -101,23 +101,22 @@ export const createUnknownOracleFilter = (showUnknownOracle: boolean, oracleMeta return () => true; } return (market) => { - const oracleType = getOracleType(market.oracle?.data, market.oracleAddress, market.morphoBlue.chain.id, oracleMetadataMap); + const chainId = market.morphoBlue.chain.id; + const oracleType = getOracleType(market.oracleAddress, chainId, oracleMetadataMap); if (oracleType === OracleType.Meta) { - const metadata = getOracleFromMetadata(oracleMetadataMap, market.oracleAddress, market.morphoBlue.chain.id); - if (metadata?.data && isMetaOracleData(metadata.data)) { - const info = parseMetaOracleVendors(metadata.data); + const metadata = getMetaOracleDataFromMetadata(oracleMetadataMap, market.oracleAddress, chainId); + if (metadata) { + const info = parseMetaOracleVendors(metadata); return !info.hasUnknown; } return false; } - if (!market.oracle) return false; + const standardOracleData = getStandardOracleDataFromMetadata(oracleMetadataMap, market.oracleAddress, chainId); + if (!standardOracleData) return false; - const info = parsePriceFeedVendors(market.oracle.data, market.morphoBlue.chain.id, { - metadataMap: oracleMetadataMap, - oracleAddress: market.oracleAddress, - }); + const info = parsePriceFeedVendors(standardOracleData); const isCustom = oracleType === OracleType.Custom; const isUnknown = isCustom || (info?.hasUnknown ?? false); @@ -174,11 +173,9 @@ export const createOracleFilter = (selectedOracles: PriceFeedVendors[], oracleMe return () => true; } return (market) => { - if (!market.oracle) return false; - const marketOracles = parsePriceFeedVendors(market.oracle.data, market.morphoBlue.chain.id, { - metadataMap: oracleMetadataMap, - oracleAddress: market.oracleAddress, - }).vendors; + const marketOracles = parsePriceFeedVendors( + getStandardOracleDataFromMetadata(oracleMetadataMap, market.oracleAddress, market.morphoBlue.chain.id), + ).vendors; return marketOracles.some((oracle) => selectedOracles.includes(oracle)); }; }; @@ -243,10 +240,9 @@ export const createSearchFilter = (searchQuery: string, oracleMetadataMap?: Orac } const lowercaseQuery = searchQuery.toLowerCase().trim(); return (market) => { - const { vendors } = parsePriceFeedVendors(market.oracle?.data, market.morphoBlue.chain.id, { - metadataMap: oracleMetadataMap, - oracleAddress: market.oracleAddress, - }); + const { vendors } = parsePriceFeedVendors( + getStandardOracleDataFromMetadata(oracleMetadataMap, market.oracleAddress, market.morphoBlue.chain.id), + ); const vendorsName = vendors.join(','); return ( market.uniqueKey.toLowerCase().includes(lowercaseQuery) || diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts index f2a8aa8c..fb3cb2a3 100644 --- a/src/utils/oracle.ts +++ b/src/utils/oracle.ts @@ -1,32 +1,24 @@ /** * Oracle Utilities * - * This module provides utilities for working with oracle data from two sources: - * 1. Morpho API - Basic oracle/feed info (OracleFeed, MorphoChainlinkOracleData) - * 2. Oracles Scanner - Extended metadata (EnrichedFeed via useOracleMetadata hook) - * - * Type hierarchy: - * - OracleFeed: Basic feed from Morpho API - * - EnrichedFeed: Extended feed from oracles scanner (includes provider, tier, heartbeat, deviationThreshold, ens, feedType) - * - FeedData: Simplified type for UI components + * This module provides utilities for working with scanner-backed oracle + * metadata. Standard and meta oracle rendering, filtering, and warning logic + * should resolve through that metadata rather than Morpho API feed payloads. * * For full type system documentation, see: * https://github.com/monarch-xyz/oracles/blob/master/docs/TYPES.md */ -import { formatUnits, zeroAddress, type Address } from 'viem'; +import { formatUnits } from 'viem'; import { - getFeedFromOracleData, getOracleFromMetadata, - isMetaOracleData, type EnrichedFeed, type MetaOracleOutputData, type OracleFeedProvider, type OracleMetadataRecord, type OracleOutputData, } from '@/hooks/useOracleMetadata'; -import { SupportedNetworks, isSupportedChain } from './networks'; -import type { MorphoChainlinkOracleData, OracleFeed } from './types'; +import { SupportedNetworks } from './networks'; type VendorInfo = { coreVendors: PriceFeedVendors[]; // Well-known vendors (Chainlink, Redstone, etc.) @@ -110,29 +102,8 @@ export function getChainlinkFeedUrl(chainId: number, ens: string): string { return `https://data.chain.link/feeds/${path}/${ens}`; } -// Simplified feed data structure (replacing old vendor-specific types) -export type FeedData = { - address: string; - vendor: string; - description: string; - pair: [string, string]; - decimals: number; - tier?: string; // Chainlink feed category: "verified", "high", "medium", "low", "custom", etc. - heartbeat?: number; - deviationThreshold?: number; - ens?: string; // Chainlink ENS name for feed URL (e.g. "eth-usd") - feedType?: string; // Redstone feed type: "market" or "fundamental" - baseDiscountPerYear?: string; // Pendle base discount per year (raw 18-decimal value) - innerOracle?: string; // Pendle inner oracle address - pt?: string; // Pendle PT token address - ptSymbol?: string; // Pendle PT token symbol - pendleFeedKind?: string; // Pendle feed kind (e.g. "PendleChainlinkOracle", "LinearDiscount") - pendleFeedSubtype?: string; // Pendle subtype (e.g. "SparkLinearDiscountOracle") -}; - export type FeedVendorResult = { vendor: PriceFeedVendors; - data: FeedData | null; assetPair: { baseAsset: string; quoteAsset: string; @@ -147,7 +118,6 @@ export function detectFeedVendorFromMetadata(feed: EnrichedFeed | null | undefin if (!feed) { return { vendor: PriceFeedVendors.Unknown, - data: null, assetPair: { baseAsset: 'Unknown', quoteAsset: 'Unknown' }, }; } @@ -188,112 +158,33 @@ export function detectFeedVendorFromMetadata(feed: EnrichedFeed | null | undefin } } - const feedData: FeedData = { - address: feed.address, - vendor: feed.provider ?? (isPendleFeed ? PriceFeedVendors.Pendle : 'Unknown'), - description: feed.description, - pair: [baseAsset, quoteAsset] as [string, string], - decimals: feed.decimals ?? 18, - tier: feed.tier, - heartbeat: feed.heartbeat, - deviationThreshold: feed.deviationThreshold, - ens: feed.ens, - feedType: feed.feedType, - baseDiscountPerYear: feed.baseDiscountPerYear, - innerOracle: feed.innerOracle, - pt: feed.pt, - ptSymbol: feed.ptSymbol, - pendleFeedKind: feed.pendleFeedKind, - pendleFeedSubtype: feed.pendleFeedSubtype, - }; - return { vendor, - data: feedData, assetPair: { baseAsset, quoteAsset }, }; } -/** - * Legacy feed vendor detection (fallback when metadata not available) - * @deprecated Use detectFeedVendorFromMetadata with oracle metadata instead - */ -export function detectFeedVendor(_feedAddress: Address | string, _chainId: number): FeedVendorResult { - // Without static data files, we return Unknown - // The metadata-based detection should be used instead - return { - vendor: PriceFeedVendors.Unknown, - data: null, - assetPair: { baseAsset: 'Unknown', quoteAsset: 'Unknown' }, - }; -} - export function getOracleTypeDescription(oracleType: OracleType): string { if (oracleType === OracleType.Standard) return 'Standard Oracle'; if (oracleType === OracleType.Meta) return 'Meta Oracle'; return 'Custom Oracle'; } -/** - * Get feed path from oracle feed - */ -function getFeedPath( - feed: OracleFeed | null | undefined, - _chainId: number, - oracleMetadataData?: OracleOutputData, -): { base: string; quote: string } { - if (!feed || !feed.address) return { base: 'EMPTY', quote: 'EMPTY' }; - - // Try to get from metadata first - if (oracleMetadataData) { - const enrichedFeed = getFeedFromOracleData(oracleMetadataData, feed.address); - if (enrichedFeed && enrichedFeed.pair.length === 2) { - return { base: enrichedFeed.pair[0], quote: enrichedFeed.pair[1] }; - } - } - - return { base: 'Unknown', quote: 'Unknown' }; -} - export function getOracleType( - oracleData: MorphoChainlinkOracleData | null | undefined, oracleAddress?: string, chainId?: number, metadataMap?: OracleMetadataRecord, ) { - // Check scanner metadata for oracle type (meta or standard with vault-only) if (metadataMap && oracleAddress) { const metadata = getOracleFromMetadata(metadataMap, oracleAddress, chainId); if (metadata?.type === 'meta') return OracleType.Meta; if (metadata?.type === 'standard') return OracleType.Standard; } - // Morpho API only contains oracleData if it follows the standard MorphoOracle structure with feeds - if (!oracleData) return OracleType.Custom; - - if ( - oracleData.baseFeedOne !== null || - oracleData.baseFeedTwo !== null || - oracleData.quoteFeedOne !== null || - oracleData.quoteFeedTwo !== null - ) - return OracleType.Standard; - - // Other logics to determine oracle types - if (oracleAddress === zeroAddress || (chainId && isSupportedChain(chainId))) return OracleType.Custom; return OracleType.Custom; } -type ParsePriceFeedVendorsOptions = { - metadataMap?: OracleMetadataRecord; - oracleAddress?: string; -}; - -export function parsePriceFeedVendors( - oracleData: MorphoChainlinkOracleData | null | undefined, - chainId: number, - options?: ParsePriceFeedVendorsOptions, -): VendorInfo { +export function parsePriceFeedVendors(oracleData: OracleOutputData | null | undefined): VendorInfo { if (!oracleData) { return { coreVendors: [], @@ -305,92 +196,8 @@ export function parsePriceFeedVendors( }; } - if (!oracleData.baseFeedOne && !oracleData.baseFeedTwo && !oracleData.quoteFeedOne && !oracleData.quoteFeedTwo) { - // Check if this is a vault-only oracle (no feeds but has vault conversion) - const oracleMetadata = - options?.metadataMap && options.oracleAddress - ? getOracleFromMetadata(options.metadataMap, options.oracleAddress, chainId) - : undefined; - const oracleMetadataData = oracleMetadata?.data && !isMetaOracleData(oracleMetadata.data) ? oracleMetadata.data : undefined; - const hasVault = oracleMetadataData?.baseVault || oracleMetadataData?.quoteVault; - - // Vault-only oracles are valid — don't mark as unknown - if (hasVault) { - return { - coreVendors: [], - taggedVendors: [], - hasCompletelyUnknown: false, - hasTaggedUnknown: false, - vendors: [], - hasUnknown: false, - }; - } - - return { - coreVendors: [], - taggedVendors: [], - hasCompletelyUnknown: true, - hasTaggedUnknown: false, - vendors: [], - hasUnknown: true, - }; - } - const feeds = [oracleData.baseFeedOne, oracleData.baseFeedTwo, oracleData.quoteFeedOne, oracleData.quoteFeedTwo]; - - const coreVendors = new Set(); - const taggedVendors = new Set(); - let hasCompletelyUnknown = false; - let hasTaggedUnknown = false; - - // Try to get enriched metadata for this oracle - const oracleMetadata = - options?.metadataMap && options.oracleAddress ? getOracleFromMetadata(options.metadataMap, options.oracleAddress, chainId) : undefined; - const oracleMetadataData = oracleMetadata?.data && !isMetaOracleData(oracleMetadata.data) ? oracleMetadata.data : undefined; - - for (const feed of feeds) { - if (feed?.address) { - // Prefer metadata-based detection - let feedResult: FeedVendorResult; - if (oracleMetadataData) { - const enrichedFeed = getFeedFromOracleData(oracleMetadataData, feed.address); - feedResult = enrichedFeed ? detectFeedVendorFromMetadata(enrichedFeed) : detectFeedVendor(feed.address, chainId); - } else { - feedResult = detectFeedVendor(feed.address, chainId); - } - - if (feedResult.vendor === PriceFeedVendors.Unknown) { - // Check if this unknown feed actually has data (tagged by metadata) - const taggedVendor = feedResult.data?.vendor; - if (taggedVendor && taggedVendor !== 'Unknown') { - taggedVendors.add(taggedVendor); - hasTaggedUnknown = true; - } else { - hasCompletelyUnknown = true; - } - } else { - coreVendors.add(feedResult.vendor); - } - } - } - - // If we have no feeds with addresses, that should be considered as completely unknown - const hasFeeds = feeds.some((feed) => feed?.address); - if (!hasFeeds) { - hasCompletelyUnknown = true; - } - - const legacyVendors = Array.from(coreVendors); - const legacyHasUnknown = hasCompletelyUnknown || hasTaggedUnknown; - - return { - coreVendors: Array.from(coreVendors), - taggedVendors: Array.from(taggedVendors), - hasCompletelyUnknown, - hasTaggedUnknown, - vendors: legacyVendors, - hasUnknown: legacyHasUnknown, - }; + return classifyEnrichedFeeds(feeds); } /** @@ -408,8 +215,8 @@ function classifyEnrichedFeeds(feeds: (EnrichedFeed | null)[]): VendorInfo { const feedResult = detectFeedVendorFromMetadata(feed); if (feedResult.vendor === PriceFeedVendors.Unknown) { - const taggedVendor = feedResult.data?.vendor; - if (taggedVendor && taggedVendor !== 'Unknown') { + const taggedVendor = feed.provider?.trim(); + if (taggedVendor) { taggedVendors.add(taggedVendor); hasTaggedUnknown = true; } else { @@ -421,13 +228,7 @@ function classifyEnrichedFeeds(feeds: (EnrichedFeed | null)[]): VendorInfo { } } - const hasFeeds = feeds.some((feed) => feed?.address); - if (!hasFeeds) { - hasCompletelyUnknown = true; - } - const legacyVendors = Array.from(coreVendors); - const legacyHasUnknown = hasCompletelyUnknown || hasTaggedUnknown; return { coreVendors: Array.from(coreVendors), @@ -435,7 +236,7 @@ function classifyEnrichedFeeds(feeds: (EnrichedFeed | null)[]): VendorInfo { hasCompletelyUnknown, hasTaggedUnknown, vendors: legacyVendors, - hasUnknown: legacyHasUnknown, + hasUnknown: hasCompletelyUnknown || hasTaggedUnknown, }; } @@ -560,43 +361,32 @@ function validateFeedPaths(feedPaths: FeedPathEntry[], collateralSymbol: string, }; } -export function checkFeedsPath( - oracleData: MorphoChainlinkOracleData | null | undefined, - chainId: number, - collateralSymbol: string, - loanSymbol: string, - options?: ParsePriceFeedVendorsOptions, -): CheckFeedsPathResult { +export function checkFeedsPath(oracleData: OracleOutputData | null | undefined, collateralSymbol: string, loanSymbol: string): CheckFeedsPathResult { if (!oracleData) { return { isValid: false, missingPath: 'No oracle data provided' }; } - // Get metadata for feed path resolution - const oracleMetadata = - options?.metadataMap && options?.oracleAddress ? getOracleFromMetadata(options.metadataMap, options.oracleAddress, chainId) : undefined; - const oracleMetadataData = oracleMetadata?.data && !isMetaOracleData(oracleMetadata.data) ? oracleMetadata.data : undefined; - const feedPaths: FeedPathEntry[] = [ { feed: oracleData.baseFeedOne, type: 'base1' as const }, { feed: oracleData.baseFeedTwo, type: 'base2' as const }, { feed: oracleData.quoteFeedOne, type: 'quote1' as const }, { feed: oracleData.quoteFeedTwo, type: 'quote2' as const }, ].map(({ feed, type }) => ({ - path: getFeedPath(feed, chainId, oracleMetadataData), + path: getEnrichedFeedPath(feed), type, hasData: !!feed?.address, })); - if (oracleMetadataData?.baseVault?.pair?.length === 2) { + if (oracleData.baseVault?.pair?.length === 2) { feedPaths.push({ - path: { base: oracleMetadataData.baseVault.pair[0], quote: oracleMetadataData.baseVault.pair[1] }, + path: { base: oracleData.baseVault.pair[0], quote: oracleData.baseVault.pair[1] }, type: 'baseVault', hasData: true, }); } - if (oracleMetadataData?.quoteVault?.pair?.length === 2) { + if (oracleData.quoteVault?.pair?.length === 2) { feedPaths.push({ - path: { base: oracleMetadataData.quoteVault.pair[0], quote: oracleMetadataData.quoteVault.pair[1] }, + path: { base: oracleData.quoteVault.pair[0], quote: oracleData.quoteVault.pair[1] }, type: 'quoteVault', hasData: true, }); @@ -618,33 +408,7 @@ function getEnrichedFeedPath(feed: EnrichedFeed | null): { base: string; quote: * Check feed paths for meta oracles using pre-enriched scanner data */ export function checkEnrichedFeedsPath(oracleData: OracleOutputData, collateralSymbol: string, loanSymbol: string): CheckFeedsPathResult { - const feedPaths: FeedPathEntry[] = [ - { feed: oracleData.baseFeedOne, type: 'base1' as const }, - { feed: oracleData.baseFeedTwo, type: 'base2' as const }, - { feed: oracleData.quoteFeedOne, type: 'quote1' as const }, - { feed: oracleData.quoteFeedTwo, type: 'quote2' as const }, - ].map(({ feed, type }) => ({ - path: getEnrichedFeedPath(feed), - type, - hasData: !!feed?.address, - })); - - if (oracleData.baseVault?.pair?.length === 2) { - feedPaths.push({ - path: { base: oracleData.baseVault.pair[0], quote: oracleData.baseVault.pair[1] }, - type: 'baseVault', - hasData: true, - }); - } - if (oracleData.quoteVault?.pair?.length === 2) { - feedPaths.push({ - path: { base: oracleData.quoteVault.pair[0], quote: oracleData.quoteVault.pair[1] }, - type: 'quoteVault', - hasData: true, - }); - } - - return validateFeedPaths(feedPaths, collateralSymbol, loanSymbol); + return checkFeedsPath(oracleData, collateralSymbol, loanSymbol); } /** diff --git a/src/utils/types.ts b/src/utils/types.ts index 0f733b9d..c9130c31 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -68,24 +68,6 @@ export type WhitelistMarketResponse = { }; }; -export type OracleFeedsInfo = { - baseFeedOneAddress: string; - baseFeedOneDescription: string | null; - baseFeedTwoAddress: string; - baseFeedTwoDescription: string | null; - quoteFeedOneAddress: string; - quoteFeedOneDescription: string | null; - quoteFeedTwoAddress: string; - quoteFeedTwoDescription: string | null; - baseVault: string; - baseVaultDescription: string | null; - baseVaultVendor: string | null; - quoteVault: string; - quoteVaultDescription: string | null; - quoteVaultVendor: string | null; - __typename: string; -}; - export type MarketWarning = { type: string; level: string; @@ -252,47 +234,6 @@ export type GroupedPosition = { allWarnings: WarningWithDetail[]; }; -export type OracleFeed = { - address: string; - chain: { - id: number; - }; - id: string; - pair: string[] | null; -}; - -export type MorphoChainlinkOracleData = { - baseFeedOne: OracleFeed | null; - baseFeedTwo: OracleFeed | null; - quoteFeedOne: OracleFeed | null; - quoteFeedTwo: OracleFeed | null; -}; - -// Oracle item from Morpho API oracles query -export type OracleItem = { - address: string; - chain: { - id: number; - }; - data: MorphoChainlinkOracleData | null; -}; - -// Oracles query response from Morpho API -export type OraclesQueryResponse = { - data: { - oracles: { - items: OracleItem[]; - pageInfo: { - countTotal: number; - count: number; - limit: number; - skip: number; - }; - }; - }; - errors?: { message: string }[]; -}; - export type Market = { id: string; lltv: string; @@ -345,9 +286,6 @@ export type Market = { }[]; hasUSDPrice: boolean; warnings: MarketWarning[]; - oracle?: { - data: MorphoChainlinkOracleData; - }; }; export type TimeseriesDataPoint = { diff --git a/src/utils/warnings.ts b/src/utils/warnings.ts index 2eb0654d..bde36882 100644 --- a/src/utils/warnings.ts +++ b/src/utils/warnings.ts @@ -1,4 +1,4 @@ -import { getOracleFromMetadata, isMetaOracleData, type OracleMetadataRecord } from '@/hooks/useOracleMetadata'; +import { getMetaOracleDataFromMetadata, getStandardOracleDataFromMetadata, type OracleMetadataRecord } from '@/hooks/useOracleMetadata'; import type { Market, MarketWarning } from '@/utils/types'; import { monarchWhitelistedMarkets, getMarketOverrideWarnings } from './markets'; import { getOracleType, OracleType, parsePriceFeedVendors, parseMetaOracleVendors, checkFeedsPath, checkEnrichedFeedsPath } from './oracle'; @@ -175,14 +175,14 @@ export const getMarketWarningsWithDetail = (market: Market, optionsOrWhitelist?: } // Append our own oracle warnings - const oracleType = getOracleType(market.oracle?.data, market.oracleAddress, market.morphoBlue.chain.id, oracleMetadataMap); + const chainId = market.morphoBlue.chain.id; + const oracleType = getOracleType(market.oracleAddress, chainId, oracleMetadataMap); if (oracleType === OracleType.Custom) result.push(UNRECOGNIZED_ORACLE); // if any of the feeds are not null but also not recognized, return appropriate feed warning - if (oracleType === OracleType.Standard && market.oracle?.data) { - const metadataOptions = oracleMetadataMap ? { metadataMap: oracleMetadataMap, oracleAddress: market.oracleAddress } : undefined; - - const vendorInfo = parsePriceFeedVendors(market.oracle.data, market.morphoBlue.chain.id, metadataOptions); + const standardOracleData = getStandardOracleDataFromMetadata(oracleMetadataMap, market.oracleAddress, chainId); + if (oracleType === OracleType.Standard && standardOracleData) { + const vendorInfo = parsePriceFeedVendors(standardOracleData); // Completely unknown feeds get the stronger warning if (vendorInfo.hasCompletelyUnknown) { @@ -196,13 +196,7 @@ export const getMarketWarningsWithDetail = (market: Market, optionsOrWhitelist?: // Check if oracle feeds can produce a valid price path if (market.collateralAsset?.symbol && market.loanAsset?.symbol) { - const feedsPathResult = checkFeedsPath( - market.oracle.data, - market.morphoBlue.chain.id, - market.collateralAsset.symbol, - market.loanAsset.symbol, - metadataOptions, - ); + const feedsPathResult = checkFeedsPath(standardOracleData, market.collateralAsset.symbol, market.loanAsset.symbol); if (feedsPathResult.hasUnknownFeed) { // only append this error if it doesn't already have "UNRECOGNIZED_FEEDS" @@ -220,18 +214,18 @@ export const getMarketWarningsWithDetail = (market: Market, optionsOrWhitelist?: // Meta oracles: run vendor + feed path checks on both primary and backup oracle feeds if (oracleType === OracleType.Meta && oracleMetadataMap) { - const metadata = getOracleFromMetadata(oracleMetadataMap, market.oracleAddress); - if (metadata?.data && isMetaOracleData(metadata.data)) { - const vendorInfo = parseMetaOracleVendors(metadata.data); + const metadata = getMetaOracleDataFromMetadata(oracleMetadataMap, market.oracleAddress, chainId); + if (metadata) { + const vendorInfo = parseMetaOracleVendors(metadata); if (vendorInfo.hasCompletelyUnknown) result.push(UNRECOGNIZED_FEEDS); if (vendorInfo.hasTaggedUnknown) result.push(UNRECOGNIZED_FEEDS_TAGGED); if (market.collateralAsset?.symbol && market.loanAsset?.symbol) { - const primaryResult = metadata.data.oracleSources.primary - ? checkEnrichedFeedsPath(metadata.data.oracleSources.primary, market.collateralAsset.symbol, market.loanAsset.symbol) + const primaryResult = metadata.oracleSources.primary + ? checkEnrichedFeedsPath(metadata.oracleSources.primary, market.collateralAsset.symbol, market.loanAsset.symbol) : { isValid: true }; - const backupResult = metadata.data.oracleSources.backup - ? checkEnrichedFeedsPath(metadata.data.oracleSources.backup, market.collateralAsset.symbol, market.loanAsset.symbol) + const backupResult = metadata.oracleSources.backup + ? checkEnrichedFeedsPath(metadata.oracleSources.backup, market.collateralAsset.symbol, market.loanAsset.symbol) : { isValid: true }; const hasUnknown = primaryResult.hasUnknownFeed || backupResult.hasUnknownFeed; From d83470b417dd4adc099ff81f1e21b57098ff8540 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 21 Mar 2026 01:09:17 +0800 Subject: [PATCH 2/2] chore: fix lint --- src/data-sources/morpho-api/positions.ts | 4 +--- src/hooks/useFeedLastUpdatedByChain.ts | 7 +------ src/hooks/useUserPositions.ts | 2 +- src/utils/oracle.ts | 12 ++++++------ 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/data-sources/morpho-api/positions.ts b/src/data-sources/morpho-api/positions.ts index a5470d52..5d0335cb 100644 --- a/src/data-sources/morpho-api/positions.ts +++ b/src/data-sources/morpho-api/positions.ts @@ -85,9 +85,7 @@ type RawPositionMarketItem = { const MORPHO_POSITION_MARKETS_PAGE_SIZE = 500; -const hasNonZeroPositionState = ( - state: ValidPositionMarketItem['state'], -): boolean => { +const hasNonZeroPositionState = (state: ValidPositionMarketItem['state']): boolean => { if (!state) { return false; } diff --git a/src/hooks/useFeedLastUpdatedByChain.ts b/src/hooks/useFeedLastUpdatedByChain.ts index a43406d0..468a5407 100644 --- a/src/hooks/useFeedLastUpdatedByChain.ts +++ b/src/hooks/useFeedLastUpdatedByChain.ts @@ -4,12 +4,7 @@ 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 { - useOracleMetadata, - type EnrichedFeed, - type OracleMetadataRecord, - type OracleOutputData, -} from '@/hooks/useOracleMetadata'; +import { useOracleMetadata, type EnrichedFeed, type OracleMetadataRecord, type OracleOutputData } from '@/hooks/useOracleMetadata'; import type { SupportedNetworks } from '@/utils/networks'; const MAX_MULTICALL_FEEDS_PER_BATCH = 1000; diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 2d848dc8..1bb9e649 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -4,7 +4,7 @@ import type { Address } from 'viem'; import { supportsMorphoApi } from '@/config/dataSources'; import { fetchMorphoUserPositionMarkets, fetchMorphoUserPositionMarketsForNetworks } from '@/data-sources/morpho-api/positions'; import { fetchSubgraphUserPositionMarkets } from '@/data-sources/subgraph/positions'; -import { ALL_SUPPORTED_NETWORKS, SupportedNetworks } from '@/utils/networks'; +import { ALL_SUPPORTED_NETWORKS, type SupportedNetworks } from '@/utils/networks'; import { fetchLatestPositionSnapshotsWithOraclePrices, type PositionSnapshot, type PositionMarketOracleInput } from '@/utils/positions'; import { getClient } from '@/utils/rpc'; import type { Market, MarketPosition } from '@/utils/types'; diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts index fb3cb2a3..9b0f6d30 100644 --- a/src/utils/oracle.ts +++ b/src/utils/oracle.ts @@ -170,11 +170,7 @@ export function getOracleTypeDescription(oracleType: OracleType): string { return 'Custom Oracle'; } -export function getOracleType( - oracleAddress?: string, - chainId?: number, - metadataMap?: OracleMetadataRecord, -) { +export function getOracleType(oracleAddress?: string, chainId?: number, metadataMap?: OracleMetadataRecord) { if (metadataMap && oracleAddress) { const metadata = getOracleFromMetadata(metadataMap, oracleAddress, chainId); if (metadata?.type === 'meta') return OracleType.Meta; @@ -361,7 +357,11 @@ function validateFeedPaths(feedPaths: FeedPathEntry[], collateralSymbol: string, }; } -export function checkFeedsPath(oracleData: OracleOutputData | null | undefined, collateralSymbol: string, loanSymbol: string): CheckFeedsPathResult { +export function checkFeedsPath( + oracleData: OracleOutputData | null | undefined, + collateralSymbol: string, + loanSymbol: string, +): CheckFeedsPathResult { if (!oracleData) { return { isValid: false, missingPath: 'No oracle data provided' }; }