Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions src/db/feedCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

/* ===========================
Expand Down Expand Up @@ -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 }))

Expand Down Expand Up @@ -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<Map<string, FeedTierResult>> {
export async function getFeedRiskTiersBatch(feedRequests: FeedRequest[]): Promise<Map<string, FeedTierResult>> {
const out = new Map<string, FeedTierResult>()
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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
88 changes: 30 additions & 58 deletions src/features/feeds/components/Tables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -335,7 +335,7 @@ const TOKENIZED_EQUITY_CONTACT_EMAIL = "[email protected]"
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"
Expand All @@ -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 =
Expand Down Expand Up @@ -441,7 +441,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d
</dt>
)}
<dd>
{shouldHideAddress ? (
{hideAddress ? (
// Calculated-price feeds show a contact email instead of proxy address
<span>
Contact us:{" "}
Expand Down Expand Up @@ -516,7 +516,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d
<span className="label">{isAaveSVR(metadata) ? "AAVE SVR Proxy:" : "SVR Proxy:"}</span>
</dt>
<dd>
{shouldHideAddress ? (
{hideAddress ? (
// Calculated-price feeds show a contact email instead of SVR proxy address
<span>
Contact us:{" "}
Expand Down Expand Up @@ -552,7 +552,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d
)}
</dd>
</div>
{isAaveSVR(metadata) && !shouldHideAddress && (
{isAaveSVR(metadata) && !hideAddress && (
<div className={clsx(tableStyles.aaveCallout)}>
<strong>⚠️ Aave Dedicated Feed:</strong> This SVR proxy feed is dedicated exclusively for use by the
Aave protocol. Learn more about{" "}
Expand All @@ -562,7 +562,7 @@ const DefaultTr = ({ network, metadata, showExtraDetails, batchedCategoryData, d
.
</div>
)}
{isSharedSVR(metadata) && !shouldHideAddress && (
{isSharedSVR(metadata) && !hideAddress && (
<div className={clsx(tableStyles.sharedCallout)}>
<strong>🔗 SVR Feed:</strong> This SVR proxy feed is usable by any protocol. Learn more about{" "}
<a href="/data-feeds/svr-feeds" target="_blank">
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 }
})
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 }
})
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 7 additions & 8 deletions src/features/feeds/components/useBatchedFeedCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function useBatchedFeedCategories(network: ChainNetwork | null): BatchedF
const feedRequests: Array<{
contractAddress: string
network: string
shutdownDate?: string
fallbackCategory?: string
}> = []

Expand All @@ -81,6 +82,7 @@ export function useBatchedFeedCategories(network: ChainNetwork | null): BatchedF
feedRequests.push({
contractAddress: feedKey,
network: networkKey,
shutdownDate: metadata.docs?.shutdownDate,
fallbackCategory: metadata.feedCategory,
})
}
Expand Down Expand Up @@ -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<string, FeedCategoryData>,
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 }
}
36 changes: 36 additions & 0 deletions src/features/feeds/utils/feedVisibility.ts
Original file line number Diff line number Diff line change
@@ -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<string>([
// 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
*/
Expand Down
Loading