-
Notifications
You must be signed in to change notification settings - Fork 35
feat(pricefeed) Migrate Price Feed IDs to documentation #493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
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> | ||
</> | ||
); | ||
} |
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:
priceFeeds
to an empty array on line 24, you should properly handle loading states and represent that state differently from a loaded array.priceFeeds
anywhere 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.catch
any time you call a promise without usingawait
, e.g. on line 41. Omitting thecatch
means uncaught exceptions could potentially put the app into an unstable state; additionally here you should make the UI indicate errors properly.fetchPriceFeeds
outside 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
fetchAssetTypes
with: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
useAssetTypes
tousePriceFeeds
). Then update the component to e.g.:Let me know if this makes sense.