@ignitionfi/spl-stake-pool 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,1522 @@
1
+ import {
2
+ createApproveInstruction,
3
+ createAssociatedTokenAccountIdempotentInstruction,
4
+ getAccount,
5
+ getAssociatedTokenAddressSync,
6
+ NATIVE_MINT,
7
+ } from '@solana/spl-token'
8
+ import {
9
+ AccountInfo,
10
+ Connection,
11
+ Keypair,
12
+ PublicKey,
13
+ Signer,
14
+ StakeAuthorizationLayout,
15
+ StakeProgram,
16
+ SystemProgram,
17
+ TransactionInstruction,
18
+ } from '@solana/web3.js'
19
+ import BN from 'bn.js'
20
+ import { create } from 'superstruct'
21
+ import {
22
+ DEVNET_STAKE_POOL_PROGRAM_ID,
23
+ MAX_VALIDATORS_TO_UPDATE,
24
+ MINIMUM_ACTIVE_STAKE,
25
+ STAKE_POOL_PROGRAM_ID,
26
+ } from './constants'
27
+ import { StakePoolInstruction } from './instructions'
28
+ import {
29
+ StakeAccount,
30
+ StakePool,
31
+ StakePoolLayout,
32
+ ValidatorList,
33
+ ValidatorListLayout,
34
+ ValidatorStakeInfo,
35
+ } from './layouts'
36
+ import {
37
+ arrayChunk,
38
+ calcLamportsWithdrawAmount,
39
+ findEphemeralStakeProgramAddress,
40
+ findMetadataAddress,
41
+ findStakeProgramAddress,
42
+ findTransientStakeProgramAddress,
43
+ findWithdrawAuthorityProgramAddress,
44
+ findWsolTransientProgramAddress,
45
+ getValidatorListAccount,
46
+ lamportsToSol,
47
+ newStakeAccount,
48
+ prepareWithdrawAccounts,
49
+ solToLamports,
50
+ ValidatorAccount,
51
+ } from './utils'
52
+
53
+ export {
54
+ DEVNET_STAKE_POOL_PROGRAM_ID,
55
+ STAKE_POOL_PROGRAM_ID,
56
+ } from './constants'
57
+ export * from './instructions'
58
+ export type {
59
+ AccountType,
60
+ StakePool,
61
+ ValidatorList,
62
+ ValidatorStakeInfo,
63
+ } from './layouts'
64
+ export {
65
+ StakePoolLayout,
66
+ ValidatorListLayout,
67
+ ValidatorStakeInfoLayout,
68
+ } from './layouts'
69
+
70
+ export interface ValidatorListAccount {
71
+ pubkey: PublicKey
72
+ account: AccountInfo<ValidatorList>
73
+ }
74
+
75
+ export interface StakePoolAccount {
76
+ pubkey: PublicKey
77
+ account: AccountInfo<StakePool>
78
+ }
79
+
80
+ export interface WithdrawAccount {
81
+ stakeAddress: PublicKey
82
+ voteAddress?: PublicKey
83
+ poolAmount: BN
84
+ }
85
+
86
+ /**
87
+ * Wrapper class for a stake pool.
88
+ * Each stake pool has a stake pool account and a validator list account.
89
+ */
90
+ export interface StakePoolAccounts {
91
+ stakePool: StakePoolAccount | undefined
92
+ validatorList: ValidatorListAccount | undefined
93
+ }
94
+
95
+ export function getStakePoolProgramId(rpcEndpoint: string): PublicKey {
96
+ if (rpcEndpoint.includes('devnet')) {
97
+ return DEVNET_STAKE_POOL_PROGRAM_ID
98
+ } else {
99
+ return STAKE_POOL_PROGRAM_ID
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Retrieves and deserializes a StakePool account using a web3js connection and the stake pool address.
105
+ * @param connection An active web3js connection.
106
+ * @param stakePoolAddress The public key (address) of the stake pool account.
107
+ */
108
+ export async function getStakePoolAccount(
109
+ connection: Connection,
110
+ stakePoolAddress: PublicKey,
111
+ ): Promise<StakePoolAccount> {
112
+ const account = await connection.getAccountInfo(stakePoolAddress)
113
+
114
+ if (!account) {
115
+ throw new Error('Invalid stake pool account')
116
+ }
117
+
118
+ return {
119
+ pubkey: stakePoolAddress,
120
+ account: {
121
+ data: StakePoolLayout.decode(account.data),
122
+ executable: account.executable,
123
+ lamports: account.lamports,
124
+ owner: account.owner,
125
+ },
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Retrieves and deserializes a Stake account using a web3js connection and the stake address.
131
+ * @param connection An active web3js connection.
132
+ * @param stakeAccount The public key (address) of the stake account.
133
+ */
134
+ export async function getStakeAccount(
135
+ connection: Connection,
136
+ stakeAccount: PublicKey,
137
+ ): Promise<StakeAccount> {
138
+ const result = (await connection.getParsedAccountInfo(stakeAccount)).value
139
+ if (!result || !('parsed' in result.data)) {
140
+ throw new Error('Invalid stake account')
141
+ }
142
+ const program = result.data.program
143
+ if (program !== 'stake') {
144
+ throw new Error('Not a stake account')
145
+ }
146
+ return create(result.data.parsed, StakeAccount)
147
+ }
148
+
149
+ /**
150
+ * Retrieves all StakePool and ValidatorList accounts that are running a particular StakePool program.
151
+ * @param connection An active web3js connection.
152
+ * @param stakePoolProgramAddress The public key (address) of the StakePool program.
153
+ */
154
+ export async function getStakePoolAccounts(
155
+ connection: Connection,
156
+ stakePoolProgramAddress: PublicKey,
157
+ ): Promise<
158
+ (StakePoolAccount | ValidatorListAccount | undefined)[] | undefined
159
+ > {
160
+ const response = await connection.getProgramAccounts(stakePoolProgramAddress)
161
+
162
+ return response
163
+ .map((a) => {
164
+ try {
165
+ if (a.account.data.readUInt8() === 1) {
166
+ const data = StakePoolLayout.decode(a.account.data)
167
+ return {
168
+ pubkey: a.pubkey,
169
+ account: {
170
+ data,
171
+ executable: a.account.executable,
172
+ lamports: a.account.lamports,
173
+ owner: a.account.owner,
174
+ },
175
+ }
176
+ } else if (a.account.data.readUInt8() === 2) {
177
+ const data = ValidatorListLayout.decode(a.account.data)
178
+ return {
179
+ pubkey: a.pubkey,
180
+ account: {
181
+ data,
182
+ executable: a.account.executable,
183
+ lamports: a.account.lamports,
184
+ owner: a.account.owner,
185
+ },
186
+ }
187
+ } else {
188
+ console.error(
189
+ `Could not decode. StakePoolAccount Enum is ${a.account.data.readUInt8()}, expected 1 or 2!`,
190
+ )
191
+ return undefined
192
+ }
193
+ } catch (error) {
194
+ console.error('Could not decode account. Error:', error)
195
+ return undefined
196
+ }
197
+ })
198
+ .filter(a => a !== undefined)
199
+ }
200
+
201
+ /**
202
+ * Creates instructions required to deposit stake to stake pool.
203
+ */
204
+ export async function depositStake(
205
+ connection: Connection,
206
+ stakePoolAddress: PublicKey,
207
+ authorizedPubkey: PublicKey,
208
+ validatorVote: PublicKey,
209
+ depositStake: PublicKey,
210
+ poolTokenReceiverAccount?: PublicKey,
211
+ ) {
212
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
213
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
214
+
215
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
216
+ stakePoolProgramId,
217
+ stakePoolAddress,
218
+ )
219
+
220
+ const validatorStake = await findStakeProgramAddress(
221
+ stakePoolProgramId,
222
+ validatorVote,
223
+ stakePoolAddress,
224
+ )
225
+
226
+ const instructions: TransactionInstruction[] = []
227
+ const signers: Signer[] = []
228
+
229
+ const poolMint = stakePool.account.data.poolMint
230
+
231
+ // Create token account if not specified
232
+ if (!poolTokenReceiverAccount) {
233
+ const associatedAddress = getAssociatedTokenAddressSync(
234
+ poolMint,
235
+ authorizedPubkey,
236
+ )
237
+ instructions.push(
238
+ createAssociatedTokenAccountIdempotentInstruction(
239
+ authorizedPubkey,
240
+ associatedAddress,
241
+ authorizedPubkey,
242
+ poolMint,
243
+ ),
244
+ )
245
+ poolTokenReceiverAccount = associatedAddress
246
+ }
247
+
248
+ instructions.push(
249
+ ...StakeProgram.authorize({
250
+ stakePubkey: depositStake,
251
+ authorizedPubkey,
252
+ newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority,
253
+ stakeAuthorizationType: StakeAuthorizationLayout.Staker,
254
+ }).instructions,
255
+ )
256
+
257
+ instructions.push(
258
+ ...StakeProgram.authorize({
259
+ stakePubkey: depositStake,
260
+ authorizedPubkey,
261
+ newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority,
262
+ stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer,
263
+ }).instructions,
264
+ )
265
+
266
+ instructions.push(
267
+ StakePoolInstruction.depositStake({
268
+ programId: stakePoolProgramId,
269
+ stakePool: stakePoolAddress,
270
+ validatorList: stakePool.account.data.validatorList,
271
+ depositAuthority: stakePool.account.data.stakeDepositAuthority,
272
+ reserveStake: stakePool.account.data.reserveStake,
273
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
274
+ referralPoolAccount: poolTokenReceiverAccount,
275
+ destinationPoolAccount: poolTokenReceiverAccount,
276
+ withdrawAuthority,
277
+ depositStake,
278
+ validatorStake,
279
+ poolMint,
280
+ }),
281
+ )
282
+
283
+ return {
284
+ instructions,
285
+ signers,
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Creates instructions required to deposit sol to stake pool.
291
+ */
292
+ export async function depositWsolWithSession(
293
+ connection: Connection,
294
+ stakePoolAddress: PublicKey,
295
+ signerOrSession: PublicKey,
296
+ userPubkey: PublicKey,
297
+ lamports: number,
298
+ destinationTokenAccount?: PublicKey,
299
+ referrerTokenAccount?: PublicKey,
300
+ depositAuthority?: PublicKey,
301
+ payer?: PublicKey,
302
+ ) {
303
+ const wsolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, userPubkey)
304
+
305
+ const tokenAccountInfo = await connection.getTokenAccountBalance(
306
+ wsolTokenAccount,
307
+ 'confirmed',
308
+ )
309
+ const wsolBalance = tokenAccountInfo
310
+ ? parseInt(tokenAccountInfo.value.amount)
311
+ : 0
312
+
313
+ if (wsolBalance < lamports) {
314
+ throw new Error(
315
+ `Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
316
+ wsolBalance,
317
+ )} WSOL.`,
318
+ )
319
+ }
320
+
321
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
322
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
323
+ const stakePool = stakePoolAccount.account.data
324
+
325
+ // stakePool.tokenProgramId
326
+
327
+ const instructions: TransactionInstruction[] = []
328
+
329
+ // Create token account if not specified
330
+ if (!destinationTokenAccount) {
331
+ const associatedAddress = getAssociatedTokenAddressSync(
332
+ stakePool.poolMint,
333
+ userPubkey,
334
+ )
335
+ instructions.push(
336
+ createAssociatedTokenAccountIdempotentInstruction(
337
+ payer ?? signerOrSession,
338
+ associatedAddress,
339
+ userPubkey,
340
+ stakePool.poolMint,
341
+ ),
342
+ )
343
+ destinationTokenAccount = associatedAddress
344
+ }
345
+
346
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
347
+ stakePoolProgramId,
348
+ stakePoolAddress,
349
+ )
350
+
351
+ const [programSigner] = PublicKey.findProgramAddressSync(
352
+ [Buffer.from('fogo_session_program_signer')],
353
+ stakePoolProgramId,
354
+ )
355
+
356
+ const wsolTransientAccount = findWsolTransientProgramAddress(
357
+ stakePoolProgramId,
358
+ userPubkey,
359
+ )
360
+
361
+ instructions.push(
362
+ StakePoolInstruction.depositWsolWithSession({
363
+ programId: stakePoolProgramId,
364
+ stakePool: stakePoolAddress,
365
+ reserveStake: stakePool.reserveStake,
366
+ fundingAccount: signerOrSession,
367
+ destinationPoolAccount: destinationTokenAccount,
368
+ managerFeeAccount: stakePool.managerFeeAccount,
369
+ referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount,
370
+ poolMint: stakePool.poolMint,
371
+ lamports,
372
+ withdrawAuthority,
373
+ depositAuthority,
374
+
375
+ wsolMint: NATIVE_MINT,
376
+ wsolTokenAccount,
377
+ wsolTransientAccount,
378
+ tokenProgramId: stakePool.tokenProgramId,
379
+ programSigner,
380
+ payer,
381
+ }),
382
+ )
383
+ return {
384
+ instructions,
385
+ signers: [],
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Creates instructions required to deposit sol to stake pool.
391
+ */
392
+ export async function depositSol(
393
+ connection: Connection,
394
+ stakePoolAddress: PublicKey,
395
+ from: PublicKey,
396
+ lamports: number,
397
+ destinationTokenAccount?: PublicKey,
398
+ referrerTokenAccount?: PublicKey,
399
+ depositAuthority?: PublicKey,
400
+ ) {
401
+ const fromBalance = await connection.getBalance(from, 'confirmed')
402
+ if (fromBalance < lamports) {
403
+ throw new Error(
404
+ `Not enough SOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
405
+ fromBalance,
406
+ )} SOL.`,
407
+ )
408
+ }
409
+
410
+ const stakePoolAccount = await getStakePoolAccount(
411
+ connection,
412
+ stakePoolAddress,
413
+ )
414
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
415
+ const stakePool = stakePoolAccount.account.data
416
+
417
+ // Ephemeral SOL account just to do the transfer
418
+ const userSolTransfer = new Keypair()
419
+ const signers: Signer[] = [userSolTransfer]
420
+ const instructions: TransactionInstruction[] = []
421
+
422
+ // Create the ephemeral SOL account
423
+ instructions.push(
424
+ SystemProgram.transfer({
425
+ fromPubkey: from,
426
+ toPubkey: userSolTransfer.publicKey,
427
+ lamports,
428
+ }),
429
+ )
430
+
431
+ // Create token account if not specified
432
+ if (!destinationTokenAccount) {
433
+ const associatedAddress = getAssociatedTokenAddressSync(
434
+ stakePool.poolMint,
435
+ from,
436
+ )
437
+ instructions.push(
438
+ createAssociatedTokenAccountIdempotentInstruction(
439
+ from,
440
+ associatedAddress,
441
+ from,
442
+ stakePool.poolMint,
443
+ ),
444
+ )
445
+ destinationTokenAccount = associatedAddress
446
+ }
447
+
448
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
449
+ stakePoolProgramId,
450
+ stakePoolAddress,
451
+ )
452
+
453
+ instructions.push(
454
+ StakePoolInstruction.depositSol({
455
+ programId: stakePoolProgramId,
456
+ stakePool: stakePoolAddress,
457
+ reserveStake: stakePool.reserveStake,
458
+ fundingAccount: userSolTransfer.publicKey,
459
+ destinationPoolAccount: destinationTokenAccount,
460
+ managerFeeAccount: stakePool.managerFeeAccount,
461
+ referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount,
462
+ poolMint: stakePool.poolMint,
463
+ lamports,
464
+ withdrawAuthority,
465
+ depositAuthority,
466
+ }),
467
+ )
468
+
469
+ return {
470
+ instructions,
471
+ signers,
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Creates instructions required to withdraw stake from a stake pool.
477
+ */
478
+ export async function withdrawStake(
479
+ connection: Connection,
480
+ stakePoolAddress: PublicKey,
481
+ tokenOwner: PublicKey,
482
+ amount: number,
483
+ useReserve = false,
484
+ voteAccountAddress?: PublicKey,
485
+ stakeReceiver?: PublicKey,
486
+ poolTokenAccount?: PublicKey,
487
+ validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
488
+ ) {
489
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
490
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
491
+ const poolAmount = new BN(solToLamports(amount))
492
+
493
+ if (!poolTokenAccount) {
494
+ poolTokenAccount = getAssociatedTokenAddressSync(
495
+ stakePool.account.data.poolMint,
496
+ tokenOwner,
497
+ )
498
+ }
499
+
500
+ const tokenAccount = await getAccount(connection, poolTokenAccount)
501
+
502
+ // Check withdrawFrom balance
503
+ if (tokenAccount.amount < poolAmount.toNumber()) {
504
+ throw new Error(
505
+ `Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
506
+ Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
507
+ )
508
+ }
509
+
510
+ const stakeAccountRentExemption
511
+ = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
512
+
513
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
514
+ stakePoolProgramId,
515
+ stakePoolAddress,
516
+ )
517
+
518
+ let stakeReceiverAccount = null
519
+ if (stakeReceiver) {
520
+ stakeReceiverAccount = await getStakeAccount(connection, stakeReceiver)
521
+ }
522
+
523
+ const withdrawAccounts: WithdrawAccount[] = []
524
+
525
+ if (useReserve) {
526
+ withdrawAccounts.push({
527
+ stakeAddress: stakePool.account.data.reserveStake,
528
+ voteAddress: undefined,
529
+ poolAmount,
530
+ })
531
+ } else if (
532
+ stakeReceiverAccount
533
+ && stakeReceiverAccount?.type === 'delegated'
534
+ ) {
535
+ const voteAccount = stakeReceiverAccount.info?.stake?.delegation.voter
536
+ if (!voteAccount) {
537
+ throw new Error(`Invalid stake receiver ${stakeReceiver} delegation`)
538
+ }
539
+ const validatorListAccount = await connection.getAccountInfo(
540
+ stakePool.account.data.validatorList,
541
+ )
542
+ const validatorList = ValidatorListLayout.decode(
543
+ validatorListAccount?.data,
544
+ ) as ValidatorList
545
+ const isValidVoter = validatorList.validators.find(val =>
546
+ val.voteAccountAddress.equals(voteAccount),
547
+ )
548
+ if (voteAccountAddress && voteAccountAddress !== voteAccount) {
549
+ throw new Error(`Provided withdrawal vote account ${voteAccountAddress} does not match delegation on stake receiver account ${voteAccount},
550
+ remove this flag or provide a different stake account delegated to ${voteAccountAddress}`)
551
+ }
552
+ if (isValidVoter) {
553
+ const stakeAccountAddress = await findStakeProgramAddress(
554
+ stakePoolProgramId,
555
+ voteAccount,
556
+ stakePoolAddress,
557
+ )
558
+
559
+ const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
560
+ if (!stakeAccount) {
561
+ throw new Error(
562
+ `Preferred withdraw valdator's stake account is invalid`,
563
+ )
564
+ }
565
+
566
+ const availableForWithdrawal = calcLamportsWithdrawAmount(
567
+ stakePool.account.data,
568
+ new BN(
569
+ stakeAccount.lamports
570
+ - MINIMUM_ACTIVE_STAKE
571
+ - stakeAccountRentExemption,
572
+ ),
573
+ )
574
+
575
+ if (availableForWithdrawal.lt(poolAmount)) {
576
+ throw new Error(
577
+ `Not enough lamports available for withdrawal from ${stakeAccountAddress},
578
+ ${poolAmount} asked, ${availableForWithdrawal} available.`,
579
+ )
580
+ }
581
+ withdrawAccounts.push({
582
+ stakeAddress: stakeAccountAddress,
583
+ voteAddress: voteAccount,
584
+ poolAmount,
585
+ })
586
+ } else {
587
+ throw new Error(
588
+ `Provided stake account is delegated to a vote account ${voteAccount} which does not exist in the stake pool`,
589
+ )
590
+ }
591
+ } else if (voteAccountAddress) {
592
+ const stakeAccountAddress = await findStakeProgramAddress(
593
+ stakePoolProgramId,
594
+ voteAccountAddress,
595
+ stakePoolAddress,
596
+ )
597
+ const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
598
+ if (!stakeAccount) {
599
+ throw new Error('Invalid Stake Account')
600
+ }
601
+
602
+ const availableLamports = new BN(
603
+ stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
604
+ )
605
+ if (availableLamports.lt(new BN(0))) {
606
+ throw new Error('Invalid Stake Account')
607
+ }
608
+ const availableForWithdrawal = calcLamportsWithdrawAmount(
609
+ stakePool.account.data,
610
+ availableLamports,
611
+ )
612
+
613
+ if (availableForWithdrawal.lt(poolAmount)) {
614
+ // noinspection ExceptionCaughtLocallyJS
615
+ throw new Error(
616
+ `Not enough lamports available for withdrawal from ${stakeAccountAddress},
617
+ ${poolAmount} asked, ${availableForWithdrawal} available.`,
618
+ )
619
+ }
620
+ withdrawAccounts.push({
621
+ stakeAddress: stakeAccountAddress,
622
+ voteAddress: voteAccountAddress,
623
+ poolAmount,
624
+ })
625
+ } else {
626
+ // Get the list of accounts to withdraw from
627
+ withdrawAccounts.push(
628
+ ...(await prepareWithdrawAccounts(
629
+ connection,
630
+ stakePool.account.data,
631
+ stakePoolAddress,
632
+ poolAmount,
633
+ validatorComparator,
634
+ poolTokenAccount.equals(stakePool.account.data.managerFeeAccount),
635
+ )),
636
+ )
637
+ }
638
+
639
+ // Construct transaction to withdraw from withdrawAccounts account list
640
+ const instructions: TransactionInstruction[] = []
641
+ const userTransferAuthority = Keypair.generate()
642
+
643
+ const signers: Signer[] = [userTransferAuthority]
644
+
645
+ instructions.push(
646
+ createApproveInstruction(
647
+ poolTokenAccount,
648
+ userTransferAuthority.publicKey,
649
+ tokenOwner,
650
+ poolAmount.toNumber(),
651
+ ),
652
+ )
653
+
654
+ let totalRentFreeBalances = 0
655
+
656
+ // Max 5 accounts to prevent an error: "Transaction too large"
657
+ const maxWithdrawAccounts = 5
658
+ let i = 0
659
+
660
+ // Go through prepared accounts and withdraw/claim them
661
+ for (const withdrawAccount of withdrawAccounts) {
662
+ if (i > maxWithdrawAccounts) {
663
+ break
664
+ }
665
+ // Convert pool tokens amount to lamports
666
+ const solWithdrawAmount = calcLamportsWithdrawAmount(
667
+ stakePool.account.data,
668
+ withdrawAccount.poolAmount,
669
+ )
670
+
671
+ let infoMsg = `Withdrawing ◎${solWithdrawAmount},
672
+ from stake account ${withdrawAccount.stakeAddress?.toBase58()}`
673
+
674
+ if (withdrawAccount.voteAddress) {
675
+ infoMsg = `${infoMsg}, delegated to ${withdrawAccount.voteAddress?.toBase58()}`
676
+ }
677
+
678
+ console.info(infoMsg)
679
+ let stakeToReceive
680
+
681
+ if (
682
+ !stakeReceiver
683
+ || (stakeReceiverAccount && stakeReceiverAccount.type === 'delegated')
684
+ ) {
685
+ const stakeKeypair = newStakeAccount(
686
+ tokenOwner,
687
+ instructions,
688
+ stakeAccountRentExemption,
689
+ )
690
+ signers.push(stakeKeypair)
691
+ totalRentFreeBalances += stakeAccountRentExemption
692
+ stakeToReceive = stakeKeypair.publicKey
693
+ } else {
694
+ stakeToReceive = stakeReceiver
695
+ }
696
+
697
+ instructions.push(
698
+ StakePoolInstruction.withdrawStake({
699
+ programId: stakePoolProgramId,
700
+ stakePool: stakePoolAddress,
701
+ validatorList: stakePool.account.data.validatorList,
702
+ validatorStake: withdrawAccount.stakeAddress,
703
+ destinationStake: stakeToReceive,
704
+ destinationStakeAuthority: tokenOwner,
705
+ sourceTransferAuthority: userTransferAuthority.publicKey,
706
+ sourcePoolAccount: poolTokenAccount,
707
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
708
+ poolMint: stakePool.account.data.poolMint,
709
+ poolTokens: withdrawAccount.poolAmount.toNumber(),
710
+ withdrawAuthority,
711
+ }),
712
+ )
713
+ i++
714
+ }
715
+ if (
716
+ stakeReceiver
717
+ && stakeReceiverAccount
718
+ && stakeReceiverAccount.type === 'delegated'
719
+ ) {
720
+ signers.forEach((newStakeKeypair) => {
721
+ instructions.concat(
722
+ StakeProgram.merge({
723
+ stakePubkey: stakeReceiver,
724
+ sourceStakePubKey: newStakeKeypair.publicKey,
725
+ authorizedPubkey: tokenOwner,
726
+ }).instructions,
727
+ )
728
+ })
729
+ }
730
+
731
+ return {
732
+ instructions,
733
+ signers,
734
+ stakeReceiver,
735
+ totalRentFreeBalances,
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Creates instructions required to withdraw SOL directly from a stake pool.
741
+ */
742
+ export async function withdrawSol(
743
+ connection: Connection,
744
+ stakePoolAddress: PublicKey,
745
+ tokenOwner: PublicKey,
746
+ solReceiver: PublicKey,
747
+ amount: number,
748
+ solWithdrawAuthority?: PublicKey,
749
+ ) {
750
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
751
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
752
+ const poolAmount = solToLamports(amount)
753
+
754
+ const poolTokenAccount = getAssociatedTokenAddressSync(
755
+ stakePool.account.data.poolMint,
756
+ tokenOwner,
757
+ )
758
+
759
+ const tokenAccount = await getAccount(connection, poolTokenAccount)
760
+
761
+ // Check withdrawFrom balance
762
+ if (tokenAccount.amount < poolAmount) {
763
+ throw new Error(
764
+ `Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
765
+ Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
766
+ )
767
+ }
768
+
769
+ // Construct transaction to withdraw from withdrawAccounts account list
770
+ const instructions: TransactionInstruction[] = []
771
+ const userTransferAuthority = Keypair.generate()
772
+ const signers: Signer[] = [userTransferAuthority]
773
+
774
+ instructions.push(
775
+ createApproveInstruction(
776
+ poolTokenAccount,
777
+ userTransferAuthority.publicKey,
778
+ tokenOwner,
779
+ poolAmount,
780
+ ),
781
+ )
782
+
783
+ const poolWithdrawAuthority = await findWithdrawAuthorityProgramAddress(
784
+ stakePoolProgramId,
785
+ stakePoolAddress,
786
+ )
787
+
788
+ if (solWithdrawAuthority) {
789
+ const expectedSolWithdrawAuthority
790
+ = stakePool.account.data.solWithdrawAuthority
791
+ if (!expectedSolWithdrawAuthority) {
792
+ throw new Error(
793
+ 'SOL withdraw authority specified in arguments but stake pool has none',
794
+ )
795
+ }
796
+ if (
797
+ solWithdrawAuthority.toBase58() !== expectedSolWithdrawAuthority.toBase58()
798
+ ) {
799
+ throw new Error(
800
+ `Invalid deposit withdraw specified, expected ${expectedSolWithdrawAuthority.toBase58()}, received ${solWithdrawAuthority.toBase58()}`,
801
+ )
802
+ }
803
+ }
804
+
805
+ const withdrawTransaction = StakePoolInstruction.withdrawSol({
806
+ programId: stakePoolProgramId,
807
+ stakePool: stakePoolAddress,
808
+ withdrawAuthority: poolWithdrawAuthority,
809
+ reserveStake: stakePool.account.data.reserveStake,
810
+ sourcePoolAccount: poolTokenAccount,
811
+ sourceTransferAuthority: userTransferAuthority.publicKey,
812
+ destinationSystemAccount: solReceiver,
813
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
814
+ poolMint: stakePool.account.data.poolMint,
815
+ poolTokens: poolAmount,
816
+ solWithdrawAuthority,
817
+ })
818
+
819
+ instructions.push(withdrawTransaction)
820
+
821
+ return {
822
+ instructions,
823
+ signers,
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Creates instructions required to withdraw wSOL from a stake pool.
829
+ */
830
+ export async function withdrawWsolWithSession(
831
+ connection: Connection,
832
+ stakePoolAddress: PublicKey,
833
+ signerOrSession: PublicKey,
834
+ userPubkey: PublicKey,
835
+ amount: number,
836
+ solWithdrawAuthority?: PublicKey,
837
+ ) {
838
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
839
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
840
+ const stakePool = stakePoolAccount.account.data
841
+ const poolTokens = solToLamports(amount)
842
+
843
+ const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey)
844
+ const tokenAccount = await getAccount(connection, poolTokenAccount)
845
+
846
+ if (tokenAccount.amount < poolTokens) {
847
+ throw new Error(
848
+ `Not enough token balance to withdraw ${amount} pool tokens.
849
+ Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
850
+ )
851
+ }
852
+
853
+ const userWsolAccount = getAssociatedTokenAddressSync(NATIVE_MINT, userPubkey)
854
+
855
+ const instructions: TransactionInstruction[] = []
856
+ const signers: Signer[] = []
857
+
858
+ instructions.push(
859
+ createAssociatedTokenAccountIdempotentInstruction(
860
+ signerOrSession,
861
+ userWsolAccount,
862
+ userPubkey,
863
+ NATIVE_MINT,
864
+ ),
865
+ )
866
+
867
+ const [programSigner] = PublicKey.findProgramAddressSync(
868
+ [Buffer.from('fogo_session_program_signer')],
869
+ stakePoolProgramId,
870
+ )
871
+
872
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
873
+ stakePoolProgramId,
874
+ stakePoolAddress,
875
+ )
876
+
877
+ instructions.push(
878
+ StakePoolInstruction.withdrawWsolWithSession({
879
+ programId: stakePoolProgramId,
880
+ stakePool: stakePoolAddress,
881
+ withdrawAuthority,
882
+ userTransferAuthority: signerOrSession,
883
+ poolTokensFrom: poolTokenAccount,
884
+ reserveStake: stakePool.reserveStake,
885
+ userWsolAccount,
886
+ managerFeeAccount: stakePool.managerFeeAccount,
887
+ poolMint: stakePool.poolMint,
888
+ tokenProgramId: stakePool.tokenProgramId,
889
+ solWithdrawAuthority,
890
+ wsolMint: NATIVE_MINT,
891
+ programSigner,
892
+ poolTokens,
893
+ }),
894
+ )
895
+
896
+ return {
897
+ instructions,
898
+ signers,
899
+ }
900
+ }
901
+
902
+ export async function addValidatorToPool(
903
+ connection: Connection,
904
+ stakePoolAddress: PublicKey,
905
+ validatorVote: PublicKey,
906
+ seed?: number,
907
+ ) {
908
+ const stakePoolAccount = await getStakePoolAccount(
909
+ connection,
910
+ stakePoolAddress,
911
+ )
912
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
913
+ const stakePool = stakePoolAccount.account.data
914
+ const { reserveStake, staker, validatorList } = stakePool
915
+
916
+ const validatorListAccount = await getValidatorListAccount(
917
+ connection,
918
+ validatorList,
919
+ )
920
+
921
+ const validatorInfo = validatorListAccount.account.data.validators.find(
922
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
923
+ )
924
+
925
+ if (validatorInfo) {
926
+ throw new Error('Vote account is already in validator list')
927
+ }
928
+
929
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
930
+ stakePoolProgramId,
931
+ stakePoolAddress,
932
+ )
933
+
934
+ const validatorStake = await findStakeProgramAddress(
935
+ stakePoolProgramId,
936
+ validatorVote,
937
+ stakePoolAddress,
938
+ seed,
939
+ )
940
+
941
+ const instructions: TransactionInstruction[] = [
942
+ StakePoolInstruction.addValidatorToPool({
943
+ programId: stakePoolProgramId,
944
+ stakePool: stakePoolAddress,
945
+ staker,
946
+ reserveStake,
947
+ withdrawAuthority,
948
+ validatorList,
949
+ validatorStake,
950
+ validatorVote,
951
+ }),
952
+ ]
953
+
954
+ return {
955
+ instructions,
956
+ }
957
+ }
958
+
959
+ export async function removeValidatorFromPool(
960
+ connection: Connection,
961
+ stakePoolAddress: PublicKey,
962
+ validatorVote: PublicKey,
963
+ seed?: number,
964
+ ) {
965
+ const stakePoolAccount = await getStakePoolAccount(
966
+ connection,
967
+ stakePoolAddress,
968
+ )
969
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
970
+ const stakePool = stakePoolAccount.account.data
971
+ const { staker, validatorList } = stakePool
972
+
973
+ const validatorListAccount = await getValidatorListAccount(
974
+ connection,
975
+ validatorList,
976
+ )
977
+
978
+ const validatorInfo = validatorListAccount.account.data.validators.find(
979
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
980
+ )
981
+
982
+ if (!validatorInfo) {
983
+ throw new Error('Vote account is not already in validator list')
984
+ }
985
+
986
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
987
+ stakePoolProgramId,
988
+ stakePoolAddress,
989
+ )
990
+
991
+ const validatorStake = await findStakeProgramAddress(
992
+ stakePoolProgramId,
993
+ validatorVote,
994
+ stakePoolAddress,
995
+ seed,
996
+ )
997
+
998
+ const transientStakeSeed = validatorInfo.transientSeedSuffixStart
999
+
1000
+ const transientStake = await findTransientStakeProgramAddress(
1001
+ stakePoolProgramId,
1002
+ validatorInfo.voteAccountAddress,
1003
+ stakePoolAddress,
1004
+ transientStakeSeed,
1005
+ )
1006
+
1007
+ const instructions: TransactionInstruction[] = [
1008
+ StakePoolInstruction.removeValidatorFromPool({
1009
+ programId: stakePoolProgramId,
1010
+ stakePool: stakePoolAddress,
1011
+ staker,
1012
+ withdrawAuthority,
1013
+ validatorList,
1014
+ validatorStake,
1015
+ transientStake,
1016
+ }),
1017
+ ]
1018
+
1019
+ return {
1020
+ instructions,
1021
+ }
1022
+ }
1023
+
1024
+ /**
1025
+ * Creates instructions required to increase validator stake.
1026
+ */
1027
+ export async function increaseValidatorStake(
1028
+ connection: Connection,
1029
+ stakePoolAddress: PublicKey,
1030
+ validatorVote: PublicKey,
1031
+ lamports: number,
1032
+ ephemeralStakeSeed?: number,
1033
+ ) {
1034
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1035
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1036
+
1037
+ const validatorList = await getValidatorListAccount(
1038
+ connection,
1039
+ stakePool.account.data.validatorList,
1040
+ )
1041
+
1042
+ const validatorInfo = validatorList.account.data.validators.find(
1043
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
1044
+ )
1045
+
1046
+ if (!validatorInfo) {
1047
+ throw new Error('Vote account not found in validator list')
1048
+ }
1049
+
1050
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1051
+ stakePoolProgramId,
1052
+ stakePoolAddress,
1053
+ )
1054
+
1055
+ // Bump transient seed suffix by one to avoid reuse when not using the increaseAdditionalStake instruction
1056
+ const transientStakeSeed
1057
+ = ephemeralStakeSeed === undefined
1058
+ ? validatorInfo.transientSeedSuffixStart.addn(1)
1059
+ : validatorInfo.transientSeedSuffixStart
1060
+
1061
+ const transientStake = await findTransientStakeProgramAddress(
1062
+ stakePoolProgramId,
1063
+ validatorInfo.voteAccountAddress,
1064
+ stakePoolAddress,
1065
+ transientStakeSeed,
1066
+ )
1067
+
1068
+ const validatorStake = await findStakeProgramAddress(
1069
+ stakePoolProgramId,
1070
+ validatorInfo.voteAccountAddress,
1071
+ stakePoolAddress,
1072
+ )
1073
+
1074
+ const instructions: TransactionInstruction[] = []
1075
+
1076
+ if (ephemeralStakeSeed !== undefined) {
1077
+ const ephemeralStake = await findEphemeralStakeProgramAddress(
1078
+ stakePoolProgramId,
1079
+ stakePoolAddress,
1080
+ new BN(ephemeralStakeSeed),
1081
+ )
1082
+ instructions.push(
1083
+ StakePoolInstruction.increaseAdditionalValidatorStake({
1084
+ programId: stakePoolProgramId,
1085
+ stakePool: stakePoolAddress,
1086
+ staker: stakePool.account.data.staker,
1087
+ validatorList: stakePool.account.data.validatorList,
1088
+ reserveStake: stakePool.account.data.reserveStake,
1089
+ transientStakeSeed: transientStakeSeed.toNumber(),
1090
+ withdrawAuthority,
1091
+ transientStake,
1092
+ validatorStake,
1093
+ validatorVote,
1094
+ lamports,
1095
+ ephemeralStake,
1096
+ ephemeralStakeSeed,
1097
+ }),
1098
+ )
1099
+ } else {
1100
+ instructions.push(
1101
+ StakePoolInstruction.increaseValidatorStake({
1102
+ programId: stakePoolProgramId,
1103
+ stakePool: stakePoolAddress,
1104
+ staker: stakePool.account.data.staker,
1105
+ validatorList: stakePool.account.data.validatorList,
1106
+ reserveStake: stakePool.account.data.reserveStake,
1107
+ transientStakeSeed: transientStakeSeed.toNumber(),
1108
+ withdrawAuthority,
1109
+ transientStake,
1110
+ validatorStake,
1111
+ validatorVote,
1112
+ lamports,
1113
+ }),
1114
+ )
1115
+ }
1116
+
1117
+ return {
1118
+ instructions,
1119
+ }
1120
+ }
1121
+
1122
+ /**
1123
+ * Creates instructions required to decrease validator stake.
1124
+ */
1125
+ export async function decreaseValidatorStake(
1126
+ connection: Connection,
1127
+ stakePoolAddress: PublicKey,
1128
+ validatorVote: PublicKey,
1129
+ lamports: number,
1130
+ ephemeralStakeSeed?: number,
1131
+ ) {
1132
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1133
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1134
+ const validatorList = await getValidatorListAccount(
1135
+ connection,
1136
+ stakePool.account.data.validatorList,
1137
+ )
1138
+
1139
+ const validatorInfo = validatorList.account.data.validators.find(
1140
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
1141
+ )
1142
+
1143
+ if (!validatorInfo) {
1144
+ throw new Error('Vote account not found in validator list')
1145
+ }
1146
+
1147
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1148
+ stakePoolProgramId,
1149
+ stakePoolAddress,
1150
+ )
1151
+
1152
+ const validatorStake = await findStakeProgramAddress(
1153
+ stakePoolProgramId,
1154
+ validatorInfo.voteAccountAddress,
1155
+ stakePoolAddress,
1156
+ )
1157
+
1158
+ // Bump transient seed suffix by one to avoid reuse when not using the decreaseAdditionalStake instruction
1159
+ const transientStakeSeed
1160
+ = ephemeralStakeSeed === undefined
1161
+ ? validatorInfo.transientSeedSuffixStart.addn(1)
1162
+ : validatorInfo.transientSeedSuffixStart
1163
+
1164
+ const transientStake = await findTransientStakeProgramAddress(
1165
+ stakePoolProgramId,
1166
+ validatorInfo.voteAccountAddress,
1167
+ stakePoolAddress,
1168
+ transientStakeSeed,
1169
+ )
1170
+
1171
+ const instructions: TransactionInstruction[] = []
1172
+
1173
+ if (ephemeralStakeSeed !== undefined) {
1174
+ const ephemeralStake = await findEphemeralStakeProgramAddress(
1175
+ stakePoolProgramId,
1176
+ stakePoolAddress,
1177
+ new BN(ephemeralStakeSeed),
1178
+ )
1179
+ instructions.push(
1180
+ StakePoolInstruction.decreaseAdditionalValidatorStake({
1181
+ programId: stakePoolProgramId,
1182
+ stakePool: stakePoolAddress,
1183
+ staker: stakePool.account.data.staker,
1184
+ validatorList: stakePool.account.data.validatorList,
1185
+ reserveStake: stakePool.account.data.reserveStake,
1186
+ transientStakeSeed: transientStakeSeed.toNumber(),
1187
+ withdrawAuthority,
1188
+ validatorStake,
1189
+ transientStake,
1190
+ lamports,
1191
+ ephemeralStake,
1192
+ ephemeralStakeSeed,
1193
+ }),
1194
+ )
1195
+ } else {
1196
+ instructions.push(
1197
+ StakePoolInstruction.decreaseValidatorStakeWithReserve({
1198
+ programId: stakePoolProgramId,
1199
+ stakePool: stakePoolAddress,
1200
+ staker: stakePool.account.data.staker,
1201
+ validatorList: stakePool.account.data.validatorList,
1202
+ reserveStake: stakePool.account.data.reserveStake,
1203
+ transientStakeSeed: transientStakeSeed.toNumber(),
1204
+ withdrawAuthority,
1205
+ validatorStake,
1206
+ transientStake,
1207
+ lamports,
1208
+ }),
1209
+ )
1210
+ }
1211
+
1212
+ return {
1213
+ instructions,
1214
+ }
1215
+ }
1216
+
1217
+ /**
1218
+ * Creates instructions required to completely update a stake pool after epoch change.
1219
+ */
1220
+ export async function updateStakePool(
1221
+ connection: Connection,
1222
+ stakePool: StakePoolAccount,
1223
+ noMerge = false,
1224
+ ) {
1225
+ const stakePoolAddress = stakePool.pubkey
1226
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1227
+
1228
+ const validatorList = await getValidatorListAccount(
1229
+ connection,
1230
+ stakePool.account.data.validatorList,
1231
+ )
1232
+
1233
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1234
+ stakePoolProgramId,
1235
+ stakePoolAddress,
1236
+ )
1237
+
1238
+ const updateListInstructions: TransactionInstruction[] = []
1239
+ const instructions: TransactionInstruction[] = []
1240
+
1241
+ let startIndex = 0
1242
+ const validatorChunks: Array<ValidatorStakeInfo[]> = arrayChunk(
1243
+ validatorList.account.data.validators,
1244
+ MAX_VALIDATORS_TO_UPDATE,
1245
+ )
1246
+
1247
+ for (const validatorChunk of validatorChunks) {
1248
+ const validatorAndTransientStakePairs: PublicKey[] = []
1249
+
1250
+ for (const validator of validatorChunk) {
1251
+ const validatorStake = await findStakeProgramAddress(
1252
+ stakePoolProgramId,
1253
+ validator.voteAccountAddress,
1254
+ stakePoolAddress,
1255
+ )
1256
+ validatorAndTransientStakePairs.push(validatorStake)
1257
+
1258
+ const transientStake = await findTransientStakeProgramAddress(
1259
+ stakePoolProgramId,
1260
+ validator.voteAccountAddress,
1261
+ stakePoolAddress,
1262
+ validator.transientSeedSuffixStart,
1263
+ )
1264
+ validatorAndTransientStakePairs.push(transientStake)
1265
+ }
1266
+
1267
+ updateListInstructions.push(
1268
+ StakePoolInstruction.updateValidatorListBalance({
1269
+ programId: stakePoolProgramId,
1270
+ stakePool: stakePoolAddress,
1271
+ validatorList: stakePool.account.data.validatorList,
1272
+ reserveStake: stakePool.account.data.reserveStake,
1273
+ validatorAndTransientStakePairs,
1274
+ withdrawAuthority,
1275
+ startIndex,
1276
+ noMerge,
1277
+ }),
1278
+ )
1279
+ startIndex += MAX_VALIDATORS_TO_UPDATE
1280
+ }
1281
+
1282
+ instructions.push(
1283
+ StakePoolInstruction.updateStakePoolBalance({
1284
+ programId: stakePoolProgramId,
1285
+ stakePool: stakePoolAddress,
1286
+ validatorList: stakePool.account.data.validatorList,
1287
+ reserveStake: stakePool.account.data.reserveStake,
1288
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
1289
+ poolMint: stakePool.account.data.poolMint,
1290
+ withdrawAuthority,
1291
+ }),
1292
+ )
1293
+
1294
+ instructions.push(
1295
+ StakePoolInstruction.cleanupRemovedValidatorEntries({
1296
+ programId: stakePoolProgramId,
1297
+ stakePool: stakePoolAddress,
1298
+ validatorList: stakePool.account.data.validatorList,
1299
+ }),
1300
+ )
1301
+
1302
+ return {
1303
+ updateListInstructions,
1304
+ finalInstructions: instructions,
1305
+ }
1306
+ }
1307
+
1308
+ /**
1309
+ * Retrieves detailed information about the StakePool.
1310
+ */
1311
+ export async function stakePoolInfo(
1312
+ connection: Connection,
1313
+ stakePoolAddress: PublicKey,
1314
+ ) {
1315
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1316
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1317
+ const reserveAccountStakeAddress = stakePool.account.data.reserveStake
1318
+ const totalLamports = stakePool.account.data.totalLamports
1319
+ const lastUpdateEpoch = stakePool.account.data.lastUpdateEpoch
1320
+
1321
+ const validatorList = await getValidatorListAccount(
1322
+ connection,
1323
+ stakePool.account.data.validatorList,
1324
+ )
1325
+
1326
+ const maxNumberOfValidators = validatorList.account.data.maxValidators
1327
+ const currentNumberOfValidators
1328
+ = validatorList.account.data.validators.length
1329
+
1330
+ const epochInfo = await connection.getEpochInfo()
1331
+ const reserveStake = await connection.getAccountInfo(
1332
+ reserveAccountStakeAddress,
1333
+ )
1334
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1335
+ stakePoolProgramId,
1336
+ stakePoolAddress,
1337
+ )
1338
+
1339
+ const minimumReserveStakeBalance
1340
+ = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
1341
+
1342
+ const stakeAccounts = await Promise.all(
1343
+ validatorList.account.data.validators.map(async (validator) => {
1344
+ const stakeAccountAddress = await findStakeProgramAddress(
1345
+ stakePoolProgramId,
1346
+ validator.voteAccountAddress,
1347
+ stakePoolAddress,
1348
+ )
1349
+ const transientStakeAccountAddress
1350
+ = await findTransientStakeProgramAddress(
1351
+ stakePoolProgramId,
1352
+ validator.voteAccountAddress,
1353
+ stakePoolAddress,
1354
+ validator.transientSeedSuffixStart,
1355
+ )
1356
+ const updateRequired = !validator.lastUpdateEpoch.eqn(epochInfo.epoch)
1357
+ return {
1358
+ voteAccountAddress: validator.voteAccountAddress.toBase58(),
1359
+ stakeAccountAddress: stakeAccountAddress.toBase58(),
1360
+ validatorActiveStakeLamports: validator.activeStakeLamports.toString(),
1361
+ validatorLastUpdateEpoch: validator.lastUpdateEpoch.toString(),
1362
+ validatorLamports: validator.activeStakeLamports
1363
+ .add(validator.transientStakeLamports)
1364
+ .toString(),
1365
+ validatorTransientStakeAccountAddress:
1366
+ transientStakeAccountAddress.toBase58(),
1367
+ validatorTransientStakeLamports:
1368
+ validator.transientStakeLamports.toString(),
1369
+ updateRequired,
1370
+ }
1371
+ }),
1372
+ )
1373
+
1374
+ const totalPoolTokens = lamportsToSol(stakePool.account.data.poolTokenSupply)
1375
+ const updateRequired = !lastUpdateEpoch.eqn(epochInfo.epoch)
1376
+
1377
+ return {
1378
+ address: stakePoolAddress.toBase58(),
1379
+ poolWithdrawAuthority: withdrawAuthority.toBase58(),
1380
+ manager: stakePool.account.data.manager.toBase58(),
1381
+ staker: stakePool.account.data.staker.toBase58(),
1382
+ stakeDepositAuthority:
1383
+ stakePool.account.data.stakeDepositAuthority.toBase58(),
1384
+ stakeWithdrawBumpSeed: stakePool.account.data.stakeWithdrawBumpSeed,
1385
+ maxValidators: maxNumberOfValidators,
1386
+ validatorList: validatorList.account.data.validators.map((validator) => {
1387
+ return {
1388
+ activeStakeLamports: validator.activeStakeLamports.toString(),
1389
+ transientStakeLamports: validator.transientStakeLamports.toString(),
1390
+ lastUpdateEpoch: validator.lastUpdateEpoch.toString(),
1391
+ transientSeedSuffixStart: validator.transientSeedSuffixStart.toString(),
1392
+ transientSeedSuffixEnd: validator.transientSeedSuffixEnd.toString(),
1393
+ status: validator.status.toString(),
1394
+ voteAccountAddress: validator.voteAccountAddress.toString(),
1395
+ }
1396
+ }), // CliStakePoolValidator
1397
+ validatorListStorageAccount:
1398
+ stakePool.account.data.validatorList.toBase58(),
1399
+ reserveStake: stakePool.account.data.reserveStake.toBase58(),
1400
+ poolMint: stakePool.account.data.poolMint.toBase58(),
1401
+ managerFeeAccount: stakePool.account.data.managerFeeAccount.toBase58(),
1402
+ tokenProgramId: stakePool.account.data.tokenProgramId.toBase58(),
1403
+ totalLamports: stakePool.account.data.totalLamports.toString(),
1404
+ poolTokenSupply: stakePool.account.data.poolTokenSupply.toString(),
1405
+ lastUpdateEpoch: stakePool.account.data.lastUpdateEpoch.toString(),
1406
+ lockup: stakePool.account.data.lockup, // pub lockup: CliStakePoolLockup
1407
+ epochFee: stakePool.account.data.epochFee,
1408
+ nextEpochFee: stakePool.account.data.nextEpochFee,
1409
+ preferredDepositValidatorVoteAddress:
1410
+ stakePool.account.data.preferredDepositValidatorVoteAddress,
1411
+ preferredWithdrawValidatorVoteAddress:
1412
+ stakePool.account.data.preferredWithdrawValidatorVoteAddress,
1413
+ stakeDepositFee: stakePool.account.data.stakeDepositFee,
1414
+ stakeWithdrawalFee: stakePool.account.data.stakeWithdrawalFee,
1415
+ // CliStakePool the same
1416
+ nextStakeWithdrawalFee: stakePool.account.data.nextStakeWithdrawalFee,
1417
+ stakeReferralFee: stakePool.account.data.stakeReferralFee,
1418
+ solDepositAuthority: stakePool.account.data.solDepositAuthority?.toBase58(),
1419
+ solDepositFee: stakePool.account.data.solDepositFee,
1420
+ solReferralFee: stakePool.account.data.solReferralFee,
1421
+ solWithdrawAuthority:
1422
+ stakePool.account.data.solWithdrawAuthority?.toBase58(),
1423
+ solWithdrawalFee: stakePool.account.data.solWithdrawalFee,
1424
+ nextSolWithdrawalFee: stakePool.account.data.nextSolWithdrawalFee,
1425
+ lastEpochPoolTokenSupply:
1426
+ stakePool.account.data.lastEpochPoolTokenSupply.toString(),
1427
+ lastEpochTotalLamports:
1428
+ stakePool.account.data.lastEpochTotalLamports.toString(),
1429
+ details: {
1430
+ reserveStakeLamports: reserveStake?.lamports,
1431
+ reserveAccountStakeAddress: reserveAccountStakeAddress.toBase58(),
1432
+ minimumReserveStakeBalance,
1433
+ stakeAccounts,
1434
+ totalLamports,
1435
+ totalPoolTokens,
1436
+ currentNumberOfValidators,
1437
+ maxNumberOfValidators,
1438
+ updateRequired,
1439
+ }, // CliStakePoolDetails
1440
+ }
1441
+ }
1442
+
1443
+ /**
1444
+ * Creates instructions required to create pool token metadata.
1445
+ */
1446
+ export async function createPoolTokenMetadata(
1447
+ connection: Connection,
1448
+ stakePoolAddress: PublicKey,
1449
+ payer: PublicKey,
1450
+ name: string,
1451
+ symbol: string,
1452
+ uri: string,
1453
+ ) {
1454
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1455
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1456
+
1457
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1458
+ stakePoolProgramId,
1459
+ stakePoolAddress,
1460
+ )
1461
+ const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint)
1462
+ const manager = stakePool.account.data.manager
1463
+
1464
+ const instructions: TransactionInstruction[] = []
1465
+ instructions.push(
1466
+ StakePoolInstruction.createTokenMetadata({
1467
+ programId: stakePoolProgramId,
1468
+ stakePool: stakePoolAddress,
1469
+ poolMint: stakePool.account.data.poolMint,
1470
+ payer,
1471
+ manager,
1472
+ tokenMetadata,
1473
+ withdrawAuthority,
1474
+ name,
1475
+ symbol,
1476
+ uri,
1477
+ }),
1478
+ )
1479
+
1480
+ return {
1481
+ instructions,
1482
+ }
1483
+ }
1484
+
1485
+ /**
1486
+ * Creates instructions required to update pool token metadata.
1487
+ */
1488
+ export async function updatePoolTokenMetadata(
1489
+ connection: Connection,
1490
+ stakePoolAddress: PublicKey,
1491
+ name: string,
1492
+ symbol: string,
1493
+ uri: string,
1494
+ ) {
1495
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1496
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1497
+
1498
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1499
+ stakePoolProgramId,
1500
+ stakePoolAddress,
1501
+ )
1502
+
1503
+ const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint)
1504
+
1505
+ const instructions: TransactionInstruction[] = []
1506
+ instructions.push(
1507
+ StakePoolInstruction.updateTokenMetadata({
1508
+ programId: stakePoolProgramId,
1509
+ stakePool: stakePoolAddress,
1510
+ manager: stakePool.account.data.manager,
1511
+ tokenMetadata,
1512
+ withdrawAuthority,
1513
+ name,
1514
+ symbol,
1515
+ uri,
1516
+ }),
1517
+ )
1518
+
1519
+ return {
1520
+ instructions,
1521
+ }
1522
+ }