Skip to content

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

Closed
wants to merge 6 commits into from

Conversation

aditya520
Copy link
Member

@aditya520 aditya520 commented Nov 6, 2024

TODOs

  • Stying for Dark mode.
  • Styling for Price Feed IDs.
  • Add instructions and some callouts for the reader on utilizing these IDs.
  • Add Beta Price Feed IDs.
  • Add Solana Price Feed Accounts.

Copy link

vercel bot commented Nov 6, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
docs ❌ Failed (Inspect) Nov 14, 2024 3:08am
documentation ❌ Failed (Inspect) Nov 14, 2024 3:08am

Copy link
Contributor

@cprussin cprussin left a comment

Choose a reason for hiding this comment

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

I think you can get rid of mui entirely here; everything I see you doing with it is trivial to do with tailwind, or there are already built-in components in nextra or in the app that you can use instead.

Additionally, please clean up the state management for the asset types list as per my comment. Make sure to properly handle loading and error statesat least, and ideally also follow some of the best practices I shared in that comment.

MenuItem,
Box,
Snackbar,
} from "@mui/material";
Copy link
Contributor

Choose a reason for hiding this comment

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

Mentioned this in slack but we should try to use built-in nextra components where possible. I don't really have an issue with using mui for stuff that isn't in nextra, but I also don't think you get much out of the stuff you're using here and can probably do away with mui entirely.

package.json Outdated
@@ -16,8 +16,12 @@
"@codesandbox/sandpack-react": "^2.6.1",
"@cookbookdev/docsbot": "^4.21.5",
"@cosmjs/cosmwasm-stargate": "^0.31.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
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 see anywhere that you're actually using emotion, is this a peer dependency of mui or something?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@cprussin cprussin Nov 7, 2024

Choose a reason for hiding this comment

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

That stackoverflow just tells me that they're using emotion, not that it's a peer dependency. There shouldn't be any reason to add emotion to our dependencies unless if either:

  1. you need to use it directly (in general you shouldn't, we have tailwind for this, the only possible exception would be if you need to pass overrides to a library that internally uses it)
  2. it's a peer dependency of a package you depend on (what this means is that the package has it listed in it's peerDependencies list in the package.json, indicating that the dependency must be installed by the consumer; typically this only happens for things like plugins defining what they are plugging into or things like React where there can only be one copy of the dependency in the build graph)

};

fetchPriceFeeds();
}, []);
Copy link
Contributor

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:

  1. It's not accurate to initialize priceFeeds to an empty array on line 24, you should properly handle loading states and represent that state differently from a loaded array.
  2. You aren't actually using 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.
  3. The transformation on lines 33 - 37 is IMO not useful; you're basically just changing snake case to camel case. IMO there isn't any serious benefit to doing that, and there are costs (added cognitive overhead, makes the code harder to search, etc). I know that camel case is the convention, but sticking to convention is not worth added complexity and making the code harder to search.
  4. You should always use a catch any time you call a promise without using await, e.g. on line 41. Omitting the catch means uncaught exceptions could potentially put the app into an unstable state; additionally here you should make the UI indicate errors properly.
  5. I always strongly recommend extracting functions like 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.
  6. You should always prefer ?? over || (on lines 35 and 36), see https://typescript-eslint.io/rules/prefer-nullish-coalescing/.
  7. Minor nit but in es6 you can do [...new Set(...)] instead of Array.from(new Set(...)) to dedupe, the former is considered more idiomatic.
  8. You can use the "fake sum types" pattern I shared to encapsulate the loading/error state management here if you like.

Putting all this together you can change the component to something like this:

export const PriceFeedTable = () => {
  const [selectedAssetType, setSelectedAssetType] = useState("All");

  // ...
  <AssetTypesSelect
    selectedAssetType={selectedAssetType}
    setSelectedAssetType={setSelectedAssetType}
  />
}

type AssetTypesSelectProps = {
  selectedAssetType: string;
  setSelectedAssetType: (value: string) => void;
}

