diff --git a/apps/insights/src/app/api/pyth/get-feeds-for-publisher/[publisher]/route.ts b/apps/insights/src/app/api/pyth/get-feeds-for-publisher/[publisher]/route.ts
index a15c0c9b16..bb49c024aa 100644
--- a/apps/insights/src/app/api/pyth/get-feeds-for-publisher/[publisher]/route.ts
+++ b/apps/insights/src/app/api/pyth/get-feeds-for-publisher/[publisher]/route.ts
@@ -38,7 +38,6 @@ export const GET = async (
const filteredFeeds = feeds.filter((feed) =>
feed.price.priceComponents.some((c) => c.publisher === publisher),
);
-
return new Response(stringify(filteredFeeds), {
headers: {
"Content-Type": "application/json",
diff --git a/apps/insights/src/components/Explanations/index.tsx b/apps/insights/src/components/Explanations/index.tsx
index a23e239b64..aa093571a4 100644
--- a/apps/insights/src/components/Explanations/index.tsx
+++ b/apps/insights/src/components/Explanations/index.tsx
@@ -24,19 +24,25 @@ export const ExplainPermissioned = ({
scoreTime,
}: {
scoreTime?: Date | undefined;
-}) => {
- return (
-
-
- This is the number of Price Feeds that a Publisher has
- permissions to publish to. The publisher is not necessarily pushing data
- for all the feeds they have access to, and some feeds may not be live
- yet.
-
- {scoreTime && }
-
- );
-};
+}) => (
+
+
+ This is the number of Price Feeds that a Publisher has
+ permissions to publish to. The publisher is not necessarily pushing data
+ for all the feeds they have access to, and some feeds may not be live yet.
+
+ {scoreTime && }
+
+);
+
+export const ExplainUnpermissioned = () => (
+
+
+ This is the number of Price Feeds that a Publisher does not
+ have permissions to publish to.
+
+
+);
export const ExplainAverage = ({
scoreTime,
@@ -96,31 +102,3 @@ export const EvaluationTime = ({ scoreTime }: { scoreTime: Date }) => {
);
};
-
-export const ExplainActive = () => (
-
-
- This is the number of feeds which the publisher is permissioned for, where
- the publisher{"'"}s feed has 50% or better uptime over the last day.
-
-
-
-);
-
-export const ExplainInactive = () => (
-
-
- This is the number of feeds which the publisher is permissioned for, but
- for which the publisher{"'"}s feed has less than 50% uptime over the last
- day.
-
-
-
-);
-
-const NeitherActiveNorInactiveNote = () => (
-
- Note that a publisher{"'"}s feed may not be considered either active or
- inactive if Pyth has not yet calculated quality rankings for it.
-
-);
diff --git a/apps/insights/src/components/FormattedNumber/index.tsx b/apps/insights/src/components/FormattedNumber/index.tsx
index 3fadfd2950..c5e2bce5fc 100644
--- a/apps/insights/src/components/FormattedNumber/index.tsx
+++ b/apps/insights/src/components/FormattedNumber/index.tsx
@@ -4,7 +4,7 @@ import { useMemo } from "react";
import { useNumberFormatter } from "react-aria";
type Props = Parameters[0] & {
- value: number;
+ value: number | bigint;
};
export const FormattedNumber = ({ value, ...args }: Props) => {
diff --git a/apps/insights/src/components/LivePrices/index.tsx b/apps/insights/src/components/LivePrices/index.tsx
index 0bf30b37e7..fc79fe7889 100644
--- a/apps/insights/src/components/LivePrices/index.tsx
+++ b/apps/insights/src/components/LivePrices/index.tsx
@@ -14,6 +14,7 @@ import {
} from "../../hooks/use-live-price-data";
import { usePriceFormatter } from "../../hooks/use-price-formatter";
import type { Cluster } from "../../services/pyth";
+import { useLivePublishersData } from '../../hooks/use-live-publishers-data';
export const SKELETON_WIDTH = 20;
@@ -210,7 +211,6 @@ export const LiveComponentValue = ({
cluster,
}: LiveComponentValueProps) => {
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.tsx b/apps/insights/src/components/PriceComponentDrawer/index.tsx
index 49540eacb8..f4150c97e2 100644
--- a/apps/insights/src/components/PriceComponentDrawer/index.tsx
+++ b/apps/insights/src/components/PriceComponentDrawer/index.tsx
@@ -35,15 +35,14 @@ import type { CategoricalChartState } from "recharts/types/chart/types";
import { z } from "zod";
import { Cluster, ClusterToName } from "../../services/pyth";
-import type { Status } from "../../status";
import ConformanceReport from "../ConformanceReport/conformance-report";
+import type { Interval } from "../ConformanceReport/types";
+import { useDownloadReportForFeed } from '../ConformanceReport/use-download-report-for-feed';
import { LiveComponentValue, LiveConfidence, LivePrice } from "../LivePrices";
import { PriceName } from "../PriceName";
import { Score } from "../Score";
-import { Status as StatusComponent } from "../Status";
+import { StatusLive } from "../Status";
import styles from "./index.module.scss";
-import type { Interval } from "../ConformanceReport/types";
-import { useDownloadReportForFeed } from "../ConformanceReport/use-download-report-for-feed";
const LineChart = dynamic(
() => import("recharts").then((recharts) => recharts.LineChart),
@@ -61,7 +60,6 @@ type PriceComponent = {
feedKey: string;
score: number | undefined;
rank: number | undefined;
- status: Status;
identifiesPublisher?: boolean | undefined;
firstEvaluation?: Date | undefined;
cluster: Cluster;
@@ -137,16 +135,20 @@ export const usePriceComponentDrawer = ({
),
headingAfter: (
-
+
),
contents: (
@@ -267,14 +269,14 @@ export const usePriceComponentDrawer = ({
};
type HeadingExtraProps = {
- status: Status;
identifiesPublisher?: boolean | undefined;
cluster: Cluster;
publisherKey: string;
symbol: string;
+ feedKey: string;
};
-const HeadingExtra = ({ status, ...props }: HeadingExtraProps) => {
+const HeadingExtra = ({ feedKey, ...props }: HeadingExtraProps) => {
const downloadReportForFeed = useDownloadReportForFeed();
const handleDownloadReport = useCallback(
@@ -293,7 +295,11 @@ const HeadingExtra = ({ status, ...props }: HeadingExtraProps) => {
<>
-
+
{
+ const publisherData = useLivePublishersData(feedKey);
+ if(!publisherData?.slot) {
+ return
+ }
+ return publisherData.slot;
+};
+
export const ResolvedPriceComponentsCard = <
U extends string,
T extends PriceComponent & Record,
@@ -119,7 +134,6 @@ export const ResolvedPriceComponentsCard = <
}) => {
const logger = useLogger();
const collator = useCollator();
- const filter = useFilter({ sensitivity: "base", usage: "search" });
const { selectComponent } = usePriceComponentDrawer({
components: priceComponents,
identifiesPublisher,
@@ -158,7 +172,7 @@ export const ResolvedPriceComponentsCard = <
mkPageLink,
} = useQueryParamFilterPagination(
componentsFilteredByStatus,
- (component, search) => filter.contains(component.nameAsString, search),
+ ()=>true,
(a, b, { column, direction }) => {
switch (column) {
case "score":
@@ -186,6 +200,9 @@ export const ResolvedPriceComponentsCard = <
}
case "status": {
+ if (a.status === undefined || b.status === undefined) {
+ return 0;
+ }
const resultByStatus = b.status - a.status;
const result =
resultByStatus === 0
@@ -200,7 +217,11 @@ export const ResolvedPriceComponentsCard = <
}
}
},
- (items) => items,
+ (items, search) => {
+ return matchSorter(items, search, {
+ keys: ["nameAsString","feedKey"],
+ });
+ },
{
defaultPageSize: 50,
defaultSort: "name",
@@ -246,12 +267,7 @@ export const ResolvedPriceComponentsCard = <
/>
),
slot: (
-
+
),
price: (
),
- status: ,
+ status: component.status !== undefined && (
+
+ ),
},
})),
[paginatedItems, props.extraColumns, selectComponent],
@@ -293,25 +311,27 @@ export const ResolvedPriceComponentsCard = <
);
return (
-
+
+
+
);
};
diff --git a/apps/insights/src/components/PriceFeed/publishers-card.tsx b/apps/insights/src/components/PriceFeed/publishers-card.tsx
index 53b60ebc91..a9ccc75561 100644
--- a/apps/insights/src/components/PriceFeed/publishers-card.tsx
+++ b/apps/insights/src/components/PriceFeed/publishers-card.tsx
@@ -2,13 +2,15 @@
import { Switch } from "@pythnetwork/component-library/Switch";
import { useLogger } from "@pythnetwork/component-library/useLogger";
-import { useQueryState, parseAsBoolean } from "nuqs";
+import { parseAsBoolean, useQueryState } from "nuqs";
import { Suspense, useCallback, useMemo } from "react";
+import { useLivePriceData } from "../../hooks/use-live-price-data";
import { Cluster } from "../../services/pyth";
import type { PriceComponent } from "../PriceComponentsCard";
import { PriceComponentsCard } from "../PriceComponentsCard";
import { PublisherTag } from "../PublisherTag";
+import { getStatus } from "../Status";
type PublishersCardProps =
| { isLoading: true }
@@ -29,7 +31,10 @@ type ResolvedPublishersCardProps = {
symbol: string;
displaySymbol: string;
assetClass: string;
- publishers: Omit[];
+ publishers: Omit<
+ PriceComponent,
+ "status" | "symbol" | "displaySymbol" | "assetClass"
+ >[];
metricsTime?: Date | undefined;
};
@@ -38,6 +43,7 @@ const ResolvedPublishersCard = ({
...props
}: ResolvedPublishersCardProps) => {
const logger = useLogger();
+ const data = useLivePriceData(Cluster.Pythnet, publishers[0]?.feedKey);
const [includeTestFeeds, setIncludeTestFeeds] = useQueryState(
"includeTestFeeds",
@@ -63,11 +69,20 @@ const ResolvedPublishersCard = ({
[includeTestFeeds, publishers],
);
+ const publishersWithStatus = useMemo(() => {
+ return publishersFilteredByCluster.map((publisher) => {
+ return {
+ ...publisher,
+ status: getStatus(data.current, publisher.publisherKey),
+ };
+ });
+ }, [publishersFilteredByCluster, data]);
+
return (
);
@@ -75,10 +90,14 @@ const ResolvedPublishersCard = ({
type PublishersCardImplProps =
| { isLoading: true }
- | (ResolvedPublishersCardProps & {
+ | (Omit & {
isLoading?: false | undefined;
includeTestFeeds: boolean;
updateIncludeTestFeeds: (newValue: boolean) => void;
+ publishers: Omit<
+ PriceComponent,
+ "symbol" | "displaySymbol" | "assetClass"
+ >[];
});
const PublishersCardImpl = (props: PublishersCardImplProps) => (
diff --git a/apps/insights/src/components/PriceFeed/publishers.tsx b/apps/insights/src/components/PriceFeed/publishers.tsx
index e15d8569ad..df5022a014 100644
--- a/apps/insights/src/components/PriceFeed/publishers.tsx
+++ b/apps/insights/src/components/PriceFeed/publishers.tsx
@@ -7,7 +7,6 @@ import {
} from "../../server/pyth";
import { getRankingsBySymbol } from "../../services/clickhouse";
import { Cluster, ClusterToName } from "../../services/pyth";
-import { getStatus } from "../../status";
import { PublisherIcon } from "../PublisherIcon";
import { PublisherTag } from "../PublisherTag";
import { PublishersCard } from "./publishers-card";
@@ -34,7 +33,6 @@ export const Publishers = async ({ params }: Props) => {
const metricsTime = pythnetPublishers.find(
(publisher) => publisher.ranking !== undefined,
)?.ranking?.time;
-
return feed === undefined ? (
notFound()
) : (
@@ -44,7 +42,7 @@ export const Publishers = async ({ params }: Props) => {
displaySymbol={feed.product.display_symbol}
assetClass={feed.product.asset_type}
publishers={publishers.map(
- ({ ranking, publisher, status, cluster, knownPublisher }) => ({
+ ({ ranking, publisher, cluster, knownPublisher }) => ({
id: `${publisher}-${ClusterToName[cluster]}`,
feedKey:
cluster === Cluster.Pythnet
@@ -55,7 +53,6 @@ export const Publishers = async ({ params }: Props) => {
deviationScore: ranking?.deviation_score,
stalledScore: ranking?.stalled_score,
cluster,
- status,
publisherKey: publisher,
rank: ranking?.final_rank,
firstEvaluation: ranking?.first_ranking_time,
@@ -94,7 +91,6 @@ const getPublishers = async (cluster: Cluster, symbol: string) => {
return {
ranking,
publisher,
- status: getStatus(ranking),
cluster,
knownPublisher: lookupPublisher(publisher),
};
diff --git a/apps/insights/src/components/Publisher/get-price-feeds.tsx b/apps/insights/src/components/Publisher/get-price-feeds.tsx
index 6a392badf7..a66bcc11c8 100644
--- a/apps/insights/src/components/Publisher/get-price-feeds.tsx
+++ b/apps/insights/src/components/Publisher/get-price-feeds.tsx
@@ -1,7 +1,6 @@
import { getFeedsForPublisherRequest } from "../../server/pyth";
import { getRankingsByPublisher } from "../../services/clickhouse";
import { Cluster, ClusterToName } from "../../services/pyth";
-import { getStatus } from "../../status";
export const getPriceFeeds = async (cluster: Cluster, key: string) => {
const [feeds, rankings] = await Promise.all([
@@ -17,7 +16,6 @@ export const getPriceFeeds = async (cluster: Cluster, key: string) => {
return {
ranking,
feed,
- status: getStatus(ranking),
};
});
};
diff --git a/apps/insights/src/components/Publisher/layout.tsx b/apps/insights/src/components/Publisher/layout.tsx
index 3765d522c1..3d04131384 100644
--- a/apps/insights/src/components/Publisher/layout.tsx
+++ b/apps/insights/src/components/Publisher/layout.tsx
@@ -35,8 +35,8 @@ import { ChartCard } from "../ChartCard";
import { Explain } from "../Explain";
import {
ExplainAverage,
- ExplainActive,
- ExplainInactive,
+ ExplainPermissioned,
+ ExplainUnpermissioned,
} from "../Explanations";
import { FormattedNumber } from "../FormattedNumber";
import { PublisherIcon } from "../PublisherIcon";
@@ -376,8 +376,8 @@ const ActiveFeedsCard = async ({
) : (
@@ -391,8 +391,8 @@ type ActiveFeedsCardImplProps =
isLoading?: false | undefined;
cluster: Cluster;
publisherKey: string;
- activeFeeds: number;
- inactiveFeeds: number;
+ permissionedFeeds: number;
+ unpermissionedFeeds: number;
allFeeds: number;
};
@@ -400,14 +400,14 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
- Active Feeds
-
+ Permissioned
+
>
}
header2={
<>
-
- Inactive Feeds
+ Unpermissioned
+
>
}
stat1={
@@ -415,10 +415,10 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
) : (
- {props.activeFeeds}
+ {props.permissionedFeeds}
)
}
@@ -427,10 +427,10 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
) : (
- {props.inactiveFeeds}
+ {props.unpermissionedFeeds}
)
}
@@ -441,7 +441,7 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
<>
%
>
@@ -454,7 +454,7 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
<>
%
>
@@ -463,9 +463,9 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
>
{!props.isLoading && (
)}
diff --git a/apps/insights/src/components/Publisher/performance.tsx b/apps/insights/src/components/Publisher/performance.tsx
index 05da88e9fd..5e8ac08cf5 100644
--- a/apps/insights/src/components/Publisher/performance.tsx
+++ b/apps/insights/src/components/Publisher/performance.tsx
@@ -10,19 +10,15 @@ import { NoResults } from "@pythnetwork/component-library/NoResults";
import { Table } from "@pythnetwork/component-library/Table";
import { lookup } from "@pythnetwork/known-publishers";
import { notFound } from "next/navigation";
-import type { ReactNode, ComponentProps } from "react";
+import type { ComponentProps, ReactNode } from "react";
-import { getPriceFeeds } from "./get-price-feeds";
-import styles from "./performance.module.scss";
-import { TopFeedsTable } from "./top-feeds-table";
import { getPublishers } from "../../services/clickhouse";
import type { Cluster } from "../../services/pyth";
import { ClusterToName, parseCluster } from "../../services/pyth";
-import { Status } from "../../status";
import {
- ExplainActive,
- ExplainInactive,
ExplainAverage,
+ ExplainPermissioned,
+ ExplainUnpermissioned,
} from "../Explanations";
import { PriceFeedIcon } from "../PriceFeedIcon";
import { PriceFeedTag } from "../PriceFeedTag";
@@ -30,6 +26,9 @@ import { PublisherIcon } from "../PublisherIcon";
import { PublisherTag } from "../PublisherTag";
import { Ranking } from "../Ranking";
import { Score } from "../Score";
+import { getPriceFeeds } from "./get-price-feeds";
+import styles from "./performance.module.scss";
+import { TopFeedsTable } from "./top-feeds-table";
const PUBLISHER_SCORE_WIDTH = 24;
@@ -68,22 +67,22 @@ export const Performance = async ({ params }: Props) => {
{publisher.rank}
),
- activeFeeds: (
+ permissionedFeeds: (
- {publisher.activeFeeds}
+ {publisher.permissionedFeeds}
),
- inactiveFeeds: (
+ unpermissionedFeeds: (
- {publisher.inactiveFeeds}
+ {publisher.unpermissionedFeeds}
),
averageScore: (
@@ -147,8 +146,8 @@ type PerformanceImplProps =
typeof Table<
| "ranking"
| "averageScore"
- | "activeFeeds"
- | "inactiveFeeds"
+ | "permissionedFeeds"
+ | "unpermissionedFeeds"
| "name"
>
>["rows"]
@@ -175,8 +174,8 @@ const PerformanceImpl = (props: PerformanceImplProps) => (
fields={[
{ id: "ranking", name: "Ranking" },
{ id: "averageScore", name: "Average Score" },
- { id: "activeFeeds", name: "Active Feeds" },
- { id: "inactiveFeeds", name: "Inactive Feeds" },
+ { id: "permissionedFeeds", name: "Permissioned" },
+ { id: "unpermissionedFeeds", name: "Unpermissioned" },
]}
{...(props.isLoading
? { isLoading: true }
@@ -207,22 +206,22 @@ const PerformanceImpl = (props: PerformanceImplProps) => (
loadingSkeleton: ,
},
{
- id: "activeFeeds",
+ id: "permissionedFeeds",
name: (
<>
- ACTIVE FEEDS
-
+ PERMISSIONED FEEDS
+
>
),
alignment: "center",
width: 30,
},
{
- id: "inactiveFeeds",
+ id: "unpermissionedFeeds",
name: (
<>
- INACTIVE FEEDS
-
+ UNPERMISSIONED FEEDS
+
>
),
alignment: "center",
@@ -292,27 +291,23 @@ const getFeedRows = (
>;
})[],
) =>
- priceFeeds
- .filter((feed) => feed.status === Status.Active)
- .slice(0, 20)
- .map(({ feed, ranking, status }) => ({
- key: feed.product.price_account,
- symbol: feed.symbol,
- displaySymbol: feed.product.display_symbol,
- description: feed.product.description,
- assetClass: feed.product.asset_type,
- score: ranking.final_score,
- rank: ranking.final_rank,
- status,
- firstEvaluation: ranking.first_ranking_time,
- icon: (
-
- ),
- href: `/price-feeds/${encodeURIComponent(feed.symbol)}`,
- }));
+ priceFeeds.slice(0, 20).map(({ feed, ranking }) => ({
+ key: feed.product.price_account,
+ symbol: feed.symbol,
+ displaySymbol: feed.product.display_symbol,
+ description: feed.product.description,
+ assetClass: feed.product.asset_type,
+ score: ranking.final_score,
+ rank: ranking.final_rank,
+ firstEvaluation: ranking.first_ranking_time,
+ icon: (
+
+ ),
+ href: `/price-feeds/${encodeURIComponent(feed.symbol)}`,
+ }));
const sliceAround = (
arr: T[],
diff --git a/apps/insights/src/components/Publisher/price-feeds.tsx b/apps/insights/src/components/Publisher/price-feeds.tsx
index c1dfb0313e..86d9500b09 100644
--- a/apps/insights/src/components/Publisher/price-feeds.tsx
+++ b/apps/insights/src/components/Publisher/price-feeds.tsx
@@ -27,13 +27,12 @@ export const PriceFeeds = async ({ params }: Props) => {
const feeds = await getPriceFeeds(parsedCluster, key);
const metricsTime = feeds.find((feed) => feed.ranking !== undefined)?.ranking
?.time;
-
return (
({
+ priceFeeds={feeds.map(({ ranking, feed }) => ({
symbol: feed.symbol,
name: (
{
}
/>
),
+ lastSlot: feed.price.lastSlot,
score: ranking?.final_score,
rank: ranking?.final_rank,
uptimeScore: ranking?.uptime_score,
deviationScore: ranking?.deviation_score,
stalledScore: ranking?.stalled_score,
- status,
feedKey: feed.product.price_account,
nameAsString: feed.product.display_symbol,
id: feed.product.price_account,
@@ -72,7 +71,7 @@ type PriceFeedsCardProps =
isLoading?: false | undefined;
publisherKey: string;
cluster: Cluster;
- priceFeeds: Omit[];
+ priceFeeds: Omit[];
metricsTime?: Date | undefined;
};
diff --git a/apps/insights/src/components/Publisher/top-feeds-table.tsx b/apps/insights/src/components/Publisher/top-feeds-table.tsx
index 7a9d7a6c7d..d04dec0fb0 100644
--- a/apps/insights/src/components/Publisher/top-feeds-table.tsx
+++ b/apps/insights/src/components/Publisher/top-feeds-table.tsx
@@ -6,13 +6,12 @@ import { Table } from "@pythnetwork/component-library/Table";
import type { ReactNode } from "react";
import { useMemo } from "react";
-import styles from "./top-feeds-table.module.scss";
import type { Cluster } from "../../services/pyth";
-import type { Status } from "../../status";
import { AssetClassBadge } from "../AssetClassBadge";
import { usePriceComponentDrawer } from "../PriceComponentDrawer";
import { PriceFeedTag } from "../PriceFeedTag";
import { Score } from "../Score";
+import styles from "./top-feeds-table.module.scss";
type Props =
| LoadingTopFeedsTableImplProps
@@ -36,7 +35,6 @@ type ResolvedTopFeedsTableProps = BaseTopFeedsTableImplProps & {
assetClass: string;
score: number;
rank: number;
- status: Status;
firstEvaluation: Date;
icon: ReactNode;
href: string;
diff --git a/apps/insights/src/components/Publishers/index.tsx b/apps/insights/src/components/Publishers/index.tsx
index c48e901e88..340e52fc30 100644
--- a/apps/insights/src/components/Publishers/index.tsx
+++ b/apps/insights/src/components/Publishers/index.tsx
@@ -33,6 +33,8 @@ export const Publishers = async () => {
]);
const rankingTime = pythnetPublishers[0]?.timestamp;
const scoreTime = pythnetPublishers[0]?.scoreTime;
+ // eslint-disable-next-line no-console
+ console.log({ pythnetPublishers, pythtestConformancePublishers });
return (
@@ -147,7 +149,7 @@ const toTableRow = ({
key,
rank,
permissionedFeeds,
- activeFeeds,
+ unpermissionedFeeds,
averageScore,
}: Awaited
>[number]) => {
const knownPublisher = lookupPublisher(key);
@@ -155,7 +157,7 @@ const toTableRow = ({
id: key,
ranking: rank,
permissionedFeeds,
- activeFeeds,
+ unpermissionedFeeds,
averageScore,
...(knownPublisher && {
name: knownPublisher.name,
diff --git a/apps/insights/src/components/Publishers/publishers-card.tsx b/apps/insights/src/components/Publishers/publishers-card.tsx
index 4a39ad29de..084abde27b 100644
--- a/apps/insights/src/components/Publishers/publishers-card.tsx
+++ b/apps/insights/src/components/Publishers/publishers-card.tsx
@@ -5,7 +5,6 @@ import { Database } from "@phosphor-icons/react/dist/ssr/Database";
import { Badge } from "@pythnetwork/component-library/Badge";
import { Card } from "@pythnetwork/component-library/Card";
import { EntityList } from "@pythnetwork/component-library/EntityList";
-import { Link } from "@pythnetwork/component-library/Link";
import { NoResults } from "@pythnetwork/component-library/NoResults";
import { Paginator } from "@pythnetwork/component-library/Paginator";
import { SearchInput } from "@pythnetwork/component-library/SearchInput";
@@ -27,8 +26,8 @@ import { useQueryParamFilterPagination } from "../../hooks/use-query-param-filte
import { CLUSTER_NAMES } from "../../services/pyth";
import {
ExplainPermissioned,
- ExplainActive,
ExplainRanking,
+ ExplainUnpermissioned,
} from "../Explanations";
import { PublisherTag } from "../PublisherTag";
import { Ranking } from "../Ranking";
@@ -47,7 +46,7 @@ type Publisher = {
id: string;
ranking: number;
permissionedFeeds: number;
- activeFeeds: number;
+ unpermissionedFeeds: number;
averageScore: number;
} & (
| { name: string; icon: ReactNode }
@@ -80,7 +79,6 @@ const ResolvedPublishersCard = ({
"cluster",
parseAsStringEnum([...CLUSTER_NAMES]).withDefault("pythnet"),
);
-
const {
search,
sortDescriptor,
@@ -103,7 +101,7 @@ const ResolvedPublishersCard = ({
switch (column) {
case "ranking":
case "permissionedFeeds":
- case "activeFeeds":
+ case "unpermissionedFeeds":
case "averageScore": {
return (
(direction === "descending" ? -1 : 1) * (a[column] - b[column])
@@ -136,7 +134,7 @@ const ResolvedPublishersCard = ({
ranking,
averageScore,
permissionedFeeds,
- activeFeeds,
+ unpermissionedFeeds,
...publisher
}) => ({
id,
@@ -155,15 +153,7 @@ const ResolvedPublishersCard = ({
/>
),
permissionedFeeds,
- activeFeeds: (
-
- {activeFeeds}
-
- ),
+ unpermissionedFeeds,
averageScore: (
),
@@ -226,7 +216,7 @@ type PublishersCardContentsProps = Pick &
| "ranking"
| "name"
| "permissionedFeeds"
- | "activeFeeds"
+ | "unpermissionedFeeds"
| "averageScore"
> & { textValue: string })[];
}
@@ -303,7 +293,7 @@ const PublishersCardContents = ({
fields={[
{ id: "averageScore", name: "Average Score" },
{ id: "permissionedFeeds", name: "Permissioned Feeds" },
- { id: "activeFeeds", name: "Active Feeds" },
+ { id: "unpermissionedFeeds", name: "Unpermissioned Feeds" },
]}
isLoading={props.isLoading}
rows={
@@ -360,11 +350,11 @@ const PublishersCardContents = ({
allowsSorting: true,
},
{
- id: "activeFeeds",
+ id: "unpermissionedFeeds",
name: (
<>
- ACTIVE
-
+ UNPERMISSIONED
+
>
),
alignment: "center",
diff --git a/apps/insights/src/components/Status/index.tsx b/apps/insights/src/components/Status/index.tsx
index 921cd3b966..1147b938d7 100644
--- a/apps/insights/src/components/Status/index.tsx
+++ b/apps/insights/src/components/Status/index.tsx
@@ -1,5 +1,9 @@
+import type { PriceData } from "@pythnetwork/client";
import { Status as StatusComponent } from "@pythnetwork/component-library/Status";
+import { useMemo } from "react";
+import { useLivePriceData } from "../../hooks/use-live-price-data";
+import type { Cluster } from "../../services/pyth";
import { Status as StatusType } from "../../status";
export const Status = ({ status }: { status: StatusType }) => (
@@ -7,31 +11,67 @@ export const Status = ({ status }: { status: StatusType }) => (
{getText(status)}
);
+export const StatusLive = ({
+ cluster,
+ feedKey,
+ publisherKey,
+}: {
+ cluster: Cluster;
+ feedKey: string;
+ publisherKey: string;
+}) => {
+ const status = useGetStatus(cluster, feedKey, publisherKey);
+ if (!status) {
+ return;
+ }
+ return ;
+};
+
+const useGetStatus = (
+ cluster: Cluster,
+ feedKey: string,
+ publisherKey: string,
+) => {
+ const data = useLivePriceData(cluster, feedKey);
+ return useMemo(() => {
+ return getStatus(data.current, publisherKey);
+ }, [data.current, feedKey, publisherKey]);
+};
+
+export const getStatus = (
+ currentPriceData: PriceData | undefined,
+ publisherKey: string,
+) => {
+ if (!currentPriceData) {
+ return;
+ }
+ const lastPublishedSlot = currentPriceData.priceComponents.find(
+ (price) => price.publisher.toString() === publisherKey,
+ )?.latest.publishSlot;
+ const isPublisherInactive =
+ Number(lastPublishedSlot ?? 0) < Number(currentPriceData.validSlot) - 100;
+
+ return isPublisherInactive ? StatusType.Down : StatusType.Live;
+};
const getVariant = (status: StatusType) => {
switch (status) {
- case StatusType.Active: {
+ case StatusType.Live: {
return "success";
}
- case StatusType.Inactive: {
+ case StatusType.Down: {
return "error";
}
- case StatusType.Unranked: {
- return "disabled";
- }
}
};
const getText = (status: StatusType) => {
switch (status) {
- case StatusType.Active: {
- return "Active";
- }
- case StatusType.Inactive: {
- return "Inactive";
+ case StatusType.Live: {
+ return "Live";
}
- case StatusType.Unranked: {
- return "Unranked";
+ case StatusType.Down: {
+ return "Down";
}
}
};
diff --git a/apps/insights/src/hooks/use-live-price-data.tsx b/apps/insights/src/hooks/use-live-price-data.tsx
index 9a57a5262c..6f4a36c01a 100644
--- a/apps/insights/src/hooks/use-live-price-data.tsx
+++ b/apps/insights/src/hooks/use-live-price-data.tsx
@@ -35,7 +35,7 @@ export const LivePriceDataProvider = (props: LivePriceDataProviderProps) => {
return ;
};
-export const useLivePriceData = (cluster: Cluster, feedKey: string) => {
+export const useLivePriceData = (cluster: Cluster, feedKey?: string) => {
const { addSubscription, removeSubscription } =
useLivePriceDataContext()[cluster];
@@ -45,6 +45,9 @@ export const useLivePriceData = (cluster: Cluster, feedKey: string) => {
}>({ current: undefined, prev: undefined });
useEffect(() => {
+ if (!feedKey) {
+ return;
+ }
addSubscription(feedKey, setData);
return () => {
removeSubscription(feedKey, setData);
diff --git a/apps/insights/src/hooks/use-live-publishers-data.tsx b/apps/insights/src/hooks/use-live-publishers-data.tsx
new file mode 100644
index 0000000000..27e05f79b0
--- /dev/null
+++ b/apps/insights/src/hooks/use-live-publishers-data.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import {
+ createContext,
+ use,
+ useEffect,
+ useState
+} from "react";
+
+import { PythSubscriber } from '../services/pyth-stream';
+
+type PublisherFeedData = Record
+
+const LivePublishersDataContext = createContext<
+ PublisherFeedData | undefined
+>(undefined);
+
+type LivePublishersDataProviderProps = {
+ publisherKey: string;
+ children: React.ReactNode;
+}
+
+export const LivePublishersDataProvider = ({ publisherKey, children }: LivePublishersDataProviderProps) => {
+ const [localPublishersData, setLocalPublishersData] = useState({});
+ useEffect(() => {
+ const pythSubscriber = new PythSubscriber();
+
+ pythSubscriber.onPublisherUpdate((update) => {
+ setLocalPublishersData((prev) => {
+ const newData = { ...prev };
+ for (const u of update.updates) {
+ if(u.feed_id === '7jAVut34sgRj6erznsYvLYvjc9GJwXTpN88ThZSDJ65G') {
+ console.log("update", u);
+ }
+ newData[u.feed_id] = { price: u.price, slot: BigInt(u.slot) };
+ }
+ return newData;
+ });
+ });
+ pythSubscriber.connect().then(
+ () => {pythSubscriber.subscribePublisher([publisherKey]);}
+ ).catch((error) => {
+ console.error("Failed to subscribe to publisher", error);
+ });
+ return () => {
+ pythSubscriber.disconnect();
+ };
+ }, [publisherKey]);
+ return {children} ;
+};
+
+export const useLivePublishersData = (feedKey: string) => {
+ const publisherData = useLivePublishersDataContext()
+ return publisherData[feedKey];
+};
+
+const useLivePublishersDataContext = () => {
+ const publisherData = use(LivePublishersDataContext);
+ if (publisherData === undefined) {
+ throw new LivePublishersDataProviderNotInitializedError();
+ }
+ return publisherData;
+};
+
+class LivePublishersDataProviderNotInitializedError extends Error {
+ constructor() {
+ super("This component must be a child of ");
+ this.name = "LivePublishersDataProviderNotInitializedError";
+ }
+}
\ No newline at end of file
diff --git a/apps/insights/src/services/clickhouse.ts b/apps/insights/src/services/clickhouse.ts
index c0b3b2521a..8e60a32e79 100644
--- a/apps/insights/src/services/clickhouse.ts
+++ b/apps/insights/src/services/clickhouse.ts
@@ -19,10 +19,7 @@ const _getPublishers = async (cluster: Cluster) =>
permissionedFeeds: z
.string()
.transform((value) => Number.parseInt(value, 10)),
- activeFeeds: z
- .string()
- .transform((value) => Number.parseInt(value, 10)),
- inactiveFeeds: z
+ unpermissionedFeeds: z
.string()
.transform((value) => Number.parseInt(value, 10)),
averageScore: z.number(),
@@ -36,9 +33,7 @@ const _getPublishers = async (cluster: Cluster) =>
SELECT
publisher,
time,
- avg(final_score) AS averageScore,
- countIf(uptime_score >= 0.5) AS activeFeeds,
- countIf(uptime_score < 0.5) AS inactiveFeeds
+ avg(final_score) AS averageScore
FROM publisher_quality_ranking
WHERE cluster = {cluster:String}
AND time = (
@@ -55,8 +50,7 @@ const _getPublishers = async (cluster: Cluster) =>
publisher AS key,
rank,
LENGTH(symbols) AS permissionedFeeds,
- activeFeeds,
- inactiveFeeds,
+ (SELECT count(symbol) FROM symbols WHERE cluster = {cluster:String}) - LENGTH(symbols) AS unpermissionedFeeds,
score_data.averageScore,
score_data.time as scoreTime
FROM publishers_ranking
diff --git a/apps/insights/src/services/pyth-stream.ts b/apps/insights/src/services/pyth-stream.ts
new file mode 100644
index 0000000000..c52307ea14
--- /dev/null
+++ b/apps/insights/src/services/pyth-stream.ts
@@ -0,0 +1,161 @@
+// ─── Client → Server ───────────────────────────────────────────────────────────
+
+type ClientMessage =
+ | { type: "subscribe_price"; ids: string[]; verbose: boolean }
+ | { type: "unsubscribe_price"; ids: string[]; verbose: boolean }
+ | { type: "subscribe_publisher"; ids: string[]; verbose: boolean }
+ | { type: "unsubscribe_publisher"; ids: string[]; verbose: boolean };
+
+// ─── Types for price feeds ─────────────────────────────────────────────────────
+
+export type PriceInfo = {
+ price: string;
+ conf: string;
+ expo: number;
+ publish_time: number;
+ slot: number;
+};
+
+export type PriceFeed = {
+ id: string;
+ price: PriceInfo;
+ ema_price: PriceInfo;
+};
+
+// ─── Server → Client (single) ─────────────────────────────────────────────────
+
+export type PriceUpdate = {
+ type: "price_update";
+ price_feed: PriceFeed;
+};
+
+
+// ─── Server → Client (batched) ────────────────────────────────────────────────
+// Batch frame that contains many publisher updates in one message.
+export type PublisherPriceUpdateItem = {
+ publisher: string;
+ feed_id: string;
+ price: string;
+ slot: number;
+};
+
+export type PublisherPriceUpdate = {
+ type: "publisher_price_update";
+ updates: PublisherPriceUpdateItem[];
+};
+
+// Server can send a single message, a batch message, or an array of messages.
+export type ServerMessage =
+ | PriceUpdate
+ | PublisherPriceUpdate;
+
+export type ServerPayload = ServerMessage | ServerMessage[];
+
+export class PythSubscriber {
+ private ws: WebSocket | undefined = undefined;
+ private url: string;
+
+ private onPriceUpdateHandler?: (update: PriceUpdate) => void;
+ private onPublisherUpdateHandler?: (update: PublisherPriceUpdate) => void;
+
+ constructor(url = "ws://0.0.0.0:8080") {
+ this.url = url;
+ }
+
+ public async connect() {
+ return new Promise((resolve, reject) => {
+ if (this.ws) return resolve();
+
+ this.ws = new WebSocket(this.url);
+
+ this.ws.addEventListener("open", () => {
+ console.log("Connected to WebSocket");
+ resolve();
+ });
+
+ this.ws.addEventListener("message", (event: MessageEvent) => {
+ try {
+ const data = JSON.parse(event.data) as ServerPayload;
+
+ if (Array.isArray(data)) {
+ for (const msg of data) this.handleServerMessage(msg);
+ } else {
+ this.handleServerMessage(data);
+ }
+ } catch (e) {
+ console.error("Failed to parse message:", event.data, e);
+ }
+ });
+
+ this.ws.addEventListener("close", () => {
+ console.warn("WebSocket closed");
+ this.ws = undefined;
+ });
+
+ this.ws.addEventListener("error", (event: Event) => {
+ console.error("WebSocket error:", event);
+ });
+ });
+ }
+
+ private handleServerMessage(msg: ServerMessage) {
+ switch (msg.type) {
+ case "price_update":
+ this.onPriceUpdateHandler?.(msg);
+ return;
+
+ case "publisher_price_update":
+ // Prefer batch handler if provided; otherwise fan out to per-item handler
+ if (this.onPublisherUpdateHandler) {
+ this.onPublisherUpdateHandler(msg);
+ }
+ return;
+
+ default:
+ console.error("Unknown message from server:", msg);
+ }
+ }
+
+ private send(msg: ClientMessage) {
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+ this.ws.send(JSON.stringify(msg));
+ } else {
+ console.warn("WebSocket not ready. Message not sent:", msg);
+ }
+ }
+
+ // ── Subscriptions ───────────────────────────────────────────────────────────
+
+ public subscribePrice(ids: string[], verbose = true) {
+ this.send({ type: "subscribe_price", ids, verbose });
+ }
+
+ public unsubscribePrice(ids: string[], verbose = true) {
+ this.send({ type: "unsubscribe_price", ids, verbose });
+ }
+
+ public subscribePublisher(ids: string[], verbose = true) {
+ this.send({ type: "subscribe_publisher", ids, verbose });
+ }
+
+ public unsubscribePublisher(ids: string[], verbose = true) {
+ this.send({ type: "unsubscribe_publisher", ids, verbose });
+ }
+
+ // ── Callbacks ───────────────────────────────────────────────────────────────
+
+ public onPriceUpdate(cb: (update: PriceUpdate) => void) {
+ this.onPriceUpdateHandler = cb;
+ }
+
+ public onPublisherUpdate(
+ cb: (update: PublisherPriceUpdate) => void
+ ) {
+ this.onPublisherUpdateHandler = cb;
+ }
+
+ public disconnect() {
+ this.ws?.close();
+ this.ws = undefined;
+ }
+}
\ No newline at end of file
diff --git a/apps/insights/src/services/pyth/index.ts b/apps/insights/src/services/pyth/index.ts
index 54de1966fa..761156331f 100644
--- a/apps/insights/src/services/pyth/index.ts
+++ b/apps/insights/src/services/pyth/index.ts
@@ -7,6 +7,7 @@ import type { PythPriceCallback } from "@pythnetwork/client/lib/PythConnection";
import { Connection, PublicKey } from "@solana/web3.js";
import { PYTHNET_RPC, PYTHTEST_CONFORMANCE_RPC } from "../../config/isomorphic";
+import { getPythMetadata } from './get-metadata';
export enum Cluster {
Pythnet,
@@ -79,3 +80,27 @@ export const subscribe = (
pythConn.onPriceChange(cb);
return pythConn;
};
+
+
+// const testWebsocket = () => {
+// clients[Cluster.Pythnet].getData().then((metadata) => {
+// console.log(metadata);
+// });
+// console.log("Test websocket");
+// const ws = new WebSocket('ws://0.0.0.0:8080');
+// ws.onopen = (event) => {
+// console.log("WebSocket opened");
+// ws.send(JSON.stringify({"type":"subscribe_publisher","ids":["6DNocjFJjocPLZnKBZyEJAC5o2QaiT5Mx8AkphfxDm5i"],"verbose":true }));
+// };
+// ws.onmessage = (event) => {
+// console.log("WebSocket message received", event.data);
+// };
+// ws.onerror = (event) => {
+// console.log("WebSocket error", event);
+// };
+// ws.onclose = (event) => {
+// console.log(ws);
+// }
+// }
+
+// testWebsocket();
\ No newline at end of file
diff --git a/apps/insights/src/status.ts b/apps/insights/src/status.ts
index 96940a5fc2..8d508ae08c 100644
--- a/apps/insights/src/status.ts
+++ b/apps/insights/src/status.ts
@@ -1,35 +1,22 @@
export enum Status {
- Unranked,
- Inactive,
- Active,
+ Down,
+ Live,
}
-export const getStatus = (ranking?: { uptime_score: number }): Status => {
- if (ranking) {
- return ranking.uptime_score >= 0.5 ? Status.Active : Status.Inactive;
- } else {
- return Status.Unranked;
- }
-};
-
export const STATUS_NAMES = {
- [Status.Active]: "Active",
- [Status.Inactive]: "Inactive",
- [Status.Unranked]: "Unranked",
+ [Status.Live]: "Live",
+ [Status.Down]: "Down",
} as const;
export type StatusName = (typeof STATUS_NAMES)[Status];
export const statusNameToStatus = (name: string): Status | undefined => {
switch (name) {
- case "Active": {
- return Status.Active;
- }
- case "Inactive": {
- return Status.Inactive;
+ case "Live": {
+ return Status.Live;
}
- case "Unranked": {
- return Status.Unranked;
+ case "Down": {
+ return Status.Down;
}
default: {
return undefined;