Skip to content

Commit c166d1c

Browse files
authored
Merge pull request #2921 from pyth-network/feat/perf-caching
feat(insights): pyth metadata caching
2 parents ca0b2e8 + 498dbd0 commit c166d1c

File tree

29 files changed

+1112
-646
lines changed

29 files changed

+1112
-646
lines changed

apps/insights/next.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ const config = {
33
useCache: true,
44
reactCompiler: true,
55
},
6-
76
reactStrictMode: true,
87

98
pageExtensions: ["ts", "tsx", "mdx"],

apps/insights/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
"@pythnetwork/known-publishers": "workspace:*",
2929
"@react-hookz/web": "catalog:",
3030
"@solana/web3.js": "catalog:",
31+
"async-cache-dedupe": "catalog:",
3132
"bs58": "catalog:",
3233
"clsx": "catalog:",
3334
"cryptocurrency-icons": "catalog:",
3435
"dnum": "catalog:",
36+
"ioredis": "^5.7.0",
3537
"lightweight-charts": "catalog:",
3638
"motion": "catalog:",
3739
"next": "catalog:",
@@ -44,7 +46,8 @@
4446
"superjson": "catalog:",
4547
"swr": "catalog:",
4648
"zod": "catalog:",
47-
"zod-validation-error": "catalog:"
49+
"zod-validation-error": "catalog:",
50+
"zod-search-params": "catalog:"
4851
},
4952
"devDependencies": {
5053
"@cprussin/eslint-config": "catalog:",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { NextRequest } from "next/server";
2+
import { stringify } from "superjson";
3+
import { z } from "zod";
4+
import { parseSearchParams } from "zod-search-params";
5+
6+
import {
7+
Cluster,
8+
CLUSTER_NAMES,
9+
ClusterToName,
10+
toCluster,
11+
} from "../../../../../services/pyth";
12+
import { getFeeds } from "../../../../../services/pyth/get-feeds";
13+
14+
export const GET = async (
15+
request: NextRequest,
16+
{ params }: { params: Promise<{ publisher: string }> },
17+
) => {
18+
const { publisher } = await params;
19+
const searchParams = request.nextUrl.searchParams;
20+
const parsedSearchParams = parseSearchParams(queryParamsSchema, searchParams);
21+
22+
if (!parsedSearchParams) {
23+
return new Response("Invalid params", {
24+
status: 400,
25+
});
26+
}
27+
28+
const { cluster } = parsedSearchParams;
29+
30+
if (!publisher) {
31+
return new Response("Publisher is required", {
32+
status: 400,
33+
});
34+
}
35+
36+
const feeds = await getFeeds(cluster);
37+
38+
const filteredFeeds = feeds.filter((feed) =>
39+
feed.price.priceComponents.some((c) => c.publisher === publisher),
40+
);
41+
42+
return new Response(stringify(filteredFeeds), {
43+
headers: {
44+
"Content-Type": "application/json",
45+
},
46+
});
47+
};
48+
49+
const queryParamsSchema = z.object({
50+
cluster: z
51+
.enum(CLUSTER_NAMES)
52+
.transform((value) => toCluster(value))
53+
.default(ClusterToName[Cluster.Pythnet]),
54+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NextRequest } from "next/server";
2+
import { stringify } from "superjson";
3+
import { z } from "zod";
4+
import { parseSearchParams } from "zod-search-params";
5+
6+
import {
7+
Cluster,
8+
CLUSTER_NAMES,
9+
ClusterToName,
10+
toCluster,
11+
} from "../../../../../services/pyth";
12+
import { getFeeds } from "../../../../../services/pyth/get-feeds";
13+
14+
export const GET = async (
15+
request: NextRequest,
16+
{ params }: { params: Promise<{ symbol: string }> },
17+
) => {
18+
const { symbol } = await params;
19+
const searchParams = request.nextUrl.searchParams;
20+
const parsedSearchParams = parseSearchParams(queryParamsSchema, searchParams);
21+
22+
if (!parsedSearchParams) {
23+
return new Response("Invalid params", {
24+
status: 400,
25+
});
26+
}
27+
28+
const { cluster } = parsedSearchParams;
29+
30+
const feeds = await getFeeds(cluster);
31+
const feed = feeds.find((feed) => feed.symbol === symbol);
32+
33+
return new Response(stringify(feed), {
34+
headers: {
35+
"Content-Type": "application/json",
36+
},
37+
});
38+
};
39+
40+
const queryParamsSchema = z.object({
41+
cluster: z
42+
.enum(CLUSTER_NAMES)
43+
.transform((value) => toCluster(value))
44+
.default(ClusterToName[Cluster.Pythnet]),
45+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextRequest } from "next/server";
2+
import { stringify } from "superjson";
3+
import { z } from "zod";
4+
import { parseSearchParams } from "zod-search-params";
5+
6+
import { CLUSTER_NAMES, toCluster } from "../../../../services/pyth";
7+
import { getFeeds } from "../../../../services/pyth/get-feeds";
8+
9+
export const GET = async (request: NextRequest) => {
10+
// get cluster from query params
11+
const searchParams = request.nextUrl.searchParams;
12+
const parsedSearchParams = parseSearchParams(queryParamsSchema, searchParams);
13+
14+
if (!parsedSearchParams) {
15+
return new Response("Invalid params", {
16+
status: 400,
17+
});
18+
}
19+
20+
const { excludePriceComponents, cluster } = parsedSearchParams;
21+
22+
const feeds = await getFeeds(cluster);
23+
const filteredFeeds = excludePriceComponents
24+
? feeds.map((feed) => {
25+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26+
const { price, ...rest } = feed;
27+
return rest;
28+
})
29+
: feeds;
30+
31+
return new Response(stringify(filteredFeeds), {
32+
headers: {
33+
"Content-Type": "application/json",
34+
},
35+
});
36+
};
37+
38+
const queryParamsSchema = z.object({
39+
cluster: z.enum(CLUSTER_NAMES).transform((value) => toCluster(value)),
40+
excludePriceComponents: z.boolean(),
41+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { z } from "zod";
3+
import { parseSearchParams } from "zod-search-params";
4+
5+
import {
6+
Cluster,
7+
CLUSTER_NAMES,
8+
ClusterToName,
9+
toCluster,
10+
} from "../../../../../services/pyth";
11+
import { getPublishersForCluster } from "../../../../../services/pyth/get-publishers-for-cluster";
12+
13+
export const GET = async (
14+
request: NextRequest,
15+
{ params }: { params: Promise<{ symbol: string }> },
16+
) => {
17+
const { symbol } = await params;
18+
const searchParams = request.nextUrl.searchParams;
19+
const parsedSearchParams = parseSearchParams(queryParamsSchema, searchParams);
20+
21+
if (!parsedSearchParams) {
22+
return new Response("Invalid params", {
23+
status: 400,
24+
});
25+
}
26+
27+
const { cluster } = parsedSearchParams;
28+
29+
if (!symbol) {
30+
return new Response("Symbol is required", {
31+
status: 400,
32+
});
33+
}
34+
35+
const map = await getPublishersForCluster(cluster);
36+
37+
return NextResponse.json(map[symbol] ?? []);
38+
};
39+
40+
const queryParamsSchema = z.object({
41+
cluster: z
42+
.enum(CLUSTER_NAMES)
43+
.transform((value) => toCluster(value))
44+
.default(ClusterToName[Cluster.Pythnet]),
45+
});

apps/insights/src/app/component-score-history/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ export const GET = async (req: NextRequest) => {
1616
);
1717
if (parsed.success) {
1818
const { cluster, publisherKey, symbol, from, to } = parsed.data;
19-
const data = await getFeedScoreHistory(
19+
const data = await getFeedScoreHistory({
2020
cluster,
2121
publisherKey,
2222
symbol,
2323
from,
2424
to,
25-
);
25+
});
2626
return Response.json(data);
2727
} else {
2828
return new Response(fromError(parsed.error).toString(), {

apps/insights/src/app/historical-prices/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ export async function GET(req: NextRequest) {
66
const symbol = req.nextUrl.searchParams.get("symbol");
77
const until = req.nextUrl.searchParams.get("until");
88
if (symbol && until) {
9-
const res = await getHistoricalPrices(decodeURIComponent(symbol), until);
9+
const res = await getHistoricalPrices({
10+
symbol: decodeURIComponent(symbol),
11+
until,
12+
});
1013
return Response.json(res);
1114
} else {
1215
return new Response("Must provide `symbol` and `until`", { status: 400 });

apps/insights/src/app/price-feeds/[slug]/layout.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { Metadata } from "next";
22
import { notFound } from "next/navigation";
33
import type { ReactNode } from "react";
44

5-
import { Cluster, getFeeds } from "../../../services/pyth";
5+
import { Cluster } from "../../../services/pyth";
6+
import { getFeeds } from "../../../services/pyth/get-feeds";
67

78
export { PriceFeedLayout as default } from "../../../components/PriceFeed/layout";
89

apps/insights/src/components/Overview/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import PriceFeedsLight from "./price-feeds-light.svg";
99
import PublishersDark from "./publishers-dark.svg";
1010
import PublishersLight from "./publishers-light.svg";
1111
import { TabList } from "./tab-list";
12-
import { Cluster, getFeeds } from "../../services/pyth";
12+
import { Cluster } from "../../services/pyth";
13+
import { getFeeds } from "../../services/pyth/get-feeds";
1314
import {
1415
totalVolumeTraded,
1516
activeChains,
@@ -25,6 +26,7 @@ import { FormattedNumber } from "../FormattedNumber";
2526
export const Overview = async () => {
2627
const priceFeeds = await getFeeds(Cluster.Pythnet);
2728
const today = new Date();
29+
2830
const feedCounts = [
2931
...activeFeeds.map(({ date, numFeeds }) => ({
3032
x: date,
@@ -37,6 +39,7 @@ export const Overview = async () => {
3739
y: priceFeeds.length,
3840
},
3941
];
42+
4043
return (
4144
<div className={styles.overview}>
4245
<h1 className={styles.header}>Overview</h1>

0 commit comments

Comments
 (0)