From 991c32b9ada8cf09b5de10059d26450bc22894c6 Mon Sep 17 00:00:00 2001 From: Alexandru Cambose Date: Wed, 27 Aug 2025 12:09:15 +0200 Subject: [PATCH 1/6] feat: publisher conformance report --- .../Publisher/conformance-reports.module.scss | 4 +++ .../Publisher/conformance-reports.tsx | 31 +++++++++++++++++++ .../components/Publisher/layout.module.scss | 6 ++++ .../src/components/Publisher/layout.tsx | 7 ++++- .../components/PublisherTag/index.module.scss | 2 +- 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 apps/insights/src/components/Publisher/conformance-reports.module.scss create mode 100644 apps/insights/src/components/Publisher/conformance-reports.tsx diff --git a/apps/insights/src/components/Publisher/conformance-reports.module.scss b/apps/insights/src/components/Publisher/conformance-reports.module.scss new file mode 100644 index 0000000000..4dc52a3fe1 --- /dev/null +++ b/apps/insights/src/components/Publisher/conformance-reports.module.scss @@ -0,0 +1,4 @@ +.conformanceReports { + display: flex; + gap: .5rem; +} \ No newline at end of file diff --git a/apps/insights/src/components/Publisher/conformance-reports.tsx b/apps/insights/src/components/Publisher/conformance-reports.tsx new file mode 100644 index 0000000000..eb8d2a8fe2 --- /dev/null +++ b/apps/insights/src/components/Publisher/conformance-reports.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Download } from '@phosphor-icons/react/dist/ssr/Download'; +import { Button } from '@pythnetwork/component-library/Button'; +import { Select } from '@pythnetwork/component-library/Select'; +import { useState } from 'react'; +import styles from "./conformance-reports.module.scss"; + +const ConformanceReports = () => { + const [timeframe, setTimeframe] = useState("Daily"); + const handleReport = () => { + console.log("Report", timeframe); + } + return ( +
+ { + setTimeframe(value); + }} + size="sm" + label="Timeframe" + variant="outline" + hideLabel + /> + +
+ ); +}; + +export default ConformanceReports; diff --git a/apps/insights/src/components/PriceFeed/header.tsx b/apps/insights/src/components/PriceFeed/header.tsx index 69f3971f09..ad32e06863 100644 --- a/apps/insights/src/components/PriceFeed/header.tsx +++ b/apps/insights/src/components/PriceFeed/header.tsx @@ -5,23 +5,24 @@ import { Skeleton } from "@pythnetwork/component-library/Skeleton"; import { StatCard } from "@pythnetwork/component-library/StatCard"; import { Suspense } from "react"; -import styles from "./header.module.scss"; -import { PriceFeedSelect } from "./price-feed-select"; -import { ReferenceData } from "./reference-data"; import { Cluster } from "../../services/pyth"; import { AssetClassBadge } from "../AssetClassBadge"; import { Cards } from "../Cards"; import { Explain } from "../Explain"; import { FeedKey } from "../FeedKey"; -import { LivePrice, LiveConfidence, LiveLastUpdated } from "../LivePrices"; +import { LiveConfidence, LiveLastUpdated, LivePrice } from "../LivePrices"; import { - YesterdaysPricesProvider, PriceFeedChangePercent, + YesterdaysPricesProvider, } from "../PriceFeedChangePercent"; import { PriceFeedIcon } from "../PriceFeedIcon"; import { PriceFeedTag } from "../PriceFeedTag"; import { PriceName } from "../PriceName"; +import ConformanceReports from "./conformance-reports"; import { getFeed } from "./get-feed"; +import styles from "./header.module.scss"; +import { PriceFeedSelect } from "./price-feed-select"; +import { ReferenceData } from "./reference-data"; type Props = { params: Promise<{ @@ -174,6 +175,14 @@ const PriceFeedHeaderImpl = (props: PriceFeedHeaderImplProps) => ( > Reference Data + diff --git a/apps/insights/src/components/Publisher/conformance-reports.tsx b/apps/insights/src/components/Publisher/conformance-reports.tsx deleted file mode 100644 index eb8d2a8fe2..0000000000 --- a/apps/insights/src/components/Publisher/conformance-reports.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { Download } from '@phosphor-icons/react/dist/ssr/Download'; -import { Button } from '@pythnetwork/component-library/Button'; -import { Select } from '@pythnetwork/component-library/Select'; -import { useState } from 'react'; -import styles from "./conformance-reports.module.scss"; - -const ConformanceReports = () => { - const [timeframe, setTimeframe] = useState("Daily"); - const handleReport = () => { - console.log("Report", timeframe); - } - return ( -
- ({ id: interval }))} + placement="bottom end" + selectedKey={timeframe} + onSelectionChange={setTimeframe} + size="sm" + label="Timeframe" + variant="outline" + hideLabel + /> + +
+ ); +}; + +export default ConformanceReport; diff --git a/apps/insights/src/components/ConformanceReport/constants.ts b/apps/insights/src/components/ConformanceReport/constants.ts new file mode 100644 index 0000000000..8d45dc2b1b --- /dev/null +++ b/apps/insights/src/components/ConformanceReport/constants.ts @@ -0,0 +1 @@ +export const WEB_API_BASE_URL = "https://web-api.pyth.network"; diff --git a/apps/insights/src/components/ConformanceReport/types.ts b/apps/insights/src/components/ConformanceReport/types.ts new file mode 100644 index 0000000000..0563756b50 --- /dev/null +++ b/apps/insights/src/components/ConformanceReport/types.ts @@ -0,0 +1,2 @@ +export const INTERVALS = ["24H", "48H", "72H", "1W", "1M"] as const; +export type Interval = (typeof INTERVALS)[number]; diff --git a/apps/insights/src/components/ConformanceReport/use-download-report-for-feed.tsx b/apps/insights/src/components/ConformanceReport/use-download-report-for-feed.tsx new file mode 100644 index 0000000000..cb895cfdec --- /dev/null +++ b/apps/insights/src/components/ConformanceReport/use-download-report-for-feed.tsx @@ -0,0 +1,52 @@ +import { useCallback } from "react"; + +import { WEB_API_BASE_URL } from "./constants"; +import { useDownloadBlob } from "../../hooks/use-download-blob"; + +const PYTHTEST_CONFORMANCE_REFERENCE_PUBLISHER = + "HUZu4xMSHbxTWbkXR6jkGdjvDPJLjrpSNXSoUFBRgjWs"; + +export const useDownloadReportForFeed = () => { + const download = useDownloadBlob(); + + return useCallback( + async ({ + symbol, + publisher, + timeframe, + cluster, + }: { + symbol: string; + publisher: string; + timeframe: string; + cluster: string; + }) => { + const url = new URL("/metrics/conformance", WEB_API_BASE_URL); + url.searchParams.set("symbol", symbol); + url.searchParams.set("range", timeframe); + url.searchParams.set("cluster", cluster); + url.searchParams.set("publisher", publisher); + + if (cluster === "pythtest-conformance") { + url.searchParams.set( + "pythnet_aggregate_publisher", + PYTHTEST_CONFORMANCE_REFERENCE_PUBLISHER, + ); + } + + const response = await fetch(url, { + headers: new Headers({ + Accept: "application/octet-stream", + }), + }); + const blob = await response.blob(); + download( + blob, + `${publisher}-${symbol + .split("/") + .join("")}-${timeframe}-${cluster}-conformance-report.tsv`, + ); + }, + [download], + ); +}; diff --git a/apps/insights/src/components/ConformanceReport/use-download-report-for-publisher.tsx b/apps/insights/src/components/ConformanceReport/use-download-report-for-publisher.tsx new file mode 100644 index 0000000000..8d94f4eb8e --- /dev/null +++ b/apps/insights/src/components/ConformanceReport/use-download-report-for-publisher.tsx @@ -0,0 +1,221 @@ +import { stringify as stringifyCsv } from "csv-stringify/sync"; +import { + addDays, + differenceInDays, + format, + isBefore, + startOfMonth, + startOfWeek, + subMonths, +} from "date-fns"; +import { useCallback } from "react"; +import { parse } from "superjson"; +import { z } from "zod"; + +import { WEB_API_BASE_URL } from "./constants"; +import type { Interval } from "./types"; +import { useDownloadBlob } from "../../hooks/use-download-blob"; +import { priceFeedsSchema } from "../../schemas/pyth"; + +// If interval is 'daily', set interval_days=1 +// If interval is 'weekly', get the previous Sunday and set interval_days=7 +// If interval is 'monthly', get the 15th of the current month and set interval_day to the +// difference between the 15th of the current month and the 15th of the previous month which is 28-31 days. +const getRankingDateAndIntervalDays = (date: Date, interval: Interval) => { + switch (interval) { + case "24H": { + return { + date, + intervalDays: 1, + }; + } + case "48H": { + return { + date, + intervalDays: 2, + }; + } + case "72H": { + return { + date, + intervalDays: 3, + }; + } + case "1W": { + return { + date: startOfWeek(date), + intervalDays: 7, + }; + } + case "1M": { + const monthStart = startOfMonth(date); + let midMonth = addDays(monthStart, 14); + if (isBefore(date, midMonth)) { + midMonth = subMonths(midMonth, 1); + } + const midMonthBefore = subMonths(midMonth, 1); + return { + date: midMonth, + intervalDays: differenceInDays(midMonth, midMonthBefore), + }; + } + } +}; + +const getFeeds = async (cluster: string) => { + const url = new URL(`/api/pyth/get-feeds`, globalThis.window.origin); + url.searchParams.set("cluster", cluster); + const data = await fetch(url); + const rawData = await data.text(); + const parsedData = parse(rawData); + return priceFeedsSchema.element.array().parse(parsedData); +}; + +const PublisherQualityScoreSchema = z.object({ + symbol: z.string(), + uptime_score: z.string(), + deviation_penalty: z.string(), + deviation_score: z.string(), + stalled_penalty: z.string(), + stalled_score: z.string(), + final_score: z.string(), +}); + +const PublisherQuantityScoreSchema = z.object({ + numSymbols: z.number(), + rank: z.number(), + symbols: z.array(z.string()), + timestamp: z.string(), +}); + +const fetchRankingData = async ( + cluster: string, + publisher: string, + interval: Interval, +) => { + const { date, intervalDays } = getRankingDateAndIntervalDays( + new Date(), + interval, + ); + const quantityRankUrl = new URL( + `/publisher_ranking?publisher=${publisher}&cluster=${cluster}`, + WEB_API_BASE_URL, + ); + quantityRankUrl.searchParams.set("cluster", cluster); + quantityRankUrl.searchParams.set("publisher", publisher); + const qualityRankUrl = new URL( + `/publisher_quality_ranking_score`, + WEB_API_BASE_URL, + ); + qualityRankUrl.searchParams.set("cluster", cluster); + qualityRankUrl.searchParams.set("publisher", publisher); + qualityRankUrl.searchParams.set("date", format(date, "yyyy-MM-dd")); + qualityRankUrl.searchParams.set("interval_days", intervalDays.toString()); + + const [quantityRankRes, qualityRankRes] = await Promise.all([ + fetch(quantityRankUrl), + fetch(qualityRankUrl), + ]); + + return { + quantityRankData: PublisherQuantityScoreSchema.array().parse( + await quantityRankRes.json(), + ), + qualityRankData: PublisherQualityScoreSchema.array().parse( + await qualityRankRes.json(), + ), + }; +}; +const csvHeaders = [ + "priceFeed", + "assetType", + "description", + "status", + "permissioned", + "uptime_score", + "deviation_penalty", + "deviation_score", + "stalled_penalty", + "stalled_score", + "final_score", +]; + +export const useDownloadReportForPublisher = () => { + const download = useDownloadBlob(); + + return useCallback( + async ({ + publisher, + cluster, + interval, + }: { + publisher: string; + cluster: string; + interval: Interval; + }) => { + const [rankingData, allFeeds] = await Promise.all([ + fetchRankingData(cluster, publisher, interval), + getFeeds(cluster), + ]); + + const isPermissioned = (feed: string) => + allFeeds + .find((f) => f.symbol === feed) + ?.price.priceComponents.some((c) => c.publisher === publisher); + + const getPriceFeedData = (feed: string) => { + const rankData = rankingData.qualityRankData.find( + (obj) => obj.symbol === feed, + ); + const feedMetadata = allFeeds.find((f) => f.symbol === feed); + return { + priceFeed: feedMetadata?.product.display_symbol ?? "", + assetType: feedMetadata?.product.asset_type ?? "", + description: feedMetadata?.product.description ?? "", + ...rankData, + }; + }; + + const activePriceFeeds = rankingData.quantityRankData[0]?.symbols ?? []; + + const allSymbols = allFeeds + .flatMap((feed) => feed.symbol) + .filter((symbol: string) => symbol && !symbol.includes("NULL")); + // filter out inactive price feeds + const inactivePriceFeeds = allSymbols + .filter((symbol) => { + const meta = allFeeds.find((f) => f.symbol === symbol); + return ( + meta !== undefined && + !activePriceFeeds.includes(symbol) && + meta.price.numComponentPrices > 0 + ); + }) + .sort((a, b) => { + const aSplit = a.split("."); + const bSplit = b.split("."); + const aLast = aSplit.at(-1); + const bLast = bSplit.at(-1); + return aLast?.localeCompare(bLast ?? "") ?? 0; + }); + const data = [ + ...activePriceFeeds.map((feed) => ({ + ...getPriceFeedData(feed), + status: "active", + permissioned: "permissioned", + })), + ...inactivePriceFeeds.map((feed) => ({ + ...getPriceFeedData(feed), + status: "inactive", + permissioned: isPermissioned(feed) + ? "permissioned" + : "unpermissioned", + })), + ]; + const csv = stringifyCsv(data, { header: true, columns: csvHeaders }); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); + download(blob, `${publisher}-${cluster}-price-feeds.csv`); + }, + [download], + ); +}; diff --git a/apps/insights/src/components/PriceComponentDrawer/index.tsx b/apps/insights/src/components/PriceComponentDrawer/index.tsx index c608e1a769..82b4aac58d 100644 --- a/apps/insights/src/components/PriceComponentDrawer/index.tsx +++ b/apps/insights/src/components/PriceComponentDrawer/index.tsx @@ -15,32 +15,33 @@ import { useLogger } from "@pythnetwork/component-library/useLogger"; import { useMountEffect } from "@react-hookz/web"; import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; -import { useQueryState, parseAsString } from "nuqs"; +import { parseAsString, useQueryState } from "nuqs"; import type { ReactNode } from "react"; import { Suspense, - useState, useCallback, useMemo, - useTransition, useRef, + useState, + useTransition, } from "react"; import { RouterProvider, useDateFormatter, useNumberFormatter, } from "react-aria"; -import { ResponsiveContainer, Tooltip, Line, XAxis, YAxis } from "recharts"; +import { Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import type { CategoricalChartState } from "recharts/types/chart/types"; import { z } from "zod"; -import styles from "./index.module.scss"; import { Cluster, ClusterToName } from "../../services/pyth"; import type { Status } from "../../status"; -import { LiveConfidence, LivePrice, LiveComponentValue } from "../LivePrices"; +import ConformanceReport from "../ConformanceReport/conformance-report"; +import { LiveComponentValue, LiveConfidence, LivePrice } from "../LivePrices"; import { PriceName } from "../PriceName"; import { Score } from "../Score"; import { Status as StatusComponent } from "../Status"; +import styles from "./index.module.scss"; const LineChart = dynamic( () => import("recharts").then((recharts) => recharts.LineChart), @@ -274,6 +275,11 @@ type HeadingExtraProps = { const HeadingExtra = ({ status, ...props }: HeadingExtraProps) => { return ( <> +
diff --git a/apps/insights/src/components/PriceFeed/conformance-reports.tsx b/apps/insights/src/components/PriceFeed/conformance-reports.tsx deleted file mode 100644 index c2e5f0ec49..0000000000 --- a/apps/insights/src/components/PriceFeed/conformance-reports.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { Download } from "@phosphor-icons/react/dist/ssr/Download"; -import { Button } from "@pythnetwork/component-library/Button"; -import { Select } from "@pythnetwork/component-library/Select"; -import { Skeleton } from "@pythnetwork/component-library/Skeleton"; -import { useAlert } from "@pythnetwork/component-library/useAlert"; -import { useState } from "react"; - -import styles from "./conformance-reports.module.scss"; - -const PYTHTEST_CONFORMANCE_REFERENCE_PUBLISHER = - "HUZu4xMSHbxTWbkXR6jkGdjvDPJLjrpSNXSoUFBRgjWs"; - -const download = (blob: Blob, filename: string) => { - const url = globalThis.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.append(a); - a.click(); - a.remove(); -}; - -type ConformanceReportsProps = - | { isLoading: true } - | { - isLoading?: false | undefined; - symbol: string; - cluster: string; - publisher: string; - }; - -const ConformanceReports = (props: ConformanceReportsProps) => { - const [timeframe, setTimeframe] = useState("24H"); - const [isGeneratingReport, setIsGeneratingReport] = useState(false); - const { open } = useAlert(); - - const downloadReport = async () => { - if (props.isLoading) { - return; - } - const url = new URL( - "/pyth/metrics/conformance", - "https://web-api.pyth.network/", - ); - url.searchParams.set("symbol", props.symbol); - url.searchParams.set("range", timeframe); - url.searchParams.set("cluster", "pythnet"); - url.searchParams.set("publisher", props.publisher); - - if (props.cluster === "pythtest-conformance") { - url.searchParams.set( - "pythnet_aggregate_publisher", - PYTHTEST_CONFORMANCE_REFERENCE_PUBLISHER, - ); - } - - const response = await fetch(url, { - headers: new Headers({ - Accept: "application/octet-stream", - }), - }); - const blob = await response.blob(); - download( - blob, - `${props.publisher}-${props.symbol - .split("/") - .join("")}-${timeframe}-${props.cluster}-conformance-report.tsv`, - ); - }; - - const handleReport = () => { - setIsGeneratingReport(true); - try { - downloadReport().catch(() => { - open({ - title: "Error", - contents: "Error generating conformance report", - }); - }); - } finally { - setIsGeneratingReport(false); - } - }; - if (props.isLoading) { - return ; - } - return ( -
- { return priceFeedsSchema.element.array().parse(parsedData); }; -const PublisherQualityScoreSchema = z.object({ +const publisherQualityScoreSchema = z.object({ symbol: z.string(), uptime_score: z.string(), deviation_penalty: z.string(), @@ -82,7 +82,7 @@ const PublisherQualityScoreSchema = z.object({ final_score: z.string(), }); -const PublisherQuantityScoreSchema = z.object({ +const publisherQuantityScoreSchema = z.object({ numSymbols: z.number(), rank: z.number(), symbols: z.array(z.string()), @@ -98,10 +98,7 @@ const fetchRankingData = async ( new Date(), interval, ); - const quantityRankUrl = new URL( - `/publisher_ranking?publisher=${publisher}&cluster=${cluster}`, - WEB_API_BASE_URL, - ); + const quantityRankUrl = new URL(`/publisher_ranking`, WEB_API_BASE_URL); quantityRankUrl.searchParams.set("cluster", cluster); quantityRankUrl.searchParams.set("publisher", publisher); const qualityRankUrl = new URL( @@ -119,12 +116,12 @@ const fetchRankingData = async ( ]); return { - quantityRankData: PublisherQuantityScoreSchema.array().parse( - await quantityRankRes.json(), - ), - qualityRankData: PublisherQualityScoreSchema.array().parse( - await qualityRankRes.json(), - ), + quantityRankData: publisherQuantityScoreSchema + .array() + .parse(await quantityRankRes.json()), + qualityRankData: publisherQualityScoreSchema + .array() + .parse(await qualityRankRes.json()), }; }; const csvHeaders = [ diff --git a/apps/insights/src/components/PriceComponentDrawer/index.tsx b/apps/insights/src/components/PriceComponentDrawer/index.tsx index 82b4aac58d..49540eacb8 100644 --- a/apps/insights/src/components/PriceComponentDrawer/index.tsx +++ b/apps/insights/src/components/PriceComponentDrawer/index.tsx @@ -42,6 +42,8 @@ import { PriceName } from "../PriceName"; import { Score } from "../Score"; import { Status as StatusComponent } 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), @@ -273,13 +275,23 @@ type HeadingExtraProps = { }; const HeadingExtra = ({ status, ...props }: HeadingExtraProps) => { + const downloadReportForFeed = useDownloadReportForFeed(); + + const handleDownloadReport = useCallback( + (timeframe: Interval) => { + return downloadReportForFeed({ + symbol: props.symbol, + publisher: props.publisherKey, + timeframe, + cluster: ClusterToName[props.cluster], + }); + }, + [downloadReportForFeed, props.cluster, props.publisherKey, props.symbol], + ); + return ( <> - +
diff --git a/apps/insights/src/components/Publisher/layout.tsx b/apps/insights/src/components/Publisher/layout.tsx index 5d97b15c6e..30543eef31 100644 --- a/apps/insights/src/components/Publisher/layout.tsx +++ b/apps/insights/src/components/Publisher/layout.tsx @@ -13,7 +13,7 @@ import { StatCard } from "@pythnetwork/component-library/StatCard"; import { lookup } from "@pythnetwork/known-publishers"; import { notFound } from "next/navigation"; import type { ReactNode } from "react"; -import { Suspense } from "react"; +import { Suspense, useCallback } from "react"; import { getPublishers, @@ -46,6 +46,8 @@ import { TabPanel, TabRoot, Tabs } from "../Tabs"; import { TokenIcon } from "../TokenIcon"; import { OisApyHistory } from "./ois-apy-history"; import ConformanceReport from "../ConformanceReport/conformance-report"; +import type { Interval } from "../ConformanceReport/types"; +import { useDownloadReportForPublisher } from "../ConformanceReport/use-download-report-for-publisher"; type Props = { children: ReactNode; @@ -62,53 +64,9 @@ export const PublisherLayout = async ({ children, params }: Props) => { if (parsedCluster === undefined) { notFound(); } else { - const knownPublisher = lookup(key); return (
-
-
- }, - ]} - /> -
-
- , - })} - /> - -
- - - }> - - - }> - - - }> - - - {parsedCluster === Cluster.Pythnet && ( - }> - - - )} - -
+ { } }; +const PublisherHeader = ({ + cluster, + publisherKey, +}: { + cluster: Cluster; + publisherKey: string; +}) => { + const knownPublisher = lookup(publisherKey); + + const downloadReportForPublisher = useDownloadReportForPublisher(); + + const handleDownloadReport = useCallback( + (interval: Interval) => { + return downloadReportForPublisher({ + publisher: publisherKey, + cluster: ClusterToName[cluster], + interval, + }); + }, + [publisherKey, cluster, downloadReportForPublisher], + ); + + return ( +
+
+ }, + ]} + /> +
+
+ , + })} + /> + +
+ + + }> + + + }> + + + }> + + + {cluster === Cluster.Pythnet && ( + }> + + + )} + +
+ ); +}; + const NumFeeds = async ({ cluster, publisherKey, diff --git a/apps/insights/src/components/PublisherTag/index.module.scss b/apps/insights/src/components/PublisherTag/index.module.scss index f6a25f6e61..20fb0b99ad 100644 --- a/apps/insights/src/components/PublisherTag/index.module.scss +++ b/apps/insights/src/components/PublisherTag/index.module.scss @@ -28,7 +28,7 @@ .name { color: theme.color("heading"); - @include theme.text("lg", "medium"); + @include theme.text("base", "medium"); } .publisherKey, diff --git a/apps/insights/src/schemas/pyth.ts b/apps/insights/src/schemas/pyth/price-feeds-schema.ts similarity index 100% rename from apps/insights/src/schemas/pyth.ts rename to apps/insights/src/schemas/pyth/price-feeds-schema.ts diff --git a/apps/insights/src/server/pyth.ts b/apps/insights/src/server/pyth.ts index a3d81c17c7..1e4161895e 100644 --- a/apps/insights/src/server/pyth.ts +++ b/apps/insights/src/server/pyth.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { DEFAULT_CACHE_TTL } from "../cache"; import { VERCEL_REQUEST_HEADERS } from "../config/server"; import { getHost } from "../get-host"; -import { priceFeedsSchema } from "../schemas/pyth"; +import { priceFeedsSchema } from "../schemas/pyth/price-feeds-schema"; import { Cluster, ClusterToName } from "../services/pyth"; export async function getPublishersForFeedRequest( diff --git a/apps/insights/src/services/pyth/get-feeds.ts b/apps/insights/src/services/pyth/get-feeds.ts index fc7bb47c8a..17a2d66387 100644 --- a/apps/insights/src/services/pyth/get-feeds.ts +++ b/apps/insights/src/services/pyth/get-feeds.ts @@ -1,7 +1,7 @@ import { Cluster } from "."; import { getPythMetadata } from "./get-metadata"; import { redisCache } from "../../cache"; -import { priceFeedsSchema } from "../../schemas/pyth"; +import { priceFeedsSchema } from "../../schemas/pyth/price-feeds-schema"; const _getFeeds = async (cluster: Cluster) => { const unfilteredData = await getPythMetadata(cluster);