pyth-staking-client.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005
  1. import * as crypto from "crypto";
  2. import { AnchorProvider, BN, Program } from "@coral-xyz/anchor";
  3. import {
  4. getTokenOwnerRecordAddress,
  5. PROGRAM_VERSION_V2,
  6. withCreateTokenOwnerRecord,
  7. } from "@solana/spl-governance";
  8. import {
  9. type Account,
  10. createAssociatedTokenAccountInstruction,
  11. createTransferInstruction,
  12. getAccount,
  13. getAssociatedTokenAddressSync,
  14. getMint,
  15. type Mint,
  16. } from "@solana/spl-token";
  17. import type { AnchorWallet } from "@solana/wallet-adapter-react";
  18. import {
  19. Connection,
  20. PublicKey,
  21. SystemProgram,
  22. Transaction,
  23. TransactionInstruction,
  24. } from "@solana/web3.js";
  25. import {
  26. GOVERNANCE_ADDRESS,
  27. MAX_VOTER_WEIGHT,
  28. FRACTION_PRECISION_N,
  29. ONE_YEAR_IN_SECONDS,
  30. POSITIONS_ACCOUNT_SIZE,
  31. } from "./constants";
  32. import {
  33. getConfigAddress,
  34. getDelegationRecordAddress,
  35. getPoolConfigAddress,
  36. getStakeAccountCustodyAddress,
  37. getStakeAccountMetadataAddress,
  38. getTargetAccountAddress,
  39. } from "./pdas";
  40. import {
  41. PositionState,
  42. type GlobalConfig,
  43. type PoolConfig,
  44. type PoolDataAccount,
  45. type StakeAccountPositions,
  46. type TargetAccount,
  47. type VoterWeightAction,
  48. type VestingSchedule,
  49. } from "./types";
  50. import { convertBigIntToBN, convertBNToBigInt } from "./utils/bn";
  51. import { epochToDate, getCurrentEpoch } from "./utils/clock";
  52. import { extractPublisherData } from "./utils/pool";
  53. import {
  54. deserializeStakeAccountPositions,
  55. getPositionState,
  56. getVotingTokenAmount,
  57. } from "./utils/position";
  58. import { sendTransaction } from "./utils/transaction";
  59. import { getUnlockSchedule } from "./utils/vesting";
  60. import { DummyWallet } from "./utils/wallet";
  61. import * as IntegrityPoolIdl from "../idl/integrity-pool.json";
  62. import * as PublisherCapsIdl from "../idl/publisher-caps.json";
  63. import * as StakingIdl from "../idl/staking.json";
  64. import type { IntegrityPool } from "../types/integrity-pool";
  65. import type { PublisherCaps } from "../types/publisher-caps";
  66. import type { Staking } from "../types/staking";
  67. export type PythStakingClientConfig = {
  68. connection: Connection;
  69. wallet?: AnchorWallet;
  70. };
  71. export class PythStakingClient {
  72. connection: Connection;
  73. wallet: AnchorWallet;
  74. provider: AnchorProvider;
  75. stakingProgram: Program<Staking>;
  76. integrityPoolProgram: Program<IntegrityPool>;
  77. publisherCapsProgram: Program<PublisherCaps>;
  78. constructor(config: PythStakingClientConfig) {
  79. const { connection, wallet = DummyWallet } = config;
  80. this.connection = connection;
  81. this.wallet = wallet;
  82. this.provider = new AnchorProvider(this.connection, this.wallet, {
  83. skipPreflight: true,
  84. });
  85. this.stakingProgram = new Program(StakingIdl as Staking, this.provider);
  86. this.integrityPoolProgram = new Program(
  87. IntegrityPoolIdl as IntegrityPool,
  88. this.provider,
  89. );
  90. this.publisherCapsProgram = new Program(
  91. PublisherCapsIdl as PublisherCaps,
  92. this.provider,
  93. );
  94. }
  95. async initGlobalConfig(config: GlobalConfig) {
  96. const globalConfigAnchor = convertBigIntToBN(config);
  97. const instruction = await this.stakingProgram.methods
  98. .initConfig(globalConfigAnchor)
  99. .instruction();
  100. return sendTransaction([instruction], this.connection, this.wallet);
  101. }
  102. async getGlobalConfig(): Promise<GlobalConfig> {
  103. const globalConfigAnchor =
  104. await this.stakingProgram.account.globalConfig.fetch(
  105. getConfigAddress()[0],
  106. );
  107. return convertBNToBigInt(globalConfigAnchor);
  108. }
  109. /** Gets a users stake accounts */
  110. public async getAllStakeAccountPositions(
  111. owner?: PublicKey,
  112. ): Promise<PublicKey[]> {
  113. const positionDataMemcmp = this.stakingProgram.coder.accounts.memcmp(
  114. "positionData",
  115. ) as {
  116. offset: number;
  117. bytes: string;
  118. };
  119. const res =
  120. await this.stakingProgram.provider.connection.getProgramAccounts(
  121. this.stakingProgram.programId,
  122. {
  123. encoding: "base64",
  124. filters: [
  125. {
  126. memcmp: positionDataMemcmp,
  127. },
  128. {
  129. memcmp: {
  130. offset: 8,
  131. bytes: owner?.toBase58() ?? this.wallet.publicKey.toBase58(),
  132. },
  133. },
  134. ],
  135. },
  136. );
  137. return res.map((account) => account.pubkey);
  138. }
  139. public async getStakeAccountPositions(
  140. stakeAccountPositions: PublicKey,
  141. ): Promise<StakeAccountPositions> {
  142. const account =
  143. await this.stakingProgram.provider.connection.getAccountInfo(
  144. stakeAccountPositions,
  145. );
  146. if (account === null) {
  147. throw new Error("Stake account not found");
  148. }
  149. return deserializeStakeAccountPositions(
  150. stakeAccountPositions,
  151. account.data,
  152. this.stakingProgram.idl,
  153. );
  154. }
  155. public async getDelegationRecord(
  156. stakeAccountPositions: PublicKey,
  157. publisher: PublicKey,
  158. ) {
  159. return this.integrityPoolProgram.account.delegationRecord
  160. .fetchNullable(
  161. getDelegationRecordAddress(stakeAccountPositions, publisher),
  162. )
  163. .then((record) => convertBNToBigInt(record));
  164. }
  165. public async getStakeAccountCustody(
  166. stakeAccountPositions: PublicKey,
  167. ): Promise<Account> {
  168. return getAccount(
  169. this.connection,
  170. getStakeAccountCustodyAddress(stakeAccountPositions),
  171. );
  172. }
  173. public async initializePool({
  174. rewardProgramAuthority,
  175. poolData,
  176. y,
  177. }: {
  178. rewardProgramAuthority: PublicKey;
  179. poolData: PublicKey;
  180. y: bigint;
  181. }) {
  182. const yAnchor = convertBigIntToBN(y);
  183. const instruction = await this.integrityPoolProgram.methods
  184. .initializePool(rewardProgramAuthority, yAnchor)
  185. .accounts({
  186. poolData,
  187. slashCustody: getStakeAccountCustodyAddress(poolData),
  188. })
  189. .instruction();
  190. return sendTransaction([instruction], this.connection, this.wallet);
  191. }
  192. public async getOwnerPythAtaAccount(): Promise<Account> {
  193. const globalConfig = await this.getGlobalConfig();
  194. return getAccount(
  195. this.connection,
  196. getAssociatedTokenAddressSync(
  197. globalConfig.pythTokenMint,
  198. this.wallet.publicKey,
  199. true,
  200. ),
  201. );
  202. }
  203. public async getOwnerPythBalance(): Promise<bigint> {
  204. try {
  205. const ataAccount = await this.getOwnerPythAtaAccount();
  206. return ataAccount.amount;
  207. } catch {
  208. return 0n;
  209. }
  210. }
  211. public async getPoolConfigAccount(): Promise<PoolConfig> {
  212. const poolConfigAnchor =
  213. await this.integrityPoolProgram.account.poolConfig.fetch(
  214. getPoolConfigAddress(),
  215. );
  216. return convertBNToBigInt(poolConfigAnchor);
  217. }
  218. public async getPoolDataAccount(): Promise<PoolDataAccount> {
  219. const poolConfig = await this.getPoolConfigAccount();
  220. const poolDataAddress = poolConfig.poolData;
  221. const poolDataAccountAnchor =
  222. await this.integrityPoolProgram.account.poolData.fetch(poolDataAddress);
  223. return convertBNToBigInt(poolDataAccountAnchor);
  224. }
  225. public async stakeToGovernance(
  226. stakeAccountPositions: PublicKey,
  227. amount: bigint,
  228. ) {
  229. const instructions = [];
  230. if (!(await this.hasJoinedDaoLlc(stakeAccountPositions))) {
  231. instructions.push(
  232. await this.getJoinDaoLlcInstruction(stakeAccountPositions),
  233. );
  234. }
  235. instructions.push(
  236. await this.stakingProgram.methods
  237. .createPosition(
  238. {
  239. voting: {},
  240. },
  241. new BN(amount.toString()),
  242. )
  243. .accounts({
  244. stakeAccountPositions,
  245. })
  246. .instruction(),
  247. await this.stakingProgram.methods
  248. .mergeTargetPositions({ voting: {} })
  249. .accounts({
  250. stakeAccountPositions,
  251. })
  252. .instruction(),
  253. );
  254. return sendTransaction(instructions, this.connection, this.wallet);
  255. }
  256. public async unstakeFromGovernance(
  257. stakeAccountPositions: PublicKey,
  258. positionState: PositionState.LOCKED | PositionState.LOCKING,
  259. amount: bigint,
  260. ) {
  261. const stakeAccountPositionsData = await this.getStakeAccountPositions(
  262. stakeAccountPositions,
  263. );
  264. const currentEpoch = await getCurrentEpoch(this.connection);
  265. let remainingAmount = amount;
  266. const instructionPromises: Promise<TransactionInstruction>[] = [];
  267. const eligiblePositions = stakeAccountPositionsData.data.positions
  268. .map((p, i) => ({ position: p, index: i }))
  269. .reverse()
  270. .filter(
  271. ({ position }) =>
  272. position.targetWithParameters.voting !== undefined &&
  273. positionState === getPositionState(position, currentEpoch),
  274. );
  275. for (const { position, index } of eligiblePositions) {
  276. if (position.amount < remainingAmount) {
  277. instructionPromises.push(
  278. this.stakingProgram.methods
  279. .closePosition(index, convertBigIntToBN(position.amount), {
  280. voting: {},
  281. })
  282. .accounts({
  283. stakeAccountPositions,
  284. })
  285. .instruction(),
  286. );
  287. remainingAmount -= position.amount;
  288. } else {
  289. instructionPromises.push(
  290. this.stakingProgram.methods
  291. .closePosition(index, convertBigIntToBN(remainingAmount), {
  292. voting: {},
  293. })
  294. .accounts({
  295. stakeAccountPositions,
  296. })
  297. .instruction(),
  298. );
  299. break;
  300. }
  301. }
  302. const instructions = await Promise.all(instructionPromises);
  303. return sendTransaction(instructions, this.connection, this.wallet);
  304. }
  305. public async unstakeFromPublisher(
  306. stakeAccountPositions: PublicKey,
  307. publisher: PublicKey,
  308. positionState: PositionState.LOCKED | PositionState.LOCKING,
  309. amount: bigint,
  310. ) {
  311. const stakeAccountPositionsData = await this.getStakeAccountPositions(
  312. stakeAccountPositions,
  313. );
  314. const currentEpoch = await getCurrentEpoch(this.connection);
  315. let remainingAmount = amount;
  316. const instructionPromises: Promise<TransactionInstruction>[] = [];
  317. const eligiblePositions = stakeAccountPositionsData.data.positions
  318. .map((p, i) => ({ position: p, index: i }))
  319. .reverse()
  320. .filter(
  321. ({ position }) =>
  322. position.targetWithParameters.integrityPool?.publisher !==
  323. undefined &&
  324. position.targetWithParameters.integrityPool.publisher.equals(
  325. publisher,
  326. ) &&
  327. positionState === getPositionState(position, currentEpoch),
  328. );
  329. for (const { position, index } of eligiblePositions) {
  330. if (position.amount < remainingAmount) {
  331. instructionPromises.push(
  332. this.integrityPoolProgram.methods
  333. .undelegate(index, convertBigIntToBN(position.amount))
  334. .accounts({
  335. stakeAccountPositions,
  336. publisher,
  337. })
  338. .instruction(),
  339. );
  340. remainingAmount -= position.amount;
  341. } else {
  342. instructionPromises.push(
  343. this.integrityPoolProgram.methods
  344. .undelegate(index, convertBigIntToBN(remainingAmount))
  345. .accounts({
  346. stakeAccountPositions,
  347. publisher,
  348. })
  349. .instruction(),
  350. );
  351. break;
  352. }
  353. }
  354. const instructions = await Promise.all(instructionPromises);
  355. return sendTransaction(instructions, this.connection, this.wallet);
  356. }
  357. public async unstakeFromAllPublishers(
  358. stakeAccountPositions: PublicKey,
  359. positionStates: (PositionState.LOCKED | PositionState.LOCKING)[],
  360. ) {
  361. const [stakeAccountPositionsData, currentEpoch] = await Promise.all([
  362. this.getStakeAccountPositions(stakeAccountPositions),
  363. getCurrentEpoch(this.connection),
  364. ]);
  365. const instructions = await Promise.all(
  366. stakeAccountPositionsData.data.positions
  367. .map((position, index) => {
  368. const publisher =
  369. position.targetWithParameters.integrityPool?.publisher;
  370. return publisher === undefined
  371. ? undefined
  372. : { position, index, publisher };
  373. })
  374. // By separating this filter from the next, typescript can narrow the
  375. // type and automatically infer that there will be no `undefined` values
  376. // in the array after this line. If we combine those filters,
  377. // typescript won't narrow properly.
  378. .filter((positionInfo) => positionInfo !== undefined)
  379. .filter(({ position }) =>
  380. (positionStates as PositionState[]).includes(
  381. getPositionState(position, currentEpoch),
  382. ),
  383. )
  384. .reverse()
  385. .map(({ position, index, publisher }) =>
  386. this.integrityPoolProgram.methods
  387. .undelegate(index, convertBigIntToBN(position.amount))
  388. .accounts({ stakeAccountPositions, publisher })
  389. .instruction(),
  390. ),
  391. );
  392. return sendTransaction(instructions, this.connection, this.wallet);
  393. }
  394. public async hasGovernanceRecord(config: GlobalConfig): Promise<boolean> {
  395. const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress(
  396. GOVERNANCE_ADDRESS,
  397. config.pythGovernanceRealm,
  398. config.pythTokenMint,
  399. this.wallet.publicKey,
  400. );
  401. const voterAccountInfo =
  402. await this.stakingProgram.provider.connection.getAccountInfo(
  403. tokenOwnerRecordAddress,
  404. );
  405. return Boolean(voterAccountInfo);
  406. }
  407. public async createStakeAccountAndDeposit(amount: bigint) {
  408. const globalConfig = await this.getGlobalConfig();
  409. const senderTokenAccount = getAssociatedTokenAddressSync(
  410. globalConfig.pythTokenMint,
  411. this.wallet.publicKey,
  412. true,
  413. );
  414. const nonce = crypto.randomBytes(16).toString("hex");
  415. const stakeAccountPositions = await PublicKey.createWithSeed(
  416. this.wallet.publicKey,
  417. nonce,
  418. this.stakingProgram.programId,
  419. );
  420. const minimumBalance =
  421. await this.stakingProgram.provider.connection.getMinimumBalanceForRentExemption(
  422. POSITIONS_ACCOUNT_SIZE,
  423. );
  424. const instructions = [];
  425. instructions.push(
  426. SystemProgram.createAccountWithSeed({
  427. fromPubkey: this.wallet.publicKey,
  428. newAccountPubkey: stakeAccountPositions,
  429. basePubkey: this.wallet.publicKey,
  430. seed: nonce,
  431. lamports: minimumBalance,
  432. space: POSITIONS_ACCOUNT_SIZE,
  433. programId: this.stakingProgram.programId,
  434. }),
  435. await this.stakingProgram.methods
  436. .createStakeAccount(this.wallet.publicKey, { fullyVested: {} })
  437. .accounts({
  438. stakeAccountPositions,
  439. })
  440. .instruction(),
  441. await this.stakingProgram.methods
  442. .createVoterRecord()
  443. .accounts({
  444. stakeAccountPositions,
  445. })
  446. .instruction(),
  447. );
  448. if (!(await this.hasGovernanceRecord(globalConfig))) {
  449. await withCreateTokenOwnerRecord(
  450. instructions,
  451. GOVERNANCE_ADDRESS,
  452. PROGRAM_VERSION_V2,
  453. globalConfig.pythGovernanceRealm,
  454. this.wallet.publicKey,
  455. globalConfig.pythTokenMint,
  456. this.wallet.publicKey,
  457. );
  458. }
  459. instructions.push(
  460. await this.stakingProgram.methods
  461. .joinDaoLlc(globalConfig.agreementHash)
  462. .accounts({
  463. stakeAccountPositions,
  464. })
  465. .instruction(),
  466. createTransferInstruction(
  467. senderTokenAccount,
  468. getStakeAccountCustodyAddress(stakeAccountPositions),
  469. this.wallet.publicKey,
  470. amount,
  471. ),
  472. );
  473. await sendTransaction(instructions, this.connection, this.wallet);
  474. return stakeAccountPositions;
  475. }
  476. public async depositTokensToStakeAccountCustody(
  477. stakeAccountPositions: PublicKey,
  478. amount: bigint,
  479. ) {
  480. const globalConfig = await this.getGlobalConfig();
  481. const mint = globalConfig.pythTokenMint;
  482. const senderTokenAccount = getAssociatedTokenAddressSync(
  483. mint,
  484. this.wallet.publicKey,
  485. true,
  486. );
  487. const instruction = createTransferInstruction(
  488. senderTokenAccount,
  489. getStakeAccountCustodyAddress(stakeAccountPositions),
  490. this.wallet.publicKey,
  491. amount,
  492. );
  493. return sendTransaction([instruction], this.connection, this.wallet);
  494. }
  495. public async withdrawTokensFromStakeAccountCustody(
  496. stakeAccountPositions: PublicKey,
  497. amount: bigint,
  498. ) {
  499. const globalConfig = await this.getGlobalConfig();
  500. const mint = globalConfig.pythTokenMint;
  501. const instructions = [];
  502. const receiverTokenAccount = getAssociatedTokenAddressSync(
  503. mint,
  504. this.wallet.publicKey,
  505. true,
  506. );
  507. // Edge case: if the user doesn't have an ATA, create one
  508. try {
  509. await this.getOwnerPythAtaAccount();
  510. } catch {
  511. instructions.push(
  512. createAssociatedTokenAccountInstruction(
  513. this.wallet.publicKey,
  514. receiverTokenAccount,
  515. this.wallet.publicKey,
  516. mint,
  517. ),
  518. );
  519. }
  520. instructions.push(
  521. await this.stakingProgram.methods
  522. .withdrawStake(new BN(amount.toString()))
  523. .accounts({
  524. destination: receiverTokenAccount,
  525. stakeAccountPositions,
  526. })
  527. .instruction(),
  528. );
  529. return sendTransaction(instructions, this.connection, this.wallet);
  530. }
  531. public async stakeToPublisher(
  532. stakeAccountPositions: PublicKey,
  533. publisher: PublicKey,
  534. amount: bigint,
  535. ) {
  536. const instructions = [];
  537. if (!(await this.hasJoinedDaoLlc(stakeAccountPositions))) {
  538. instructions.push(
  539. await this.getJoinDaoLlcInstruction(stakeAccountPositions),
  540. );
  541. }
  542. instructions.push(
  543. await this.integrityPoolProgram.methods
  544. .delegate(convertBigIntToBN(amount))
  545. .accounts({
  546. owner: this.wallet.publicKey,
  547. publisher,
  548. stakeAccountPositions,
  549. })
  550. .instruction(),
  551. );
  552. return sendTransaction(instructions, this.connection, this.wallet);
  553. }
  554. public async getUnlockSchedule(
  555. stakeAccountPositions: PublicKey,
  556. includePastPeriods = false,
  557. ) {
  558. const stakeAccountMetadataAddress = getStakeAccountMetadataAddress(
  559. stakeAccountPositions,
  560. );
  561. const stakeAccountMetadata =
  562. await this.stakingProgram.account.stakeAccountMetadataV2.fetch(
  563. stakeAccountMetadataAddress,
  564. );
  565. const vestingSchedule = convertBNToBigInt(stakeAccountMetadata.lock);
  566. const config = await this.getGlobalConfig();
  567. if (config.pythTokenListTime === null) {
  568. throw new Error("Pyth token list time not set in global config");
  569. }
  570. return getUnlockSchedule({
  571. vestingSchedule,
  572. pythTokenListTime: config.pythTokenListTime,
  573. includePastPeriods,
  574. });
  575. }
  576. public async getCirculatingSupply() {
  577. const vestingSchedule: VestingSchedule = {
  578. periodicVestingAfterListing: {
  579. initialBalance: 8_500_000_000n * FRACTION_PRECISION_N,
  580. numPeriods: 4n,
  581. periodDuration: ONE_YEAR_IN_SECONDS,
  582. },
  583. };
  584. const config = await this.getGlobalConfig();
  585. if (config.pythTokenListTime === null) {
  586. throw new Error("Pyth token list time not set in global config");
  587. }
  588. const unlockSchedule = getUnlockSchedule({
  589. vestingSchedule,
  590. pythTokenListTime: config.pythTokenListTime,
  591. includePastPeriods: false,
  592. });
  593. const totalLocked = unlockSchedule.schedule.reduce(
  594. (total, unlock) => total + unlock.amount,
  595. 0n,
  596. );
  597. const mint = await this.getPythTokenMint();
  598. return mint.supply - totalLocked;
  599. }
  600. async getAdvanceDelegationRecordInstructions(
  601. stakeAccountPositions: PublicKey,
  602. ) {
  603. const poolData = await this.getPoolDataAccount();
  604. const stakeAccountPositionsData = await this.getStakeAccountPositions(
  605. stakeAccountPositions,
  606. );
  607. const allPublishers = extractPublisherData(poolData);
  608. const publishers = allPublishers.filter(({ pubkey }) =>
  609. stakeAccountPositionsData.data.positions.some(
  610. ({ targetWithParameters }) =>
  611. targetWithParameters.integrityPool?.publisher.equals(pubkey),
  612. ),
  613. );
  614. const delegationRecords = await Promise.all(
  615. publishers.map(({ pubkey }) =>
  616. this.getDelegationRecord(stakeAccountPositions, pubkey),
  617. ),
  618. );
  619. const currentEpoch = await getCurrentEpoch(this.connection);
  620. // Filter out delegationRecord that are up to date
  621. const filteredPublishers = publishers.filter((_, index) => {
  622. return !(delegationRecords[index]?.lastEpoch === currentEpoch);
  623. });
  624. // anchor does not calculate the correct pda for other programs
  625. // therefore we need to manually calculate the pdas
  626. const advanceDelegationRecordInstructions = await Promise.all(
  627. filteredPublishers.map(({ pubkey, stakeAccount }) =>
  628. this.integrityPoolProgram.methods
  629. .advanceDelegationRecord()
  630. .accountsPartial({
  631. payer: this.wallet.publicKey,
  632. publisher: pubkey,
  633. publisherStakeAccountPositions: stakeAccount,
  634. publisherStakeAccountCustody: stakeAccount
  635. ? getStakeAccountCustodyAddress(stakeAccount)
  636. : null,
  637. stakeAccountPositions,
  638. stakeAccountCustody: getStakeAccountCustodyAddress(
  639. stakeAccountPositions,
  640. ),
  641. })
  642. .instruction(),
  643. ),
  644. );
  645. const mergePositionsInstruction = await Promise.all(
  646. publishers.map(({ pubkey }) =>
  647. this.integrityPoolProgram.methods
  648. .mergeDelegationPositions()
  649. .accounts({
  650. owner: this.wallet.publicKey,
  651. publisher: pubkey,
  652. stakeAccountPositions,
  653. })
  654. .instruction(),
  655. ),
  656. );
  657. return {
  658. advanceDelegationRecordInstructions,
  659. mergePositionsInstruction,
  660. publishers,
  661. };
  662. }
  663. public async advanceDelegationRecord(stakeAccountPositions: PublicKey) {
  664. const instructions = await this.getAdvanceDelegationRecordInstructions(
  665. stakeAccountPositions,
  666. );
  667. return sendTransaction(
  668. [
  669. ...instructions.advanceDelegationRecordInstructions,
  670. ...instructions.mergePositionsInstruction,
  671. ],
  672. this.connection,
  673. this.wallet,
  674. );
  675. }
  676. public async getClaimableRewards(stakeAccountPositions: PublicKey) {
  677. const instructions = await this.getAdvanceDelegationRecordInstructions(
  678. stakeAccountPositions,
  679. );
  680. let totalRewards = 0n;
  681. for (const instruction of instructions.advanceDelegationRecordInstructions) {
  682. const tx = new Transaction().add(instruction);
  683. tx.feePayer = this.wallet.publicKey;
  684. const res = await this.connection.simulateTransaction(tx);
  685. const val = res.value.returnData?.data[0];
  686. if (val === undefined) {
  687. continue;
  688. }
  689. const buffer = Buffer.from(val, "base64").reverse();
  690. totalRewards += BigInt("0x" + buffer.toString("hex"));
  691. }
  692. const delegationRecords = await Promise.all(
  693. instructions.publishers.map(({ pubkey }) =>
  694. this.getDelegationRecord(stakeAccountPositions, pubkey),
  695. ),
  696. );
  697. let lowestEpoch: bigint | undefined;
  698. for (const record of delegationRecords) {
  699. if (
  700. record !== null &&
  701. (lowestEpoch === undefined || record.lastEpoch < lowestEpoch)
  702. ) {
  703. lowestEpoch = record.lastEpoch;
  704. }
  705. }
  706. return {
  707. totalRewards,
  708. expiry:
  709. lowestEpoch === undefined ? undefined : epochToDate(lowestEpoch + 52n),
  710. };
  711. }
  712. async setPublisherStakeAccount(
  713. publisher: PublicKey,
  714. stakeAccountPositions: PublicKey,
  715. newStakeAccountPositions: PublicKey | undefined,
  716. ) {
  717. const instruction = await this.integrityPoolProgram.methods
  718. .setPublisherStakeAccount()
  719. .accounts({
  720. currentStakeAccountPositionsOption: stakeAccountPositions,
  721. newStakeAccountPositionsOption: newStakeAccountPositions ?? null,
  722. publisher,
  723. })
  724. .instruction();
  725. await sendTransaction([instruction], this.connection, this.wallet);
  726. return;
  727. }
  728. public async reassignPublisherStakeAccount(
  729. publisher: PublicKey,
  730. stakeAccountPositions: PublicKey,
  731. newStakeAccountPositions: PublicKey,
  732. ) {
  733. return this.setPublisherStakeAccount(
  734. publisher,
  735. stakeAccountPositions,
  736. newStakeAccountPositions,
  737. );
  738. }
  739. public async removePublisherStakeAccount(
  740. publisher: PublicKey,
  741. stakeAccountPositions: PublicKey,
  742. ) {
  743. return this.setPublisherStakeAccount(
  744. publisher,
  745. stakeAccountPositions,
  746. undefined,
  747. );
  748. }
  749. public async getTargetAccount(): Promise<TargetAccount> {
  750. const targetAccount =
  751. await this.stakingProgram.account.targetMetadata.fetch(
  752. getTargetAccountAddress(),
  753. );
  754. return convertBNToBigInt(targetAccount);
  755. }
  756. /**
  757. * This returns the current scaling factor between staked tokens and realms voter weight.
  758. * The formula is n_staked_tokens = scaling_factor * n_voter_weight
  759. */
  760. public async getScalingFactor(): Promise<number> {
  761. const targetAccount = await this.getTargetAccount();
  762. return Number(targetAccount.locked) / Number(MAX_VOTER_WEIGHT);
  763. }
  764. public async getRecoverAccountInstruction(
  765. stakeAccountPositions: PublicKey,
  766. governanceAuthority: PublicKey,
  767. ): Promise<TransactionInstruction> {
  768. return this.stakingProgram.methods
  769. .recoverAccount()
  770. .accountsPartial({
  771. stakeAccountPositions,
  772. governanceAuthority,
  773. })
  774. .instruction();
  775. }
  776. public async getUpdatePoolAuthorityInstruction(
  777. governanceAuthority: PublicKey,
  778. poolAuthority: PublicKey,
  779. ): Promise<TransactionInstruction> {
  780. return this.stakingProgram.methods
  781. .updatePoolAuthority(poolAuthority)
  782. .accounts({
  783. governanceAuthority,
  784. })
  785. .instruction();
  786. }
  787. public async getUpdateVoterWeightInstruction(
  788. stakeAccountPositions: PublicKey,
  789. action: VoterWeightAction,
  790. remainingAccount?: PublicKey,
  791. ) {
  792. return this.stakingProgram.methods
  793. .updateVoterWeight(action)
  794. .accounts({
  795. stakeAccountPositions,
  796. })
  797. .remainingAccounts(
  798. remainingAccount
  799. ? [
  800. {
  801. pubkey: remainingAccount,
  802. isWritable: false,
  803. isSigner: false,
  804. },
  805. ]
  806. : [],
  807. )
  808. .instruction();
  809. }
  810. public async hasJoinedDaoLlc(
  811. stakeAccountPositions: PublicKey,
  812. ): Promise<boolean> {
  813. const config = await this.getGlobalConfig();
  814. const stakeAccountMetadataAddress = getStakeAccountMetadataAddress(
  815. stakeAccountPositions,
  816. );
  817. const stakeAccountMetadata =
  818. await this.stakingProgram.account.stakeAccountMetadataV2.fetch(
  819. stakeAccountMetadataAddress,
  820. );
  821. return (
  822. JSON.stringify(stakeAccountMetadata.signedAgreementHash) ===
  823. JSON.stringify(config.agreementHash)
  824. );
  825. }
  826. public async getJoinDaoLlcInstruction(
  827. stakeAccountPositions: PublicKey,
  828. ): Promise<TransactionInstruction> {
  829. const config = await this.getGlobalConfig();
  830. return this.stakingProgram.methods
  831. .joinDaoLlc(config.agreementHash)
  832. .accounts({
  833. stakeAccountPositions,
  834. })
  835. .instruction();
  836. }
  837. public async getMainStakeAccount(owner?: PublicKey) {
  838. const stakeAccountPositions = await this.getAllStakeAccountPositions(owner);
  839. const currentEpoch = await getCurrentEpoch(this.connection);
  840. const stakeAccountVotingTokens = await Promise.all(
  841. stakeAccountPositions.map(async (position) => {
  842. const stakeAccountPositionsData =
  843. await this.getStakeAccountPositions(position);
  844. return {
  845. stakeAccountPosition: position,
  846. votingTokens: getVotingTokenAmount(
  847. stakeAccountPositionsData,
  848. currentEpoch,
  849. ),
  850. };
  851. }),
  852. );
  853. let mainAccount = stakeAccountVotingTokens[0];
  854. if (mainAccount === undefined) {
  855. return;
  856. }
  857. for (let i = 1; i < stakeAccountVotingTokens.length; i++) {
  858. const currentAccount = stakeAccountVotingTokens[i];
  859. if (
  860. currentAccount !== undefined &&
  861. currentAccount.votingTokens > mainAccount.votingTokens
  862. ) {
  863. mainAccount = currentAccount;
  864. }
  865. }
  866. return mainAccount;
  867. }
  868. public async getVoterWeight(owner?: PublicKey) {
  869. const mainAccount = await this.getMainStakeAccount(owner);
  870. if (mainAccount === undefined) {
  871. return 0;
  872. }
  873. const targetAccount = await this.getTargetAccount();
  874. return (mainAccount.votingTokens * MAX_VOTER_WEIGHT) / targetAccount.locked;
  875. }
  876. public async getPythTokenMint(): Promise<Mint> {
  877. const globalConfig = await this.getGlobalConfig();
  878. return getMint(this.connection, globalConfig.pythTokenMint);
  879. }
  880. public async getRewardCustodyAccount(): Promise<Account> {
  881. const poolConfigAddress = getPoolConfigAddress();
  882. const config = await this.getGlobalConfig();
  883. const rewardCustodyAccountAddress = getAssociatedTokenAddressSync(
  884. config.pythTokenMint,
  885. poolConfigAddress,
  886. true,
  887. );
  888. return getAccount(this.connection, rewardCustodyAccountAddress);
  889. }
  890. }