Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"bs58": "catalog:",
"clsx": "catalog:",
"cryptocurrency-icons": "catalog:",
"date-fns": "catalog:",
"csv-stringify": "catalog:",
"dnum": "catalog:",
"ioredis": "^5.7.0",
"lightweight-charts": "catalog:",
Expand All @@ -46,8 +48,8 @@
"superjson": "catalog:",
"swr": "catalog:",
"zod": "catalog:",
"zod-validation-error": "catalog:",
"zod-search-params": "catalog:"
"zod-search-params": "catalog:",
"zod-validation-error": "catalog:"
},
"devDependencies": {
"@cprussin/eslint-config": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion apps/insights/src/app/api/pyth/get-feeds/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ export const GET = async (request: NextRequest) => {

const queryParamsSchema = z.object({
cluster: z.enum(CLUSTER_NAMES).transform((value) => toCluster(value)),
excludePriceComponents: z.boolean(),
excludePriceComponents: z.boolean().optional().default(false),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.conformanceReport {
display: flex;
gap: 0.5rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"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 { useLogger } from "@pythnetwork/component-library/useLogger";
import { useState } from "react";

import styles from "./conformance-report.module.scss";
import type { Interval } from "./types";
import { INTERVALS } from "./types";
import { useDownloadReportForFeed } from "./use-download-report-for-feed";
import { useDownloadReportForPublisher } from "./use-download-report-for-publisher";
import { CLUSTER_NAMES } from "../../services/pyth";

type ConformanceReportProps =
| { isLoading: true }
| {
isLoading?: false | undefined;
symbol?: string;
cluster: (typeof CLUSTER_NAMES)[number];
publisher?: string;
};

const ConformanceReport = (props: ConformanceReportProps) => {
const [timeframe, setTimeframe] = useState<Interval>(INTERVALS[0]);
const [isGeneratingReport, setIsGeneratingReport] = useState(false);
const { open } = useAlert();
const downloadReportForFeed = useDownloadReportForFeed();
const downloadReportForPublisher = useDownloadReportForPublisher();
const logger = useLogger();

/**
* Download the conformance report for the given symbol or publisher
*/
const downloadReport = async () => {
if (props.isLoading) {
return;
}
if (props.symbol && props.publisher) {
return downloadReportForFeed({
symbol: props.symbol,
publisher: props.publisher,
timeframe,
cluster: props.cluster,
});
}

if (props.publisher) {
return downloadReportForPublisher({
publisher: props.publisher,
cluster: props.cluster,
interval: timeframe,
});
}
};

const handleReport = () => {
setIsGeneratingReport(true);
downloadReport()
.catch((error: unknown) => {
open({
title: "Error",
contents: "Error generating conformance report",
});
logger.error(error);
})
.finally(() => {
setIsGeneratingReport(false);
});
};

if (props.isLoading) {
return <Skeleton width={100} />;
}

return (
<div className={styles.conformanceReport}>
<Select
options={INTERVALS.map((interval) => ({ id: interval }))}
placement="bottom end"
selectedKey={timeframe}
onSelectionChange={setTimeframe}
size="sm"
label="Timeframe"
variant="outline"
hideLabel
/>
<Button
variant="outline"
size="sm"
onClick={handleReport}
afterIcon={<Download key="download" />}
isPending={isGeneratingReport}
>
Report
</Button>
</div>
);
};

export default ConformanceReport;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const WEB_API_BASE_URL = "https://web-api.pyth.network";
2 changes: 2 additions & 0 deletions apps/insights/src/components/ConformanceReport/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const INTERVALS = ["24H", "48H", "72H", "1W", "1M"] as const;
export type Interval = (typeof INTERVALS)[number];
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useCallback } from "react";

import { WEB_API_BASE_URL } from "./constants";
import type { Interval } from "./types";
import { useDownloadBlob } from "../../hooks/use-download-blob";
import { CLUSTER_NAMES } from "../../services/pyth";

const PYTHTEST_CONFORMANCE_REFERENCE_PUBLISHER =
"HUZu4xMSHbxTWbkXR6jkGdjvDPJLjrpSNXSoUFBRgjWs";

export const useDownloadReportForFeed = () => {
const download = useDownloadBlob();

return useCallback(
async ({
symbol,
publisher,
timeframe,
cluster,
}: {
symbol: string;
publisher: string;
timeframe: Interval;
cluster: (typeof CLUSTER_NAMES)[number];
}) => {
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],
);
};
Loading
Loading