Skip to content

Commit 45180fb

Browse files
committed
fix(insights): use RPC as source of truth for publisher list
1 parent 8723e12 commit 45180fb

File tree

13 files changed

+248
-134
lines changed

13 files changed

+248
-134
lines changed

apps/insights/src/app/api/pyth/get-publishers/[symbol]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ClusterToName,
99
toCluster,
1010
} from "../../../../../services/pyth";
11-
import { getPublishersForCluster } from "../../../../../services/pyth/get-publishers-for-cluster";
11+
import { getPublishersByFeedForCluster } from "../../../../../services/pyth/get-publishers-for-cluster";
1212

1313
export const GET = async (
1414
request: NextRequest,
@@ -32,7 +32,7 @@ export const GET = async (
3232
});
3333
}
3434

35-
const map = await getPublishersForCluster(cluster);
35+
const map = await getPublishersByFeedForCluster(cluster);
3636

3737
return NextResponse.json(map[symbol] ?? []);
3838
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { z } from "zod";
3+
4+
import type { Cluster } from "../../../../services/pyth";
5+
import { CLUSTER_NAMES, toCluster } from "../../../../services/pyth";
6+
import { getFeedsByPublisherForCluster } from "../../../../services/pyth/get-publishers-for-cluster";
7+
8+
export const GET = async (request: NextRequest) => {
9+
const cluster = clusterSchema.safeParse(
10+
request.nextUrl.searchParams.get("cluster"),
11+
);
12+
13+
return cluster.success
14+
? NextResponse.json(await getPublishers(cluster.data))
15+
: new Response("Invalid params", { status: 400 });
16+
};
17+
18+
const clusterSchema = z
19+
.enum(CLUSTER_NAMES)
20+
.transform((value) => toCluster(value));
21+
22+
const getPublishers = async (cluster: Cluster) =>
23+
Object.entries(await getFeedsByPublisherForCluster(cluster)).map(
24+
([key, feeds]) => ({ key, permissionedFeeds: feeds.length }),
25+
);

apps/insights/src/components/Publisher/get-price-feeds.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { getFeedsForPublisherRequest } from "../../server/pyth";
2-
import { getRankingsByPublisher } from "../../services/clickhouse";
2+
import { getFeedRankingsByPublisher } from "../../services/clickhouse";
33
import { Cluster, ClusterToName } from "../../services/pyth";
44
import { getStatus } from "../../status";
55

