Skip to content

Feat(pricefeed/price feed id) add data #445

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

Merged
merged 10 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions components/PriceFeedTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useState, useCallback } from "react";
import { StyledTd } from "./Table";

interface PriceFeed {
ids: string;
assetType: string;
name: string;
}

interface TableColumnWidths {
assetType: string;
name: string;
ids: any;
}

const columnWidths: TableColumnWidths = {
assetType: "w-3/10",
name: "w-1/5",
ids: "w-1/2",
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's any reason to extract all this out, just put the tailwind classes in at the call sites where they're being consumed (i.e. lines 70 - 72)


const PriceFeedTable = ({ priceFeeds }: { priceFeeds: PriceFeed[] }) => {
const [selectedAssetType, setSelectedAssetType] = useState<string>("All");
const [copiedId, setCopiedId] = useState<string | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally recommend using undefined instead of null, see sindresorhus/meta#7 for a good discussion on why


const assetTypes = [
"All",
...Array.from(new Set(priceFeeds.map((feed) => feed.assetType))),
];

const filteredFeeds =
selectedAssetType === "All"
? priceFeeds
: priceFeeds.filter((feed) => feed.assetType === selectedAssetType);

const copyToClipboard = useCallback((text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
setCopiedId(text);
setTimeout(() => setCopiedId(null), 2000); // Hide the popup after 2 seconds
})
.catch((err) => {
console.error("Failed to copy: ", err);
});
}, []);

return (
<div>
<div className="mb-4">
<label htmlFor="assetTypeFilter" className="mr-2">
Filter by Asset Type:
</label>
<select
id="assetTypeFilter"
value={selectedAssetType}
onChange={(e) => setSelectedAssetType(e.target.value)}
className="p-2 border rounded"
>
{assetTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
<table>
<thead>
<tr>
<th className={columnWidths.assetType}>Asset Type</th>
<th className={columnWidths.name}>Name</th>
<th className={columnWidths.ids}>Feed ID</th>
</tr>
</thead>
<tbody>
{filteredFeeds.map((feed, index) => (
<tr key={index}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use something that's unique and not index based here, e.g. feed.name, as the key. Using the index will cause issues when you change the filter because React will try to recycle HTML elements that were used for other feeds when indices change and you'll get hard to debug issues where you'll see stale data in the UI.

<StyledTd>{feed.assetType}</StyledTd>
<StyledTd>{feed.name}</StyledTd>
<StyledTd>
<div className="relative">
<button
onClick={() => copyToClipboard(feed.ids)}
className="flex items-center space-x-2 px-2 py-1 bg-gray-100 hover:bg-gray-200 dark:bg-darkGray2 dark:hover:bg-darkGray3 rounded transition duration-200"
>
<code className="dark:text-darkLinks text-lightLinks dark:bg-darkGray2 bg-gray-100 px-2 py-1 rounded">
{feed.ids}
</code>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming that this is a copy icon right? Why not use the CopyIcon.tsx that's already in the codebase?

</button>
{copiedId === feed.ids && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-sm rounded shadow">
Copied to clipboard
</div>
)}
</div>
Copy link
Contributor

@cprussin cprussin Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this component is doing the same stuff that's already in Nextra in copy-to-clipboard.tsx, code.tsx, and pre.tsx. There's also built-in table components too. I'd advise trying to find a way to leverage the nextra built in components for this stuff -- e.g. I'm guessing you can replace this entire component with something very similar to this:

import { Code, Pre, Table, Td, Th, Tr } from 'nextra/components';

const PriceFeedTable = ({ priceFeeds }: { priceFeeds: PriceFeed[] }) => {
  // .... filtering stuff

  return (
    <Table>
      <thead>
        <Tr>
          <Th className="w-3/10">Asset Type</Th>
          <Th className="w-1/5">Name</Th>
          <Th className="w-1/2">Feed ID</Th>
        </Tr>
      </thead>
      <tbody>
        {filteredFeeds.map(({ assetType, name, ids }) => (
          <Tr key={name}>
            <Td>{assetType}</Td>
            <Td>{name}</Td>
            <Td>
              <Pre data-copy="">
                <Code>
                  {ids}
                </Code>
              </Pre>
            </Td>
          </Tr>
        ))}
      </thead>
    </Table>
  );
};

Note pre is short for Preformatted and is nearly always the right html tag to wrap a code block, see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre.

Not only will this drastically reduce the amount of custom code here, but you'll also make things much more consistent with the rest of the site.

</StyledTd>
</tr>
))}
</tbody>
</table>
</div>
);
};

