@ignitionfi/fogo-stake-pool 1.0.0

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,1823 @@
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
+ findUserStakeProgramAddress,
44
+ findWithdrawAuthorityProgramAddress,
45
+ findWsolTransientProgramAddress,
46
+ getValidatorListAccount,
47
+ lamportsToSol,
48
+ newStakeAccount,
49
+ prepareWithdrawAccounts,
50
+ solToLamports,
51
+ ValidatorAccount,
52
+ } from './utils'
53
+
54
+ export {
55
+ DEVNET_STAKE_POOL_PROGRAM_ID,
56
+ STAKE_POOL_PROGRAM_ID,
57
+ } from './constants'
58
+ export * from './instructions'
59
+ export type {
60
+ AccountType,
61
+ StakePool,
62
+ ValidatorList,
63
+ ValidatorStakeInfo,
64
+ } from './layouts'
65
+ export {
66
+ StakePoolLayout,
67
+ ValidatorListLayout,
68
+ ValidatorStakeInfoLayout,
69
+ } from './layouts'
70
+ export {
71
+ findEphemeralStakeProgramAddress,
72
+ findStakeProgramAddress,
73
+ findTransientStakeProgramAddress,
74
+ findUserStakeProgramAddress,
75
+ findWithdrawAuthorityProgramAddress,
76
+ findWsolTransientProgramAddress,
77
+ } from './utils'
78
+
79
+ export interface ValidatorListAccount {
80
+ pubkey: PublicKey
81
+ account: AccountInfo<ValidatorList>
82
+ }
83
+
84
+ export interface StakePoolAccount {
85
+ pubkey: PublicKey
86
+ account: AccountInfo<StakePool>
87
+ }
88
+
89
+ export interface WithdrawAccount {
90
+ stakeAddress: PublicKey
91
+ voteAddress?: PublicKey
92
+ poolAmount: BN
93
+ }
94
+
95
+ /**
96
+ * Wrapper class for a stake pool.
97
+ * Each stake pool has a stake pool account and a validator list account.
98
+ */
99
+ export interface StakePoolAccounts {
100
+ stakePool: StakePoolAccount | undefined
101
+ validatorList: ValidatorListAccount | undefined
102
+ }
103
+
104
+ export function getStakePoolProgramId(rpcEndpoint: string): PublicKey {
105
+ if (rpcEndpoint.includes('devnet')) {
106
+ return DEVNET_STAKE_POOL_PROGRAM_ID
107
+ } else {
108
+ return STAKE_POOL_PROGRAM_ID
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Retrieves and deserializes a StakePool account using a web3js connection and the stake pool address.
114
+ * @param connection An active web3js connection.
115
+ * @param stakePoolAddress The public key (address) of the stake pool account.
116
+ */
117
+ export async function getStakePoolAccount(
118
+ connection: Connection,
119
+ stakePoolAddress: PublicKey,
120
+ ): Promise<StakePoolAccount> {
121
+ const account = await connection.getAccountInfo(stakePoolAddress)
122
+
123
+ if (!account) {
124
+ throw new Error('Invalid stake pool account')
125
+ }
126
+
127
+ return {
128
+ pubkey: stakePoolAddress,
129
+ account: {
130
+ data: StakePoolLayout.decode(account.data),
131
+ executable: account.executable,
132
+ lamports: account.lamports,
133
+ owner: account.owner,
134
+ },
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Retrieves and deserializes a Stake account using a web3js connection and the stake address.
140
+ * @param connection An active web3js connection.
141
+ * @param stakeAccount The public key (address) of the stake account.
142
+ */
143
+ export async function getStakeAccount(
144
+ connection: Connection,
145
+ stakeAccount: PublicKey,
146
+ ): Promise<StakeAccount> {
147
+ const result = (await connection.getParsedAccountInfo(stakeAccount)).value
148
+ if (!result || !('parsed' in result.data)) {
149
+ throw new Error('Invalid stake account')
150
+ }
151
+ const program = result.data.program
152
+ if (program !== 'stake') {
153
+ throw new Error('Not a stake account')
154
+ }
155
+ return create(result.data.parsed, StakeAccount)
156
+ }
157
+
158
+ /**
159
+ * Retrieves all StakePool and ValidatorList accounts that are running a particular StakePool program.
160
+ * @param connection An active web3js connection.
161
+ * @param stakePoolProgramAddress The public key (address) of the StakePool program.
162
+ */
163
+ export async function getStakePoolAccounts(
164
+ connection: Connection,
165
+ stakePoolProgramAddress: PublicKey,
166
+ ): Promise<
167
+ (StakePoolAccount | ValidatorListAccount | undefined)[] | undefined
168
+ > {
169
+ const response = await connection.getProgramAccounts(stakePoolProgramAddress)
170
+
171
+ return response
172
+ .map((a) => {
173
+ try {
174
+ if (a.account.data.readUInt8() === 1) {
175
+ const data = StakePoolLayout.decode(a.account.data)
176
+ return {
177
+ pubkey: a.pubkey,
178
+ account: {
179
+ data,
180
+ executable: a.account.executable,
181
+ lamports: a.account.lamports,
182
+ owner: a.account.owner,
183
+ },
184
+ }
185
+ } else if (a.account.data.readUInt8() === 2) {
186
+ const data = ValidatorListLayout.decode(a.account.data)
187
+ return {
188
+ pubkey: a.pubkey,
189
+ account: {
190
+ data,
191
+ executable: a.account.executable,
192
+ lamports: a.account.lamports,
193
+ owner: a.account.owner,
194
+ },
195
+ }
196
+ } else {
197
+ console.error(
198
+ `Could not decode. StakePoolAccount Enum is ${a.account.data.readUInt8()}, expected 1 or 2!`,
199
+ )
200
+ return undefined
201
+ }
202
+ } catch (error) {
203
+ console.error('Could not decode account. Error:', error)
204
+ return undefined
205
+ }
206
+ })
207
+ .filter(a => a !== undefined)
208
+ }
209
+
210
+ /**
211
+ * Creates instructions required to deposit stake to stake pool.
212
+ */
213
+ export async function depositStake(
214
+ connection: Connection,
215
+ stakePoolAddress: PublicKey,
216
+ authorizedPubkey: PublicKey,
217
+ validatorVote: PublicKey,
218
+ depositStake: PublicKey,
219
+ poolTokenReceiverAccount?: PublicKey,
220
+ ) {
221
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
222
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
223
+
224
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
225
+ stakePoolProgramId,
226
+ stakePoolAddress,
227
+ )
228
+
229
+ const validatorStake = await findStakeProgramAddress(
230
+ stakePoolProgramId,
231
+ validatorVote,
232
+ stakePoolAddress,
233
+ )
234
+
235
+ const instructions: TransactionInstruction[] = []
236
+ const signers: Signer[] = []
237
+
238
+ const poolMint = stakePool.account.data.poolMint
239
+
240
+ // Create token account if not specified
241
+ if (!poolTokenReceiverAccount) {
242
+ const associatedAddress = getAssociatedTokenAddressSync(
243
+ poolMint,
244
+ authorizedPubkey,
245
+ )
246
+ instructions.push(
247
+ createAssociatedTokenAccountIdempotentInstruction(
248
+ authorizedPubkey,
249
+ associatedAddress,
250
+ authorizedPubkey,
251
+ poolMint,
252
+ ),
253
+ )
254
+ poolTokenReceiverAccount = associatedAddress
255
+ }
256
+
257
+ instructions.push(
258
+ ...StakeProgram.authorize({
259
+ stakePubkey: depositStake,
260
+ authorizedPubkey,
261
+ newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority,
262
+ stakeAuthorizationType: StakeAuthorizationLayout.Staker,
263
+ }).instructions,
264
+ )
265
+
266
+ instructions.push(
267
+ ...StakeProgram.authorize({
268
+ stakePubkey: depositStake,
269
+ authorizedPubkey,
270
+ newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority,
271
+ stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer,
272
+ }).instructions,
273
+ )
274
+
275
+ instructions.push(
276
+ StakePoolInstruction.depositStake({
277
+ programId: stakePoolProgramId,
278
+ stakePool: stakePoolAddress,
279
+ validatorList: stakePool.account.data.validatorList,
280
+ depositAuthority: stakePool.account.data.stakeDepositAuthority,
281
+ reserveStake: stakePool.account.data.reserveStake,
282
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
283
+ referralPoolAccount: poolTokenReceiverAccount,
284
+ destinationPoolAccount: poolTokenReceiverAccount,
285
+ withdrawAuthority,
286
+ depositStake,
287
+ validatorStake,
288
+ poolMint,
289
+ }),
290
+ )
291
+
292
+ return {
293
+ instructions,
294
+ signers,
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Creates instructions required to deposit sol to stake pool.
300
+ */
301
+ export async function depositWsolWithSession(
302
+ connection: Connection,
303
+ stakePoolAddress: PublicKey,
304
+ signerOrSession: PublicKey,
305
+ userPubkey: PublicKey,
306
+ lamports: number,
307
+ minimumPoolTokensOut: number = 0,
308
+ destinationTokenAccount?: PublicKey,
309
+ referrerTokenAccount?: PublicKey,
310
+ depositAuthority?: PublicKey,
311
+ payer?: PublicKey,
312
+ /**
313
+ * Skip WSOL balance validation. Set to true when adding wrap instructions
314
+ * in the same transaction that will fund the WSOL account before deposit.
315
+ */
316
+ skipBalanceCheck: boolean = false,
317
+ ) {
318
+ const wsolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, userPubkey)
319
+
320
+ if (!skipBalanceCheck) {
321
+ const tokenAccountInfo = await connection.getTokenAccountBalance(
322
+ wsolTokenAccount,
323
+ 'confirmed',
324
+ )
325
+ const wsolBalance = tokenAccountInfo
326
+ ? parseInt(tokenAccountInfo.value.amount)
327
+ : 0
328
+
329
+ if (wsolBalance < lamports) {
330
+ throw new Error(
331
+ `Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
332
+ wsolBalance,
333
+ )} WSOL.`,
334
+ )
335
+ }
336
+ }
337
+
338
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
339
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
340
+ const stakePool = stakePoolAccount.account.data
341
+
342
+ const instructions: TransactionInstruction[] = []
343
+
344
+ // The program handles ATA creation internally using funds from the user's deposit
345
+ // This prevents rent drain attacks where paymaster pays for ATA and user reclaims rent
346
+ if (!destinationTokenAccount) {
347
+ destinationTokenAccount = getAssociatedTokenAddressSync(
348
+ stakePool.poolMint,
349
+ userPubkey,
350
+ )
351
+ }
352
+
353
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
354
+ stakePoolProgramId,
355
+ stakePoolAddress,
356
+ )
357
+
358
+ const [programSigner] = PublicKey.findProgramAddressSync(
359
+ [Buffer.from('fogo_session_program_signer')],
360
+ stakePoolProgramId,
361
+ )
362
+
363
+ const wsolTransientAccount = findWsolTransientProgramAddress(
364
+ stakePoolProgramId,
365
+ userPubkey,
366
+ )
367
+
368
+ instructions.push(
369
+ StakePoolInstruction.depositWsolWithSession({
370
+ programId: stakePoolProgramId,
371
+ stakePool: stakePoolAddress,
372
+ reserveStake: stakePool.reserveStake,
373
+ fundingAccount: signerOrSession,
374
+ destinationPoolAccount: destinationTokenAccount,
375
+ managerFeeAccount: stakePool.managerFeeAccount,
376
+ referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount,
377
+ poolMint: stakePool.poolMint,
378
+ lamportsIn: lamports,
379
+ minimumPoolTokensOut,
380
+ withdrawAuthority,
381
+ depositAuthority,
382
+ wsolMint: NATIVE_MINT,
383
+ wsolTokenAccount,
384
+ wsolTransientAccount,
385
+ tokenProgramId: stakePool.tokenProgramId,
386
+ programSigner,
387
+ payer,
388
+ userWallet: userPubkey,
389
+ }),
390
+ )
391
+ return {
392
+ instructions,
393
+ signers: [],
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Creates instructions required to deposit sol to stake pool.
399
+ */
400
+ export async function depositSol(
401
+ connection: Connection,
402
+ stakePoolAddress: PublicKey,
403
+ from: PublicKey,
404
+ lamports: number,
405
+ destinationTokenAccount?: PublicKey,
406
+ referrerTokenAccount?: PublicKey,
407
+ depositAuthority?: PublicKey,
408
+ ) {
409
+ const fromBalance = await connection.getBalance(from, 'confirmed')
410
+ if (fromBalance < lamports) {
411
+ throw new Error(
412
+ `Not enough SOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
413
+ fromBalance,
414
+ )} SOL.`,
415
+ )
416
+ }
417
+
418
+ const stakePoolAccount = await getStakePoolAccount(
419
+ connection,
420
+ stakePoolAddress,
421
+ )
422
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
423
+ const stakePool = stakePoolAccount.account.data
424
+
425
+ // Ephemeral SOL account just to do the transfer
426
+ const userSolTransfer = new Keypair()
427
+ const signers: Signer[] = [userSolTransfer]
428
+ const instructions: TransactionInstruction[] = []
429
+
430
+ // Create the ephemeral SOL account
431
+ instructions.push(
432
+ SystemProgram.transfer({
433
+ fromPubkey: from,
434
+ toPubkey: userSolTransfer.publicKey,
435
+ lamports,
436
+ }),
437
+ )
438
+
439
+ // Create token account if not specified
440
+ if (!destinationTokenAccount) {
441
+ const associatedAddress = getAssociatedTokenAddressSync(
442
+ stakePool.poolMint,
443
+ from,
444
+ )
445
+ instructions.push(
446
+ createAssociatedTokenAccountIdempotentInstruction(
447
+ from,
448
+ associatedAddress,
449
+ from,
450
+ stakePool.poolMint,
451
+ ),
452
+ )
453
+ destinationTokenAccount = associatedAddress
454
+ }
455
+
456
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
457
+ stakePoolProgramId,
458
+ stakePoolAddress,
459
+ )
460
+
461
+ instructions.push(
462
+ StakePoolInstruction.depositSol({
463
+ programId: stakePoolProgramId,
464
+ stakePool: stakePoolAddress,
465
+ reserveStake: stakePool.reserveStake,
466
+ fundingAccount: userSolTransfer.publicKey,
467
+ destinationPoolAccount: destinationTokenAccount,
468
+ managerFeeAccount: stakePool.managerFeeAccount,
469
+ referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount,
470
+ poolMint: stakePool.poolMint,
471
+ lamports,
472
+ withdrawAuthority,
473
+ depositAuthority,
474
+ }),
475
+ )
476
+
477
+ return {
478
+ instructions,
479
+ signers,
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Creates instructions required to withdraw stake from a stake pool.
485
+ */
486
+ export async function withdrawStake(
487
+ connection: Connection,
488
+ stakePoolAddress: PublicKey,
489
+ tokenOwner: PublicKey,
490
+ amount: number,
491
+ useReserve = false,
492
+ voteAccountAddress?: PublicKey,
493
+ stakeReceiver?: PublicKey,
494
+ poolTokenAccount?: PublicKey,
495
+ validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
496
+ ) {
497
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
498
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
499
+ const poolAmount = new BN(solToLamports(amount))
500
+
501
+ if (!poolTokenAccount) {
502
+ poolTokenAccount = getAssociatedTokenAddressSync(
503
+ stakePool.account.data.poolMint,
504
+ tokenOwner,
505
+ )
506
+ }
507
+
508
+ const tokenAccount = await getAccount(connection, poolTokenAccount)
509
+
510
+ // Check withdrawFrom balance
511
+ if (tokenAccount.amount < poolAmount.toNumber()) {
512
+ throw new Error(
513
+ `Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
514
+ Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
515
+ )
516
+ }
517
+
518
+ const stakeAccountRentExemption
519
+ = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
520
+
521
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
522
+ stakePoolProgramId,
523
+ stakePoolAddress,
524
+ )
525
+
526
+ let stakeReceiverAccount = null
527
+ if (stakeReceiver) {
528
+ stakeReceiverAccount = await getStakeAccount(connection, stakeReceiver)
529
+ }
530
+
531
+ const withdrawAccounts: WithdrawAccount[] = []
532
+
533
+ if (useReserve) {
534
+ withdrawAccounts.push({
535
+ stakeAddress: stakePool.account.data.reserveStake,
536
+ voteAddress: undefined,
537
+ poolAmount,
538
+ })
539
+ } else if (
540
+ stakeReceiverAccount
541
+ && stakeReceiverAccount?.type === 'delegated'
542
+ ) {
543
+ const voteAccount = stakeReceiverAccount.info?.stake?.delegation.voter
544
+ if (!voteAccount) {
545
+ throw new Error(`Invalid stake receiver ${stakeReceiver} delegation`)
546
+ }
547
+ const validatorListAccount = await connection.getAccountInfo(
548
+ stakePool.account.data.validatorList,
549
+ )
550
+ const validatorList = ValidatorListLayout.decode(
551
+ validatorListAccount?.data,
552
+ ) as ValidatorList
553
+ const isValidVoter = validatorList.validators.find(val =>
554
+ val.voteAccountAddress.equals(voteAccount),
555
+ )
556
+ if (voteAccountAddress && voteAccountAddress !== voteAccount) {
557
+ throw new Error(`Provided withdrawal vote account ${voteAccountAddress} does not match delegation on stake receiver account ${voteAccount},
558
+ remove this flag or provide a different stake account delegated to ${voteAccountAddress}`)
559
+ }
560
+ if (isValidVoter) {
561
+ const stakeAccountAddress = await findStakeProgramAddress(
562
+ stakePoolProgramId,
563
+ voteAccount,
564
+ stakePoolAddress,
565
+ )
566
+
567
+ const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
568
+ if (!stakeAccount) {
569
+ throw new Error(
570
+ `Preferred withdraw valdator's stake account is invalid`,
571
+ )
572
+ }
573
+
574
+ const availableForWithdrawal = calcLamportsWithdrawAmount(
575
+ stakePool.account.data,
576
+ new BN(
577
+ stakeAccount.lamports
578
+ - MINIMUM_ACTIVE_STAKE
579
+ - stakeAccountRentExemption,
580
+ ),
581
+ )
582
+
583
+ if (availableForWithdrawal.lt(poolAmount)) {
584
+ throw new Error(
585
+ `Not enough lamports available for withdrawal from ${stakeAccountAddress},
586
+ ${poolAmount} asked, ${availableForWithdrawal} available.`,
587
+ )
588
+ }
589
+ withdrawAccounts.push({
590
+ stakeAddress: stakeAccountAddress,
591
+ voteAddress: voteAccount,
592
+ poolAmount,
593
+ })
594
+ } else {
595
+ throw new Error(
596
+ `Provided stake account is delegated to a vote account ${voteAccount} which does not exist in the stake pool`,
597
+ )
598
+ }
599
+ } else if (voteAccountAddress) {
600
+ const stakeAccountAddress = await findStakeProgramAddress(
601
+ stakePoolProgramId,
602
+ voteAccountAddress,
603
+ stakePoolAddress,
604
+ )
605
+ const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
606
+ if (!stakeAccount) {
607
+ throw new Error('Invalid Stake Account')
608
+ }
609
+
610
+ const availableLamports = new BN(
611
+ stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
612
+ )
613
+ if (availableLamports.lt(new BN(0))) {
614
+ throw new Error('Invalid Stake Account')
615
+ }
616
+ const availableForWithdrawal = calcLamportsWithdrawAmount(
617
+ stakePool.account.data,
618
+ availableLamports,
619
+ )
620
+
621
+ if (availableForWithdrawal.lt(poolAmount)) {
622
+ // noinspection ExceptionCaughtLocallyJS
623
+ throw new Error(
624
+ `Not enough lamports available for withdrawal from ${stakeAccountAddress},
625
+ ${poolAmount} asked, ${availableForWithdrawal} available.`,
626
+ )
627
+ }
628
+ withdrawAccounts.push({
629
+ stakeAddress: stakeAccountAddress,
630
+ voteAddress: voteAccountAddress,
631
+ poolAmount,
632
+ })
633
+ } else {
634
+ // Get the list of accounts to withdraw from
635
+ withdrawAccounts.push(
636
+ ...(await prepareWithdrawAccounts(
637
+ connection,
638
+ stakePool.account.data,
639
+ stakePoolAddress,
640
+ poolAmount,
641
+ validatorComparator,
642
+ poolTokenAccount.equals(stakePool.account.data.managerFeeAccount),
643
+ )),
644
+ )
645
+ }
646
+
647
+ // Construct transaction to withdraw from withdrawAccounts account list
648
+ const instructions: TransactionInstruction[] = []
649
+ const userTransferAuthority = Keypair.generate()
650
+
651
+ const signers: Signer[] = [userTransferAuthority]
652
+
653
+ instructions.push(
654
+ createApproveInstruction(
655
+ poolTokenAccount,
656
+ userTransferAuthority.publicKey,
657
+ tokenOwner,
658
+ poolAmount.toNumber(),
659
+ ),
660
+ )
661
+
662
+ let totalRentFreeBalances = 0
663
+
664
+ // Max 5 accounts to prevent an error: "Transaction too large"
665
+ const maxWithdrawAccounts = 5
666
+ let i = 0
667
+
668
+ // Go through prepared accounts and withdraw/claim them
669
+ for (const withdrawAccount of withdrawAccounts) {
670
+ if (i > maxWithdrawAccounts) {
671
+ break
672
+ }
673
+ // Convert pool tokens amount to lamports
674
+ const solWithdrawAmount = calcLamportsWithdrawAmount(
675
+ stakePool.account.data,
676
+ withdrawAccount.poolAmount,
677
+ )
678
+
679
+ let infoMsg = `Withdrawing ◎${solWithdrawAmount},
680
+ from stake account ${withdrawAccount.stakeAddress?.toBase58()}`
681
+
682
+ if (withdrawAccount.voteAddress) {
683
+ infoMsg = `${infoMsg}, delegated to ${withdrawAccount.voteAddress?.toBase58()}`
684
+ }
685
+
686
+ console.info(infoMsg)
687
+ let stakeToReceive
688
+
689
+ if (
690
+ !stakeReceiver
691
+ || (stakeReceiverAccount && stakeReceiverAccount.type === 'delegated')
692
+ ) {
693
+ const stakeKeypair = newStakeAccount(
694
+ tokenOwner,
695
+ instructions,
696
+ stakeAccountRentExemption,
697
+ )
698
+ signers.push(stakeKeypair)
699
+ totalRentFreeBalances += stakeAccountRentExemption
700
+ stakeToReceive = stakeKeypair.publicKey
701
+ } else {
702
+ stakeToReceive = stakeReceiver
703
+ }
704
+
705
+ instructions.push(
706
+ StakePoolInstruction.withdrawStake({
707
+ programId: stakePoolProgramId,
708
+ stakePool: stakePoolAddress,
709
+ validatorList: stakePool.account.data.validatorList,
710
+ validatorStake: withdrawAccount.stakeAddress,
711
+ destinationStake: stakeToReceive,
712
+ destinationStakeAuthority: tokenOwner,
713
+ sourceTransferAuthority: userTransferAuthority.publicKey,
714
+ sourcePoolAccount: poolTokenAccount,
715
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
716
+ poolMint: stakePool.account.data.poolMint,
717
+ poolTokens: withdrawAccount.poolAmount.toNumber(),
718
+ withdrawAuthority,
719
+ }),
720
+ )
721
+ i++
722
+ }
723
+ if (
724
+ stakeReceiver
725
+ && stakeReceiverAccount
726
+ && stakeReceiverAccount.type === 'delegated'
727
+ ) {
728
+ signers.forEach((newStakeKeypair) => {
729
+ instructions.concat(
730
+ StakeProgram.merge({
731
+ stakePubkey: stakeReceiver,
732
+ sourceStakePubKey: newStakeKeypair.publicKey,
733
+ authorizedPubkey: tokenOwner,
734
+ }).instructions,
735
+ )
736
+ })
737
+ }
738
+
739
+ return {
740
+ instructions,
741
+ signers,
742
+ stakeReceiver,
743
+ totalRentFreeBalances,
744
+ }
745
+ }
746
+
747
+ /**
748
+ * Creates instructions required to withdraw SOL directly from a stake pool.
749
+ */
750
+ export async function withdrawSol(
751
+ connection: Connection,
752
+ stakePoolAddress: PublicKey,
753
+ tokenOwner: PublicKey,
754
+ solReceiver: PublicKey,
755
+ amount: number,
756
+ solWithdrawAuthority?: PublicKey,
757
+ ) {
758
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
759
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
760
+ const poolAmount = solToLamports(amount)
761
+
762
+ const poolTokenAccount = getAssociatedTokenAddressSync(
763
+ stakePool.account.data.poolMint,
764
+ tokenOwner,
765
+ )
766
+
767
+ const tokenAccount = await getAccount(connection, poolTokenAccount)
768
+
769
+ // Check withdrawFrom balance
770
+ if (tokenAccount.amount < poolAmount) {
771
+ throw new Error(
772
+ `Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
773
+ Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
774
+ )
775
+ }
776
+
777
+ // Construct transaction to withdraw from withdrawAccounts account list
778
+ const instructions: TransactionInstruction[] = []
779
+ const userTransferAuthority = Keypair.generate()
780
+ const signers: Signer[] = [userTransferAuthority]
781
+
782
+ instructions.push(
783
+ createApproveInstruction(
784
+ poolTokenAccount,
785
+ userTransferAuthority.publicKey,
786
+ tokenOwner,
787
+ poolAmount,
788
+ ),
789
+ )
790
+
791
+ const poolWithdrawAuthority = await findWithdrawAuthorityProgramAddress(
792
+ stakePoolProgramId,
793
+ stakePoolAddress,
794
+ )
795
+
796
+ if (solWithdrawAuthority) {
797
+ const expectedSolWithdrawAuthority
798
+ = stakePool.account.data.solWithdrawAuthority
799
+ if (!expectedSolWithdrawAuthority) {
800
+ throw new Error(
801
+ 'SOL withdraw authority specified in arguments but stake pool has none',
802
+ )
803
+ }
804
+ if (
805
+ solWithdrawAuthority.toBase58() !== expectedSolWithdrawAuthority.toBase58()
806
+ ) {
807
+ throw new Error(
808
+ `Invalid deposit withdraw specified, expected ${expectedSolWithdrawAuthority.toBase58()}, received ${solWithdrawAuthority.toBase58()}`,
809
+ )
810
+ }
811
+ }
812
+
813
+ const withdrawTransaction = StakePoolInstruction.withdrawSol({
814
+ programId: stakePoolProgramId,
815
+ stakePool: stakePoolAddress,
816
+ withdrawAuthority: poolWithdrawAuthority,
817
+ reserveStake: stakePool.account.data.reserveStake,
818
+ sourcePoolAccount: poolTokenAccount,
819
+ sourceTransferAuthority: userTransferAuthority.publicKey,
820
+ destinationSystemAccount: solReceiver,
821
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
822
+ poolMint: stakePool.account.data.poolMint,
823
+ poolTokens: poolAmount,
824
+ solWithdrawAuthority,
825
+ })
826
+
827
+ instructions.push(withdrawTransaction)
828
+
829
+ return {
830
+ instructions,
831
+ signers,
832
+ }
833
+ }
834
+
835
+ /**
836
+ * Creates instructions required to withdraw wSOL from a stake pool.
837
+ * Rent for ATA creation (if needed) is paid from the withdrawal amount.
838
+ */
839
+ export async function withdrawWsolWithSession(
840
+ connection: Connection,
841
+ stakePoolAddress: PublicKey,
842
+ signerOrSession: PublicKey,
843
+ userPubkey: PublicKey,
844
+ amount: number,
845
+ minimumLamportsOut: number = 0,
846
+ solWithdrawAuthority?: PublicKey,
847
+ ) {
848
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
849
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
850
+ const stakePool = stakePoolAccount.account.data
851
+ const poolTokens = solToLamports(amount)
852
+
853
+ const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey)
854
+ const tokenAccount = await getAccount(connection, poolTokenAccount)
855
+
856
+ if (tokenAccount.amount < poolTokens) {
857
+ throw new Error(
858
+ `Not enough token balance to withdraw ${amount} pool tokens.
859
+ Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
860
+ )
861
+ }
862
+
863
+ const userWsolAccount = getAssociatedTokenAddressSync(NATIVE_MINT, userPubkey)
864
+
865
+ const instructions: TransactionInstruction[] = []
866
+ const signers: Signer[] = []
867
+
868
+ // The program handles wSOL ATA creation internally
869
+ // This prevents rent drain attacks where paymaster pays for ATA and user reclaims rent
870
+
871
+ const [programSigner] = PublicKey.findProgramAddressSync(
872
+ [Buffer.from('fogo_session_program_signer')],
873
+ stakePoolProgramId,
874
+ )
875
+
876
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
877
+ stakePoolProgramId,
878
+ stakePoolAddress,
879
+ )
880
+
881
+ instructions.push(
882
+ StakePoolInstruction.withdrawWsolWithSession({
883
+ programId: stakePoolProgramId,
884
+ stakePool: stakePoolAddress,
885
+ withdrawAuthority,
886
+ userTransferAuthority: signerOrSession,
887
+ poolTokensFrom: poolTokenAccount,
888
+ reserveStake: stakePool.reserveStake,
889
+ userWsolAccount,
890
+ managerFeeAccount: stakePool.managerFeeAccount,
891
+ poolMint: stakePool.poolMint,
892
+ tokenProgramId: stakePool.tokenProgramId,
893
+ solWithdrawAuthority,
894
+ wsolMint: NATIVE_MINT,
895
+ programSigner,
896
+ userWallet: userPubkey,
897
+ poolTokensIn: poolTokens,
898
+ minimumLamportsOut,
899
+ }),
900
+ )
901
+
902
+ return {
903
+ instructions,
904
+ signers,
905
+ }
906
+ }
907
+
908
+ /**
909
+ * Finds the next available seed for creating a user stake PDA.
910
+ * Scans from startSeed until an unused PDA is found.
911
+ *
912
+ * @param connection - Solana connection
913
+ * @param programId - The stake pool program ID
914
+ * @param userPubkey - User's wallet (used for PDA derivation)
915
+ * @param startSeed - Starting seed to search from (default: 0)
916
+ * @param maxSeed - Maximum seed to check before giving up (default: 1000)
917
+ * @returns The next available seed
918
+ * @throws Error if no available seed found within maxSeed
919
+ */
920
+ export async function findNextUserStakeSeed(
921
+ connection: Connection,
922
+ programId: PublicKey,
923
+ userPubkey: PublicKey,
924
+ startSeed: number = 0,
925
+ maxSeed: number = 1000,
926
+ ): Promise<number> {
927
+ for (let seed = startSeed; seed < startSeed + maxSeed; seed++) {
928
+ const pda = findUserStakeProgramAddress(programId, userPubkey, seed)
929
+ const account = await connection.getAccountInfo(pda)
930
+ if (!account) {
931
+ return seed
932
+ }
933
+ }
934
+ throw new Error(`No available user stake seed found between ${startSeed} and ${startSeed + maxSeed - 1}`)
935
+ }
936
+
937
+ /**
938
+ * Represents a user stake account with its details
939
+ */
940
+ export interface UserStakeAccount {
941
+ /** The stake account public key (PDA) */
942
+ pubkey: PublicKey
943
+ /** The seed used to derive this PDA */
944
+ seed: number
945
+ /** Lamports in the stake account */
946
+ lamports: number
947
+ /** Parsed stake state */
948
+ state: 'inactive' | 'activating' | 'active' | 'deactivating'
949
+ /** Validator vote account (if delegated) */
950
+ voter?: PublicKey
951
+ /** Activation epoch (if active/activating) */
952
+ activationEpoch?: number
953
+ /** Deactivation epoch (if deactivating) */
954
+ deactivationEpoch?: number
955
+ }
956
+
957
+ /**
958
+ * Fetches all user stake accounts created via WithdrawStakeWithSession.
959
+ * These are PDAs derived from [b"user_stake", user_wallet, seed].
960
+ *
961
+ * @param connection - Solana connection
962
+ * @param programId - The stake pool program ID
963
+ * @param userPubkey - User's wallet address
964
+ * @param maxSeed - Maximum seed to check (default: 100)
965
+ * @returns Array of user stake accounts with their details
966
+ */
967
+ export async function getUserStakeAccounts(
968
+ connection: Connection,
969
+ programId: PublicKey,
970
+ userPubkey: PublicKey,
971
+ maxSeed: number = 100,
972
+ ): Promise<UserStakeAccount[]> {
973
+ const stakeAccounts: UserStakeAccount[] = []
974
+ const currentEpoch = (await connection.getEpochInfo()).epoch
975
+
976
+ for (let seed = 0; seed < maxSeed; seed++) {
977
+ const pda = findUserStakeProgramAddress(programId, userPubkey, seed)
978
+ const accountInfo = await connection.getAccountInfo(pda)
979
+
980
+ if (!accountInfo) {
981
+ continue // Skip empty slots, there might be gaps
982
+ }
983
+
984
+ // Check if owned by stake program
985
+ if (!accountInfo.owner.equals(StakeProgram.programId)) {
986
+ continue
987
+ }
988
+
989
+ // Parse stake account data
990
+ const stakeAccount: UserStakeAccount = {
991
+ pubkey: pda,
992
+ seed,
993
+ lamports: accountInfo.lamports,
994
+ state: 'inactive',
995
+ }
996
+
997
+ try {
998
+ // Parse the stake account to get delegation info
999
+ const parsedAccount = await connection.getParsedAccountInfo(pda)
1000
+ if (parsedAccount.value && 'parsed' in parsedAccount.value.data) {
1001
+ const parsed = parsedAccount.value.data.parsed
1002
+ if (parsed.type === 'delegated' && parsed.info?.stake?.delegation) {
1003
+ const delegation = parsed.info.stake.delegation
1004
+ stakeAccount.voter = new PublicKey(delegation.voter)
1005
+ stakeAccount.activationEpoch = Number(delegation.activationEpoch)
1006
+ stakeAccount.deactivationEpoch = Number(delegation.deactivationEpoch)
1007
+
1008
+ // Determine state based on epochs
1009
+ const activationEpoch = stakeAccount.activationEpoch
1010
+ const deactivationEpoch = stakeAccount.deactivationEpoch
1011
+
1012
+ if (deactivationEpoch !== undefined && deactivationEpoch < Number.MAX_SAFE_INTEGER && deactivationEpoch <= currentEpoch) {
1013
+ stakeAccount.state = 'inactive'
1014
+ } else if (deactivationEpoch !== undefined && deactivationEpoch < Number.MAX_SAFE_INTEGER) {
1015
+ stakeAccount.state = 'deactivating'
1016
+ } else if (activationEpoch !== undefined && activationEpoch <= currentEpoch) {
1017
+ stakeAccount.state = 'active'
1018
+ } else {
1019
+ stakeAccount.state = 'activating'
1020
+ }
1021
+ }
1022
+ }
1023
+ } catch {
1024
+ // If parsing fails, keep default 'inactive' state
1025
+ }
1026
+
1027
+ stakeAccounts.push(stakeAccount)
1028
+ }
1029
+
1030
+ return stakeAccounts
1031
+ }
1032
+
1033
+ /**
1034
+ * Withdraws stake from a stake pool using a Fogo session.
1035
+ *
1036
+ * The on-chain program creates stake account PDAs. The rent for these accounts
1037
+ * is paid by the payer (typically the paymaster), not deducted from the user's withdrawal.
1038
+ *
1039
+ * @param connection - Solana connection
1040
+ * @param stakePoolAddress - The stake pool to withdraw from
1041
+ * @param signerOrSession - The session signer public key
1042
+ * @param userPubkey - User's wallet (used for PDA derivation and token ownership)
1043
+ * @param payer - Payer for stake account rent (typically paymaster)
1044
+ * @param amount - Amount of pool tokens to withdraw
1045
+ * @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
1046
+ * @param useReserve - Whether to withdraw from reserve (default: false)
1047
+ * @param voteAccountAddress - Optional specific validator to withdraw from
1048
+ * @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
1049
+ * @param validatorComparator - Optional comparator for validator selection
1050
+ */
1051
+ export async function withdrawStakeWithSession(
1052
+ connection: Connection,
1053
+ stakePoolAddress: PublicKey,
1054
+ signerOrSession: PublicKey,
1055
+ userPubkey: PublicKey,
1056
+ payer: PublicKey,
1057
+ amount: number,
1058
+ userStakeSeedStart: number = 0,
1059
+ useReserve = false,
1060
+ voteAccountAddress?: PublicKey,
1061
+ minimumLamportsOut: number = 0,
1062
+ validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
1063
+ ) {
1064
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
1065
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1066
+ const stakePool = stakePoolAccount.account.data
1067
+ const poolTokens = solToLamports(amount)
1068
+ const poolAmount = new BN(poolTokens)
1069
+
1070
+ const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey)
1071
+ const tokenAccount = await getAccount(connection, poolTokenAccount)
1072
+
1073
+ if (tokenAccount.amount < poolTokens) {
1074
+ throw new Error(
1075
+ `Not enough token balance to withdraw ${amount} pool tokens.
1076
+ Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
1077
+ )
1078
+ }
1079
+
1080
+ const [programSigner] = PublicKey.findProgramAddressSync(
1081
+ [Buffer.from('fogo_session_program_signer')],
1082
+ stakePoolProgramId,
1083
+ )
1084
+
1085
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1086
+ stakePoolProgramId,
1087
+ stakePoolAddress,
1088
+ )
1089
+
1090
+ const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
1091
+
1092
+ // Determine which stake accounts to withdraw from
1093
+ const withdrawAccounts: WithdrawAccount[] = []
1094
+
1095
+ if (useReserve) {
1096
+ withdrawAccounts.push({
1097
+ stakeAddress: stakePool.reserveStake,
1098
+ voteAddress: undefined,
1099
+ poolAmount,
1100
+ })
1101
+ } else if (voteAccountAddress) {
1102
+ const stakeAccountAddress = await findStakeProgramAddress(
1103
+ stakePoolProgramId,
1104
+ voteAccountAddress,
1105
+ stakePoolAddress,
1106
+ )
1107
+ const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
1108
+ if (!stakeAccount) {
1109
+ throw new Error(`Validator stake account not found for vote address ${voteAccountAddress.toBase58()}`)
1110
+ }
1111
+
1112
+ const availableLamports = new BN(
1113
+ stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
1114
+ )
1115
+ if (availableLamports.lt(new BN(0))) {
1116
+ throw new Error('Invalid Stake Account')
1117
+ }
1118
+ const availableForWithdrawal = calcLamportsWithdrawAmount(
1119
+ stakePool,
1120
+ availableLamports,
1121
+ )
1122
+
1123
+ if (availableForWithdrawal.lt(poolAmount)) {
1124
+ throw new Error(
1125
+ `Not enough lamports available for withdrawal from ${stakeAccountAddress},
1126
+ ${poolAmount} asked, ${availableForWithdrawal} available.`,
1127
+ )
1128
+ }
1129
+ withdrawAccounts.push({
1130
+ stakeAddress: stakeAccountAddress,
1131
+ voteAddress: voteAccountAddress,
1132
+ poolAmount,
1133
+ })
1134
+ } else {
1135
+ // Get the list of accounts to withdraw from automatically
1136
+ withdrawAccounts.push(
1137
+ ...(await prepareWithdrawAccounts(
1138
+ connection,
1139
+ stakePool,
1140
+ stakePoolAddress,
1141
+ poolAmount,
1142
+ validatorComparator,
1143
+ poolTokenAccount.equals(stakePool.managerFeeAccount),
1144
+ )),
1145
+ )
1146
+ }
1147
+
1148
+ const instructions: TransactionInstruction[] = []
1149
+ const stakeAccountPubkeys: PublicKey[] = []
1150
+ const userStakeSeeds: number[] = []
1151
+
1152
+ // Max 5 accounts to prevent an error: "Transaction too large"
1153
+ const maxWithdrawAccounts = 5
1154
+ let i = 0
1155
+
1156
+ for (const withdrawAccount of withdrawAccounts) {
1157
+ if (i >= maxWithdrawAccounts) {
1158
+ break
1159
+ }
1160
+
1161
+ // Derive the stake account PDA for this withdrawal
1162
+ const userStakeSeed = userStakeSeedStart + i
1163
+ const stakeReceiverPubkey = findUserStakeProgramAddress(
1164
+ stakePoolProgramId,
1165
+ userPubkey,
1166
+ userStakeSeed,
1167
+ )
1168
+
1169
+ stakeAccountPubkeys.push(stakeReceiverPubkey)
1170
+ userStakeSeeds.push(userStakeSeed)
1171
+
1172
+ // The on-chain program creates the stake account PDA and rent is paid by payer.
1173
+ instructions.push(
1174
+ StakePoolInstruction.withdrawStakeWithSession({
1175
+ programId: stakePoolProgramId,
1176
+ stakePool: stakePoolAddress,
1177
+ validatorList: stakePool.validatorList,
1178
+ withdrawAuthority,
1179
+ stakeToSplit: withdrawAccount.stakeAddress,
1180
+ stakeToReceive: stakeReceiverPubkey,
1181
+ sessionSigner: signerOrSession,
1182
+ burnFromPool: poolTokenAccount,
1183
+ managerFeeAccount: stakePool.managerFeeAccount,
1184
+ poolMint: stakePool.poolMint,
1185
+ tokenProgramId: stakePool.tokenProgramId,
1186
+ programSigner,
1187
+ payer,
1188
+ poolTokensIn: withdrawAccount.poolAmount.toNumber(),
1189
+ minimumLamportsOut,
1190
+ userStakeSeed,
1191
+ }),
1192
+ )
1193
+ i++
1194
+ }
1195
+
1196
+ return {
1197
+ instructions,
1198
+ stakeAccountPubkeys,
1199
+ userStakeSeeds,
1200
+ }
1201
+ }
1202
+
1203
+ export async function addValidatorToPool(
1204
+ connection: Connection,
1205
+ stakePoolAddress: PublicKey,
1206
+ validatorVote: PublicKey,
1207
+ seed?: number,
1208
+ ) {
1209
+ const stakePoolAccount = await getStakePoolAccount(
1210
+ connection,
1211
+ stakePoolAddress,
1212
+ )
1213
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1214
+ const stakePool = stakePoolAccount.account.data
1215
+ const { reserveStake, staker, validatorList } = stakePool
1216
+
1217
+ const validatorListAccount = await getValidatorListAccount(
1218
+ connection,
1219
+ validatorList,
1220
+ )
1221
+
1222
+ const validatorInfo = validatorListAccount.account.data.validators.find(
1223
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
1224
+ )
1225
+
1226
+ if (validatorInfo) {
1227
+ throw new Error('Vote account is already in validator list')
1228
+ }
1229
+
1230
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1231
+ stakePoolProgramId,
1232
+ stakePoolAddress,
1233
+ )
1234
+
1235
+ const validatorStake = await findStakeProgramAddress(
1236
+ stakePoolProgramId,
1237
+ validatorVote,
1238
+ stakePoolAddress,
1239
+ seed,
1240
+ )
1241
+
1242
+ const instructions: TransactionInstruction[] = [
1243
+ StakePoolInstruction.addValidatorToPool({
1244
+ programId: stakePoolProgramId,
1245
+ stakePool: stakePoolAddress,
1246
+ staker,
1247
+ reserveStake,
1248
+ withdrawAuthority,
1249
+ validatorList,
1250
+ validatorStake,
1251
+ validatorVote,
1252
+ }),
1253
+ ]
1254
+
1255
+ return {
1256
+ instructions,
1257
+ }
1258
+ }
1259
+
1260
+ export async function removeValidatorFromPool(
1261
+ connection: Connection,
1262
+ stakePoolAddress: PublicKey,
1263
+ validatorVote: PublicKey,
1264
+ seed?: number,
1265
+ ) {
1266
+ const stakePoolAccount = await getStakePoolAccount(
1267
+ connection,
1268
+ stakePoolAddress,
1269
+ )
1270
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1271
+ const stakePool = stakePoolAccount.account.data
1272
+ const { staker, validatorList } = stakePool
1273
+
1274
+ const validatorListAccount = await getValidatorListAccount(
1275
+ connection,
1276
+ validatorList,
1277
+ )
1278
+
1279
+ const validatorInfo = validatorListAccount.account.data.validators.find(
1280
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
1281
+ )
1282
+
1283
+ if (!validatorInfo) {
1284
+ throw new Error('Vote account is not already in validator list')
1285
+ }
1286
+
1287
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1288
+ stakePoolProgramId,
1289
+ stakePoolAddress,
1290
+ )
1291
+
1292
+ const validatorStake = await findStakeProgramAddress(
1293
+ stakePoolProgramId,
1294
+ validatorVote,
1295
+ stakePoolAddress,
1296
+ seed,
1297
+ )
1298
+
1299
+ const transientStakeSeed = validatorInfo.transientSeedSuffixStart
1300
+
1301
+ const transientStake = await findTransientStakeProgramAddress(
1302
+ stakePoolProgramId,
1303
+ validatorInfo.voteAccountAddress,
1304
+ stakePoolAddress,
1305
+ transientStakeSeed,
1306
+ )
1307
+
1308
+ const instructions: TransactionInstruction[] = [
1309
+ StakePoolInstruction.removeValidatorFromPool({
1310
+ programId: stakePoolProgramId,
1311
+ stakePool: stakePoolAddress,
1312
+ staker,
1313
+ withdrawAuthority,
1314
+ validatorList,
1315
+ validatorStake,
1316
+ transientStake,
1317
+ }),
1318
+ ]
1319
+
1320
+ return {
1321
+ instructions,
1322
+ }
1323
+ }
1324
+
1325
+ /**
1326
+ * Creates instructions required to increase validator stake.
1327
+ */
1328
+ export async function increaseValidatorStake(
1329
+ connection: Connection,
1330
+ stakePoolAddress: PublicKey,
1331
+ validatorVote: PublicKey,
1332
+ lamports: number,
1333
+ ephemeralStakeSeed?: number,
1334
+ ) {
1335
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1336
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1337
+
1338
+ const validatorList = await getValidatorListAccount(
1339
+ connection,
1340
+ stakePool.account.data.validatorList,
1341
+ )
1342
+
1343
+ const validatorInfo = validatorList.account.data.validators.find(
1344
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
1345
+ )
1346
+
1347
+ if (!validatorInfo) {
1348
+ throw new Error('Vote account not found in validator list')
1349
+ }
1350
+
1351
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1352
+ stakePoolProgramId,
1353
+ stakePoolAddress,
1354
+ )
1355
+
1356
+ // Bump transient seed suffix by one to avoid reuse when not using the increaseAdditionalStake instruction
1357
+ const transientStakeSeed
1358
+ = ephemeralStakeSeed === undefined
1359
+ ? validatorInfo.transientSeedSuffixStart.addn(1)
1360
+ : validatorInfo.transientSeedSuffixStart
1361
+
1362
+ const transientStake = await findTransientStakeProgramAddress(
1363
+ stakePoolProgramId,
1364
+ validatorInfo.voteAccountAddress,
1365
+ stakePoolAddress,
1366
+ transientStakeSeed,
1367
+ )
1368
+
1369
+ const validatorStake = await findStakeProgramAddress(
1370
+ stakePoolProgramId,
1371
+ validatorInfo.voteAccountAddress,
1372
+ stakePoolAddress,
1373
+ )
1374
+
1375
+ const instructions: TransactionInstruction[] = []
1376
+
1377
+ if (ephemeralStakeSeed !== undefined) {
1378
+ const ephemeralStake = await findEphemeralStakeProgramAddress(
1379
+ stakePoolProgramId,
1380
+ stakePoolAddress,
1381
+ new BN(ephemeralStakeSeed),
1382
+ )
1383
+ instructions.push(
1384
+ StakePoolInstruction.increaseAdditionalValidatorStake({
1385
+ programId: stakePoolProgramId,
1386
+ stakePool: stakePoolAddress,
1387
+ staker: stakePool.account.data.staker,
1388
+ validatorList: stakePool.account.data.validatorList,
1389
+ reserveStake: stakePool.account.data.reserveStake,
1390
+ transientStakeSeed: transientStakeSeed.toNumber(),
1391
+ withdrawAuthority,
1392
+ transientStake,
1393
+ validatorStake,
1394
+ validatorVote,
1395
+ lamports,
1396
+ ephemeralStake,
1397
+ ephemeralStakeSeed,
1398
+ }),
1399
+ )
1400
+ } else {
1401
+ instructions.push(
1402
+ StakePoolInstruction.increaseValidatorStake({
1403
+ programId: stakePoolProgramId,
1404
+ stakePool: stakePoolAddress,
1405
+ staker: stakePool.account.data.staker,
1406
+ validatorList: stakePool.account.data.validatorList,
1407
+ reserveStake: stakePool.account.data.reserveStake,
1408
+ transientStakeSeed: transientStakeSeed.toNumber(),
1409
+ withdrawAuthority,
1410
+ transientStake,
1411
+ validatorStake,
1412
+ validatorVote,
1413
+ lamports,
1414
+ }),
1415
+ )
1416
+ }
1417
+
1418
+ return {
1419
+ instructions,
1420
+ }
1421
+ }
1422
+
1423
+ /**
1424
+ * Creates instructions required to decrease validator stake.
1425
+ */
1426
+ export async function decreaseValidatorStake(
1427
+ connection: Connection,
1428
+ stakePoolAddress: PublicKey,
1429
+ validatorVote: PublicKey,
1430
+ lamports: number,
1431
+ ephemeralStakeSeed?: number,
1432
+ ) {
1433
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1434
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1435
+ const validatorList = await getValidatorListAccount(
1436
+ connection,
1437
+ stakePool.account.data.validatorList,
1438
+ )
1439
+
1440
+ const validatorInfo = validatorList.account.data.validators.find(
1441
+ v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
1442
+ )
1443
+
1444
+ if (!validatorInfo) {
1445
+ throw new Error('Vote account not found in validator list')
1446
+ }
1447
+
1448
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1449
+ stakePoolProgramId,
1450
+ stakePoolAddress,
1451
+ )
1452
+
1453
+ const validatorStake = await findStakeProgramAddress(
1454
+ stakePoolProgramId,
1455
+ validatorInfo.voteAccountAddress,
1456
+ stakePoolAddress,
1457
+ )
1458
+
1459
+ // Bump transient seed suffix by one to avoid reuse when not using the decreaseAdditionalStake instruction
1460
+ const transientStakeSeed
1461
+ = ephemeralStakeSeed === undefined
1462
+ ? validatorInfo.transientSeedSuffixStart.addn(1)
1463
+ : validatorInfo.transientSeedSuffixStart
1464
+
1465
+ const transientStake = await findTransientStakeProgramAddress(
1466
+ stakePoolProgramId,
1467
+ validatorInfo.voteAccountAddress,
1468
+ stakePoolAddress,
1469
+ transientStakeSeed,
1470
+ )
1471
+
1472
+ const instructions: TransactionInstruction[] = []
1473
+
1474
+ if (ephemeralStakeSeed !== undefined) {
1475
+ const ephemeralStake = await findEphemeralStakeProgramAddress(
1476
+ stakePoolProgramId,
1477
+ stakePoolAddress,
1478
+ new BN(ephemeralStakeSeed),
1479
+ )
1480
+ instructions.push(
1481
+ StakePoolInstruction.decreaseAdditionalValidatorStake({
1482
+ programId: stakePoolProgramId,
1483
+ stakePool: stakePoolAddress,
1484
+ staker: stakePool.account.data.staker,
1485
+ validatorList: stakePool.account.data.validatorList,
1486
+ reserveStake: stakePool.account.data.reserveStake,
1487
+ transientStakeSeed: transientStakeSeed.toNumber(),
1488
+ withdrawAuthority,
1489
+ validatorStake,
1490
+ transientStake,
1491
+ lamports,
1492
+ ephemeralStake,
1493
+ ephemeralStakeSeed,
1494
+ }),
1495
+ )
1496
+ } else {
1497
+ instructions.push(
1498
+ StakePoolInstruction.decreaseValidatorStakeWithReserve({
1499
+ programId: stakePoolProgramId,
1500
+ stakePool: stakePoolAddress,
1501
+ staker: stakePool.account.data.staker,
1502
+ validatorList: stakePool.account.data.validatorList,
1503
+ reserveStake: stakePool.account.data.reserveStake,
1504
+ transientStakeSeed: transientStakeSeed.toNumber(),
1505
+ withdrawAuthority,
1506
+ validatorStake,
1507
+ transientStake,
1508
+ lamports,
1509
+ }),
1510
+ )
1511
+ }
1512
+
1513
+ return {
1514
+ instructions,
1515
+ }
1516
+ }
1517
+
1518
+ /**
1519
+ * Creates instructions required to completely update a stake pool after epoch change.
1520
+ */
1521
+ export async function updateStakePool(
1522
+ connection: Connection,
1523
+ stakePool: StakePoolAccount,
1524
+ noMerge = false,
1525
+ ) {
1526
+ const stakePoolAddress = stakePool.pubkey
1527
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1528
+
1529
+ const validatorList = await getValidatorListAccount(
1530
+ connection,
1531
+ stakePool.account.data.validatorList,
1532
+ )
1533
+
1534
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1535
+ stakePoolProgramId,
1536
+ stakePoolAddress,
1537
+ )
1538
+
1539
+ const updateListInstructions: TransactionInstruction[] = []
1540
+ const instructions: TransactionInstruction[] = []
1541
+
1542
+ let startIndex = 0
1543
+ const validatorChunks: Array<ValidatorStakeInfo[]> = arrayChunk(
1544
+ validatorList.account.data.validators,
1545
+ MAX_VALIDATORS_TO_UPDATE,
1546
+ )
1547
+
1548
+ for (const validatorChunk of validatorChunks) {
1549
+ const validatorAndTransientStakePairs: PublicKey[] = []
1550
+
1551
+ for (const validator of validatorChunk) {
1552
+ const validatorStake = await findStakeProgramAddress(
1553
+ stakePoolProgramId,
1554
+ validator.voteAccountAddress,
1555
+ stakePoolAddress,
1556
+ )
1557
+ validatorAndTransientStakePairs.push(validatorStake)
1558
+
1559
+ const transientStake = await findTransientStakeProgramAddress(
1560
+ stakePoolProgramId,
1561
+ validator.voteAccountAddress,
1562
+ stakePoolAddress,
1563
+ validator.transientSeedSuffixStart,
1564
+ )
1565
+ validatorAndTransientStakePairs.push(transientStake)
1566
+ }
1567
+
1568
+ updateListInstructions.push(
1569
+ StakePoolInstruction.updateValidatorListBalance({
1570
+ programId: stakePoolProgramId,
1571
+ stakePool: stakePoolAddress,
1572
+ validatorList: stakePool.account.data.validatorList,
1573
+ reserveStake: stakePool.account.data.reserveStake,
1574
+ validatorAndTransientStakePairs,
1575
+ withdrawAuthority,
1576
+ startIndex,
1577
+ noMerge,
1578
+ }),
1579
+ )
1580
+ startIndex += MAX_VALIDATORS_TO_UPDATE
1581
+ }
1582
+
1583
+ instructions.push(
1584
+ StakePoolInstruction.updateStakePoolBalance({
1585
+ programId: stakePoolProgramId,
1586
+ stakePool: stakePoolAddress,
1587
+ validatorList: stakePool.account.data.validatorList,
1588
+ reserveStake: stakePool.account.data.reserveStake,
1589
+ managerFeeAccount: stakePool.account.data.managerFeeAccount,
1590
+ poolMint: stakePool.account.data.poolMint,
1591
+ withdrawAuthority,
1592
+ }),
1593
+ )
1594
+
1595
+ instructions.push(
1596
+ StakePoolInstruction.cleanupRemovedValidatorEntries({
1597
+ programId: stakePoolProgramId,
1598
+ stakePool: stakePoolAddress,
1599
+ validatorList: stakePool.account.data.validatorList,
1600
+ }),
1601
+ )
1602
+
1603
+ return {
1604
+ updateListInstructions,
1605
+ finalInstructions: instructions,
1606
+ }
1607
+ }
1608
+
1609
+ /**
1610
+ * Retrieves detailed information about the StakePool.
1611
+ */
1612
+ export async function stakePoolInfo(
1613
+ connection: Connection,
1614
+ stakePoolAddress: PublicKey,
1615
+ ) {
1616
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1617
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1618
+ const reserveAccountStakeAddress = stakePool.account.data.reserveStake
1619
+ const totalLamports = stakePool.account.data.totalLamports
1620
+ const lastUpdateEpoch = stakePool.account.data.lastUpdateEpoch
1621
+
1622
+ const validatorList = await getValidatorListAccount(
1623
+ connection,
1624
+ stakePool.account.data.validatorList,
1625
+ )
1626
+
1627
+ const maxNumberOfValidators = validatorList.account.data.maxValidators
1628
+ const currentNumberOfValidators
1629
+ = validatorList.account.data.validators.length
1630
+
1631
+ const epochInfo = await connection.getEpochInfo()
1632
+ const reserveStake = await connection.getAccountInfo(
1633
+ reserveAccountStakeAddress,
1634
+ )
1635
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1636
+ stakePoolProgramId,
1637
+ stakePoolAddress,
1638
+ )
1639
+
1640
+ const minimumReserveStakeBalance
1641
+ = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
1642
+
1643
+ const stakeAccounts = await Promise.all(
1644
+ validatorList.account.data.validators.map(async (validator) => {
1645
+ const stakeAccountAddress = await findStakeProgramAddress(
1646
+ stakePoolProgramId,
1647
+ validator.voteAccountAddress,
1648
+ stakePoolAddress,
1649
+ )
1650
+ const transientStakeAccountAddress
1651
+ = await findTransientStakeProgramAddress(
1652
+ stakePoolProgramId,
1653
+ validator.voteAccountAddress,
1654
+ stakePoolAddress,
1655
+ validator.transientSeedSuffixStart,
1656
+ )
1657
+ const updateRequired = !validator.lastUpdateEpoch.eqn(epochInfo.epoch)
1658
+ return {
1659
+ voteAccountAddress: validator.voteAccountAddress.toBase58(),
1660
+ stakeAccountAddress: stakeAccountAddress.toBase58(),
1661
+ validatorActiveStakeLamports: validator.activeStakeLamports.toString(),
1662
+ validatorLastUpdateEpoch: validator.lastUpdateEpoch.toString(),
1663
+ validatorLamports: validator.activeStakeLamports
1664
+ .add(validator.transientStakeLamports)
1665
+ .toString(),
1666
+ validatorTransientStakeAccountAddress:
1667
+ transientStakeAccountAddress.toBase58(),
1668
+ validatorTransientStakeLamports:
1669
+ validator.transientStakeLamports.toString(),
1670
+ updateRequired,
1671
+ }
1672
+ }),
1673
+ )
1674
+
1675
+ const totalPoolTokens = lamportsToSol(stakePool.account.data.poolTokenSupply)
1676
+ const updateRequired = !lastUpdateEpoch.eqn(epochInfo.epoch)
1677
+
1678
+ return {
1679
+ address: stakePoolAddress.toBase58(),
1680
+ poolWithdrawAuthority: withdrawAuthority.toBase58(),
1681
+ manager: stakePool.account.data.manager.toBase58(),
1682
+ staker: stakePool.account.data.staker.toBase58(),
1683
+ stakeDepositAuthority:
1684
+ stakePool.account.data.stakeDepositAuthority.toBase58(),
1685
+ stakeWithdrawBumpSeed: stakePool.account.data.stakeWithdrawBumpSeed,
1686
+ maxValidators: maxNumberOfValidators,
1687
+ validatorList: validatorList.account.data.validators.map((validator) => {
1688
+ return {
1689
+ activeStakeLamports: validator.activeStakeLamports.toString(),
1690
+ transientStakeLamports: validator.transientStakeLamports.toString(),
1691
+ lastUpdateEpoch: validator.lastUpdateEpoch.toString(),
1692
+ transientSeedSuffixStart: validator.transientSeedSuffixStart.toString(),
1693
+ transientSeedSuffixEnd: validator.transientSeedSuffixEnd.toString(),
1694
+ status: validator.status.toString(),
1695
+ voteAccountAddress: validator.voteAccountAddress.toString(),
1696
+ }
1697
+ }), // CliStakePoolValidator
1698
+ validatorListStorageAccount:
1699
+ stakePool.account.data.validatorList.toBase58(),
1700
+ reserveStake: stakePool.account.data.reserveStake.toBase58(),
1701
+ poolMint: stakePool.account.data.poolMint.toBase58(),
1702
+ managerFeeAccount: stakePool.account.data.managerFeeAccount.toBase58(),
1703
+ tokenProgramId: stakePool.account.data.tokenProgramId.toBase58(),
1704
+ totalLamports: stakePool.account.data.totalLamports.toString(),
1705
+ poolTokenSupply: stakePool.account.data.poolTokenSupply.toString(),
1706
+ lastUpdateEpoch: stakePool.account.data.lastUpdateEpoch.toString(),
1707
+ lockup: stakePool.account.data.lockup, // pub lockup: CliStakePoolLockup
1708
+ epochFee: stakePool.account.data.epochFee,
1709
+ nextEpochFee: stakePool.account.data.nextEpochFee,
1710
+ preferredDepositValidatorVoteAddress:
1711
+ stakePool.account.data.preferredDepositValidatorVoteAddress,
1712
+ preferredWithdrawValidatorVoteAddress:
1713
+ stakePool.account.data.preferredWithdrawValidatorVoteAddress,
1714
+ stakeDepositFee: stakePool.account.data.stakeDepositFee,
1715
+ stakeWithdrawalFee: stakePool.account.data.stakeWithdrawalFee,
1716
+ // CliStakePool the same
1717
+ nextStakeWithdrawalFee: stakePool.account.data.nextStakeWithdrawalFee,
1718
+ stakeReferralFee: stakePool.account.data.stakeReferralFee,
1719
+ solDepositAuthority: stakePool.account.data.solDepositAuthority?.toBase58(),
1720
+ solDepositFee: stakePool.account.data.solDepositFee,
1721
+ solReferralFee: stakePool.account.data.solReferralFee,
1722
+ solWithdrawAuthority:
1723
+ stakePool.account.data.solWithdrawAuthority?.toBase58(),
1724
+ solWithdrawalFee: stakePool.account.data.solWithdrawalFee,
1725
+ nextSolWithdrawalFee: stakePool.account.data.nextSolWithdrawalFee,
1726
+ lastEpochPoolTokenSupply:
1727
+ stakePool.account.data.lastEpochPoolTokenSupply.toString(),
1728
+ lastEpochTotalLamports:
1729
+ stakePool.account.data.lastEpochTotalLamports.toString(),
1730
+ details: {
1731
+ reserveStakeLamports: reserveStake?.lamports,
1732
+ reserveAccountStakeAddress: reserveAccountStakeAddress.toBase58(),
1733
+ minimumReserveStakeBalance,
1734
+ stakeAccounts,
1735
+ totalLamports,
1736
+ totalPoolTokens,
1737
+ currentNumberOfValidators,
1738
+ maxNumberOfValidators,
1739
+ updateRequired,
1740
+ }, // CliStakePoolDetails
1741
+ }
1742
+ }
1743
+
1744
+ /**
1745
+ * Creates instructions required to create pool token metadata.
1746
+ */
1747
+ export async function createPoolTokenMetadata(
1748
+ connection: Connection,
1749
+ stakePoolAddress: PublicKey,
1750
+ payer: PublicKey,
1751
+ name: string,
1752
+ symbol: string,
1753
+ uri: string,
1754
+ ) {
1755
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1756
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1757
+
1758
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1759
+ stakePoolProgramId,
1760
+ stakePoolAddress,
1761
+ )
1762
+ const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint)
1763
+ const manager = stakePool.account.data.manager
1764
+
1765
+ const instructions: TransactionInstruction[] = []
1766
+ instructions.push(
1767
+ StakePoolInstruction.createTokenMetadata({
1768
+ programId: stakePoolProgramId,
1769
+ stakePool: stakePoolAddress,
1770
+ poolMint: stakePool.account.data.poolMint,
1771
+ payer,
1772
+ manager,
1773
+ tokenMetadata,
1774
+ withdrawAuthority,
1775
+ name,
1776
+ symbol,
1777
+ uri,
1778
+ }),
1779
+ )
1780
+
1781
+ return {
1782
+ instructions,
1783
+ }
1784
+ }
1785
+
1786
+ /**
1787
+ * Creates instructions required to update pool token metadata.
1788
+ */
1789
+ export async function updatePoolTokenMetadata(
1790
+ connection: Connection,
1791
+ stakePoolAddress: PublicKey,
1792
+ name: string,
1793
+ symbol: string,
1794
+ uri: string,
1795
+ ) {
1796
+ const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
1797
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1798
+
1799
+ const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1800
+ stakePoolProgramId,
1801
+ stakePoolAddress,
1802
+ )
1803
+
1804
+ const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint)
1805
+
1806
+ const instructions: TransactionInstruction[] = []
1807
+ instructions.push(
1808
+ StakePoolInstruction.updateTokenMetadata({
1809
+ programId: stakePoolProgramId,
1810
+ stakePool: stakePoolAddress,
1811
+ manager: stakePool.account.data.manager,
1812
+ tokenMetadata,
1813
+ withdrawAuthority,
1814
+ name,
1815
+ symbol,
1816
+ uri,
1817
+ }),
1818
+ )
1819
+
1820
+ return {
1821
+ instructions,
1822
+ }
1823
+ }