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
277 changes: 191 additions & 86 deletions src/features/feeds/components/FeedList.tsx

Large diffs are not rendered by default.

54 changes: 41 additions & 13 deletions src/features/feeds/components/Tables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import {
isApacEquitiesStreamFeed,
getTwapWindowSeconds,
} from "~/features/feeds/utils/feedMetadata.ts"
import { get247Category, is247Stream } from "~/features/feeds/utils/twentyFourSeven.ts"
import { isFeedIdAddressedNetwork } from "~/features/feeds/utils/feedIdAddressedNetworks.ts"
import { getFeedTypeFlags, type SchemaFilterValue } from "~/features/feeds/types.ts"
import { getFeedTypeFlags, type SchemaFilterValue, type TwentyFourSevenFilterValue } from "~/features/feeds/types.ts"
import { useFilteredFeedMetadata } from "~/features/feeds/hooks/useFilteredFeedMetadata.ts"

const feedItems = monitoredFeeds.mainnet
Expand Down Expand Up @@ -1398,6 +1399,10 @@ export const StreamsTr = ({ metadata, isMainnet, showRiskColumn = isMainnet }) =
metadata.docs?.attributeType === "ExchangeRate" &&
metadata.docs?.assetClass === "Tokenized Debt"
const schemaKey = getSchemaDefinitionKey(metadata)
// v2's price field is a TWAP. That covers the classic TWAP streams and the
// 24/7 TWAP product, which carries attributeType "247" instead of "TWAP".
const isTwapStream =
schemaKey === "v2" && (metadata.docs?.attributeType === "TWAP" || get247Category(metadata) === "twap")