const AssetTypesSelect = ({ selectedAssetType, setSelectedAssetType }: AssetTypesSelectProps) => {
  const assetTypes = useAssetTypes();

  switch (state.type) {
    case AssetTypesStateType.NotLoaded:
    case AssetTypesStateType.Loading: {
      // use any spinner we're already using in the app anywhere else...
      return <Spinner /> 
    }
    case AssetTypesStateType.Error: {
      return <p>Error loading asset types!</p>;
    }
    case AssetTypesStateType.Loaded: {
      return (
        <Select
          value={selectedAssetType}
          onChange={(e) => setSelectedAssetType(e.target.value)}
          size="small"
          sx={{ width: 200 }}
        >
          <MenuItem value="All">All Asset Types</MenuItem>
          {state.assetTypes.map(assetType => (
            <MenuItem key={assetType} value={assetType}>
              {assetType ?? "Uncategorized"}
            </MenuItem>
          ))}
        </Select>
      );
    }
  }
};

const useAssetTypes = (): AssetTypesState => {
  const [assetTypes, setAssetTypes] = useState<AssetTypesState>(AssetTypesState.NotLoaded());

  useEffect(() => {
    setAssetTypes(AssetTypesState.Loading());
    fetchAssetTypes()
      .then(assetTypes => setAssetTypes(AssetTypesState.Loaded(assetTypes)))
      .catch(error => setAssetTypes(AssetTypesState.Error(error)));
  }, []);

  return assetTypes;
}

const fetchAssetTypes = async () => {
  const hermesClient = new HermesClient("https://hermes.pyth.network");
  const feeds = await hermesClient.getPriceFeeds();
  return [...new Set(feeds.map(feed => feed.attributes.asset_type ?? ""))];
};

enum AssetTypesStateType {
  NotLoaded,
  Loading,
  Loaded,
  Error
}

const AssetTypesState = {
  NotLoaded: () => ({ type: AssetTypesStateType.NotLoaded as const }),
  Loading: () => ({ type: AssetTypesStateType.Loading as const }),
  Loaded: (assetTypes: string[]) => ({ type: AssetTypesStateType.Loaded as const, assetTypes }),
  Error: (error: unknown) => ({ type: AssetTypesStateType.Error as const, error }),
}

type AssetTypesState = typeof AssetTypesState[keyof typeof AssetTypesState];

Copy link
Member Author

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

    const matchesSearch =
      feed.id.toLowerCase().includes(searchTerm.toLowerCase())

and then to display the data. What should be the best way around?

Copy link
Contributor

@cprussin cprussin Nov 11, 2024

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:

const fetchPriceFeeds = async () => {
  const priceFeeds = await new HermesClient("https://hermes.pyth.network").getPriceFeeds();
  const assetTypes = [...new Set(priceFeeds.map(feed => feed.attributes.asset_type ?? ""))];
  return { priceFeeds, assetTypes };
};

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 to usePriceFeeds). Then update the component to e.g.:

export const PriceFeedTable = () => {
  const [selectedAssetType, setSelectedAssetType] = useState("All");
  const priceFeedsState = usePriceFeeds();

  // ...
  <AssetTypesSelect
    priceFeedsState={priceFeedsState}
    selectedAssetType={selectedAssetType}
    setSelectedAssetType={setSelectedAssetType}
  />
}

type AssetTypesSelectProps = {
  priceFeedsState: PriceFeedsState;
  selectedAssetType: string;
  setSelectedAssetType: (value: string) => void;
}

const AssetTypesSelect = ({ priceFeedsState, selectedAssetType, setSelectedAssetType }: AssetTypesSelectProps) => {
  switch (priceFeedsState.type) {
    // ...
  }
}

Let me know if this makes sense.

</MenuItem>
)
)}
</Select>
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 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 (Selector.tsx for the select and Input.tsx for the text input)


return (
<Box>
<Box sx={{ mb: 3, display: "flex", gap: 2 }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

The Box component isn't useful with tailwind; these styles can be directly added to a div by using the correct utility classes, e.g. something like <div className="mb-1 flex gap-2">

autoHideDuration={2000}
onClose={() => setOpenSnackbar(false)}
message="Feed ID copied to clipboard"
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

You can avoid having to use this component by using the CopyToClipboard nextra component which has feedback built in and is the same component that's used for code block copying

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants