index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import clsx from "clsx";
  2. import { useMemo, useCallback } from "react";
  3. import {
  4. type Context,
  5. delegateIntegrityStaking,
  6. cancelWarmupIntegrityStaking,
  7. unstakeIntegrityStaking,
  8. calculateApy,
  9. } from "../../api";
  10. import { ProgramSection } from "../ProgramSection";
  11. import { SparkChart } from "../SparkChart";
  12. import { StakingTimeline } from "../StakingTimeline";
  13. import { Styled } from "../Styled";
  14. import { Tokens } from "../Tokens";
  15. import { AmountType, TransferButton } from "../TransferButton";
  16. type Props = {
  17. availableToStake: bigint;
  18. locked: bigint;
  19. warmup: bigint;
  20. staked: bigint;
  21. cooldown: bigint;
  22. cooldown2: bigint;
  23. publishers: PublisherProps["publisher"][];
  24. };
  25. export const OracleIntegrityStaking = ({
  26. availableToStake,
  27. locked,
  28. warmup,
  29. staked,
  30. cooldown,
  31. cooldown2,
  32. publishers,
  33. }: Props) => {
  34. const self = useMemo(
  35. () => publishers.find((publisher) => publisher.isSelf),
  36. [publishers],
  37. );
  38. const otherPublishers = useMemo(
  39. () => publishers.filter((publisher) => !publisher.isSelf),
  40. [publishers],
  41. );
  42. return (
  43. <ProgramSection
  44. name="Oracle Integrity Staking (OIS)"
  45. description="Protect DeFi"
  46. className="pb-0 sm:pb-0"
  47. available={availableToStake}
  48. warmup={warmup}
  49. staked={staked}
  50. cooldown={cooldown}
  51. cooldown2={cooldown2}
  52. {...(locked > 0n && {
  53. availableToStakeDetails: (
  54. <div className="mt-2 text-xs text-red-600">
  55. <Tokens>{locked}</Tokens> are locked and cannot be staked in OIS
  56. </div>
  57. ),
  58. })}
  59. >
  60. {self && (
  61. <div className="relative -mx-4 mt-6 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-10 sm:mt-10">
  62. <div className="relative w-full overflow-x-auto">
  63. <h3 className="sticky left-0 mb-4 pl-4 text-2xl font-light sm:pb-4 sm:pl-10 sm:pt-6">
  64. You ({self.name})
  65. </h3>
  66. <table className="mx-auto border border-neutral-600/50 text-sm">
  67. <thead className="bg-pythpurple-400/30 font-light">
  68. <tr>
  69. <PublisherTableHeader>Pool</PublisherTableHeader>
  70. <PublisherTableHeader>Last epoch APY</PublisherTableHeader>
  71. <PublisherTableHeader>Historical APY</PublisherTableHeader>
  72. <PublisherTableHeader>Number of feeds</PublisherTableHeader>
  73. <PublisherTableHeader>Quality ranking</PublisherTableHeader>
  74. {availableToStake > 0n && <PublisherTableHeader />}
  75. </tr>
  76. </thead>
  77. <tbody className="bg-pythpurple-400/10">
  78. <Publisher
  79. isSelf
  80. availableToStake={availableToStake}
  81. publisher={self}
  82. totalStaked={staked}
  83. />
  84. </tbody>
  85. </table>
  86. </div>
  87. </div>
  88. )}
  89. <div
  90. className={clsx(
  91. "relative -mx-4 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-10 lg:mt-10",
  92. { "mt-6": self === undefined },
  93. )}
  94. >
  95. <div className="relative w-full overflow-x-auto">
  96. <h3 className="sticky left-0 mb-4 pl-4 text-2xl font-light sm:pb-4 sm:pl-10 sm:pt-6">
  97. {self ? "Other Publishers" : "Publishers"}
  98. </h3>
  99. <table className="min-w-full text-sm">
  100. <thead className="bg-pythpurple-100/30 font-light">
  101. <tr>
  102. <PublisherTableHeader className="pl-4 text-left sm:pl-10">
  103. Publisher
  104. </PublisherTableHeader>
  105. <PublisherTableHeader>Self stake</PublisherTableHeader>
  106. <PublisherTableHeader>Pool</PublisherTableHeader>
  107. <PublisherTableHeader>Last epoch APY</PublisherTableHeader>
  108. <PublisherTableHeader>Historical APY</PublisherTableHeader>
  109. <PublisherTableHeader>Number of feeds</PublisherTableHeader>
  110. <PublisherTableHeader
  111. className={clsx({ "pr-4 sm:pr-10": availableToStake <= 0n })}
  112. >
  113. Quality ranking
  114. </PublisherTableHeader>
  115. {availableToStake > 0n && (
  116. <PublisherTableHeader className="pr-4 sm:pr-10" />
  117. )}
  118. </tr>
  119. </thead>
  120. <tbody className="bg-white/5">
  121. {otherPublishers.map((publisher) => (
  122. <Publisher
  123. key={publisher.publicKey}
  124. availableToStake={availableToStake}
  125. publisher={publisher}
  126. totalStaked={staked}
  127. />
  128. ))}
  129. </tbody>
  130. </table>
  131. </div>
  132. </div>
  133. </ProgramSection>
  134. );
  135. };
  136. const PublisherTableHeader = Styled(
  137. "th",
  138. "py-2 font-normal px-5 whitespace-nowrap",
  139. );
  140. type PublisherProps = {
  141. availableToStake: bigint;
  142. totalStaked: bigint;
  143. isSelf?: boolean;
  144. publisher: {
  145. name: string;
  146. publicKey: string;
  147. isSelf: boolean;
  148. selfStake: bigint;
  149. poolCapacity: bigint;
  150. poolUtilization: bigint;
  151. numFeeds: number;
  152. qualityRanking: number;
  153. apyHistory: { date: Date; apy: number }[];
  154. positions?:
  155. | {
  156. warmup?: bigint | undefined;
  157. staked?: bigint | undefined;
  158. cooldown?: bigint | undefined;
  159. cooldown2?: bigint | undefined;
  160. }
  161. | undefined;
  162. };
  163. };
  164. const Publisher = ({
  165. publisher,
  166. availableToStake,
  167. totalStaked,
  168. isSelf,
  169. }: PublisherProps) => {
  170. const warmup = useMemo(
  171. () =>
  172. publisher.positions?.warmup !== undefined &&
  173. publisher.positions.warmup > 0n
  174. ? publisher.positions.warmup
  175. : undefined,
  176. [publisher.positions?.warmup],
  177. );
  178. const staked = useMemo(
  179. () =>
  180. publisher.positions?.staked !== undefined &&
  181. publisher.positions.staked > 0n
  182. ? publisher.positions.staked
  183. : undefined,
  184. [publisher.positions?.staked],
  185. );
  186. const cancelWarmup = useTransferActionForPublisher(
  187. cancelWarmupIntegrityStaking,
  188. publisher.publicKey,
  189. );
  190. const unstake = useTransferActionForPublisher(
  191. unstakeIntegrityStaking,
  192. publisher.publicKey,
  193. );
  194. const utilizationPercent = useMemo(
  195. () => Number((100n * publisher.poolUtilization) / publisher.poolCapacity),
  196. [publisher.poolUtilization, publisher.poolCapacity],
  197. );
  198. return (
  199. <>
  200. <tr className="border-t border-neutral-600/50 first:border-0">
  201. {!isSelf && (
  202. <>
  203. <PublisherTableCell className="py-4 pl-4 font-medium sm:pl-10">
  204. {publisher.name}
  205. </PublisherTableCell>
  206. <PublisherTableCell className="text-center">
  207. <Tokens>{publisher.selfStake}</Tokens>
  208. </PublisherTableCell>
  209. </>
  210. )}
  211. <PublisherTableCell className="text-center">
  212. <div className="relative mx-auto grid h-5 w-52 place-content-center border border-black bg-pythpurple-600/50">
  213. <div
  214. style={{
  215. width: `${utilizationPercent.toString()}%`,
  216. }}
  217. className={clsx(
  218. "absolute inset-0 max-w-full",
  219. publisher.poolUtilization > publisher.poolCapacity
  220. ? "bg-fuchsia-900"
  221. : "bg-pythpurple-400",
  222. )}
  223. />
  224. <div
  225. className={clsx("isolate text-sm font-medium", {
  226. "mix-blend-difference":
  227. publisher.poolUtilization <= publisher.poolCapacity,
  228. })}
  229. >
  230. {utilizationPercent.toString()}%
  231. </div>
  232. </div>
  233. <div className="mt-2 flex flex-row items-center justify-center gap-1 text-sm">
  234. <span>
  235. <Tokens>{publisher.poolUtilization}</Tokens>
  236. </span>
  237. <span>/</span>
  238. <span>
  239. <Tokens>{publisher.poolCapacity}</Tokens>
  240. </span>
  241. </div>
  242. </PublisherTableCell>
  243. <PublisherTableCell className="text-center">
  244. <div>
  245. {calculateApy(
  246. publisher.poolCapacity,
  247. publisher.poolUtilization,
  248. publisher.isSelf,
  249. )}
  250. %
  251. </div>
  252. </PublisherTableCell>
  253. <PublisherTableCell>
  254. <div className="mx-auto h-14 w-28">
  255. <SparkChart
  256. data={publisher.apyHistory.map(({ date, apy }) => ({
  257. date,
  258. value: apy,
  259. }))}
  260. />
  261. </div>
  262. </PublisherTableCell>
  263. <PublisherTableCell className="text-center">
  264. {publisher.numFeeds}
  265. </PublisherTableCell>
  266. <PublisherTableCell
  267. className={clsx("text-center", {
  268. "pr-4 sm:pr-10": availableToStake <= 0n && !isSelf,
  269. })}
  270. >
  271. {publisher.qualityRanking}
  272. </PublisherTableCell>
  273. {availableToStake > 0 && (
  274. <PublisherTableCell
  275. className={clsx("text-right", { "pr-4 sm:pr-10": !isSelf })}
  276. >
  277. <StakeToPublisherButton
  278. availableToStake={availableToStake}
  279. poolCapacity={publisher.poolCapacity}
  280. poolUtilization={publisher.poolUtilization}
  281. publisherKey={publisher.publicKey}
  282. publisherName={publisher.name}
  283. isSelf={publisher.isSelf}
  284. />
  285. </PublisherTableCell>
  286. )}
  287. </tr>
  288. {(warmup !== undefined || staked !== undefined) && (
  289. <tr>
  290. <td colSpan={8} className="border-separate border-spacing-8">
  291. <div className="mx-auto mb-8 mt-4 w-[30rem] border border-neutral-600/50 bg-pythpurple-800 px-8 py-6">
  292. <table className="w-full">
  293. <caption className="mb-2 text-left text-lg font-light">
  294. Your Positions
  295. </caption>
  296. <tbody>
  297. {warmup !== undefined && (
  298. <tr>
  299. <td className="opacity-80">Warmup</td>
  300. <td className="px-4">
  301. <Tokens>{warmup}</Tokens>
  302. </td>
  303. <td
  304. className={clsx("text-right", {
  305. "pb-2": staked !== undefined,
  306. })}
  307. >
  308. <TransferButton
  309. small
  310. secondary
  311. className="w-28"
  312. actionDescription={`Cancel tokens that are in warmup for staking to ${publisher.name}`}
  313. actionName="Cancel"
  314. submitButtonText="Cancel Warmup"
  315. title="Cancel Warmup"
  316. max={warmup}
  317. transfer={cancelWarmup}
  318. />
  319. </td>
  320. </tr>
  321. )}
  322. {staked !== undefined && (
  323. <tr>
  324. <td className="opacity-80">Staked</td>
  325. <td className="px-4">
  326. <div className="flex items-center gap-2">
  327. <Tokens>{staked}</Tokens>
  328. <div className="text-xs opacity-60">
  329. ({Number((100n * staked) / totalStaked)}% of your
  330. staked tokens)
  331. </div>
  332. </div>
  333. </td>
  334. <td className="py-0.5 text-right">
  335. <TransferButton
  336. small
  337. secondary
  338. className="w-28"
  339. actionDescription={`Unstake tokens from ${publisher.name}`}
  340. actionName="Unstake"
  341. max={staked}
  342. transfer={unstake}
  343. >
  344. <StakingTimeline cooldownOnly />
  345. </TransferButton>
  346. </td>
  347. </tr>
  348. )}
  349. </tbody>
  350. </table>
  351. </div>
  352. </td>
  353. </tr>
  354. )}
  355. </>
  356. );
  357. };
  358. const PublisherTableCell = Styled("td", "py-4 px-5 whitespace-nowrap");
  359. type StakeToPublisherButtonProps = {
  360. publisherName: string;
  361. publisherKey: string;
  362. availableToStake: bigint;
  363. poolCapacity: bigint;
  364. poolUtilization: bigint;
  365. isSelf: boolean;
  366. };
  367. const StakeToPublisherButton = ({
  368. publisherName,
  369. publisherKey,
  370. poolCapacity,
  371. poolUtilization,
  372. availableToStake,
  373. isSelf,
  374. }: StakeToPublisherButtonProps) => {
  375. const delegate = useTransferActionForPublisher(
  376. delegateIntegrityStaking,
  377. publisherKey,
  378. );
  379. return (
  380. <TransferButton
  381. small
  382. actionDescription={`Stake to ${publisherName}`}
  383. actionName="Stake"
  384. max={availableToStake}
  385. transfer={delegate}
  386. >
  387. {(amount) => (
  388. <>
  389. <div className="mb-8 flex flex-row items-center justify-between text-sm">
  390. <div>APY after staking</div>
  391. <div className="font-medium">
  392. {calculateApy(
  393. poolCapacity,
  394. poolUtilization +
  395. (amount.type === AmountType.Valid ? amount.amount : 0n),
  396. isSelf,
  397. )}
  398. %
  399. </div>
  400. </div>
  401. <StakingTimeline />
  402. </>
  403. )}
  404. </TransferButton>
  405. );
  406. };
  407. const useTransferActionForPublisher = (
  408. action: (
  409. context: Context,
  410. publicKey: string,
  411. amount: bigint,
  412. ) => Promise<void>,
  413. publicKey: string,
  414. ) =>
  415. useCallback(
  416. (context: Context, amount: bigint) => action(context, publicKey, amount),
  417. [action, publicKey],
  418. );