return (
<tr>
Expand Down Expand Up @@ -1425,7 +1430,12 @@ export const StreamsTr = ({ metadata, isMainnet, showRiskColumn = isMainnet }) =
Datalink
</a>
)}
{schemaKey === "v2" && metadata.docs?.attributeType === "TWAP" && (
{is247Stream(metadata) && (
<span className={tableStyles.feedVariantBadge} title="24/7 stream">
24/7
</span>
)}
{isTwapStream && (
<a
href="/data-streams/reference/report-schema-v2#time-weighted-average-price-twap"
target="_blank"
Expand All @@ -1436,10 +1446,9 @@ export const StreamsTr = ({ metadata, isMainnet, showRiskColumn = isMainnet }) =
</a>
)}
{(() => {
const twapWindow =
schemaKey === "v2" && metadata.docs?.attributeType === "TWAP"
? getTwapWindowSeconds(metadata)
: undefined
// 24/7 TWAP streams currently carry no window token in the RDD name,
// so this only renders when a `-TWAP-<N>s` value is actually present.
const twapWindow = isTwapStream ? getTwapWindowSeconds(metadata) : undefined
if (!twapWindow) return null
return (
<span className={tableStyles.feedVariantBadge} title={`${twapWindow}-second TWAP window`}>
Expand Down Expand Up @@ -1587,13 +1596,12 @@ export const StreamsTr = ({ metadata, isMainnet, showRiskColumn = isMainnet }) =
</dt>
<dd>
<a href="/data-streams/market-hours" target="_blank">
{metadata.docs.marketHours}
{is247Stream(metadata) ? "24/7" : metadata.docs.marketHours}
</a>
</dd>
</div>
) : null}
{schemaKey === "v2" &&
metadata.docs?.attributeType === "TWAP" &&
{isTwapStream &&
(() => {
const twapWindow = getTwapWindowSeconds(metadata)
if (!twapWindow) return null
Expand Down Expand Up @@ -1649,6 +1657,8 @@ export const MainnetTable = ({
cryptoSchemaFilter,
streamCategoryFilter,
show24x5Feeds,
show247Streams,
twentyFourSevenFilter,
showApacEquitiesFeeds,
showFuturesFeeds,
tradingHoursFilter,
Expand Down Expand Up @@ -1676,6 +1686,8 @@ export const MainnetTable = ({
cryptoSchemaFilter?: SchemaFilterValue
streamCategoryFilter?: "all" | "datalink" | "equities" | "forex" | "futures"
show24x5Feeds?: boolean
show247Streams?: boolean
twentyFourSevenFilter?: TwentyFourSevenFilterValue
showApacEquitiesFeeds?: boolean
showFuturesFeeds?: boolean
tradingHoursFilter?: "all" | "regular" | "extended" | "overnight"
Expand All @@ -1698,6 +1710,10 @@ export const MainnetTable = ({
const feedTypeFlags = getFeedTypeFlags(dataFeedType, "mainnet")
const { isStreams, isSmartData, isUSGovernmentMacroeconomicData, isDefaultTable: isDefault } = feedTypeFlags

// While a market-hours filter is active the schema/category dropdowns are hidden.
// Ignore any leftover values so they can't silently empty the table (e.g. from a URL).
const isMarketHoursFilterActive = Boolean(show24x5Feeds || show247Streams)

const { batchedCategoryData, filteredMetadata, isBatchLoading } = useFilteredFeedMetadata({
network,
dataFeedType,
Expand All @@ -1707,14 +1723,16 @@ export const MainnetTable = ({
searchVariant: "mainnet",
showOnlySVR,
show24x5Feeds,
show247Streams,
twentyFourSevenFilter,
showApacEquitiesFeeds,
showFuturesFeeds,
tradingHoursFilter,
visibilityOptions: {
showOnlyDEXFeeds,
showOnlyDatalinkFeeds,
streamCategoryFilter,
rwaSchemaFilter,
streamCategoryFilter: isMarketHoursFilterActive ? undefined : streamCategoryFilter,
rwaSchemaFilter: isMarketHoursFilterActive ? undefined : rwaSchemaFilter,
cryptoSchemaFilter,
showOnlyMVRFeeds,
tokenizedEquityProvider,
Expand Down Expand Up @@ -1829,6 +1847,8 @@ export const TestnetTable = ({
cryptoSchemaFilter,
streamCategoryFilter,
show24x5Feeds,
show247Streams,
twentyFourSevenFilter,
showApacEquitiesFeeds,
showFuturesFeeds,
tradingHoursFilter,
Expand All @@ -1853,6 +1873,8 @@ export const TestnetTable = ({
cryptoSchemaFilter?: SchemaFilterValue
streamCategoryFilter?: "all" | "datalink" | "equities" | "forex" | "futures"
show24x5Feeds?: boolean
show247Streams?: boolean
twentyFourSevenFilter?: TwentyFourSevenFilterValue
showApacEquitiesFeeds?: boolean
showFuturesFeeds?: boolean
tradingHoursFilter?: "all" | "regular" | "extended" | "overnight"
Expand All @@ -1869,6 +1891,10 @@ export const TestnetTable = ({
isDefaultTable: isDefault,
} = getFeedTypeFlags(dataFeedType, "testnet")

// While a market-hours filter is active the schema/category dropdowns are hidden.
// Ignore any leftover values so they can't silently empty the table (e.g. from a URL).
const isMarketHoursFilterActive = Boolean(show24x5Feeds || show247Streams)

const { batchedCategoryData, filteredMetadata, isBatchLoading } = useFilteredFeedMetadata({
network,
dataFeedType,
Expand All @@ -1877,14 +1903,16 @@ export const TestnetTable = ({
searchValue,
searchVariant: "testnet",
show24x5Feeds,
show247Streams,
twentyFourSevenFilter,
showApacEquitiesFeeds,
showFuturesFeeds,
tradingHoursFilter,
visibilityOptions: {
showOnlyDEXFeeds,
showOnlyDatalinkFeeds,
streamCategoryFilter,
rwaSchemaFilter,
streamCategoryFilter: isMarketHoursFilterActive ? undefined : streamCategoryFilter,
rwaSchemaFilter: isMarketHoursFilterActive ? undefined : rwaSchemaFilter,
cryptoSchemaFilter,
showOnlyMVRFeeds,
tokenizedEquityProvider,
Expand Down
5 changes: 5 additions & 0 deletions src/features/feeds/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export type DataFeedType =
export type SchemaFilterValue = "all" | "v2" | "v3" | "v3-dex" | "v8" | "v11" | "v14"
export type StreamsRwaFeedTypeValue = "all" | "datalink" | "equities" | "forex" | "futures"
export type TradingHoursFilterValue = "all" | "regular" | "extended" | "overnight"
/** Top-level availability filter: 24/5 session streams vs 24/7 streams. */
export type MarketHoursFilterValue = "all" | "24x5" | "24x7"
/** 24/7 sub-products, keyed by RDD attribute type. "smoothed" is a Data Feeds product, not a stream. */
export type TwentyFourSevenCategory = "twap" | "perps" | "smoothed"
export type TwentyFourSevenFilterValue = "all" | "twap" | "perps"

/** Flags derived from the active feed page type. Used by tables and FeedList. */
export interface FeedTypeFlags {
Expand Down
147 changes: 147 additions & 0 deletions src/features/feeds/utils/feedVisibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { jest } from "@jest/globals"

// feedMetadata pulls in the Supabase-backed category helpers; stub the client
// so the visibility predicates can be tested without import.meta.env.
jest.unstable_mockModule("../../../db/supabase.ts", () => ({
getSupabaseClient: () => null,
supabase: {},
}))

const { is247Stream, get247Category, matches247StreamFilter } = await import("./twentyFourSeven.ts")
const { isFeedVisible, shouldHideStreamFeedId } = await import("./feedVisibility.ts")

const verifierStream = (docs: Record<string, unknown>, overrides: Record<string, unknown> = {}) => ({
contractType: "verifier",
feedId: "0xfeed",
pair: ["AAPL", "USD"],
...overrides,
docs,
})

describe("is247Stream", () => {
it.each(["247", "247Perps", "247-TWAP"])("detects stream attributeType %s", (attributeType) => {
expect(is247Stream(verifierStream({ attributeType }))).toBe(true)
})

it.each(["247CustomPrice", "247Smoothed"])("excludes Data Feeds attributeType %s", (attributeType) => {
expect(is247Stream(verifierStream({ attributeType }))).toBe(false)
})

it("detects the 247Price feed type", () => {
expect(is247Stream(verifierStream({ feedType: "247Price" }))).toBe(true)
expect(is247Stream(verifierStream({}, { feedType: "247Price" }))).toBe(true)
})

it.each(["PerpsPrice", "TWAP", "Equities", undefined])("rejects attributeType %s", (attributeType) => {
expect(is247Stream(verifierStream({ attributeType, feedType: "Crypto" }))).toBe(false)
})
})

describe("get247Category", () => {
it.each([
["247", "twap"],
["247-TWAP", "twap"],
["247Perps", "perps"],
["247CustomPrice", "smoothed"],
["247Smoothed", "smoothed"],
])("maps attributeType %s to %s", (attributeType, category) => {
// get247Category still recognises the Data Feeds product even though the
// streams family excludes it.
expect(get247Category(verifierStream({ attributeType }))).toBe(category)
})

it.each(["PerpsPrice", "TWAP", "EquityPrice", undefined])("does not classify attributeType %s", (attributeType) => {
expect(get247Category(verifierStream({ attributeType }))).toBeUndefined()
})

it("does not infer a product from the 247Price feed type alone", () => {
expect(get247Category(verifierStream({ feedType: "247Price" }))).toBeUndefined()
expect(is247Stream(verifierStream({ feedType: "247Price" }))).toBe(true)
})
})

describe("isFeedVisible for streamsRwa", () => {
it("shows 24/7 stream products", () => {
expect(
isFeedVisible(verifierStream({ attributeType: "247", feedType: "247Price", schema: "v2" }), "streamsRwa")
).toBe(true)
expect(
isFeedVisible(verifierStream({ attributeType: "247Perps", feedType: "247Price", schema: "v3" }), "streamsRwa")
).toBe(true)
})

it("keeps the 24/7 Smoothed Data Feeds product off the streams page", () => {
expect(
isFeedVisible(
verifierStream({ attributeType: "247CustomPrice", feedType: "247Price", schema: "v11" }),
"streamsRwa"
)
).toBe(false)
})

it("still hides streams flagged hidden in the reference data", () => {
expect(
isFeedVisible(
verifierStream({ attributeType: "247", feedType: "247Price", schema: "v2", hidden: true }),
"streamsRwa"
)
).toBe(false)
})

it("keeps PerpsPrice streams off the RWA page for now", () => {
expect(
isFeedVisible(verifierStream({ attributeType: "PerpsPrice", feedType: "PerpsPrice", schema: "v3" }), "streamsRwa")
).toBe(false)
})

it("excludes 24/7 streams from the other category filters", () => {
const twentyFourSeven = verifierStream({ attributeType: "247", feedType: "247Price", schema: "v2" })

expect(isFeedVisible(twentyFourSeven, "streamsRwa", "", { streamCategoryFilter: "equities" })).toBe(false)
expect(isFeedVisible(twentyFourSeven, "streamsRwa", "", { streamCategoryFilter: "all" })).toBe(true)
})
})

describe("shouldHideStreamFeedId", () => {
it("hides feed IDs for the 24/7 stream family", () => {
expect(shouldHideStreamFeedId(verifierStream({ attributeType: "247", feedType: "247Price" }))).toBe(true)
expect(shouldHideStreamFeedId(verifierStream({ attributeType: "247Perps", feedType: "247Price" }))).toBe(true)
})

it("leaves regular TWAP, smoothed, and ordinary streams alone", () => {
expect(shouldHideStreamFeedId(verifierStream({ attributeType: "TWAP", feedType: "Crypto", schema: "v2" }))).toBe(
false
)
expect(
shouldHideStreamFeedId(verifierStream({ attributeType: "247CustomPrice", feedType: "247Price", schema: "v11" }))
).toBe(false)
expect(shouldHideStreamFeedId(verifierStream({ attributeType: "EquityPrice", feedType: "Equities" }))).toBe(false)
})
})

describe("matches247StreamFilter", () => {
const twap = verifierStream({ attributeType: "247", feedType: "247Price" })
const perps = verifierStream({ attributeType: "247Perps", feedType: "247Price" })
const smoothed = verifierStream({ attributeType: "247CustomPrice", feedType: "247Price" })
const equities = verifierStream({ attributeType: "EquityPrice", feedType: "Equities" })

it("passes every stream through when the 24/7 filter is off", () => {
expect(matches247StreamFilter(twap, false, "all")).toBe(true)
expect(matches247StreamFilter(equities, false, "all")).toBe(true)
})

it("keeps only 24/7 stream products when the 24/7 filter is on", () => {
expect(matches247StreamFilter(twap, true, "all")).toBe(true)
expect(matches247StreamFilter(perps, true, "all")).toBe(true)
expect(matches247StreamFilter(smoothed, true, "all")).toBe(false)
expect(matches247StreamFilter(equities, true, "all")).toBe(false)
})

it("splits the 24/7 stream products by attribute type", () => {
expect(matches247StreamFilter(twap, true, "twap")).toBe(true)
expect(matches247StreamFilter(perps, true, "twap")).toBe(false)

expect(matches247StreamFilter(perps, true, "perps")).toBe(true)
expect(matches247StreamFilter(twap, true, "perps")).toBe(false)
})
})
15 changes: 12 additions & 3 deletions src/features/feeds/utils/feedVisibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isFuturesStreamFeed,
normalizeCategoryKey,
} from "./feedMetadata.ts"
import { is247Stream } from "./twentyFourSeven.ts"

/**
* Proxy addresses (lowercase) for feeds that should display the contact email
Expand Down Expand Up @@ -81,14 +82,21 @@ export function shouldHideAddress(feed: any, riskTier?: string | null): boolean
return normalizeCategoryKey(riskTier) === "veryhigh"
}

/**
* Feature flag for the 24/7 stream family (24/7 TWAP and 24/7 Perpetual Markets).
* While true, their feed IDs render as a "Contact us" link instead of the ID.
* Flip to false when these streams are publicly launched and the IDs can ship.
*/
export const HIDE_247_STREAM_FEED_IDS = true

/**
* Returns true when a stream's feedId should be hidden and replaced with a
* contact link. Add new feed-type checks here to extend the behaviour; remove
* them when the stream goes live.
* them (or flip their flag) when the stream goes live.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function shouldHideStreamFeedId(feed: any): boolean {
return isApacEquitiesStreamFeed(feed) || isFuturesStreamFeed(feed)
return isApacEquitiesStreamFeed(feed) || isFuturesStreamFeed(feed) || (HIDE_247_STREAM_FEED_IDS && is247Stream(feed))
}

/**
Expand Down Expand Up @@ -181,7 +189,8 @@ export function isFeedVisible(
isVisible = false
}
} else if (dataFeedType === "streamsRwa") {
isVisible = ["Equities", "Forex", "Futures"].includes(feed.docs?.feedType)
// 24/7 streams span multiple schemas but all belong on the RWA page.
isVisible = ["Equities", "Forex", "Futures"].includes(feed.docs?.feedType) || is247Stream(feed)
} else if (dataFeedType === "streamsNav") {
isVisible = getSchemaVersion(feed) === "v9"
} else if (dataFeedType === "streamsExRate") {
Expand Down
Loading
Loading