This repository was archived by the owner on Nov 28, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 38
feat(pricefeed) Migrate Price Feed IDs to documentation #493
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| import { useState, useEffect } from "react"; | ||
| import { TextField, Select, MenuItem } from "@mui/material"; | ||
| import { HermesClient, PriceFeedMetadata } from "@pythnetwork/hermes-client"; | ||
| import { Table, Td, Th, Tr, CopyToClipboard } from "nextra/components"; | ||
| import base58 from "bs58"; | ||
| import { getPriceFeedAccountForProgram } from "@pythnetwork/pyth-solana-receiver"; | ||
|
|
||
| const fetchPriceFeeds = async (stable: boolean) => { | ||
| const priceFeeds = await new HermesClient( | ||
| stable ? "https://hermes.pyth.network" : "https://hermes-beta.pyth.network" | ||
| ).getPriceFeeds(); | ||
| const assetTypes = Array.from( | ||
| new Set(priceFeeds.map((feed) => feed.attributes.asset_type ?? "")) | ||
| ); | ||
| console.log(priceFeeds); | ||
| return { priceFeeds, assetTypes }; | ||
| }; | ||
|
|
||
| const fetchSolanaPriceFeedAccounts = async () => { | ||
| const priceFeeds = await fetchPriceFeeds(true); | ||
| const priceFeedIds = priceFeeds.priceFeeds.map((feed) => | ||
| getPriceFeedAccountForProgram(0, base58.decode(feed.id)).toBase58() | ||
| ); | ||
| return priceFeedIds; | ||
| }; | ||
|
|
||
| type AssetTypesSelectorProps = { | ||
| priceFeedsState: PriceFeedsState; | ||
| selectedAssetType: string; | ||
| setSelectedAssetType: (assetType: string) => void; | ||
| }; | ||
|
|
||
| enum PriceFeedsStateType { | ||
| NotLoaded, | ||
| Loading, | ||
| Loaded, | ||
| Error, | ||
| } | ||
|
|
||
| const PriceFeedsState = { | ||
| NotLoaded: () => ({ type: PriceFeedsStateType.NotLoaded as const }), | ||
| Loading: () => ({ type: PriceFeedsStateType.Loading as const }), | ||
| Loaded: (priceFeeds: PriceFeedMetadata[], assetTypes: string[]) => ({ | ||
| type: PriceFeedsStateType.Loaded as const, | ||
| priceFeeds, | ||
| assetTypes, | ||
| }), | ||
| Error: (error: unknown) => ({ | ||
| type: PriceFeedsStateType.Error as const, | ||
| error, | ||
| }), | ||
| }; | ||
|
|
||
| type PriceFeedsState = ReturnType< | ||
| typeof PriceFeedsState[keyof typeof PriceFeedsState] | ||
| >; | ||
|
|
||
| const usePriceFeeds = (): PriceFeedsState => { | ||
| const [priceFeeds, setPriceFeeds] = useState<PriceFeedsState>( | ||
| PriceFeedsState.NotLoaded() | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| setPriceFeeds(PriceFeedsState.Loading()); | ||
| // console.log(fetchSolanaPriceFeedAccounts()) | ||
| fetchPriceFeeds(true) | ||
| .then(({ priceFeeds, assetTypes }) => { | ||
| setPriceFeeds(PriceFeedsState.Loaded(priceFeeds, assetTypes)); | ||
| }) | ||
| .catch((error) => { | ||
| setPriceFeeds(PriceFeedsState.Error(error)); | ||
| }); | ||
| }, []); | ||
|
|
||
| return priceFeeds; | ||
| }; | ||
|
|
||
| const AssetTypesSelect = ({ | ||
| priceFeedsState, | ||
| selectedAssetType, | ||
| setSelectedAssetType, | ||
| }: AssetTypesSelectorProps) => { | ||
| const priceFeeds = usePriceFeeds(); | ||
|
|
||
| switch (priceFeeds.type) { | ||
| case PriceFeedsStateType.NotLoaded: | ||
| case PriceFeedsStateType.Loading: | ||
| case PriceFeedsStateType.Error: | ||
| return <p>Error loading asset types</p>; | ||
| case PriceFeedsStateType.Loaded: | ||
| return ( | ||
| <Select | ||
| value={selectedAssetType} | ||
| onChange={(e) => setSelectedAssetType(e.target.value)} | ||
| size="small" | ||
| sx={{ width: 200 }} | ||
| > | ||
| <MenuItem value="All">All Asset Types</MenuItem> | ||
| {priceFeeds.assetTypes.map((type) => ( | ||
| <MenuItem key={type} value={type}> | ||
| {type ?? "Uncategorized"} | ||
| </MenuItem> | ||
| ))} | ||
| </Select> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's a great reason to use MUI for text fields or selects; we already have other components in the codebase for both cases, see e.g. https://docs.pyth.network/price-feeds/api-reference/cosmwasm/query-price-feed and the components on that page ( |
||
| ); | ||
| } | ||
| }; | ||
|
|
||
| export function PriceFeedTable() { | ||
| const [searchTerm, setSearchTerm] = useState(""); | ||
| const [selectedAssetType, setSelectedAssetType] = useState("All"); | ||
| const priceFeedsState = usePriceFeeds(); | ||
|
|
||
| <AssetTypesSelect | ||
| priceFeedsState={priceFeedsState} | ||
| selectedAssetType={selectedAssetType} | ||
| setSelectedAssetType={setSelectedAssetType} | ||
| />; | ||
|
|
||
| let filteredData: PriceFeedMetadata[] = []; | ||
| if (priceFeedsState.type === PriceFeedsStateType.Loaded) { | ||
| filteredData = priceFeedsState.priceFeeds.filter((feed) => { | ||
| const matchesSearch = | ||
| feed.id.toLowerCase().includes(searchTerm.toLowerCase()) || | ||
| feed.attributes.display_symbol | ||
| .toLowerCase() | ||
| .includes(searchTerm.toLowerCase()); | ||
| const matchesAssetType = | ||
| selectedAssetType === "All" || | ||
| feed.attributes.asset_type === selectedAssetType; | ||
| return matchesSearch && matchesAssetType; | ||
| }); | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| <TextField | ||
| label="Search price feeds" | ||
| variant="outlined" | ||
| size="small" | ||
| value={searchTerm} | ||
| onChange={(e) => setSearchTerm(e.target.value)} | ||
| sx={{ width: 300 }} | ||
| /> | ||
| <Select | ||
| value={selectedAssetType} | ||
| onChange={(e) => setSelectedAssetType(e.target.value)} | ||
| size="small" | ||
| sx={{ width: 200 }} | ||
| > | ||
| <MenuItem value="All">All Asset Types</MenuItem> | ||
| {priceFeedsState.type === PriceFeedsStateType.Loaded && | ||
| priceFeedsState.assetTypes.map((type) => ( | ||
| <MenuItem key={type} value={type}> | ||
| {type || "Uncategorized"} | ||
| </MenuItem> | ||
| ))} | ||
| </Select> | ||
|
|
||
| <Table> | ||
| <thead> | ||
| <Tr> | ||
| <Th>Ticker</Th> | ||
| <Th>Asset Type</Th> | ||
| <Th>Feed ID</Th> | ||
| </Tr> | ||
| </thead> | ||
| <tbody> | ||
| {filteredData.map((feed) => ( | ||
| <Tr key={feed.id}> | ||
| <Td>{feed.attributes.display_symbol}</Td> | ||
| <Td>{feed.attributes.asset_type}</Td> | ||
| <Td className="font-mono whitespace-nowrap"> | ||
| {feed.id} | ||
| <CopyToClipboard className="ml-2" getValue={() => feed.id} /> | ||
| </Td> | ||
| </Tr> | ||
| ))} | ||
| </tbody> | ||
| </Table> | ||
| </> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a few issues with this effect and the state it creates:
priceFeedsto an empty array on line 24, you should properly handle loading states and represent that state differently from a loaded array.priceFeedsanywhere except to get the list of asset types. You should extract the asset types and dedupe them when you load the data and store that in the state, rather than deduping in the render path on line 76 below.catchany time you call a promise without usingawait, e.g. on line 41. Omitting thecatchmeans uncaught exceptions could potentially put the app into an unstable state; additionally here you should make the UI indicate errors properly.fetchPriceFeedsoutside the closure scope. For one, it makes the code simpler and easier to follow; for two it's easy to introduce performance issues by creating closures in a hot path by accident.??over||(on lines 35 and 36), see https://typescript-eslint.io/rules/prefer-nullish-coalescing/.[...new Set(...)]instead ofArray.from(new Set(...))to dedupe, the former is considered more idiomatic.Putting all this together you can change the component to something like this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replying to point 3.
I am using PriceFeeds variable to filter here
and then to display the data. What should be the best way around?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, apologies, I was so focused on the asset types that I didn't think about the other usage of the feeds. In my suggested code, just replace
fetchAssetTypeswith:Then update variable names etc as appropriate to reflect that the state is storing the price feeds and not just asset types (e.g. rename
useAssetTypestousePriceFeeds). Then update the component to e.g.:Let me know if this makes sense.