66
export const getPriceFeeds = async (cluster: Cluster, key: string) => {
77
const [feeds, rankings] = await Promise.all([
88
getFeedsForPublisherRequest(cluster, key),
9-
getRankingsByPublisher(key),
9+
getFeedRankingsByPublisher(key),
1010
]);
1111
return feeds.map((feed) => {
1212
const ranking = rankings.find(

apps/insights/src/components/Publisher/layout.tsx

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { notFound } from "next/navigation";
1515
import type { ReactNode } from "react";
1616
import { Suspense } from "react";
1717

18+
import { getPublishersWithRankings } from "../../get-publishers-with-rankings";
1819
import {
19-
getPublishers,
2020
getPublisherRankingHistory,
2121
getPublisherAverageScoreHistory,
2222
} from "../../services/clickhouse";
@@ -38,13 +38,13 @@ import {
3838
ExplainActive,
3939
ExplainInactive,
4040
} from "../Explanations";
41+
import { FormattedDate } from "../FormattedDate";
4142
import { FormattedNumber } from "../FormattedNumber";
4243
import { PublisherIcon } from "../PublisherIcon";
4344
import { PublisherKey } from "../PublisherKey";
4445
import { PublisherTag } from "../PublisherTag";
4546
import { getPriceFeeds } from "./get-price-feeds";
4647
import styles from "./layout.module.scss";
47-
import { FormattedDate } from "../FormattedDate";
4848
import { FormattedTokens } from "../FormattedTokens";
4949
import { SemicircleMeter } from "../SemicircleMeter";
5050
import { TabPanel, TabRoot, Tabs } from "../Tabs";
@@ -365,7 +365,7 @@ const ActiveFeedsCard = async ({
365365
publisherKey: string;
366366
}) => {
367367
const [publishers, priceFeeds] = await Promise.all([
368-
getPublishers(cluster),
368+
getPublishersWithRankings(cluster),
369369
getPriceFeeds(cluster, publisherKey),
370370
]);
371371
const publisher = publishers.find(
@@ -391,8 +391,8 @@ type ActiveFeedsCardImplProps =
391391
isLoading?: false | undefined;
392392
cluster: Cluster;
393393
publisherKey: string;
394-
activeFeeds: number;
395-
inactiveFeeds: number;
394+
activeFeeds?: number | undefined;
395+
inactiveFeeds?: number | undefined;
396396
allFeeds: number;
397397
};
398398

@@ -435,33 +435,27 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
435435
)
436436
}
437437
miniStat1={
438-
props.isLoading ? (
439-
<Skeleton width={10} />
440-
) : (
441-
<>
442-
<FormattedNumber
443-
maximumFractionDigits={1}
444-
value={(100 * props.activeFeeds) / props.allFeeds}
445-
/>
446-
%
447-
</>
448-
)
438+
<RankingMiniStat
439+
{...(props.isLoading
440+
? { isLoading: true }
441+
: {
442+
stat: props.activeFeeds,
443+
allFeeds: props.allFeeds,
444+
})}
445+
/>
449446
}
450447
miniStat2={
451-
props.isLoading ? (
452-
<Skeleton width={10} />
453-
) : (
454-
<>
455-
<FormattedNumber
456-
maximumFractionDigits={1}
457-
value={(100 * props.inactiveFeeds) / props.allFeeds}
458-
/>
459-
%
460-
</>
461-
)
448+
<RankingMiniStat
449+
{...(props.isLoading
450+
? { isLoading: true }
451+
: {
452+
stat: props.inactiveFeeds,
453+
allFeeds: props.allFeeds,
454+
})}
455+
/>
462456
}
463457
>
464-
{!props.isLoading && (
458+
{!props.isLoading && props.activeFeeds !== undefined && (
465459
<Meter
466460
value={props.activeFeeds}
467461
maxValue={props.allFeeds}
@@ -471,6 +465,33 @@ const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => (
471465
</StatCard>
472466
);
473467

468+
const RankingMiniStat = (
469+
props:
470+
| { isLoading: true }
471+
| {
472+
isLoading?: false | undefined;
473+
stat: number | undefined;
474+
allFeeds: number;
475+
},
476+
) => {
477+
if (props.isLoading) {
478+
return <Skeleton width={10} />;
479+
} else if (props.stat === undefined) {
480+
// eslint-disable-next-line unicorn/no-null
481+
return null;
482+
} else {
483+
return (
484+
<>
485+
<FormattedNumber
486+
maximumFractionDigits={1}
487+
value={(100 * props.stat) / props.allFeeds}
488+
/>
489+
%
490+
</>
491+
);
492+
}
493+
};
494+
474495
const OisPoolCard = async ({ publisherKey }: { publisherKey: string }) => {
475496
const [publisherPoolData, publisherCaps] = await Promise.all([
476497
getPublisherPoolData(),

apps/insights/src/components/Publisher/performance.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { ReactNode, ComponentProps } from "react";
1616
import { getPriceFeeds } from "./get-price-feeds";
1717
import styles from "./performance.module.scss";
1818
import { TopFeedsTable } from "./top-feeds-table";
19-
import { getPublishers } from "../../services/clickhouse";
19+
import { getPublishersWithRankings } from "../../get-publishers-with-rankings";
2020
import type { Cluster } from "../../services/pyth";
2121
import { ClusterToName, parseCluster } from "../../services/pyth";
2222
import { Status } from "../../status";
@@ -48,7 +48,7 @@ export const Performance = async ({ params }: Props) => {
4848
notFound();
4949
}
5050
const [publishers, priceFeeds] = await Promise.all([
51-
getPublishers(parsedCluster),
51+
getPublishersWithRankings(parsedCluster),
5252
getPriceFeeds(parsedCluster, key),
5353
]);
5454
const slicedPublishers = sliceAround(
@@ -86,9 +86,14 @@ export const Performance = async ({ params }: Props) => {
8686
{publisher.inactiveFeeds}
8787
</Link>
8888
),
89-
averageScore: (
90-
<Score width={PUBLISHER_SCORE_WIDTH} score={publisher.averageScore} />
91-
),
89+
averageScore:
90+
// eslint-disable-next-line unicorn/no-null
91+
publisher.averageScore === undefined ? null : (
92+
<Score
93+
width={PUBLISHER_SCORE_WIDTH}
94+
score={publisher.averageScore}
95+
/>
96+
),
9297
name: (
9398
<PublisherTag
9499
cluster={parsedCluster}

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { lookup as lookupPublisher } from "@pythnetwork/known-publishers";
77

88
import styles from "./index.module.scss";
99
import { PublishersCard } from "./publishers-card";
10-
import { getPublishers } from "../../services/clickhouse";
10+
import { getPublishersWithRankings } from "../../get-publishers-with-rankings";
1111
import { getPublisherCaps } from "../../services/hermes";
1212
import { Cluster } from "../../services/pyth";
1313
import {
@@ -27,12 +27,15 @@ const INITIAL_REWARD_POOL_SIZE = 60_000_000_000_000n;
2727
export const Publishers = async () => {
2828
const [pythnetPublishers, pythtestConformancePublishers, oisStats] =
2929
await Promise.all([
30-
getPublishers(Cluster.Pythnet),
31-
getPublishers(Cluster.PythtestConformance),
30+
getPublishersWithRankings(Cluster.Pythnet),
31+
getPublishersWithRankings(Cluster.PythtestConformance),
3232
getOisStats(),
3333
]);
34-
const rankingTime = pythnetPublishers[0]?.timestamp;
35-
const scoreTime = pythnetPublishers[0]?.scoreTime;
34+
const rankedPublishers = pythnetPublishers.filter(
35+
(publisher) => publisher.scoreTime !== undefined,
36+
);
37+
const rankingTime = rankedPublishers[0]?.timestamp;
38+
const scoreTime = rankedPublishers[0]?.scoreTime;
3639

3740
return (
3841
<div className={styles.publishers}>
@@ -65,10 +68,10 @@ export const Publishers = async () => {
6568
corner={<ExplainAverage scoreTime={scoreTime} />}
6669
className={styles.statCard ?? ""}
6770
stat={(
68-
pythnetPublishers.reduce(
69-
(sum, publisher) => sum + publisher.averageScore,
71+
rankedPublishers.reduce(
72+
(sum, publisher) => sum + (publisher.averageScore ?? 0),
7073
0,
71-
) / pythnetPublishers.length
74+
) / rankedPublishers.length
7275
).toFixed(2)}
7376
/>
7477
<PublishersCard
@@ -149,7 +152,7 @@ const toTableRow = ({
149152
permissionedFeeds,
150153
activeFeeds,
151154
averageScore,
152-
}: Awaited<ReturnType<typeof getPublishers>>[number]) => {
155+
}: Awaited<ReturnType<typeof getPublishersWithRankings>>[number]) => {
153156
const knownPublisher = lookupPublisher(key);
154157
return {
155158
id: key,

apps/insights/src/components/Publishers/publishers-card.tsx

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ type Props = {
4545

4646
type Publisher = {
4747
id: string;
48-
ranking: number;
48+
ranking?: number | undefined;
4949
permissionedFeeds: number;
50-
activeFeeds: number;
51-
averageScore: number;
50+
activeFeeds?: number | undefined;
51+
averageScore?: number | undefined;
5252
} & (
5353
| { name: string; icon: ReactNode }
5454
| { name?: undefined; icon?: undefined }
@@ -100,27 +100,38 @@ const ResolvedPublishersCard = ({
100100
filter.contains(publisher.id, search) ||
101101
(publisher.name !== undefined && filter.contains(publisher.name, search)),
102102
(a, b, { column, direction }) => {
103+
const desc = direction === "descending" ? -1 : 1;
104+
105+
const sortByName =
106+
desc * collator.compare(a.name ?? a.id, b.name ?? b.id);
107+
108+
const sortByRankingField = (
109+
column: "ranking" | "activeFeeds" | "averageScore",
110+
) => {
111+
if (a[column] === undefined) {
112+
return b[column] === undefined ? sortByName : 1;
113+
} else {
114+
return b[column] === undefined ? -1 : desc * (a[column] - b[column]);
115+
}
116+
};
117+
103118
switch (column) {
119+
case "permissionedFeeds": {
120+
return desc * (a[column] - b[column]);
121+
}
122+
104123
case "ranking":
105-
case "permissionedFeeds":
106124
case "activeFeeds":
107125
case "averageScore": {
108-
return (
109-
(direction === "descending" ? -1 : 1) * (a[column] - b[column])
110-
);
126+
return sortByRankingField(column);
111127
}
112128

113129
case "name": {
114-
return (
115-
(direction === "descending" ? -1 : 1) *
116-
collator.compare(a.name ?? a.id, b.name ?? b.id)
117-
);
130+
return sortByName;
118131
}
119132

120133
default: {
121-
return (
122-
(direction === "descending" ? -1 : 1) * (a.ranking - b.ranking)
123-
);
134+
return sortByRankingField("ranking");
124135
}
125136
}
126137
},
@@ -144,7 +155,9 @@ const ResolvedPublishersCard = ({
144155
textValue: publisher.name ?? id,
145156
prefetch: false,
146157
data: {
147-
ranking: <Ranking>{ranking}</Ranking>,
158+
ranking:
159+
// eslint-disable-next-line unicorn/no-null
160+
ranking === undefined ? null : <Ranking>{ranking}</Ranking>,
148161
name: (
149162
<PublisherTag
150163
publisherKey={id}
@@ -164,9 +177,11 @@ const ResolvedPublishersCard = ({
164177
{activeFeeds}
165178
</Link>
166179
),
167-
averageScore: (
168-
<Score score={averageScore} width={PUBLISHER_SCORE_WIDTH} />
169-
),
180+
averageScore:
181+
// eslint-disable-next-line unicorn/no-null
182+
averageScore === undefined ? null : (
183+
<Score score={averageScore} width={PUBLISHER_SCORE_WIDTH} />
184+
),
170185
},
171186
}),
172187
),

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import { lookup as lookupPublisher } from "@pythnetwork/known-publishers";
33
import { NuqsAdapter } from "nuqs/adapters/next/app";
44
import type { ReactNode } from "react";
55

6+
import { SearchButton as SearchButtonImpl } from "./search-button";
67
import {
78
AMPLITUDE_API_KEY,
89
ENABLE_ACCESSIBILITY_REPORTING,
910
GOOGLE_ANALYTICS_ID,
1011
} from "../../config/server";
12+
import { getPublishersWithRankings } from "../../get-publishers-with-rankings";
1113
import { LivePriceDataProvider } from "../../hooks/use-live-price-data";
12-
import { getPublishers } from "../../services/clickhouse";
1314
import { Cluster } from "../../services/pyth";
1415
import { getFeeds } from "../../services/pyth/get-feeds";
1516
import { PriceFeedIcon } from "../PriceFeedIcon";
1617
import { PublisherIcon } from "../PublisherIcon";
17-
import { SearchButton as SearchButtonImpl } from "./search-button";
1818

1919
export const TABS = [
2020
{ segment: "", children: "Overview" },
@@ -53,7 +53,7 @@ const SearchButton = async () => {
5353
};
5454

5555
const getPublishersForSearchDialog = async (cluster: Cluster) => {
56-
const publishers = await getPublishers(cluster);
56+
const publishers = await getPublishersWithRankings(cluster);
5757
return publishers.map((publisher) => {
5858
const knownPublisher = lookupPublisher(publisher.key);
5959

0 commit comments

Comments
 (0)