export default PriceFeedTable;
3 changes: 2 additions & 1 deletion pages/price-feeds/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
},

"api-reference": "API Reference",
"price-feed-ids": "Price Feed IDs",
"price-feeds": "Price Feeds",
"sponsored-feeds": "Sponsored Feeds",
"asset-classes": "Asset Classes",
"market-hours": "Market Hours",
"best-practices": "Best Practices",
"error-codes": "Error Codes",
Expand Down
16 changes: 0 additions & 16 deletions pages/price-feeds/price-feed-ids.mdx

This file was deleted.

31 changes: 31 additions & 0 deletions pages/price-feeds/price-feeds.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Price Feeds

Pyth Price Feeds provide real-time, first-party, market data for a wide range of assets.

Every price feed has a **unique ID**, representing the specific pair of assets being priced.
These specific pairs are part of an asset class, which is a broader category of assets.

Anyone can fetch available price feeds and their IDs via [Hermes API](https://hermes.pyth.network/docs/#/rest/price_feeds_metadata).

## Asset Classes

Every price feed belongs to an asset class. These asset classes distinguish between different types of assets, such as crypto, US equities, and metals.

Refer to the [Asset Classes](./price-feeds/asset-classes.md) page to learn more about the existing asset classes.

## Price Feed IDs

Price Feed IDs are unique identifiers for each specific pair of assets being priced (e.g. BTC/USD).
Every price update is tagged with the corresponding price feed ID.

Applications need to store the IDs of the feeds they wish to read.
However, the IDs may be represented in different formats (e.g. hex or base58) depending on the blockchain.
Price feeds also have different IDs in the Stable and Beta channels.

Refer to the [Price Feed ID reference catalog](./price-feeds/price-feed-ids.md) to identify a feed's ID in your chosen ecosystem.

### Solana Price Feed Accounts

On Solana, each feed additionally has a collection of **price feed accounts** containing the feed's data.
The addresses of these accounts are programmatically derived from the feed id and a shard id, which is simply a 16-bit number.
See [How to Use Real-Time Data on Solana](./use-real-time-data/solana#price-feed-accounts) for more information on price feed accounts.
4 changes: 4 additions & 0 deletions pages/price-feeds/price-feeds/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"asset-classes": "Asset Classes",
"price-feed-ids": "Price Feed IDs"
}
14 changes: 14 additions & 0 deletions pages/price-feeds/price-feeds/asset-classes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Asset Classes

[Pyth price feeds](https://www.pyth.network/price-feeds) provide market data for the following asset classes:

| Asset Class | Subclass | Definition |
| ----------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Crypto | Spot Prices | Real-time prices for cryptocurrencies and digital assets |
| | Redemption Rates | Real-time swap rates derived from smart contracts for the redemption of liquid staking and liquid restaking tokens (LSTs and LRTs), liquidity provider tokens (LP Tokens) and interest-bearing assets, including tokenised notes |
| | Indices | Real-time prices that measure the performance of baskets of cryptocurrencies and digital assets |
| US Equities | Spot Prices | Real-time prices for US equities |
| FX | Spot Prices | Real-time prices for fiat currency pairs |
| Metals | Spot Prices | Real-time prices for precious metals |
| Rates | Future Prices | Real-time prices for fixed income products, including bond futures |
| Commodities | Futures Prices | Real-time prices for commodity futures |
37 changes: 37 additions & 0 deletions pages/price-feeds/price-feeds/price-feed-ids.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import PriceFeedTable from "../../../components/PriceFeedTable";
import { useState, useEffect } from 'react';

export const PriceFeedData = () => {
const [priceFeeds, setPriceFeeds] = useState([]);

useEffect(() => {
const fetchPriceFeeds = async () => {
try {
const response = await fetch('https://hermes.pyth.network/v2/price_feeds');
const data = await response.json();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, you should validate the response type here. This is where zod can be helpful.

Alternatively if you use the Hermes js client, it already has validation and everything built in


// Transform the data to match our PriceFeed interface
const transformedData = data.map(feed => ({
assetType: feed.attributes.asset_type,
name: feed.attributes.display_symbol,
ids: feed.id
}));

setPriceFeeds(transformedData);
} catch (error) {
console.error('Error fetching price feeds:', error);
}
};

fetchPriceFeeds();
Copy link
Contributor

@cprussin cprussin Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should ideally pass a canceler here using AbortController. You should also attach a catch handler to the promise, all promises should be caught explicitly to avoid uncaught exceptions in the boundary between promise and async code (even though you have a catch in the async code there, the code could easily be refactored such that uncaught exceptions make their way through, and without having a catch handler on the promise the code is unsafe).

You can also extract out the fetchPriceFeeds function to clean things up a bit.

You should also add loading and error states.

Finally I'd suggest putting all the component code in an actual .tsx file and just importing it here, rather than keeping it inline. Your formatting isn't handled by prettier, you don't get linting, etc, when you have the code inline.

Putting this all together would look something like this:

import { useState, useEffect } from 'react';
import { z } from "zod";

import PriceFeedTable from "../../../components/PriceFeedTable";

export const PriceFeedData = () => {
  const priceFeedData = usePriceFeedData();

  switch (state.type) {
    case StateType.NotLoaded:
    case StateType.Loading: {
      return <Spinner /> // TODO implement a spinner / loader...
    }

    case StateType.Error: {
      return <p>Oh no!  An error occurred: {state.error}</p>
    }

    case StateType.Loaded: {
      return <PriceFeedTable priceFeeds={state.data} />;
    }
  }
};

const usePriceFeedData = (): State => {
  const [state, setState] = useState<State>(State.NotLoaded());

  useEffect(() => {
    setState(State.Loading());
    const controller = new AbortController();
    fetchPriceFeeds(controller)
      .then(data => setState(State.Loaded(data)))
      .catch((error: unknown) => {
        console.error('Error fetching price feeds:', error);
        setState(State.Error(error));
      });
    return () => controller.abort();
  }, []);

  return state;
}

const dataSchema = z.array(z.object({
  asset_type: z.string(),
  display_symbol: z.string(),
  ids: z.array() // TODO: I don't know what this shape should be but you should type this correctly!
}).transform(({ asset_type, display_symbol, id }) => ({
  assetType: asset_type,
  name: display_symbol,
  ids: id
})));

type Data = z.infer<typeof dataSchema>;

enum StateType = {
  NotLoaded,
  Loading,
  Error,
  Loaded
}

const State = {
  NotLoaded: () => ({ type: StateType.NotLoaded as const }),
  Loading: () => ({ type: StateType.Loading as const }),
  Error: (error: unknown) => ({ type: StateType.Error as const, error }),
  Loaded: (data: Data) => ({ type: StateType.Loaded as const, data }),
}
type State = ReturnType<typeof State[keyof typeof State]>;

const fetchPriceFeeds = async (signal: AbortSignal): Promise<Data> => {
  const response = await fetch('https://hermes.pyth.network/v2/price_feeds', { signal });
  return dataSchema.parse(await response.json());
};

Note this is untested and still needs a few TODO items filled in so don't copy-paste blindly!


}, []);

return <PriceFeedTable priceFeeds={priceFeeds} />;
};

# Price Feed IDs

Below is a table of all available price feed IDs:

<PriceFeedData />
Loading