diff --git a/src/db/feedCategories.ts b/src/db/feedCategories.ts index 60003381a76..396b0a120b8 100644 --- a/src/db/feedCategories.ts +++ b/src/db/feedCategories.ts @@ -10,6 +10,13 @@ type FeedRiskRow = { risk_status: string | null } +type FeedRequest = { + contractAddress: string + network: string + shutdownDate?: string + fallbackCategory?: string +} + export type FeedTierResult = { final: string | null } /* =========================== @@ -89,7 +96,19 @@ const normalizeKey = (v?: string | null): CategoryKey | undefined => { return key in FEED_CATEGORY_CONFIG ? key : undefined } -const chooseTier = (dbTier: string | null | undefined, fallback?: string): string | null => dbTier ?? fallback ?? null +const FALLBACK_ONLY_CATEGORIES = new Set(["new", "custom"]) + +const resolveRiskStatus = ( + dbTier: string | null | undefined, + shutdownDate?: string, + fallbackCategory?: string +): string | null => { + if (dbTier != null) return dbTier + if (shutdownDate) return "deprecating" + if (fallbackCategory && FALLBACK_ONLY_CATEGORIES.has(fallbackCategory.toLowerCase())) + return fallbackCategory.toLowerCase() + return null +} const defaultCategoryList = () => Object.values(FEED_CATEGORY_CONFIG).map(({ key, name }) => ({ key, name })) @@ -129,21 +148,16 @@ export async function getFeedCategories() { /** * Batch lookup: returns a Map of `${address}-${network}` → { final }. - * Uses DB value when present; otherwise uses per-item fallback. + * Uses DB risk_status when present. If absent, infers "deprecating" from shutdownDate. + * Returns null when neither is available. */ -export async function getFeedRiskTiersBatch( - feedRequests: Array<{ - contractAddress: string - network: string - fallbackCategory?: string - }> -): Promise> { +export async function getFeedRiskTiersBatch(feedRequests: FeedRequest[]): Promise> { const out = new Map() const keyFor = (addr: string, net: string) => `${addr}-${net}` if (!supabase) { - feedRequests.forEach(({ contractAddress, network, fallbackCategory }) => - out.set(keyFor(contractAddress, network), { final: chooseTier(null, fallbackCategory) }) + feedRequests.forEach(({ contractAddress, network, shutdownDate, fallbackCategory }) => + out.set(keyFor(contractAddress, network), { final: resolveRiskStatus(null, shutdownDate, fallbackCategory) }) ) return out } @@ -160,8 +174,8 @@ export async function getFeedRiskTiersBatch( .limit(1000) if (error) { - feedRequests.forEach(({ contractAddress, network, fallbackCategory }) => - out.set(keyFor(contractAddress, network), { final: chooseTier(null, fallbackCategory) }) + feedRequests.forEach(({ contractAddress, network, shutdownDate, fallbackCategory }) => + out.set(keyFor(contractAddress, network), { final: resolveRiskStatus(null, shutdownDate, fallbackCategory) }) ) return out } @@ -171,15 +185,15 @@ export async function getFeedRiskTiersBatch( lookup.set(keyFor(row.proxy_address, row.network), row.risk_status ?? null) ) - feedRequests.forEach(({ contractAddress, network, fallbackCategory }) => { + feedRequests.forEach(({ contractAddress, network, shutdownDate, fallbackCategory }) => { const key = keyFor(contractAddress, network) - out.set(key, { final: chooseTier(lookup.get(key), fallbackCategory) }) + out.set(key, { final: resolveRiskStatus(lookup.get(key), shutdownDate, fallbackCategory) }) }) return out } catch (error) { - feedRequests.forEach(({ contractAddress, network, fallbackCategory }) => - out.set(keyFor(contractAddress, network), { final: chooseTier(null, fallbackCategory) }) + feedRequests.forEach(({ contractAddress, network, shutdownDate, fallbackCategory }) => + out.set(keyFor(contractAddress, network), { final: resolveRiskStatus(null, shutdownDate, fallbackCategory) }) ) return out } diff --git a/src/features/feeds/components/Tables.tsx b/src/features/feeds/components/Tables.tsx index 682276a44fe..c08f40a4091 100644 --- a/src/features/feeds/components/Tables.tsx +++ b/src/features/feeds/components/Tables.tsx @@ -14,7 +14,7 @@ import { REPORT_SCHEMA_DEFINITIONS, type SchemaDefinition } from "./reportSchema import { useBatchedFeedCategories, getFeedCategoryFromBatch, getNetworkIdentifier } from "./useBatchedFeedCategories.ts" import { isSharedSVR, isAaveSVR } from "~/features/feeds/utils/svrDetection.ts" import { ExpandableTableWrapper } from "./ExpandableTableWrapper.tsx" -import { isFeedVisible } from "~/features/feeds/utils/feedVisibility.ts" +import { isFeedVisible, shouldHideAddress } from "~/features/feeds/utils/feedVisibility.ts" const feedItems = monitoredFeeds.mainnet type StreamNetworkType = "mainnet" | "testnet" @@ -335,7 +335,7 @@ const TOKENIZED_EQUITY_CONTACT_EMAIL = "chainlink_data_feeds@smartcontract.com" const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, dataFeedType }) => { // Use the pre-computed finalCategory from enriched metadata // (already includes deprecating status and Supabase risk tier) - const finalTier = metadata.finalCategory || metadata.feedCategory + const finalTier = metadata.finalCategory ?? null // Feed type checks const isUSGovernmentMacroeconomicData = dataFeedType === "usGovernmentMacroeconomicData" @@ -345,9 +345,9 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d metadata.contractType !== "verifier" && metadata.docs?.productTypeCode === "primaryTokenizedPrice" - // Any feed with a calculated price (productSubType === "calculatedPrice") should - // have its address hidden and show a contact email instead. - const shouldHideAddress = metadata.docs?.productSubType === "calculatedPrice" + // Any feed with a calculated price, or one explicitly listed in CONTACT_EMAIL_PROXY_ADDRESSES, + // should have its address hidden and show a contact email instead. + const hideAddress = shouldHideAddress(metadata) // Stablecoin price-bound note: only when the source marks the feed as explicitly capped const stablecoinBound = @@ -441,7 +441,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d )}
- {shouldHideAddress ? ( + {hideAddress ? ( // Calculated-price feeds show a contact email instead of proxy address Contact us:{" "} @@ -516,7 +516,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d {isAaveSVR(metadata) ? "AAVE SVR Proxy:" : "SVR Proxy:"}
- {shouldHideAddress ? ( + {hideAddress ? ( // Calculated-price feeds show a contact email instead of SVR proxy address Contact us:{" "} @@ -552,7 +552,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d )}
- {isAaveSVR(metadata) && !shouldHideAddress && ( + {isAaveSVR(metadata) && !hideAddress && (
⚠️ Aave Dedicated Feed: This SVR proxy feed is dedicated exclusively for use by the Aave protocol. Learn more about{" "} @@ -562,7 +562,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d .
)} - {isSharedSVR(metadata) && !shouldHideAddress && ( + {isSharedSVR(metadata) && !hideAddress && (
🔗 SVR Feed: This SVR proxy feed is usable by any protocol. Learn more about{" "} @@ -602,7 +602,7 @@ const SmartDataTr = ({ network, metadata, showExtraDetails, batchedCategoryData // Use the pre-computed finalCategory from enriched metadata // (already includes deprecating status and Supabase risk tier) - const finalTier = metadata.finalCategory || metadata.feedCategory + const finalTier = metadata.finalCategory ?? null // Stablecoin price-bound note: only when the source marks the feed as explicitly capped const stablecoinBound = @@ -1437,32 +1437,17 @@ export const MainnetTable = ({ const isUSGovernmentMacroeconomicData = dataFeedType === "usGovernmentMacroeconomicData" const isDefault = !isStreams && !isSmartData && !isUSGovernmentMacroeconomicData - // Enrich metadata with final category (combining RDD and Supabase data) - // Priority: deprecating status from RDD > Supabase risk tier > RDD category fallback + // Enrich metadata with final category from Supabase. + // Deprecating is inferred from shutdownDate when no DB risk status is present. const enrichedMetadata = network.metadata.map((metadata) => { - // Check for deprecating status from RDD first (has shutdown date) - if (metadata.docs?.shutdownDate) { - return { ...metadata, finalCategory: "deprecating" } - } - - // Otherwise, get risk category from Supabase (or fall back to RDD) - const contractAddress = metadata.contractAddress || metadata.proxyAddress + const isAptos = network.name.toLowerCase().includes("aptos") + const contractAddress = isAptos ? metadata.proxyAddress : metadata.contractAddress || metadata.proxyAddress const networkIdentifier = getNetworkIdentifier(network) - let finalCategory = metadata.feedCategory - - if (contractAddress && batchedCategoryData?.size) { - const categoryResult = getFeedCategoryFromBatch( - batchedCategoryData, - contractAddress, - networkIdentifier, - metadata.feedCategory - ) - const supabaseCategory = categoryResult?.final ?? null - if (supabaseCategory) { - finalCategory = supabaseCategory - } - } + const finalCategory = + contractAddress && batchedCategoryData?.size + ? getFeedCategoryFromBatch(batchedCategoryData, contractAddress, networkIdentifier).final + : null return { ...metadata, finalCategory } }) @@ -1536,7 +1521,8 @@ export const MainnetTable = ({ const normalizedFinalCategory = metadata.finalCategory?.toLowerCase().replace(/\s+/g, "") return ( selectedFeedCategories.length === 0 || - selectedFeedCategories.map((cat) => cat.toLowerCase().replace(/\s+/g, "")).includes(normalizedFinalCategory) + (normalizedFinalCategory !== undefined && + selectedFeedCategories.map((cat) => cat.toLowerCase().replace(/\s+/g, "")).includes(normalizedFinalCategory)) ) }) .filter( @@ -1702,32 +1688,17 @@ export const TestnetTable = ({ const isUSGovernmentMacroeconomicData = dataFeedType === "usGovernmentMacroeconomicData" const isDefault = !isSmartData && !isRates && !isStreams && !isUSGovernmentMacroeconomicData - // Enrich metadata with final category (combining RDD and Supabase data) - // Priority: deprecating status from RDD > Supabase risk tier > RDD category fallback + // Enrich metadata with final category from Supabase. + // Deprecating is inferred from shutdownDate when no DB risk status is present. const enrichedMetadata = network.metadata.map((metadata) => { - // Check for deprecating status from RDD first (has shutdown date) - if (metadata.docs?.shutdownDate) { - return { ...metadata, finalCategory: "deprecating" } - } - - // Otherwise, get risk category from Supabase (or fall back to RDD) - const contractAddress = metadata.contractAddress || metadata.proxyAddress + const isAptos = network.name.toLowerCase().includes("aptos") + const contractAddress = isAptos ? metadata.proxyAddress : metadata.contractAddress || metadata.proxyAddress const networkIdentifier = getNetworkIdentifier(network) - let finalCategory = metadata.feedCategory - - if (contractAddress && batchedCategoryData?.size) { - const categoryResult = getFeedCategoryFromBatch( - batchedCategoryData, - contractAddress, - networkIdentifier, - metadata.feedCategory - ) - const supabaseCategory = categoryResult?.final ?? null - if (supabaseCategory) { - finalCategory = supabaseCategory - } - } + const finalCategory = + contractAddress && batchedCategoryData?.size + ? getFeedCategoryFromBatch(batchedCategoryData, contractAddress, networkIdentifier).final + : null return { ...metadata, finalCategory } }) @@ -1796,7 +1767,8 @@ export const TestnetTable = ({ const normalizedFinalCategory = metadata.finalCategory?.toLowerCase().replace(/\s+/g, "") return ( selectedFeedCategories.length === 0 || - selectedFeedCategories.map((cat) => cat.toLowerCase().replace(/\s+/g, "")).includes(normalizedFinalCategory) + (normalizedFinalCategory !== undefined && + selectedFeedCategories.map((cat) => cat.toLowerCase().replace(/\s+/g, "")).includes(normalizedFinalCategory)) ) }) .filter( diff --git a/src/features/feeds/components/useBatchedFeedCategories.ts b/src/features/feeds/components/useBatchedFeedCategories.ts index f529f965d5c..cb534b9b8d4 100644 --- a/src/features/feeds/components/useBatchedFeedCategories.ts +++ b/src/features/feeds/components/useBatchedFeedCategories.ts @@ -66,6 +66,7 @@ export function useBatchedFeedCategories(network: ChainNetwork | null): BatchedF const feedRequests: Array<{ contractAddress: string network: string + shutdownDate?: string fallbackCategory?: string }> = [] @@ -81,6 +82,7 @@ export function useBatchedFeedCategories(network: ChainNetwork | null): BatchedF feedRequests.push({ contractAddress: feedKey, network: networkKey, + shutdownDate: metadata.docs?.shutdownDate, fallbackCategory: metadata.feedCategory, }) } @@ -114,21 +116,18 @@ export function useBatchedFeedCategories(network: ChainNetwork | null): BatchedF } /** - * Get final category from batched results with fallback. + * Get final category from batched results. + * Returns null when no DB risk status is available and the feed is not deprecating. */ export function getFeedCategoryFromBatch( batchData: Map, contractAddress: string, - network: string, - fallbackCategory?: string + network: string ): FeedCategoryData { if (!batchData || batchData.size === 0) { - return { final: fallbackCategory ?? null } + return { final: null } } const key = `${contractAddress}-${network}` - const found = batchData.get(key) - if (found) return found - - return { final: fallbackCategory ?? null } + return batchData.get(key) ?? { final: null } } diff --git a/src/features/feeds/utils/feedVisibility.ts b/src/features/feeds/utils/feedVisibility.ts index 881b6fdeec1..092c2d5732e 100644 --- a/src/features/feeds/utils/feedVisibility.ts +++ b/src/features/feeds/utils/feedVisibility.ts @@ -1,5 +1,41 @@ import type { DataFeedType } from "../components/FeedList.tsx" +/** + * Proxy addresses (lowercase) for feeds that should display the contact email + * instead of a clickable contract address. + * + * Add an entry here whenever a feed needs to be "hidden" on the front end + * regardless of its productSubType. The address-hiding behaviour already + * applies automatically to any feed with productSubType === "calculatedPrice"; + * this list covers one-off exceptions (e.g. a specific DAI feed on a chain + * that does not carry that productSubType). + * + * Example: + * "0xabc123..." — the proxyAddress of the feed, lower-cased + */ +export const CONTACT_EMAIL_PROXY_ADDRESSES = new Set([ + // add lowercase proxy addresses here, e.g.: + // "0x000000000000000000000000000000000000dead", + "0x0101166b3b000332000000000000000000000000000000000000000000000000", +]) + +/** + * Returns true when the feed's contract address should be hidden and replaced + * with the data-feeds contact email in the UI. + * + * Two conditions trigger hiding: + * 1. The feed's productSubType is "calculatedPrice" (blanket rule for all + * calculated-price feeds). + * 2. The feed's proxyAddress appears in CONTACT_EMAIL_PROXY_ADDRESSES (used + * for one-off overrides on a per-feed basis). + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function shouldHideAddress(feed: any): boolean { + if (feed.docs?.productSubType === "calculatedPrice") return true + const proxy: string | undefined = feed.proxyAddress + return proxy !== undefined && CONTACT_EMAIL_PROXY_ADDRESSES.has(proxy.toLowerCase()) +} + /** * Helper function to extract schema version from feed metadata */