diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/error.ts b/apps/insights/src/app/publishers/[cluster]/[key]/error.ts new file mode 100644 index 0000000000..4bd19b42bc --- /dev/null +++ b/apps/insights/src/app/publishers/[cluster]/[key]/error.ts @@ -0,0 +1,3 @@ +"use client"; + +export { Error as default } from "../../../../components/Error"; diff --git a/apps/insights/src/app/publishers/[key]/layout.ts b/apps/insights/src/app/publishers/[cluster]/[key]/layout.ts similarity index 65% rename from apps/insights/src/app/publishers/[key]/layout.ts rename to apps/insights/src/app/publishers/[cluster]/[key]/layout.ts index 88ac105b5f..fdf3c17532 100644 --- a/apps/insights/src/app/publishers/[key]/layout.ts +++ b/apps/insights/src/app/publishers/[cluster]/[key]/layout.ts @@ -1,6 +1,6 @@ import type { Metadata } from "next"; -export { PublishersLayout as default } from "../../../components/Publisher/layout"; +export { PublishersLayout as default } from "../../../../components/Publisher/layout"; export const metadata: Metadata = { title: "Publishers", diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/page.ts b/apps/insights/src/app/publishers/[cluster]/[key]/page.ts new file mode 100644 index 0000000000..48bace34ce --- /dev/null +++ b/apps/insights/src/app/publishers/[cluster]/[key]/page.ts @@ -0,0 +1,4 @@ +export { Performance as default } from "../../../../components/Publisher/performance"; + +export const dynamic = "error"; +export const revalidate = 3600; diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/page.ts b/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/page.ts new file mode 100644 index 0000000000..cd2fe95a26 --- /dev/null +++ b/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/page.ts @@ -0,0 +1,4 @@ +export { PriceFeeds as default } from "../../../../../components/Publisher/price-feeds"; + +export const dynamic = "error"; +export const revalidate = 3600; diff --git a/apps/insights/src/app/publishers/[key]/error.ts b/apps/insights/src/app/publishers/[key]/error.ts deleted file mode 100644 index 4f357cc1ba..0000000000 --- a/apps/insights/src/app/publishers/[key]/error.ts +++ /dev/null @@ -1,3 +0,0 @@ -"use client"; - -export { Error as default } from "../../../components/Error"; diff --git a/apps/insights/src/app/publishers/[key]/page.ts b/apps/insights/src/app/publishers/[key]/page.ts deleted file mode 100644 index 6d011c092a..0000000000 --- a/apps/insights/src/app/publishers/[key]/page.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Performance as default } from "../../../components/Publisher/performance"; - -export const dynamic = "error"; -export const revalidate = 3600; diff --git a/apps/insights/src/app/publishers/[key]/price-feeds/page.ts b/apps/insights/src/app/publishers/[key]/price-feeds/page.ts deleted file mode 100644 index a41e711f83..0000000000 --- a/apps/insights/src/app/publishers/[key]/price-feeds/page.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { PriceFeeds as default } from "../../../../components/Publisher/price-feeds"; - -export const dynamic = "error"; -export const revalidate = 3600; diff --git a/apps/insights/src/components/LivePrices/index.tsx b/apps/insights/src/components/LivePrices/index.tsx index afca9f2d41..0c2f3b9f95 100644 --- a/apps/insights/src/components/LivePrices/index.tsx +++ b/apps/insights/src/components/LivePrices/index.tsx @@ -11,24 +11,32 @@ import { useLivePriceComponent, useLivePriceData, } from "../../hooks/use-live-price-data"; +import type { Cluster } from "../../services/pyth"; export const SKELETON_WIDTH = 20; export const LivePrice = ({ - feedKey, publisherKey, + ...props }: { feedKey: string; publisherKey?: string | undefined; + cluster: Cluster; }) => - publisherKey ? ( - + publisherKey === undefined ? ( + ) : ( - + ); -const LiveAggregatePrice = ({ feedKey }: { feedKey: string }) => { - const { prev, current } = useLivePriceData(feedKey); +const LiveAggregatePrice = ({ + feedKey, + cluster, +}: { + feedKey: string; + cluster: Cluster; +}) => { + const { prev, current } = useLivePriceData(cluster, feedKey); return ( ); @@ -37,11 +45,17 @@ const LiveAggregatePrice = ({ feedKey }: { feedKey: string }) => { const LiveComponentPrice = ({ feedKey, publisherKey, + cluster, }: { feedKey: string; publisherKey: string; + cluster: Cluster; }) => { - const { prev, current } = useLivePriceComponent(feedKey, publisherKey); + const { prev, current } = useLivePriceComponent( + cluster, + feedKey, + publisherKey, + ); return ; }; @@ -67,31 +81,40 @@ const Price = ({ }; export const LiveConfidence = ({ - feedKey, publisherKey, + ...props }: { feedKey: string; publisherKey?: string | undefined; + cluster: Cluster; }) => publisherKey === undefined ? ( - + ) : ( - + ); -const LiveAggregateConfidence = ({ feedKey }: { feedKey: string }) => { - const { current } = useLivePriceData(feedKey); +const LiveAggregateConfidence = ({ + feedKey, + cluster, +}: { + feedKey: string; + cluster: Cluster; +}) => { + const { current } = useLivePriceData(cluster, feedKey); return ; }; const LiveComponentConfidence = ({ feedKey, publisherKey, + cluster, }: { feedKey: string; publisherKey: string; + cluster: Cluster; }) => { - const { current } = useLivePriceComponent(feedKey, publisherKey); + const { current } = useLivePriceComponent(cluster, feedKey, publisherKey); return ; }; @@ -110,8 +133,14 @@ const Confidence = ({ confidence }: { confidence?: number | undefined }) => { ); }; -export const LiveLastUpdated = ({ feedKey }: { feedKey: string }) => { - const { current } = useLivePriceData(feedKey); +export const LiveLastUpdated = ({ + feedKey, + cluster, +}: { + feedKey: string; + cluster: Cluster; +}) => { + const { current } = useLivePriceData(cluster, feedKey); const formatterWithDate = useDateFormatter({ dateStyle: "short", timeStyle: "medium", @@ -137,14 +166,16 @@ type LiveValueProps = { field: T; feedKey: string; defaultValue?: ReactNode | undefined; + cluster: Cluster; }; export const LiveValue = ({ feedKey, field, defaultValue, + cluster, }: LiveValueProps) => { - const { current } = useLivePriceData(feedKey); + const { current } = useLivePriceData(cluster, feedKey); return current !== undefined || defaultValue !== undefined ? ( (current?.[field]?.toString() ?? defaultValue) @@ -158,6 +189,7 @@ type LiveComponentValueProps = { feedKey: string; publisherKey: string; defaultValue?: ReactNode | undefined; + cluster: Cluster; }; export const LiveComponentValue = ({ @@ -165,8 +197,9 @@ export const LiveComponentValue = ({ field, publisherKey, defaultValue, + cluster, }: LiveComponentValueProps) => { - const { current } = useLivePriceComponent(feedKey, publisherKey); + const { current } = useLivePriceComponent(cluster, feedKey, publisherKey); return current !== undefined || defaultValue !== undefined ? ( (current?.latest[field].toString() ?? defaultValue) diff --git a/apps/insights/src/components/PriceComponentDrawer/index.module.scss b/apps/insights/src/components/PriceComponentDrawer/index.module.scss index 56c0f6d6d1..87c45dcc34 100644 --- a/apps/insights/src/components/PriceComponentDrawer/index.module.scss +++ b/apps/insights/src/components/PriceComponentDrawer/index.module.scss @@ -1,16 +1,17 @@ @use "@pythnetwork/component-library/theme"; .priceComponentDrawer { - display: grid; - grid-template-rows: repeat(2, max-content); - grid-template-columns: 100%; - gap: theme.spacing(10); + .testFeedMessage { + grid-column: span 2 / span 2; + margin-bottom: theme.spacing(10); + } .stats { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr); gap: theme.spacing(4); + margin-bottom: theme.spacing(10); } .spinner { diff --git a/apps/insights/src/components/PriceComponentDrawer/index.tsx b/apps/insights/src/components/PriceComponentDrawer/index.tsx index dfb9de4df5..349db5aafc 100644 --- a/apps/insights/src/components/PriceComponentDrawer/index.tsx +++ b/apps/insights/src/components/PriceComponentDrawer/index.tsx @@ -1,6 +1,8 @@ +import { Flask } from "@phosphor-icons/react/dist/ssr/Flask"; import { Button } from "@pythnetwork/component-library/Button"; import { Card } from "@pythnetwork/component-library/Card"; import { Drawer } from "@pythnetwork/component-library/Drawer"; +import { InfoBox } from "@pythnetwork/component-library/InfoBox"; import { Select } from "@pythnetwork/component-library/Select"; import { Spinner } from "@pythnetwork/component-library/Spinner"; import { StatCard } from "@pythnetwork/component-library/StatCard"; @@ -45,28 +47,32 @@ type Props = { headingExtra?: ReactNode | undefined; publisherKey: string; symbol: string; + displaySymbol: string; feedKey: string; score: number | undefined; rank: number | undefined; status: Status; - navigateButtonText: string; + identifiesPublisher?: boolean | undefined; navigateHref: string; firstEvaluation: Date; + cluster: Cluster; }; export const PriceComponentDrawer = ({ publisherKey, onClose, symbol, + displaySymbol, feedKey, score, rank, title, status, headingExtra, - navigateButtonText, navigateHref, firstEvaluation, + cluster, + identifiesPublisher, }: Props) => { const goToPriceFeedPageOnClose = useRef(false); const [isFeedDrawerOpen, setIsFeedDrawerOpen] = useState(true); @@ -93,7 +99,7 @@ export const PriceComponentDrawer = ({ const { selectedPeriod, setSelectedPeriod, evaluationPeriods } = useEvaluationPeriods(firstEvaluation); const scoreHistoryState = useData( - [Cluster.Pythnet, publisherKey, symbol, selectedPeriod], + [cluster, publisherKey, symbol, selectedPeriod], getScoreHistory, ); @@ -108,7 +114,7 @@ export const PriceComponentDrawer = ({ @@ -116,26 +122,46 @@ export const PriceComponentDrawer = ({ isOpen={isFeedDrawerOpen} bodyClassName={styles.priceComponentDrawer} > + {cluster === Cluster.PythtestConformance && ( + } + header={`This publisher is in test`} + className={styles.testFeedMessage} + > + This is a test publisher. Its prices are not included in the Pyth + aggregate price for {displaySymbol}. + + )}
} + stat={} /> } + stat={ + + } /> + } /> } /> diff --git a/apps/insights/src/components/PriceComponentsCard/index.module.scss b/apps/insights/src/components/PriceComponentsCard/index.module.scss deleted file mode 100644 index 9c0965c61a..0000000000 --- a/apps/insights/src/components/PriceComponentsCard/index.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -@use "@pythnetwork/component-library/theme"; - -.componentName { - display: flex; - flex-flow: row nowrap; - align-items: center; - gap: theme.spacing(6); -} diff --git a/apps/insights/src/components/PriceComponentsCard/index.tsx b/apps/insights/src/components/PriceComponentsCard/index.tsx index c286a75e62..f5ba71bed3 100644 --- a/apps/insights/src/components/PriceComponentsCard/index.tsx +++ b/apps/insights/src/components/PriceComponentsCard/index.tsx @@ -17,7 +17,6 @@ import { useQueryState, parseAsStringEnum, parseAsBoolean } from "nuqs"; import { type ReactNode, Suspense, useMemo, useCallback } from "react"; import { useFilter, useCollator } from "react-aria"; -import styles from "./index.module.scss"; import { useQueryParamFilterPagination } from "../../hooks/use-query-param-filter-pagination"; import { Cluster } from "../../services/pyth"; import { @@ -45,12 +44,12 @@ type Props = { label: string; searchPlaceholder: string; onPriceComponentAction: (component: T) => void; + toolbarExtra?: ReactNode; }; type PriceComponent = { id: string; score: number | undefined; - symbol: string; uptimeScore: number | undefined; deviationScore: number | undefined; stalledScore: number | undefined; @@ -172,16 +171,7 @@ export const ResolvedPriceComponentsCard = ({ paginatedItems.map((component) => ({ id: component.id, data: { - name: ( -
- {component.name} - {component.cluster === Cluster.PythtestConformance && ( - - test - - )} -
- ), + name: component.name, ...(showQuality ? { score: component.score !== undefined && ( @@ -212,18 +202,21 @@ export const ResolvedPriceComponentsCard = ({ feedKey={component.feedKey} publisherKey={component.publisherKey} field="publishSlot" + cluster={component.cluster} /> ), price: ( ), confidence: ( ), }), @@ -285,6 +278,7 @@ type PriceComponentsCardProps = Pick< | "nameLoadingSkeleton" | "label" | "searchPlaceholder" + | "toolbarExtra" > & ( | { isLoading: true } @@ -315,6 +309,7 @@ export const PriceComponentsCardContents = ({ nameLoadingSkeleton, label, searchPlaceholder, + toolbarExtra, ...props }: PriceComponentsCardProps) => { const collator = useCollator(); @@ -333,6 +328,7 @@ export const PriceComponentsCardContents = ({ } toolbar={ <> + {toolbarExtra} label="Status" size="sm" diff --git a/apps/insights/src/components/PriceFeed/chart.tsx b/apps/insights/src/components/PriceFeed/chart.tsx index 791b237005..5e7a8bdb71 100644 --- a/apps/insights/src/components/PriceFeed/chart.tsx +++ b/apps/insights/src/components/PriceFeed/chart.tsx @@ -15,6 +15,7 @@ import { z } from "zod"; import theme from "./theme.module.scss"; import { useLivePriceData } from "../../hooks/use-live-price-data"; +import { Cluster } from "../../services/pyth"; type Props = { symbol: string; @@ -38,7 +39,7 @@ const useChart = (symbol: string, feedId: string) => { const useChartElem = (symbol: string, feedId: string) => { const logger = useLogger(); - const { current } = useLivePriceData(feedId); + const { current } = useLivePriceData(Cluster.Pythnet, feedId); const chartContainerRef = useRef(null); const chartRef = useRef(undefined); const earliestDateRef = useRef(undefined); diff --git a/apps/insights/src/components/PriceFeed/layout.tsx b/apps/insights/src/components/PriceFeed/layout.tsx index 1a6d17be48..8fab8cd2ab 100644 --- a/apps/insights/src/components/PriceFeed/layout.tsx +++ b/apps/insights/src/components/PriceFeed/layout.tsx @@ -111,11 +111,21 @@ export const PriceFeedLayout = async ({ children, params }: Props) => { } + stat={ + + } /> } + stat={ + + } corner={
diff --git a/apps/insights/src/components/PriceFeed/price-feed-select.tsx b/apps/insights/src/components/PriceFeed/price-feed-select.tsx index 15a05bf87b..6b4c4025a1 100644 --- a/apps/insights/src/components/PriceFeed/price-feed-select.tsx +++ b/apps/insights/src/components/PriceFeed/price-feed-select.tsx @@ -20,6 +20,7 @@ import { useCollator, useFilter } from "react-aria"; import styles from "./price-feed-select.module.scss"; import { usePriceFeeds } from "../../hooks/use-price-feeds"; +import { Cluster } from "../../services/pyth"; import { AssetClassTag } from "../AssetClassTag"; import { PriceFeedTag } from "../PriceFeedTag"; @@ -42,7 +43,7 @@ export const PriceFeedSelect = ({ children }: Props) => { ([, { displaySymbol, assetClass, key }]) => filter.contains(displaySymbol, search) || filter.contains(assetClass, search) || - filter.contains(key, search), + filter.contains(key[Cluster.Pythnet], search), ), [feeds, search, filter], ); diff --git a/apps/insights/src/components/PriceFeed/publishers-card.tsx b/apps/insights/src/components/PriceFeed/publishers-card.tsx index 6237127edf..a5f530cb3a 100644 --- a/apps/insights/src/components/PriceFeed/publishers-card.tsx +++ b/apps/insights/src/components/PriceFeed/publishers-card.tsx @@ -1,18 +1,16 @@ "use client"; import { useLogger } from "@pythnetwork/app-logger"; -import { - useQueryState, - parseAsString, // , parseAsBoolean -} from "nuqs"; +import { Switch } from "@pythnetwork/component-library/Switch"; +import { useQueryState, parseAsString, parseAsBoolean } from "nuqs"; import { type ComponentProps, Suspense, useCallback, useMemo } from "react"; +import { Cluster, ClusterToName } from "../../services/pyth"; import { PriceComponentDrawer } from "../PriceComponentDrawer"; import { PriceComponentsCardContents, ResolvedPriceComponentsCard, } from "../PriceComponentsCard"; -// import { Cluster } from "../../services/pyth"; type Publisher = ComponentProps< typeof ResolvedPriceComponentsCard @@ -26,80 +24,98 @@ type Props = Omit< "onPriceComponentAction" | "priceComponents" > & { priceComponents: Publisher[]; + symbol: string; + displaySymbol: string; }; -export const PublishersCard = ({ priceComponents, ...props }: Props) => ( +export const PublishersCard = ({ + priceComponents, + symbol, + displaySymbol, + ...props +}: Props) => ( }> - + ); -const ResolvedPublishersCard = ({ priceComponents, ...props }: Props) => { - // const logger = useLogger(); +const ResolvedPublishersCard = ({ + priceComponents, + symbol, + displaySymbol, + ...props +}: Props) => { + const logger = useLogger(); const { handleClose, selectedPublisher, updateSelectedPublisherKey } = usePublisherDrawer(priceComponents); const onPriceComponentAction = useCallback( - ({ publisherKey }: Publisher) => { - updateSelectedPublisherKey(publisherKey); + ({ publisherKey, cluster }: Publisher) => { + updateSelectedPublisherKey( + [ClusterToName[cluster], publisherKey].join(":"), + ); }, [updateSelectedPublisherKey], ); - // const [includeTestFeeds, setIncludeTestFeeds] = useQueryState( - // "includeTestFeeds", - // parseAsBoolean.withDefault(false), - // ); - // const componentsFilteredByCluster = useMemo( - // () => - // includeTestFeeds - // ? priceComponents - // : priceComponents.filter( - // (component) => component.cluster === Cluster.Pythnet, - // ), - // [includeTestFeeds, priceComponents], - // ); - // const updateIncludeTestFeeds = useCallback( - // (newValue: boolean) => { - // setIncludeTestFeeds(newValue).catch((error: unknown) => { - // logger.error( - // "Failed to update include test components query param", - // error, - // ); - // }); - // }, - // [setIncludeTestFeeds, logger], - // ); - // - // Show test feeds - // + const [includeTestFeeds, setIncludeTestFeeds] = useQueryState( + "includeTestFeeds", + parseAsBoolean.withDefault(false), + ); + const componentsFilteredByCluster = useMemo( + () => + includeTestFeeds + ? priceComponents + : priceComponents.filter( + (component) => component.cluster === Cluster.Pythnet, + ), + [includeTestFeeds, priceComponents], + ); + const updateIncludeTestFeeds = useCallback( + (newValue: boolean) => { + setIncludeTestFeeds(newValue).catch((error: unknown) => { + logger.error( + "Failed to update include test components query param", + error, + ); + }); + }, + [setIncludeTestFeeds, logger], + ); return ( <> + Include test publishers + + } {...props} /> {selectedPublisher && ( )} @@ -122,13 +138,14 @@ const usePublisherDrawer = (publishers: Publisher[]) => { }, [setSelectedPublisher, logger], ); - const selectedPublisher = useMemo( - () => - publishers.find( - (publisher) => publisher.publisherKey === selectedPublisherKey, - ), - [selectedPublisherKey, publishers], - ); + const selectedPublisher = useMemo(() => { + const [cluster, publisherKey] = selectedPublisherKey.split(":"); + return publishers.find( + (publisher) => + publisher.publisherKey === publisherKey && + ClusterToName[publisher.cluster] === cluster, + ); + }, [selectedPublisherKey, publishers]); const handleClose = useCallback(() => { updateSelectedPublisherKey(""); }, [updateSelectedPublisherKey]); diff --git a/apps/insights/src/components/PriceFeed/publishers.tsx b/apps/insights/src/components/PriceFeed/publishers.tsx index 9b44a06aef..e265306ce9 100644 --- a/apps/insights/src/components/PriceFeed/publishers.tsx +++ b/apps/insights/src/components/PriceFeed/publishers.tsx @@ -23,16 +23,21 @@ export const Publishers = async ({ params }: Props) => { const { slug } = await params; const symbol = decodeURIComponent(slug); const [ - feeds, - pythnetPublishers, // , pythtestConformancePublishers + pythnetFeeds, + pythtestConformanceFeeds, + pythnetPublishers, + pythtestConformancePublishers, ] = await Promise.all([ getFeeds(Cluster.Pythnet), + getFeeds(Cluster.PythtestConformance), getPublishers(Cluster.Pythnet, symbol), - // getPublishers(Cluster.PythtestConformance, symbol), + getPublishers(Cluster.PythtestConformance, symbol), ]); - const feed = feeds.find((feed) => feed.symbol === symbol); - // const publishers = [...pythnetPublishers, ...pythtestConformancePublishers]; - const publishers = [...pythnetPublishers]; + const feed = pythnetFeeds.find((feed) => feed.symbol === symbol); + const testFeed = pythtestConformanceFeeds.find( + (feed) => feed.symbol === symbol, + ); + const publishers = [...pythnetPublishers, ...pythtestConformancePublishers]; const metricsTime = pythnetPublishers.find( (publisher) => publisher.ranking !== undefined, )?.ranking?.time; @@ -45,10 +50,15 @@ export const Publishers = async ({ params }: Props) => { searchPlaceholder="Publisher key or name" metricsTime={metricsTime} nameLoadingSkeleton={} + symbol={symbol} + displaySymbol={feed.product.display_symbol} priceComponents={publishers.map( ({ ranking, publisher, status, cluster, knownPublisher }) => ({ - id: `${publisher}-${ClusterToName[Cluster.Pythnet]}`, - feedKey: feed.product.price_account, + id: `${publisher}-${ClusterToName[cluster]}`, + feedKey: + cluster === Cluster.Pythnet + ? feed.product.price_account + : (testFeed?.product.price_account ?? ""), score: ranking?.final_score, uptimeScore: ranking?.uptime_score, deviationScore: ranking?.deviation_score, @@ -56,12 +66,12 @@ export const Publishers = async ({ params }: Props) => { cluster, status, publisherKey: publisher, - symbol, rank: ranking?.final_rank, firstEvaluation: ranking?.first_ranking_time, name: ( , diff --git a/apps/insights/src/components/PriceFeed/reference-data.tsx b/apps/insights/src/components/PriceFeed/reference-data.tsx index ce3fb73229..99618869a6 100644 --- a/apps/insights/src/components/PriceFeed/reference-data.tsx +++ b/apps/insights/src/components/PriceFeed/reference-data.tsx @@ -5,6 +5,7 @@ import { useMemo } from "react"; import { useCollator } from "react-aria"; import styles from "./reference-data.module.scss"; +import { Cluster } from "../../services/pyth"; import { AssetClassTag } from "../AssetClassTag"; import { LiveValue } from "../LivePrices"; @@ -74,6 +75,7 @@ export const ReferenceData = ({ feed }: Props) => { feedKey={feed.feedKey} field={value} defaultValue={feed[value]} + cluster={Cluster.Pythnet} /> , ] as const, diff --git a/apps/insights/src/components/PriceFeedChangePercent/index.tsx b/apps/insights/src/components/PriceFeedChangePercent/index.tsx index e44ed87b3a..2c1f7929c8 100644 --- a/apps/insights/src/components/PriceFeedChangePercent/index.tsx +++ b/apps/insights/src/components/PriceFeedChangePercent/index.tsx @@ -5,6 +5,7 @@ import { z } from "zod"; import { StateType, useData } from "../../hooks/use-data"; import { useLivePriceData } from "../../hooks/use-live-price-data"; +import { Cluster } from "../../services/pyth"; import { ChangePercent } from "../ChangePercent"; const ONE_SECOND_IN_MS = 1000; @@ -105,7 +106,7 @@ const PriceFeedChangePercentLoaded = ({ priorPrice, feedKey, }: PriceFeedChangePercentLoadedProps) => { - const { current } = useLivePriceData(feedKey); + const { current } = useLivePriceData(Cluster.Pythnet, feedKey); return current === undefined ? ( diff --git a/apps/insights/src/components/PriceFeeds/index.tsx b/apps/insights/src/components/PriceFeeds/index.tsx index bec8a598ba..25cc6a9750 100644 --- a/apps/insights/src/components/PriceFeeds/index.tsx +++ b/apps/insights/src/components/PriceFeeds/index.tsx @@ -176,7 +176,10 @@ const FeaturedFeedsCard = ({ {showPrices && (
- + { ...feed, assetClass: contextFeed.assetClass, displaySymbol: contextFeed.displaySymbol, - key: contextFeed.key, + key: contextFeed.key[Cluster.Pythnet], }; } else { throw new NoSuchFeedError(feed.symbol); @@ -123,17 +124,25 @@ const ResolvedPriceFeedsCard = ({ priceFeeds, ...props }: Props) => { href: `/price-feeds/${encodeURIComponent(symbol)}`, data: { exponent: ( - + ), numPublishers: ( ), - price: , - confidenceInterval: , + price: , + confidenceInterval: ( + + ), priceFeedName: , assetClass: , priceFeedId: , diff --git a/apps/insights/src/components/Publisher/layout.module.scss b/apps/insights/src/components/Publisher/layout.module.scss index 36978a00a9..e8dd6b1ace 100644 --- a/apps/insights/src/components/Publisher/layout.module.scss +++ b/apps/insights/src/components/Publisher/layout.module.scss @@ -9,16 +9,17 @@ flex-flow: column nowrap; gap: theme.spacing(8); - .headerRow, - .rightGroup, + .breadcrumbRow, .stats { display: flex; flex-flow: row nowrap; + align-items: center; } - .headerRow, - .rightGroup { + .breadcrumbRow { align-items: center; + justify-content: space-between; + margin-bottom: -#{theme.spacing(2)}; } .stats { @@ -30,10 +31,6 @@ width: 0; } - .averageScoreChart svg { - cursor: pointer; - } - .activeDate { color: theme.color("muted"); } @@ -48,18 +45,6 @@ color: theme.color("states", "error", "base"); } } - - .headerRow { - justify-content: space-between; - } - - .rightGroup { - gap: theme.spacing(2); - } - - .breadcrumbs { - margin-bottom: -#{theme.spacing(2)}; - } } .priceFeedsTabLabel { diff --git a/apps/insights/src/components/Publisher/layout.tsx b/apps/insights/src/components/Publisher/layout.tsx index f2b2dffec7..95d9d4aaf5 100644 --- a/apps/insights/src/components/Publisher/layout.tsx +++ b/apps/insights/src/components/Publisher/layout.tsx @@ -23,7 +23,7 @@ import { getPublishers, } from "../../services/clickhouse"; import { getPublisherCaps } from "../../services/hermes"; -import { Cluster } from "../../services/pyth"; +import { Cluster, ClusterToName, parseCluster } from "../../services/pyth"; import { getPublisherPoolData } from "../../services/staking"; import { ChangePercent } from "../ChangePercent"; import { ChangeValue } from "../ChangeValue"; @@ -49,12 +49,19 @@ import { TokenIcon } from "../TokenIcon"; type Props = { children: ReactNode; params: Promise<{ + cluster: string; key: string; }>; }; export const PublishersLayout = async ({ children, params }: Props) => { - const { key } = await params; + const { cluster, key } = await params; + const parsedCluster = parseCluster(cluster); + + if (parsedCluster === undefined) { + notFound(); + } + const [ rankingHistory, averageScoreHistory, @@ -62,11 +69,11 @@ export const PublishersLayout = async ({ children, params }: Props) => { priceFeeds, publishers, ] = await Promise.all([ - getPublisherRankingHistory(key), - getPublisherAverageScoreHistory(key), + getPublisherRankingHistory(parsedCluster, key), + getPublisherAverageScoreHistory(parsedCluster, key), getOisStats(key), - getPriceFeeds(Cluster.Pythnet, key), - getPublishers(), + getPriceFeeds(parsedCluster, key), + getPublishers(parsedCluster), ]); const currentRanking = rankingHistory.at(-1); @@ -79,6 +86,7 @@ export const PublishersLayout = async ({ children, params }: Props) => { return publisher && currentRanking && currentAverageScore ? ( ({ symbol: feed.symbol, @@ -94,7 +102,7 @@ export const PublishersLayout = async ({ children, params }: Props) => { >
-
+
{ ]} />
-
- , - })} - /> -
+ , + })} + />
{ /> } data={averageScoreHistory.map(({ time, averageScore }) => ({ x: time, @@ -199,7 +205,7 @@ export const PublishersLayout = async ({ children, params }: Props) => { } stat1={ {publisher.activeFeeds} @@ -207,7 +213,7 @@ export const PublishersLayout = async ({ children, params }: Props) => { } stat2={ {publisher.inactiveFeeds} @@ -238,136 +244,140 @@ export const PublishersLayout = async ({ children, params }: Props) => { label="Active Feeds" /> - - oisStats.maxPoolSize - ? "" - : undefined - } - > - + oisStats.maxPoolSize + ? "" + : undefined } - /> - % - - } - corner={} - > - - - - - - - } - endLabel={ - - - - - + > + + % } - /> - - - - - - } - > - } > - -
OIS Pool
-
- - - - - } - /> - + + + + + + } + endLabel={ + + + + + + + } + /> + + - - + + } - /> - - } - header="Oracle Integrity Staking (OIS)" > - OIS allows anyone to help secure Pyth and protect DeFi. - Through decentralized staking rewards and slashing, OIS - incentivizes Pyth publishers to maintain high-quality data - contributions. PYTH holders can stake to publishers to further - reinforce oracle security. Rewards are programmatically - distributed to high quality publishers and the stakers - supporting them to strengthen oracle integrity. - - -
+ + +
OIS Pool
+
+ + + + + } + /> + + + + + } + /> + + } + header="Oracle Integrity Staking (OIS)" + > + OIS allows anyone to help secure Pyth and protect DeFi. + Through decentralized staking rewards and slashing, OIS + incentivizes Pyth publishers to maintain high-quality data + contributions. PYTH holders can stake to publishers to + further reinforce oracle security. Rewards are + programmatically distributed to high quality publishers and + the stakers supporting them to strengthen oracle integrity. + + + + )}
; }; export const Performance = async ({ params }: Props) => { - const { key } = await params; + const { key, cluster } = await params; + const parsedCluster = parseCluster(cluster); + + if (parsedCluster === undefined) { + notFound(); + } const [publishers, priceFeeds] = await Promise.all([ - getPublishers(), - getPriceFeeds(Cluster.Pythnet, key), + getPublishers(parsedCluster), + getPriceFeeds(parsedCluster, key), ]); const slicedPublishers = sliceAround( publishers, @@ -114,7 +120,7 @@ export const Performance = async ({ params }: Props) => { ), activeFeeds: ( {publisher.activeFeeds} @@ -122,7 +128,7 @@ export const Performance = async ({ params }: Props) => { ), inactiveFeeds: ( {publisher.inactiveFeeds} @@ -136,6 +142,7 @@ export const Performance = async ({ params }: Props) => { ), name: ( { ), }, ...(publisher.key !== key && { - href: `/publishers/${publisher.key}`, + href: `/publishers/${ClusterToName[parsedCluster]}/${publisher.key}`, }), }; })} diff --git a/apps/insights/src/components/Publisher/price-feed-drawer-provider.tsx b/apps/insights/src/components/Publisher/price-feed-drawer-provider.tsx index c3262e1f9d..6ea9a34dd6 100644 --- a/apps/insights/src/components/Publisher/price-feed-drawer-provider.tsx +++ b/apps/insights/src/components/Publisher/price-feed-drawer-provider.tsx @@ -12,6 +12,7 @@ import { use, } from "react"; +import type { Cluster } from "../../services/pyth"; import type { Status } from "../../status"; import { PriceComponentDrawer } from "../PriceComponentDrawer"; import { PriceFeedTag } from "../PriceFeedTag"; @@ -25,6 +26,7 @@ type PriceFeedDrawerProviderProps = Omit< "value" > & { publisherKey: string; + cluster: Cluster; priceFeeds: PriceFeed[]; }; @@ -52,6 +54,7 @@ const PriceFeedDrawerProviderImpl = ({ publisherKey, priceFeeds, children, + cluster, }: PriceFeedDrawerProviderProps) => { const logger = useLogger(); const [selectedSymbol, setSelectedSymbol] = useQueryState( @@ -91,11 +94,12 @@ const PriceFeedDrawerProviderImpl = ({ rank={selectedFeed.rank} score={selectedFeed.score} symbol={selectedFeed.symbol} + displaySymbol={selectedFeed.displaySymbol} status={selectedFeed.status} firstEvaluation={selectedFeed.firstEvaluation ?? new Date()} - navigateButtonText="Open Feed" navigateHref={feedHref} title={} + cluster={cluster} /> )} diff --git a/apps/insights/src/components/Publisher/price-feeds-card.tsx b/apps/insights/src/components/Publisher/price-feeds-card.tsx index 437fef0bb1..65272e8160 100644 --- a/apps/insights/src/components/Publisher/price-feeds-card.tsx +++ b/apps/insights/src/components/Publisher/price-feeds-card.tsx @@ -4,7 +4,7 @@ import { type ComponentProps, useCallback } from "react"; import { useSelectPriceFeed } from "./price-feed-drawer-provider"; import { usePriceFeeds } from "../../hooks/use-price-feeds"; -import { Cluster, ClusterToName } from "../../services/pyth"; +import type { Cluster } from "../../services/pyth"; import { PriceComponentsCard } from "../PriceComponentsCard"; import { PriceFeedTag } from "../PriceFeedTag"; @@ -13,6 +13,7 @@ type Props = Omit< "onPriceComponentAction" | "priceComponents" > & { publisherKey: string; + cluster: Cluster; priceFeeds: (Pick< ComponentProps["priceComponents"][number], "score" | "uptimeScore" | "deviationScore" | "stalledScore" | "status" @@ -24,6 +25,7 @@ type Props = Omit< export const PriceFeedsCard = ({ priceFeeds, publisherKey, + cluster, ...props }: Props) => { const feeds = usePriceFeeds(); @@ -39,14 +41,14 @@ export const PriceFeedsCard = ({ const contextFeed = feeds.get(feed.symbol); if (contextFeed) { return { - id: `${contextFeed.key}-${ClusterToName[Cluster.Pythnet]}`, - feedKey: contextFeed.key, + id: contextFeed.key[cluster], + feedKey: contextFeed.key[cluster], symbol: feed.symbol, score: feed.score, uptimeScore: feed.uptimeScore, deviationScore: feed.deviationScore, stalledScore: feed.stalledScore, - cluster: Cluster.Pythnet, + cluster, status: feed.status, publisherKey, name: , diff --git a/apps/insights/src/components/Publisher/price-feeds.tsx b/apps/insights/src/components/Publisher/price-feeds.tsx index 40a3a917f7..76a91af420 100644 --- a/apps/insights/src/components/Publisher/price-feeds.tsx +++ b/apps/insights/src/components/Publisher/price-feeds.tsx @@ -1,17 +1,25 @@ +import { notFound } from "next/navigation"; + import { getPriceFeeds } from "./get-price-feeds"; import { PriceFeedsCard } from "./price-feeds-card"; -import { Cluster } from "../../services/pyth"; +import { parseCluster } from "../../services/pyth"; import { PriceFeedTag } from "../PriceFeedTag"; type Props = { params: Promise<{ + cluster: string; key: string; }>; }; export const PriceFeeds = async ({ params }: Props) => { - const { key } = await params; - const feeds = await getPriceFeeds(Cluster.Pythnet, key); + const { key, cluster } = await params; + const parsedCluster = parseCluster(cluster); + + if (parsedCluster === undefined) { + notFound(); + } + const feeds = await getPriceFeeds(parsedCluster, key); const metricsTime = feeds.find((feed) => feed.ranking !== undefined)?.ranking ?.time; @@ -22,6 +30,7 @@ export const PriceFeeds = async ({ params }: Props) => { metricsTime={metricsTime} nameLoadingSkeleton={} publisherKey={key} + cluster={parsedCluster} priceFeeds={feeds.map(({ ranking, feed, status }) => ({ symbol: feed.symbol, score: ranking?.final_score, diff --git a/apps/insights/src/components/PublisherTag/index.module.scss b/apps/insights/src/components/PublisherTag/index.module.scss index 1fb90ff267..bb1386c638 100644 --- a/apps/insights/src/components/PublisherTag/index.module.scss +++ b/apps/insights/src/components/PublisherTag/index.module.scss @@ -62,6 +62,10 @@ } } + .testBadge { + margin-left: theme.spacing(4); + } + &[data-loading] { .icon { border-radius: theme.border-radius("full"); diff --git a/apps/insights/src/components/PublisherTag/index.tsx b/apps/insights/src/components/PublisherTag/index.tsx index ef6294675d..15e63ce535 100644 --- a/apps/insights/src/components/PublisherTag/index.tsx +++ b/apps/insights/src/components/PublisherTag/index.tsx @@ -1,13 +1,18 @@ import { Broadcast } from "@phosphor-icons/react/dist/ssr/Broadcast"; +import { Badge } from "@pythnetwork/component-library/Badge"; import { Skeleton } from "@pythnetwork/component-library/Skeleton"; import clsx from "clsx"; import type { ComponentProps, ReactNode } from "react"; import styles from "./index.module.scss"; import { omitKeys } from "../../omit-keys"; +import { Cluster } from "../../services/pyth"; import { PublisherKey } from "../PublisherKey"; -type Props = ComponentProps<"div"> & { compact?: boolean | undefined } & ( +type Props = ComponentProps<"div"> & { + compact?: boolean | undefined; + cluster?: Cluster | undefined; +} & ( | { isLoading: true } | ({ isLoading?: false; @@ -37,6 +42,16 @@ export const PublisherTag = ({ className, ...props }: Props) => (
{props.icon ?? }
)} + {props.cluster === Cluster.PythtestConformance && ( + + test + + )}
); diff --git a/apps/insights/src/components/Publishers/index.tsx b/apps/insights/src/components/Publishers/index.tsx index 9446cf84b5..07034feffa 100644 --- a/apps/insights/src/components/Publishers/index.tsx +++ b/apps/insights/src/components/Publishers/index.tsx @@ -9,6 +9,7 @@ import styles from "./index.module.scss"; import { PublishersCard } from "./publishers-card"; import { getPublishers } from "../../services/clickhouse"; import { getPublisherCaps } from "../../services/hermes"; +import { Cluster } from "../../services/pyth"; import { getDelState, getClaimableRewards, @@ -24,13 +25,15 @@ import { TokenIcon } from "../TokenIcon"; const INITIAL_REWARD_POOL_SIZE = 60_000_000_000_000n; export const Publishers = async () => { - const [publishers, oisStats] = await Promise.all([ - getPublishers(), - getOisStats(), - ]); + const [pythnetPublishers, pythtestConformancePublishers, oisStats] = + await Promise.all([ + getPublishers(Cluster.Pythnet), + getPublishers(Cluster.PythtestConformance), + getOisStats(), + ]); - const rankingTime = publishers[0]?.timestamp; - const scoreTime = publishers[0]?.scoreTime; + const rankingTime = pythnetPublishers[0]?.timestamp; + const scoreTime = pythnetPublishers[0]?.scoreTime; return (
@@ -56,16 +59,16 @@ export const Publishers = async () => { } stat={( - publishers.reduce( + pythnetPublishers.reduce( (sum, publisher) => sum + publisher.averageScore, 0, - ) / publishers.length + ) / pythnetPublishers.length ).toFixed(2)} /> { } - publishers={publishers.map( - ({ key, rank, inactiveFeeds, activeFeeds, averageScore }) => { - const knownPublisher = lookupPublisher(key); - return { - id: key, - ranking: rank, - activeFeeds: activeFeeds, - inactiveFeeds: inactiveFeeds, - averageScore, - ...(knownPublisher && { - name: knownPublisher.name, - icon: , - }), - }; - }, + pythnetPublishers={pythnetPublishers.map((publisher) => + toTableRow(publisher), + )} + pythtestConformancePublishers={pythtestConformancePublishers.map( + (publisher) => toTableRow(publisher), )} />
@@ -154,6 +147,27 @@ export const Publishers = async () => { ); }; +const toTableRow = ({ + key, + rank, + inactiveFeeds, + activeFeeds, + averageScore, +}: Awaited>[number]) => { + const knownPublisher = lookupPublisher(key); + return { + id: key, + ranking: rank, + activeFeeds: activeFeeds, + inactiveFeeds: inactiveFeeds, + averageScore, + ...(knownPublisher && { + name: knownPublisher.name, + icon: , + }), + }; +}; + const getOisStats = async () => { const [delState, claimableRewards, distributedRewards, publisherCaps] = await Promise.all([ diff --git a/apps/insights/src/components/Publishers/publishers-card.tsx b/apps/insights/src/components/Publishers/publishers-card.tsx index db7c03d9a0..7f86ca57e9 100644 --- a/apps/insights/src/components/Publishers/publishers-card.tsx +++ b/apps/insights/src/components/Publishers/publishers-card.tsx @@ -1,20 +1,25 @@ "use client"; import { Broadcast } from "@phosphor-icons/react/dist/ssr/Broadcast"; +import { Database } from "@phosphor-icons/react/dist/ssr/Database"; +import { useLogger } from "@pythnetwork/app-logger"; import { Badge } from "@pythnetwork/component-library/Badge"; import { Card } from "@pythnetwork/component-library/Card"; import { Link } from "@pythnetwork/component-library/Link"; import { Paginator } from "@pythnetwork/component-library/Paginator"; import { SearchInput } from "@pythnetwork/component-library/SearchInput"; +import { Select } from "@pythnetwork/component-library/Select"; import { type RowConfig, type SortDescriptor, Table, } from "@pythnetwork/component-library/Table"; -import { type ReactNode, Suspense, useMemo } from "react"; +import { useQueryState, parseAsStringEnum } from "nuqs"; +import { type ReactNode, Suspense, useMemo, useCallback } from "react"; import { useFilter, useCollator } from "react-aria"; import { useQueryParamFilterPagination } from "../../hooks/use-query-param-filter-pagination"; +import { CLUSTER_NAMES } from "../../services/pyth"; import { ExplainActive, ExplainInactive } from "../Explanations"; import { NoResults } from "../NoResults"; import { PublisherTag } from "../PublisherTag"; @@ -26,7 +31,8 @@ const PUBLISHER_SCORE_WIDTH = 38; type Props = { className?: string | undefined; - publishers: Publisher[]; + pythnetPublishers: Publisher[]; + pythtestConformancePublishers: Publisher[]; explainAverage: ReactNode; }; @@ -41,15 +47,33 @@ type Publisher = { | { name?: undefined; icon?: undefined } ); -export const PublishersCard = ({ publishers, ...props }: Props) => ( +export const PublishersCard = ({ + pythnetPublishers, + pythtestConformancePublishers, + ...props +}: Props) => ( }> - + ); -const ResolvedPublishersCard = ({ publishers, ...props }: Props) => { +const ResolvedPublishersCard = ({ + pythnetPublishers, + pythtestConformancePublishers, + ...props +}: Props) => { + const logger = useLogger(); const collator = useCollator(); const filter = useFilter({ sensitivity: "base", usage: "search" }); + const [cluster, setCluster] = useQueryState( + "cluster", + parseAsStringEnum([...CLUSTER_NAMES]).withDefault("pythnet"), + ); + const { search, sortDescriptor, @@ -64,7 +88,7 @@ const ResolvedPublishersCard = ({ publishers, ...props }: Props) => { numPages, mkPageLink, } = useQueryParamFilterPagination( - publishers, + cluster === "pythnet" ? pythnetPublishers : pythtestConformancePublishers, (publisher, search) => filter.contains(publisher.id, search) || (publisher.name !== undefined && filter.contains(publisher.name, search)), @@ -108,7 +132,7 @@ const ResolvedPublishersCard = ({ publishers, ...props }: Props) => { ...publisher }) => ({ id, - href: `/publishers/${id}`, + href: `/publishers/${cluster}/${id}`, data: { ranking: {ranking}, name: ( @@ -121,13 +145,16 @@ const ResolvedPublishersCard = ({ publishers, ...props }: Props) => { /> ), activeFeeds: ( - + {activeFeeds} ), inactiveFeeds: ( {inactiveFeeds} @@ -139,7 +166,17 @@ const ResolvedPublishersCard = ({ publishers, ...props }: Props) => { }, }), ), - [paginatedItems], + [paginatedItems, cluster], + ); + + const updateCluster = useCallback( + (newCluster: (typeof CLUSTER_NAMES)[number]) => { + updatePage(1); + setCluster(newCluster).catch((error: unknown) => { + logger.error("Failed to update asset class", error); + }); + }, + [updatePage, setCluster, logger], ); return ( @@ -155,6 +192,8 @@ const ResolvedPublishersCard = ({ publishers, ...props }: Props) => { onPageSizeChange={updatePageSize} onPageChange={updatePage} mkPageLink={mkPageLink} + cluster={cluster} + onChangeCluster={updateCluster} rows={rows} {...props} /> @@ -177,6 +216,8 @@ type PublishersCardContentsProps = Pick & onPageSizeChange: (newPageSize: number) => void; onPageChange: (newPage: number) => void; mkPageLink: (page: number) => string; + cluster: (typeof CLUSTER_NAMES)[number]; + onChangeCluster: (value: (typeof CLUSTER_NAMES)[number]) => void; rows: RowConfig< "ranking" | "name" | "activeFeeds" | "inactiveFeeds" | "averageScore" >[]; @@ -202,17 +243,34 @@ const PublishersCardContents = ({ } toolbar={ - + <> +