Skip to content

Commit 00cefab

Browse files
committed
Add price feed ids table
1 parent fa1ee09 commit 00cefab

30 files changed

+1512
-313
lines changed

components/CopyAddress.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import CopyButton from "./CopyButton";
2+
import clsx from "clsx";
23

3-
const CopyAddress = ({ address, url }: { address: string; url?: string }) => {
4+
type Props = {
5+
address: string;
6+
url?: string,
7+
alwaysTruncate?: boolean | undefined
8+
};
9+
10+
const CopyAddress = ({ address, url, alwaysTruncate }: Props) => {
411
return (
512
<CopyButton value={address} className="-ml-1">
6-
<span className="mr-2 hidden lg:block">
7-
{url ? (
8-
<a href={url} target="_blank" rel="noopener noreferrer">
9-
{address}
10-
</a>
11-
) : (
12-
address
13-
)}
14-
</span>
15-
<span className="mr-2 lg:hidden">
13+
{!alwaysTruncate && (
14+
<span className="mr-2 hidden lg:block">
15+
{url ? (
16+
<a href={url} target="_blank" rel="noopener noreferrer">
17+
{address}
18+
</a>
19+
) : (
20+
address
21+
)}
22+
</span>
23+
)}
24+
<span className={clsx("mr-2", { "lg:hidden": !alwaysTruncate })}>
1625
{url ? (
1726
<a href={url} target="_blank" rel="noopener noreferrer">
1827
{address.slice(0, 6) + "..." + address.slice(-6)}

components/PriceFeedIds.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { useState, useEffect, useRef, useCallback, ChangeEvent, useMemo } from "react";
2+
import { z } from "zod";
3+
import { getPriceFeedAccountForProgram } from '@pythnetwork/pyth-solana-receiver'
4+
import CopyAddress from "./CopyAddress";
5+
import { Callout, Table, Th, Td, Tr } from "nextra/components";
6+
7+
export const PriceFeedIds = () => {
8+
const isLoading = useRef(false);
9+
const [state, setState] = useState<State>(State.NotLoaded());
10+
11+
useEffect(() => {
12+
if (!isLoading.current) {
13+
setState(State.Loading());
14+
isLoading.current = true;
15+
getFeeds()
16+
.then(feeds => setState(State.Loaded(feeds)))
17+
.catch(error => setState(State.Error(error)));
18+
}
19+
}, []);
20+
21+
switch (state.type) {
22+
case StateType.Loading:
23+
case StateType.NotLoaded: {
24+
return <div className="w-full text-center my-10">Loading...</div>
25+
}
26+
case StateType.Error: {
27+
console.error(state.error);
28+
return <Callout type="error">{errorToString(state.error)}</Callout>
29+
}
30+
case StateType.Loaded: {
31+
return <LoadedResults feeds={state.feeds} />
32+
}
33+
}
34+
};
35+
36+
const LoadedResults = ({ feeds }: { feeds: Awaited<ReturnType<typeof getFeeds>>}) => {
37+
const [search, setSearch] = useState("");
38+
const updateSearch = useCallback((event: ChangeEvent<HTMLInputElement>) => {
39+
setSearch(event.target.value);
40+
}, [])
41+
const filteredFeeds = useMemo(
42+
() => feeds.filter(feed => feed.symbol.toLowerCase().includes(search.toLowerCase())),
43+
[feeds, search]
44+
);
45+
46+
return (
47+
<div className="mt-4">
48+
<input
49+
type="text"
50+
placeholder="Search"
51+
className="w-full"
52+
value={search}
53+
onChange={updateSearch}
54+
/>
55+
{filteredFeeds.length === 0
56+
? <Callout type="info">No results for {search}</Callout>
57+
: (
58+
<div className="mt-4 max-h-[1200px] overflow-auto">
59+
<Table>
60+
<thead>
61+
<tr>
62+
<Th className="!font-semibold !text-right">
63+
Symbol
64+
</Th>
65+
<Th>
66+
Stable Price Feed ID
67+
</Th>
68+
<Th>
69+
Beta Price Feed ID
70+
</Th>
71+
<Th>
72+
Solana Price Feed Account
73+
</Th>
74+
</tr>
75+
</thead>
76+
<tbody>
77+
{filteredFeeds.map(({ symbol, betaFeedId, solanaPriceFeedAccount, stableFeedId }) => (
78+
<Tr key={symbol}>
79+
<Td className="font-semibold text-right !text-xs">{symbol}</Td>
80+
<Td>{stableFeedId && <CopyAddress alwaysTruncate address={stableFeedId} />}</Td>
81+
<Td>{betaFeedId && <CopyAddress alwaysTruncate address={betaFeedId} />}</Td>
82+
<Td>{solanaPriceFeedAccount && <CopyAddress alwaysTruncate address={solanaPriceFeedAccount} />}</Td>
83+
</Tr>
84+
))}
85+
</tbody>
86+
</Table>
87+
</div>
88+
)
89+
}
90+
</div>
91+
)
92+
}
93+
94+
const errorToString = (error: unknown) => {
95+
if (error instanceof Error) {
96+
return error.message;
97+
} else if (typeof error === "string") {
98+
return error;
99+
} else {
100+
return "An error occurred, please try again";
101+
}
102+
}
103+
104+
enum StateType {
105+
NotLoaded,
106+
Loading,
107+
Loaded,
108+
Error
109+
}
110+
111+
const State = {
112+
NotLoaded: () => ({ type: StateType.NotLoaded as const }),
113+
Loading: () => ({ type: StateType.Loading as const }),
114+
Loaded: (feeds: Awaited<ReturnType<typeof getFeeds>>) => ({ type: StateType.Loaded as const, feeds }),
115+
Error: (error: unknown) => ({ type: StateType.Error as const, error }),
116+
}
117+
type State = ReturnType<typeof State[keyof typeof State]>;
118+
119+
const getFeeds = async () => {
120+
const [pythnet, pythtest] = await Promise.all([
121+
getFeedsFromHermes("https://hermes.pyth.network"),
122+
getFeedsFromHermes("https://hermes-beta.pyth.network")
123+
]);
124+
125+
const feeds = new Map<string, { stableFeedId?: string, betaFeedId?: string, solanaPriceFeedAccount?: string }>();
126+
127+
for (const feed of pythnet) {
128+
feeds.set(feed.attributes.symbol, {
129+
stableFeedId: feed.id,
130+
solanaPriceFeedAccount: getPriceFeedAccountForProgram(
131+
0,
132+
Buffer.from(feed.id, 'hex')
133+
).toBase58()
134+
});
135+
}
136+
for (const feed of pythtest) {
137+
feeds.set(feed.attributes.symbol, {
138+
...feeds.get(feed.attributes.symbol),
139+
betaFeedId: feed.id,
140+
});
141+
}
142+
143+
return [...feeds.entries()]
144+
.toSorted((a, b) => a[0].localeCompare(b[0]))
145+
.map(([symbol, attrs]) => ({ symbol, ...attrs }));
146+
}
147+
148+
const getFeedsFromHermes = async (hermesUrl: string) => {
149+
const result = await fetch(new URL('/v2/price_feeds', hermesUrl));
150+
return hermesSchema.parse(await result.json());
151+
}
152+
153+
const hermesSchema = z.array(z.object({
154+
id: z.string(),
155+
attributes: z.object({ symbol: z.string() })
156+
}));

0 commit comments

Comments
 (0)