index.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { ArrowSquareOut } from "@phosphor-icons/react/dist/ssr/ArrowSquareOut";
  2. import { Info } from "@phosphor-icons/react/dist/ssr/Info";
  3. import { Lightbulb } from "@phosphor-icons/react/dist/ssr/Lightbulb";
  4. import { Alert, AlertTrigger } from "@pythnetwork/component-library/Alert";
  5. import { Button } from "@pythnetwork/component-library/Button";
  6. import { Card } from "@pythnetwork/component-library/Card";
  7. import { Skeleton } from "@pythnetwork/component-library/Skeleton";
  8. import { StatCard } from "@pythnetwork/component-library/StatCard";
  9. import { lookup as lookupPublisher } from "@pythnetwork/known-publishers";
  10. import clsx from "clsx";
  11. import type { ComponentProps } from "react";
  12. import { z } from "zod";
  13. import styles from "./index.module.scss";
  14. import { PublishersCard } from "./publishers-card";
  15. import { SemicircleMeter, Label } from "./semicircle-meter";
  16. import { client as clickhouseClient } from "../../services/clickhouse";
  17. import { client as hermesClient } from "../../services/hermes";
  18. import { CLUSTER, getData } from "../../services/pyth";
  19. import { client as stakingClient } from "../../services/staking";
  20. import { FormattedTokens } from "../FormattedTokens";
  21. import { PublisherTag } from "../PublisherTag";
  22. import { Score } from "../Score";
  23. import { TokenIcon } from "../TokenIcon";
  24. const INITIAL_REWARD_POOL_SIZE = 60_000_000_000_000n;
  25. const PUBLISHER_SCORE_WIDTH = 24;
  26. export const Publishers = async () => {
  27. const [publishers, totalFeeds, oisStats] = await Promise.all([
  28. getPublishers(),
  29. getTotalFeedCount(),
  30. getOisStats(),
  31. ]);
  32. return (
  33. <div className={styles.publishers}>
  34. <h1 className={styles.header}>Publishers</h1>
  35. <div className={styles.body}>
  36. <section className={styles.stats}>
  37. <StatCard
  38. variant="primary"
  39. header="Active Publishers"
  40. stat={publishers.length}
  41. />
  42. <StatCard
  43. header="Avg. Median Score"
  44. corner={
  45. <AlertTrigger>
  46. <Button
  47. variant="ghost"
  48. size="xs"
  49. beforeIcon={(props) => <Info weight="fill" {...props} />}
  50. rounded
  51. hideText
  52. className={styles.averageMedianScoreExplainButton ?? ""}
  53. >
  54. Explain Average Median Score
  55. </Button>
  56. <Alert title="Average Median Score" icon={<Lightbulb />}>
  57. <p className={styles.averageMedianScoreDescription}>
  58. Each <b>Price Feed Component</b> that a <b>Publisher</b>{" "}
  59. provides has an associated <b>Score</b>, which is determined
  60. by that component{"'"}s <b>Uptime</b>,{" "}
  61. <b>Price Deviation</b>, and <b>Staleness</b>. The publisher
  62. {"'"}s <b>Median Score</b> measures the 50th percentile of
  63. the <b>Score</b> across all of that publisher{"'"}s{" "}
  64. <b>Price Feed Components</b>. The{" "}
  65. <b>Average Median Score</b> is the average of the{" "}
  66. <b>Median Scores</b> of all publishers who contribute to the
  67. Pyth Network.
  68. </p>
  69. <Button
  70. size="xs"
  71. variant="solid"
  72. href="https://docs.pyth.network/home/oracle-integrity-staking/publisher-quality-ranking"
  73. target="_blank"
  74. >
  75. Learn more
  76. </Button>
  77. </Alert>
  78. </AlertTrigger>
  79. }
  80. stat={(
  81. publishers.reduce(
  82. (sum, publisher) => sum + publisher.medianScore,
  83. 0,
  84. ) / publishers.length
  85. ).toFixed(2)}
  86. />
  87. <Card
  88. title="Oracle Integrity Staking (OIS)"
  89. className={styles.oisCard}
  90. toolbar={
  91. <Button
  92. href="https://staking.pyth.network"
  93. target="_blank"
  94. size="sm"
  95. variant="outline"
  96. afterIcon={ArrowSquareOut}
  97. >
  98. Staking App
  99. </Button>
  100. }
  101. >
  102. <SemicircleMeter
  103. width={340}
  104. height={340}
  105. value={Number(oisStats.totalStaked)}
  106. maxValue={oisStats.maxPoolSize ?? 0}
  107. className={styles.oisPool ?? ""}
  108. chartClassName={styles.oisPoolChart}
  109. barClassName={styles.bar}
  110. backgroundClassName={styles.background}
  111. >
  112. <div className={styles.legend}>
  113. <Label className={styles.title}>PYTH Staking Pool</Label>
  114. <p className={styles.poolUsed}>
  115. <FormattedTokens
  116. mode="wholePart"
  117. tokens={oisStats.totalStaked}
  118. />
  119. </p>
  120. <p className={styles.poolTotal}>
  121. /{" "}
  122. <FormattedTokens
  123. mode="wholePart"
  124. tokens={BigInt(oisStats.maxPoolSize ?? 0)}
  125. />
  126. </p>
  127. </div>
  128. </SemicircleMeter>
  129. <div className={styles.oisStats}>
  130. <StatCard
  131. header="Total Staked"
  132. variant="tertiary"
  133. stat={
  134. <>
  135. <TokenIcon />
  136. <FormattedTokens tokens={oisStats.totalStaked} />
  137. </>
  138. }
  139. />
  140. <StatCard
  141. header="Total Rewards Distributed"
  142. variant="tertiary"
  143. stat={
  144. <>
  145. <TokenIcon />
  146. <FormattedTokens tokens={oisStats.rewardsDistributed} />
  147. </>
  148. }
  149. />
  150. </div>
  151. </Card>
  152. </section>
  153. <PublishersCard
  154. className={styles.publishersCard}
  155. rankingLoadingSkeleton={
  156. <Skeleton className={styles.rankingLoader} fill />
  157. }
  158. nameLoadingSkeleton={<PublisherTag isLoading />}
  159. scoreLoadingSkeleton={
  160. <Score isLoading width={PUBLISHER_SCORE_WIDTH} />
  161. }
  162. scoreWidth={PUBLISHER_SCORE_WIDTH}
  163. publishers={publishers.map(
  164. ({ key, rank, numSymbols, medianScore }) => ({
  165. id: key,
  166. nameAsString: lookupPublisher(key)?.name,
  167. name: <PublisherTag publisherKey={key} />,
  168. ranking: <Ranking>{rank}</Ranking>,
  169. activeFeeds: numSymbols,
  170. inactiveFeeds: totalFeeds - numSymbols,
  171. medianScore: (
  172. <Score score={medianScore} width={PUBLISHER_SCORE_WIDTH} />
  173. ),
  174. }),
  175. )}
  176. />
  177. </div>
  178. </div>
  179. );
  180. };
  181. const Ranking = ({ className, ...props }: ComponentProps<"span">) => (
  182. <span className={clsx(styles.ranking, className)} {...props} />
  183. );
  184. const getPublishers = async () => {
  185. const rows = await clickhouseClient.query({
  186. query:
  187. "SELECT key, rank, numSymbols, medianScore FROM insights_publishers(cluster={cluster: String})",
  188. query_params: { cluster: CLUSTER },
  189. });
  190. const result = await rows.json();
  191. return publishersSchema.parse(result.data);
  192. };
  193. const publishersSchema = z.array(
  194. z.strictObject({
  195. key: z.string(),
  196. rank: z.number(),
  197. numSymbols: z.number(),
  198. medianScore: z.number(),
  199. }),
  200. );
  201. const getTotalFeedCount = async () => {
  202. const pythData = await getData();
  203. return pythData.filter(({ price }) => price.numComponentPrices > 0).length;
  204. };
  205. const getOisStats = async () => {
  206. const [poolData, rewardCustodyAccount, publisherCaps] = await Promise.all([
  207. stakingClient.getPoolDataAccount(),
  208. stakingClient.getRewardCustodyAccount(),
  209. hermesClient.getLatestPublisherCaps({ parsed: true }),
  210. ]);
  211. return {
  212. totalStaked:
  213. sumDelegations(poolData.delState) + sumDelegations(poolData.selfDelState),
  214. rewardsDistributed:
  215. poolData.claimableRewards +
  216. INITIAL_REWARD_POOL_SIZE -
  217. rewardCustodyAccount.amount,
  218. maxPoolSize: publisherCaps.parsed?.[0]?.publisher_stake_caps
  219. .map(({ cap }) => cap)
  220. .reduce((acc, value) => acc + value),
  221. };
  222. };
  223. const sumDelegations = (
  224. values: { totalDelegation: bigint; deltaDelegation: bigint }[],
  225. ) =>
  226. values.reduce(
  227. (acc, value) => acc + value.totalDelegation + value.deltaDelegation,
  228. 0n,
  229. );