index.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { ArrowLineDown } from "@phosphor-icons/react/dist/ssr/ArrowLineDown";
  2. import { ArrowSquareOut } from "@phosphor-icons/react/dist/ssr/ArrowSquareOut";
  3. import { ArrowsOutSimple } from "@phosphor-icons/react/dist/ssr/ArrowsOutSimple";
  4. import { ClockCountdown } from "@phosphor-icons/react/dist/ssr/ClockCountdown";
  5. import { StackPlus } from "@phosphor-icons/react/dist/ssr/StackPlus";
  6. import { Badge } from "@pythnetwork/component-library/Badge";
  7. import { Button } from "@pythnetwork/component-library/Button";
  8. import {
  9. type Props as CardProps,
  10. Card,
  11. } from "@pythnetwork/component-library/Card";
  12. import { Drawer, DrawerTrigger } from "@pythnetwork/component-library/Drawer";
  13. import { StatCard } from "@pythnetwork/component-library/StatCard";
  14. import { TabList } from "@pythnetwork/component-library/TabList";
  15. import {
  16. TabPanel as UnstyledTabPanel,
  17. Tabs as UnstyledTabs,
  18. } from "@pythnetwork/component-library/unstyled/Tabs";
  19. import type { ElementType } from "react";
  20. import { AssetClassesDrawer } from "./asset-classes-drawer";
  21. import { ComingSoonList } from "./coming-soon-list";
  22. import styles from "./index.module.scss";
  23. import { PriceFeedsCard } from "./price-feeds-card";
  24. import { Cluster, getFeeds } from "../../services/pyth";
  25. import { priceFeeds as priceFeedsStaticConfig } from "../../static-data/price-feeds";
  26. import { activeChains } from "../../static-data/stats";
  27. import { Cards } from "../Cards";
  28. import { LivePrice } from "../LivePrices";
  29. import {
  30. YesterdaysPricesProvider,
  31. PriceFeedChangePercent,
  32. } from "../PriceFeedChangePercent";
  33. import { PriceFeedTag } from "../PriceFeedTag";
  34. const PRICE_FEEDS_ANCHOR = "priceFeeds";
  35. export const PriceFeeds = async () => {
  36. const priceFeeds = await getPriceFeeds();
  37. const numFeedsByAssetClass = getNumFeedsByAssetClass(priceFeeds.activeFeeds);
  38. const featuredComingSoon = [
  39. ...filterFeeds(
  40. priceFeeds.comingSoon,
  41. priceFeedsStaticConfig.featuredComingSoon,
  42. ),
  43. ...priceFeeds.comingSoon.filter(
  44. ({ symbol }) =>
  45. !priceFeedsStaticConfig.featuredComingSoon.includes(symbol),
  46. ),
  47. ].slice(0, 6);
  48. const featuredRecentlyAdded = filterFeeds(
  49. priceFeeds.activeFeeds,
  50. priceFeedsStaticConfig.featuredRecentlyAdded,
  51. );
  52. return (
  53. <div className={styles.priceFeeds}>
  54. <h1 className={styles.header}>Price Feeds</h1>
  55. <Cards className={styles.cards}>
  56. <StatCard
  57. variant="primary"
  58. header="Active Feeds"
  59. stat={priceFeeds.activeFeeds.length}
  60. href={`#${PRICE_FEEDS_ANCHOR}`}
  61. corner={<ArrowLineDown />}
  62. />
  63. <StatCard
  64. header="Frequency"
  65. stat={priceFeedsStaticConfig.updateFrequency}
  66. />
  67. <StatCard
  68. header="Active Chains"
  69. stat={activeChains.at(-1)?.chains}
  70. href="https://docs.pyth.network/price-feeds/contract-addresses"
  71. target="_blank"
  72. corner={<ArrowSquareOut weight="fill" />}
  73. />
  74. <AssetClassesDrawer numFeedsByAssetClass={numFeedsByAssetClass}>
  75. <StatCard
  76. header="Asset Classes"
  77. stat={Object.keys(numFeedsByAssetClass).length}
  78. corner={<ArrowsOutSimple />}
  79. />
  80. </AssetClassesDrawer>
  81. </Cards>
  82. <section className={styles.bigScreenBody}>
  83. <FeaturedFeeds
  84. allComingSoon={priceFeeds.comingSoon}
  85. featuredComingSoon={featuredComingSoon.slice(0, 5)}
  86. featuredRecentlyAdded={featuredRecentlyAdded.slice(0, 5)}
  87. />
  88. <PriceFeedsCard
  89. id={PRICE_FEEDS_ANCHOR}
  90. priceFeeds={priceFeeds.activeFeeds.map((feed) => ({
  91. symbol: feed.symbol,
  92. exponent: feed.price.exponent,
  93. numQuoters: feed.price.numQuoters,
  94. }))}
  95. />
  96. </section>
  97. <UnstyledTabs className={styles.smallScreenBody ?? ""}>
  98. <TabList
  99. label="Price Feeds Navigation"
  100. items={[
  101. { children: "Price Feeds", id: "feeds" },
  102. { children: "Highlights", id: "highlights" },
  103. ]}
  104. />
  105. <UnstyledTabPanel id="feeds" className={styles.tabPanel ?? ""}>
  106. <PriceFeedsCard
  107. id={PRICE_FEEDS_ANCHOR}
  108. priceFeeds={priceFeeds.activeFeeds.map((feed) => ({
  109. symbol: feed.symbol,
  110. exponent: feed.price.exponent,
  111. numQuoters: feed.price.numQuoters,
  112. }))}
  113. />
  114. </UnstyledTabPanel>
  115. <UnstyledTabPanel id="highlights" className={styles.tabPanel ?? ""}>
  116. <FeaturedFeeds
  117. allComingSoon={priceFeeds.comingSoon}
  118. featuredComingSoon={featuredComingSoon}
  119. featuredRecentlyAdded={featuredRecentlyAdded}
  120. />
  121. </UnstyledTabPanel>
  122. </UnstyledTabs>
  123. </div>
  124. );
  125. };
  126. type FeaturedFeedsProps = {
  127. featuredRecentlyAdded: FeaturedFeed[];
  128. featuredComingSoon: FeaturedFeed[];
  129. allComingSoon: { symbol: string }[];
  130. };
  131. const FeaturedFeeds = ({
  132. featuredRecentlyAdded,
  133. featuredComingSoon,
  134. allComingSoon,
  135. }: FeaturedFeedsProps) => (
  136. <>
  137. <YesterdaysPricesProvider
  138. feeds={Object.fromEntries(
  139. featuredRecentlyAdded.map(({ symbol, product }) => [
  140. symbol,
  141. product.price_account,
  142. ]),
  143. )}
  144. >
  145. <FeaturedFeedsCard
  146. title="Recently Added"
  147. icon={<StackPlus />}
  148. feeds={featuredRecentlyAdded}
  149. showPrices
  150. linkFeeds
  151. />
  152. </YesterdaysPricesProvider>
  153. <FeaturedFeedsCard
  154. title="Coming Soon"
  155. icon={<ClockCountdown />}
  156. feeds={featuredComingSoon}
  157. toolbarAlwaysOnTop
  158. toolbar={
  159. <DrawerTrigger>
  160. <Button size="xs" variant="outline">
  161. Show all
  162. </Button>
  163. <Drawer
  164. fill
  165. className={styles.comingSoonCard ?? ""}
  166. title={
  167. <>
  168. <span>Coming Soon</span>
  169. <Badge>{allComingSoon.length}</Badge>
  170. </>
  171. }
  172. >
  173. <ComingSoonList
  174. comingSoonSymbols={allComingSoon.map(({ symbol }) => symbol)}
  175. />
  176. </Drawer>
  177. </DrawerTrigger>
  178. }
  179. />
  180. </>
  181. );
  182. type FeaturedFeedsCardProps<T extends ElementType> = Omit<
  183. CardProps<T>,
  184. "children"
  185. > & {
  186. showPrices?: boolean | undefined;
  187. linkFeeds?: boolean | undefined;
  188. feeds: FeaturedFeed[];
  189. };
  190. type FeaturedFeed = {
  191. symbol: string;
  192. product: {
  193. display_symbol: string;
  194. price_account: string;
  195. description: string;
  196. };
  197. };
  198. const FeaturedFeedsCard = <T extends ElementType>({
  199. showPrices,
  200. linkFeeds,
  201. feeds,
  202. ...props
  203. }: FeaturedFeedsCardProps<T>) => (
  204. <Card {...props}>
  205. <div className={styles.featuredFeedsCard}>
  206. {feeds.map((feed) => (
  207. <Card
  208. key={feed.product.price_account}
  209. variant="tertiary"
  210. {...(linkFeeds && {
  211. href: `/price-feeds/${encodeURIComponent(feed.symbol)}`,
  212. })}
  213. >
  214. <div className={styles.feedCardContents}>
  215. <PriceFeedTag symbol={feed.symbol} />
  216. {showPrices && (
  217. <div className={styles.prices}>
  218. <LivePrice
  219. feedKey={feed.product.price_account}
  220. cluster={Cluster.Pythnet}
  221. />
  222. <PriceFeedChangePercent
  223. className={styles.changePercent}
  224. feedKey={feed.product.price_account}
  225. />
  226. </div>
  227. )}
  228. </div>
  229. </Card>
  230. ))}
  231. </div>
  232. </Card>
  233. );
  234. const getPriceFeeds = async () => {
  235. const priceFeeds = await getFeeds(Cluster.Pythnet);
  236. const activeFeeds = priceFeeds.filter((feed) => isActive(feed));
  237. const comingSoon = priceFeeds.filter((feed) => !isActive(feed));
  238. return { activeFeeds, comingSoon };
  239. };
  240. const getNumFeedsByAssetClass = (
  241. feeds: { product: { asset_type: string } }[],
  242. ): Record<string, number> => {
  243. const classes: Record<string, number> = {};
  244. for (const feed of feeds) {
  245. const assetType = feed.product.asset_type;
  246. classes[assetType] = (classes[assetType] ?? 0) + 1;
  247. }
  248. return classes;
  249. };
  250. const filterFeeds = <T extends { symbol: string }>(
  251. feeds: T[],
  252. symbols: string[],
  253. ): T[] =>
  254. symbols
  255. .map((symbol) => feeds.find((feed) => feed.symbol === symbol))
  256. .filter((feed) => feed !== undefined);
  257. const isActive = (feed: { price: { minPublishers: number } }) =>
  258. feed.price.minPublishers <= 50;