"use client"; import { useLogger } from "@pythnetwork/app-logger"; import { Badge } from "@pythnetwork/component-library/Badge"; import { Button } from "@pythnetwork/component-library/Button"; import { Card } from "@pythnetwork/component-library/Card"; import { Paginator } from "@pythnetwork/component-library/Paginator"; import { SearchInput } from "@pythnetwork/component-library/SearchInput"; import { Select } from "@pythnetwork/component-library/Select"; import { SingleToggleGroup } from "@pythnetwork/component-library/SingleToggleGroup"; import { type RowConfig, type SortDescriptor, Table, } from "@pythnetwork/component-library/Table"; 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 { type StatusName, STATUS_NAMES, Status as StatusType, statusNameToStatus, } from "../../status"; import { Explain } from "../Explain"; import { EvaluationTime } from "../Explanations"; import { FormattedNumber } from "../FormattedNumber"; import { LivePrice, LiveConfidence, LiveComponentValue } from "../LivePrices"; import { NoResults } from "../NoResults"; import rootStyles from "../Root/index.module.scss"; import { Score } from "../Score"; import { Status as StatusComponent } from "../Status"; const SCORE_WIDTH = 32; type Props = { className?: string | undefined; priceComponents: PriceComponent[]; metricsTime?: Date | undefined; nameLoadingSkeleton: ReactNode; label: string; searchPlaceholder: string; onPriceComponentAction: (component: PriceComponent) => void; }; type PriceComponent = { id: string; score: number | undefined; symbol: string; uptimeScore: number | undefined; deviationScore: number | undefined; stalledScore: number | undefined; cluster: Cluster; status: StatusType; feedKey: string; publisherKey: string; name: ReactNode; nameAsString: string; }; export const PriceComponentsCard = ({ priceComponents, onPriceComponentAction, ...props }: Props) => ( }> ); export const ResolvedPriceComponentsCard = ({ priceComponents, onPriceComponentAction, ...props }: Props) => { const logger = useLogger(); const collator = useCollator(); const filter = useFilter({ sensitivity: "base", usage: "search" }); const [status, setStatus] = useQueryState( "status", parseAsStringEnum(["", ...Object.values(STATUS_NAMES)]).withDefault(""), ); const [showQuality, setShowQuality] = useQueryState( "showQuality", parseAsBoolean.withDefault(false), ); const statusType = useMemo(() => statusNameToStatus(status), [status]); const componentsFilteredByStatus = useMemo( () => statusType === undefined ? priceComponents : priceComponents.filter( (component) => component.status === statusType, ), [statusType, priceComponents], ); const { search, sortDescriptor, page, pageSize, updateSearch, updateSortDescriptor, updatePage, updatePageSize, paginatedItems, numResults, numPages, mkPageLink, } = useQueryParamFilterPagination( componentsFilteredByStatus, (component, search) => filter.contains(component.nameAsString, search), (a, b, { column, direction }) => { switch (column) { case "score": case "uptimeScore": case "deviationScore": case "stalledScore": { if (a[column] === undefined && b[column] === undefined) { return 0; } else if (a[column] === undefined) { return direction === "descending" ? 1 : -1; } else if (b[column] === undefined) { return direction === "descending" ? -1 : 1; } else { return ( (direction === "descending" ? -1 : 1) * (a[column] - b[column]) ); } } case "name": { return ( (direction === "descending" ? -1 : 1) * collator.compare(a.nameAsString, b.nameAsString) ); } case "status": { const resultByStatus = b.status - a.status; const result = resultByStatus === 0 ? collator.compare(a.nameAsString, b.nameAsString) : resultByStatus; return (direction === "descending" ? -1 : 1) * result; } default: { return 0; } } }, { defaultPageSize: 20, defaultSort: "name", defaultDescending: false, }, ); const rows = useMemo( () => paginatedItems.map((component) => ({ id: component.id, data: { name: (
{component.name} {component.cluster === Cluster.PythtestConformance && ( test )}
), ...(showQuality ? { score: component.score !== undefined && ( ), uptimeScore: component.uptimeScore !== undefined && ( ), deviationScore: component.deviationScore !== undefined && ( ), stalledScore: component.stalledScore !== undefined && ( ), } : { slot: ( ), price: ( ), confidence: ( ), }), status: , }, onAction: () => { onPriceComponentAction(component); }, })), [paginatedItems, showQuality, onPriceComponentAction], ); const updateStatus = useCallback( (newStatus: StatusName | "") => { updatePage(1); setStatus(newStatus).catch((error: unknown) => { logger.error("Failed to update status", error); }); }, [updatePage, setStatus, logger], ); const updateShowQuality = useCallback( (newValue: boolean) => { setShowQuality(newValue).catch((error: unknown) => { logger.error("Failed to update show quality", error); }); }, [setShowQuality, logger], ); return ( ); }; type PriceComponentsCardProps = Pick< Props, | "className" | "metricsTime" | "nameLoadingSkeleton" | "label" | "searchPlaceholder" > & ( | { isLoading: true } | { isLoading?: false; numResults: number; search: string; sortDescriptor: SortDescriptor; numPages: number; page: number; pageSize: number; onSearchChange: (newSearch: string) => void; onSortChange: (newSort: SortDescriptor) => void; onPageSizeChange: (newPageSize: number) => void; onPageChange: (newPage: number) => void; mkPageLink: (page: number) => string; status: StatusName | ""; onStatusChange: (newStatus: StatusName | "") => void; showQuality: boolean; setShowQuality: (newValue: boolean) => void; rows: RowConfig[]; } ); export const PriceComponentsCardContents = ({ className, metricsTime, nameLoadingSkeleton, label, searchPlaceholder, ...props }: PriceComponentsCardProps) => { const collator = useCollator(); return ( {label} {!props.isLoading && ( {props.numResults} )} } toolbar={ <> label="Status" size="sm" variant="outline" hideLabel options={[ "", ...Object.values(STATUS_NAMES).toSorted((a, b) => collator.compare(a, b), ), ]} {...(props.isLoading ? { isPending: true, buttonLabel: "Status" } : { show: (value) => (value === "" ? "All" : value), placement: "bottom end", buttonLabel: props.status === "" ? "Status" : props.status, selectedKey: props.status, onSelectionChange: props.onStatusChange, })} /> { props.setShowQuality(newValue === "quality"); }, })} items={[ { id: "prices", children: "Prices" }, { id: "quality", children: "Quality" }, ]} /> } {...(!props.isLoading && { footer: ( ), })} > STATUS A publisher{"'"}s feed have one of the following statuses:
  • Active feeds have better than 50% uptime over the last day
  • Inactive feeds have worse than 50% uptime over the last day
  • Unranked feeds have not yet been evaluated by Pyth
{metricsTime && }
), alignment: "right", allowsSorting: true, }, ]} {...(props.isLoading ? { isLoading: true } : { rows: props.rows, sortDescriptor: props.sortDescriptor, onSortChange: props.onSortChange, emptyState: ( { props.onSearchChange(""); props.onStatusChange(""); }} /> ), })} /> ); }; const otherColumns = ({ metricsTime, ...props }: { metricsTime?: Date | undefined } & ( | { isLoading: true } | { isLoading?: false; showQuality: boolean } )) => { if (props.isLoading) { return []; } else { return props.showQuality ? [ { id: "uptimeScore", width: 20, name: ( <> UPTIME SCORE

Uptime is the percentage of time that a publisher{"'"}s feed is available and active.

{metricsTime && }
), alignment: "center" as const, allowsSorting: true, }, { id: "deviationScore", width: 20, name: ( <> DEVIATION SCORE

Deviation measures how close a publisher{"'"}s price is to what Pyth believes to be the true market price.

{metricsTime && }
), alignment: "center" as const, allowsSorting: true, }, { id: "stalledScore", width: 20, name: ( <> STALLED SCORE

A feed is considered stalled if it is publishing the same value repeatedly for the price. This score component is reduced each time a feed is stalled.

{metricsTime && }
), alignment: "center" as const, allowsSorting: true, }, { id: "score", name: ( <> FINAL SCORE The final score is calculated by combining the three score components as follows:
  • Uptime Score (40% weight)
  • Deviation Score (40% weight)
  • Stalled Score (20% weight)
{metricsTime && }
), alignment: "left" as const, width: SCORE_WIDTH, loadingSkeleton: , allowsSorting: true, }, ] : [ { id: "slot", name: "SLOT", alignment: "left" as const, width: 40 }, { id: "price", name: "PRICE", alignment: "left" as const, width: 40 }, { id: "confidence", name: "CONFIDENCE INTERVAL", alignment: "left" as const, width: 50, }, ]; } };