api.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. // TODO remove these disables when moving off the mock APIs
  2. /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-non-null-assertion */
  3. import type { AnchorWallet } from "@solana/wallet-adapter-react";
  4. import type { Connection } from "@solana/web3.js";
  5. export type StakeAccount = {
  6. publicKey: `0x${string}`;
  7. };
  8. export type Context = {
  9. connection: Connection;
  10. wallet: AnchorWallet;
  11. stakeAccount: StakeAccount;
  12. };
  13. type Data = {
  14. total: bigint;
  15. availableRewards: bigint;
  16. lastSlash:
  17. | {
  18. amount: bigint;
  19. date: Date;
  20. }
  21. | undefined;
  22. expiringRewards: {
  23. amount: bigint;
  24. expiry: Date;
  25. };
  26. locked: bigint;
  27. unlockSchedule: {
  28. date: Date;
  29. amount: bigint;
  30. }[];
  31. walletAmount: bigint;
  32. governance: {
  33. warmup: bigint;
  34. staked: bigint;
  35. cooldown: bigint;
  36. cooldown2: bigint;
  37. };
  38. integrityStakingPublishers: {
  39. name: string;
  40. publicKey: `0x${string}`;
  41. isSelf: boolean;
  42. selfStake: bigint;
  43. poolCapacity: bigint;
  44. poolUtilization: bigint;
  45. numFeeds: number;
  46. qualityRanking: number;
  47. apyHistory: { date: Date; apy: number }[];
  48. positions?:
  49. | {
  50. warmup?: bigint | undefined;
  51. staked?: bigint | undefined;
  52. cooldown?: bigint | undefined;
  53. cooldown2?: bigint | undefined;
  54. }
  55. | undefined;
  56. }[];
  57. };
  58. export enum StakeType {
  59. Governance,
  60. IntegrityStaking,
  61. }
  62. const StakeDetails = {
  63. Governance: () => ({ type: StakeType.Governance as const }),
  64. IntegrityStaking: (publisherName: string) => ({
  65. type: StakeType.IntegrityStaking as const,
  66. publisherName,
  67. }),
  68. };
  69. export type StakeDetails = ReturnType<
  70. (typeof StakeDetails)[keyof typeof StakeDetails]
  71. >;
  72. export enum AccountHistoryItemType {
  73. Deposit,
  74. LockedDeposit,
  75. Withdrawal,
  76. RewardsCredited,
  77. Claim,
  78. Slash,
  79. Unlock,
  80. StakeCreated,
  81. StakeFinishedWarmup,
  82. UnstakeCreated,
  83. UnstakeExitedCooldown,
  84. }
  85. const AccountHistoryAction = {
  86. Deposit: () => ({ type: AccountHistoryItemType.Deposit as const }),
  87. LockedDeposit: (unlockDate: Date) => ({
  88. type: AccountHistoryItemType.LockedDeposit as const,
  89. unlockDate,
  90. }),
  91. Withdrawal: () => ({ type: AccountHistoryItemType.Withdrawal as const }),
  92. RewardsCredited: () => ({
  93. type: AccountHistoryItemType.RewardsCredited as const,
  94. }),
  95. Claim: () => ({ type: AccountHistoryItemType.Claim as const }),
  96. Slash: (publisherName: string) => ({
  97. type: AccountHistoryItemType.Slash as const,
  98. publisherName,
  99. }),
  100. Unlock: () => ({ type: AccountHistoryItemType.Unlock as const }),
  101. StakeCreated: (details: StakeDetails) => ({
  102. type: AccountHistoryItemType.StakeCreated as const,
  103. details,
  104. }),
  105. StakeFinishedWarmup: (details: StakeDetails) => ({
  106. type: AccountHistoryItemType.StakeFinishedWarmup as const,
  107. details,
  108. }),
  109. UnstakeCreated: (details: StakeDetails) => ({
  110. type: AccountHistoryItemType.UnstakeCreated as const,
  111. details,
  112. }),
  113. UnstakeExitedCooldown: (details: StakeDetails) => ({
  114. type: AccountHistoryItemType.UnstakeExitedCooldown as const,
  115. details,
  116. }),
  117. };
  118. export type AccountHistoryAction = ReturnType<
  119. (typeof AccountHistoryAction)[keyof typeof AccountHistoryAction]
  120. >;
  121. type AccountHistory = {
  122. timestamp: Date;
  123. action: AccountHistoryAction;
  124. amount: bigint;
  125. accountTotal: bigint;
  126. availableToWithdraw: bigint;
  127. availableRewards: bigint;
  128. locked: bigint;
  129. }[];
  130. export const getStakeAccounts = async (
  131. _connection: Connection,
  132. _wallet: AnchorWallet,
  133. ): Promise<StakeAccount[]> => {
  134. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  135. return MOCK_STAKE_ACCOUNTS;
  136. };
  137. export const loadData = async (context: Context): Promise<Data> => {
  138. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  139. // While using mocks we need to clone the MOCK_DATA object every time
  140. // `loadData` is called so that swr treats the response as changed and
  141. // triggers a rerender.
  142. return { ...MOCK_DATA[context.stakeAccount.publicKey]! };
  143. };
  144. export const loadAccountHistory = async (
  145. context: Context,
  146. ): Promise<AccountHistory> => {
  147. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  148. return [...MOCK_HISTORY[context.stakeAccount.publicKey]!];
  149. };
  150. export const deposit = async (
  151. context: Context,
  152. amount: bigint,
  153. ): Promise<void> => {
  154. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  155. MOCK_DATA[context.stakeAccount.publicKey]!.total += amount;
  156. MOCK_DATA[context.stakeAccount.publicKey]!.walletAmount -= amount;
  157. };
  158. export const withdraw = async (
  159. context: Context,
  160. amount: bigint,
  161. ): Promise<void> => {
  162. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  163. MOCK_DATA[context.stakeAccount.publicKey]!.total -= amount;
  164. MOCK_DATA[context.stakeAccount.publicKey]!.walletAmount += amount;
  165. };
  166. export const claim = async (context: Context): Promise<void> => {
  167. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  168. MOCK_DATA[context.stakeAccount.publicKey]!.total +=
  169. MOCK_DATA[context.stakeAccount.publicKey]!.availableRewards;
  170. MOCK_DATA[context.stakeAccount.publicKey]!.availableRewards = 0n;
  171. MOCK_DATA[context.stakeAccount.publicKey]!.expiringRewards.amount = 0n;
  172. };
  173. export const stakeGovernance = async (
  174. context: Context,
  175. amount: bigint,
  176. ): Promise<void> => {
  177. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  178. MOCK_DATA[context.stakeAccount.publicKey]!.governance.warmup += amount;
  179. };
  180. export const cancelWarmupGovernance = async (
  181. context: Context,
  182. amount: bigint,
  183. ): Promise<void> => {
  184. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  185. MOCK_DATA[context.stakeAccount.publicKey]!.governance.warmup -= amount;
  186. };
  187. export const unstakeGovernance = async (
  188. context: Context,
  189. amount: bigint,
  190. ): Promise<void> => {
  191. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  192. MOCK_DATA[context.stakeAccount.publicKey]!.governance.staked -= amount;
  193. MOCK_DATA[context.stakeAccount.publicKey]!.governance.cooldown += amount;
  194. };
  195. export const delegateIntegrityStaking = async (
  196. context: Context,
  197. publisherKey: string,
  198. amount: bigint,
  199. ): Promise<void> => {
  200. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  201. const publisher = MOCK_DATA[
  202. context.stakeAccount.publicKey
  203. ]!.integrityStakingPublishers.find(
  204. (publisher) => publisher.publicKey === publisherKey,
  205. );
  206. if (publisher) {
  207. publisher.positions ||= {};
  208. publisher.positions.warmup = (publisher.positions.warmup ?? 0n) + amount;
  209. } else {
  210. throw new Error(`Invalid publisher key: "${publisherKey}"`);
  211. }
  212. };
  213. export const cancelWarmupIntegrityStaking = async (
  214. context: Context,
  215. publisherKey: string,
  216. amount: bigint,
  217. ): Promise<void> => {
  218. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  219. const publisher = MOCK_DATA[
  220. context.stakeAccount.publicKey
  221. ]!.integrityStakingPublishers.find(
  222. (publisher) => publisher.publicKey === publisherKey,
  223. );
  224. if (publisher) {
  225. if (publisher.positions?.warmup) {
  226. publisher.positions.warmup -= amount;
  227. }
  228. } else {
  229. throw new Error(`Invalid publisher key: "${publisherKey}"`);
  230. }
  231. };
  232. export const unstakeIntegrityStaking = async (
  233. context: Context,
  234. publisherKey: string,
  235. amount: bigint,
  236. ): Promise<void> => {
  237. await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
  238. const publisher = MOCK_DATA[
  239. context.stakeAccount.publicKey
  240. ]!.integrityStakingPublishers.find(
  241. (publisher) => publisher.publicKey === publisherKey,
  242. );
  243. if (publisher) {
  244. if (publisher.positions?.staked) {
  245. publisher.positions.staked -= amount;
  246. publisher.positions.cooldown =
  247. (publisher.positions.cooldown ?? 0n) + amount;
  248. }
  249. } else {
  250. throw new Error(`Invalid publisher key: "${publisherKey}"`);
  251. }
  252. };
  253. export const calculateApy = (
  254. poolCapacity: bigint,
  255. poolUtilization: bigint,
  256. isSelf: boolean,
  257. ) => {
  258. const maxApy = isSelf ? 25 : 20;
  259. const minApy = isSelf ? 10 : 5;
  260. return Math.min(
  261. Math.max(
  262. maxApy - Number((poolUtilization - poolCapacity) / 100_000_000n),
  263. minApy,
  264. ),
  265. maxApy,
  266. );
  267. };
  268. export const getUpcomingEpoch = (): Date => {
  269. const d = new Date();
  270. d.setUTCDate(d.getUTCDate() + ((5 + 7 - d.getUTCDay()) % 7 || 7));
  271. d.setUTCHours(0);
  272. d.setUTCMinutes(0);
  273. d.setUTCSeconds(0);
  274. d.setUTCMilliseconds(0);
  275. return d;
  276. };
  277. export const getNextFullEpoch = (): Date => {
  278. const d = getUpcomingEpoch();
  279. d.setUTCDate(d.getUTCDate() + 7);
  280. return d;
  281. };
  282. const MOCK_DELAY = 500;
  283. const MOCK_STAKE_ACCOUNTS: StakeAccount[] = [
  284. { publicKey: "0x000000" },
  285. { publicKey: "0x111111" },
  286. ];
  287. const mkMockData = (isDouro: boolean): Data => ({
  288. total: 15_000_000n,
  289. availableRewards: 156_000n,
  290. lastSlash: isDouro
  291. ? undefined
  292. : {
  293. amount: 2147n,
  294. date: new Date("2024-05-04T00:00:00Z"),
  295. },
  296. expiringRewards: {
  297. amount: 56_000n,
  298. expiry: new Date("2025-08-01T00:00:00Z"),
  299. },
  300. locked: isDouro ? 3_000_000n : 0n,
  301. unlockSchedule: isDouro
  302. ? [
  303. {
  304. amount: 1_000_000n,
  305. date: new Date("2025-08-01T00:00:00Z"),
  306. },
  307. {
  308. amount: 2_000_000n,
  309. date: new Date("2025-09-01T00:00:00Z"),
  310. },
  311. ]
  312. : [],
  313. walletAmount: 5_000_000_000_000n,
  314. governance: {
  315. warmup: 2_670_000n,
  316. staked: 4_150_000n,
  317. cooldown: 1_850_000n,
  318. cooldown2: 4_765_000n,
  319. },
  320. integrityStakingPublishers: [
  321. {
  322. name: "Douro Labs",
  323. publicKey: "0xF00",
  324. isSelf: isDouro,
  325. selfStake: 5_000_000_000n,
  326. poolCapacity: 500_000_000n,
  327. poolUtilization: 200_000_000n,
  328. numFeeds: 42,
  329. qualityRanking: 1,
  330. apyHistory: [
  331. { date: new Date("2024-07-22"), apy: 5 },
  332. { date: new Date("2024-07-23"), apy: 10 },
  333. { date: new Date("2024-07-24"), apy: 25 },
  334. { date: new Date("2024-07-25"), apy: 20 },
  335. ],
  336. positions: {
  337. warmup: 5_000_000n,
  338. staked: 4_000_000n,
  339. cooldown: 1_000_000n,
  340. cooldown2: 460_000n,
  341. },
  342. },
  343. {
  344. name: "Jump Trading",
  345. publicKey: "0xBA4",
  346. isSelf: false,
  347. selfStake: 400_000_000n,
  348. poolCapacity: 500_000_000n,
  349. poolUtilization: 750_000_000n,
  350. numFeeds: 84,
  351. qualityRanking: 2,
  352. apyHistory: [
  353. { date: new Date("2024-07-24"), apy: 5 },
  354. { date: new Date("2024-07-25"), apy: 10 },
  355. ],
  356. positions: {
  357. staked: 1_000_000n,
  358. },
  359. },
  360. {
  361. name: "Cboe",
  362. publicKey: "0xAA",
  363. isSelf: false,
  364. selfStake: 200_000_000n,
  365. poolCapacity: 600_000_000n,
  366. poolUtilization: 450_000_000n,
  367. numFeeds: 17,
  368. qualityRanking: 5,
  369. apyHistory: [
  370. { date: new Date("2024-07-24"), apy: 5 },
  371. { date: new Date("2024-07-25"), apy: 10 },
  372. ],
  373. },
  374. {
  375. name: "Raydium",
  376. publicKey: "0x111",
  377. isSelf: false,
  378. selfStake: 400_000_000n,
  379. poolCapacity: 500_000_000n,
  380. poolUtilization: 750_000_000n,
  381. numFeeds: 84,
  382. qualityRanking: 3,
  383. apyHistory: [
  384. { date: new Date("2024-07-24"), apy: 5 },
  385. { date: new Date("2024-07-25"), apy: 10 },
  386. ],
  387. },
  388. ],
  389. });
  390. const MOCK_DATA: Record<
  391. (typeof MOCK_STAKE_ACCOUNTS)[number]["publicKey"],
  392. Data
  393. > = {
  394. "0x000000": mkMockData(true),
  395. "0x111111": mkMockData(false),
  396. };
  397. const mkMockHistory = (): AccountHistory => [
  398. {
  399. timestamp: new Date("2024-06-10T00:00:00Z"),
  400. action: AccountHistoryAction.Deposit(),
  401. amount: 2_000_000n,
  402. accountTotal: 2_000_000n,
  403. availableRewards: 0n,
  404. availableToWithdraw: 2_000_000n,
  405. locked: 0n,
  406. },
  407. {
  408. timestamp: new Date("2024-06-14T02:00:00Z"),
  409. action: AccountHistoryAction.RewardsCredited(),
  410. amount: 200n,
  411. accountTotal: 2_000_000n,
  412. availableRewards: 200n,
  413. availableToWithdraw: 2_000_000n,
  414. locked: 0n,
  415. },
  416. {
  417. timestamp: new Date("2024-06-16T08:00:00Z"),
  418. action: AccountHistoryAction.Claim(),
  419. amount: 200n,
  420. accountTotal: 2_000_200n,
  421. availableRewards: 0n,
  422. availableToWithdraw: 2_000_200n,
  423. locked: 0n,
  424. },
  425. {
  426. timestamp: new Date("2024-06-16T08:00:00Z"),
  427. action: AccountHistoryAction.Slash("Cboe"),
  428. amount: 1000n,
  429. accountTotal: 1_999_200n,
  430. availableRewards: 0n,
  431. availableToWithdraw: 1_999_200n,
  432. locked: 0n,
  433. },
  434. ];
  435. const MOCK_HISTORY: Record<
  436. (typeof MOCK_STAKE_ACCOUNTS)[number]["publicKey"],
  437. AccountHistory
  438. > = {
  439. "0x000000": mkMockHistory(),
  440. "0x111111": mkMockHistory(),
  441. };