Skip to content

Commit 99b6a23

Browse files
committed
Add price feed ids table
1 parent fa1ee09 commit 99b6a23

30 files changed

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

0 commit comments

Comments
 (0)