@ignitionfi/spl-stake-pool 1.1.24 → 1.1.25

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.
@@ -4,7 +4,7 @@ import { InstructionType } from './utils';
4
4
  /**
5
5
  * An enumeration of valid StakePoolInstructionType's
6
6
  */
7
- export type StakePoolInstructionType = 'IncreaseValidatorStake' | 'DecreaseValidatorStake' | 'UpdateValidatorListBalance' | 'UpdateStakePoolBalance' | 'CleanupRemovedValidatorEntries' | 'DepositStake' | 'DepositSol' | 'WithdrawStake' | 'WithdrawSol' | 'IncreaseAdditionalValidatorStake' | 'DecreaseAdditionalValidatorStake' | 'DecreaseValidatorStakeWithReserve' | 'Redelegate' | 'AddValidatorToPool' | 'RemoveValidatorFromPool' | 'DepositWsolWithSession' | 'WithdrawWsolWithSession' | 'WithdrawStakeWithSession';
7
+ export type StakePoolInstructionType = 'IncreaseValidatorStake' | 'DecreaseValidatorStake' | 'UpdateValidatorListBalance' | 'UpdateStakePoolBalance' | 'CleanupRemovedValidatorEntries' | 'DepositStake' | 'DepositSol' | 'WithdrawStake' | 'WithdrawSol' | 'IncreaseAdditionalValidatorStake' | 'DecreaseAdditionalValidatorStake' | 'DecreaseValidatorStakeWithReserve' | 'Redelegate' | 'AddValidatorToPool' | 'RemoveValidatorFromPool' | 'DepositWsolWithSession' | 'WithdrawWsolWithSession' | 'WithdrawStakeWithSession' | 'WithdrawFromStakeAccountWithSession';
8
8
  export declare function tokenMetadataLayout(instruction: number, nameLength: number, symbolLength: number, uriLength: number): {
9
9
  index: number;
10
10
  layout: BufferLayout.Structure<any>;
@@ -178,11 +178,26 @@ export type WithdrawStakeWithSessionParams = {
178
178
  tokenProgramId: PublicKey;
179
179
  /** The program signer PDA derived from PROGRAM_SIGNER_SEED */
180
180
  programSigner: PublicKey;
181
+ /** The payer for stake account rent (typically the paymaster) */
182
+ payer: PublicKey;
181
183
  poolTokensIn: number;
182
184
  minimumLamportsOut: number;
183
185
  /** Seed used to derive the user stake PDA */
184
186
  userStakeSeed: number;
185
187
  };
188
+ export type WithdrawFromStakeAccountWithSessionParams = {
189
+ programId: PublicKey;
190
+ /** The user stake account PDA to withdraw from */
191
+ userStakeAccount: PublicKey;
192
+ /** The user's wallet to receive the withdrawn SOL */
193
+ userWallet: PublicKey;
194
+ /** The session signer (user or session) */
195
+ sessionSigner: PublicKey;
196
+ /** Seed used to derive the user stake PDA */
197
+ userStakeSeed: number;
198
+ /** Lamports to withdraw (use BigInt max for full withdrawal) */
199
+ lamports: bigint;
200
+ };
186
201
  /**
187
202
  * Deposit SOL directly into the pool's reserve account. The output is a "pool" token
188
203
  * representing ownership into the pool. Inputs are converted to the current ratio.
@@ -329,9 +344,15 @@ export declare class StakePoolInstruction {
329
344
  static withdrawWsolWithSession(params: WithdrawWsolWithSessionParams): TransactionInstruction;
330
345
  /**
331
346
  * Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
332
- * The stake account is created as a PDA and rent is paid from the withdrawal amount.
347
+ * The stake account is created as a PDA and rent is paid by the payer (typically paymaster).
333
348
  */
334
349
  static withdrawStakeWithSession(params: WithdrawStakeWithSessionParams): TransactionInstruction;
350
+ /**
351
+ * Creates a transaction instruction to withdraw SOL from a deactivated user stake account using a Fogo session.
352
+ * The stake account must be fully deactivated (inactive).
353
+ * User receives full stake balance (payer's rent contribution compensates for reduced split).
354
+ */
355
+ static withdrawFromStakeAccountWithSession(params: WithdrawFromStakeAccountWithSessionParams): TransactionInstruction;
335
356
  /**
336
357
  * Creates an instruction to create metadata
337
358
  * using the mpl token metadata program for the pool token
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ignitionfi/spl-stake-pool",
3
- "version": "1.1.24",
3
+ "version": "1.1.25",
4
4
  "description": "Ignition Stake Pool SDK for FOGO",
5
5
  "contributors": [
6
6
  "Anza Maintainers <maintainers@anza.xyz>",
package/src/constants.ts CHANGED
@@ -11,9 +11,7 @@ export const METADATA_MAX_URI_LENGTH = 200
11
11
  export const STAKE_POOL_PROGRAM_ID = new PublicKey('SP1s4uFeTAX9jsXXmwyDs1gxYYf7cdDZ8qHUHVxE1yr')
12
12
 
13
13
  // Public key that identifies the SPL Stake Pool program deployed to devnet.
14
- export const DEVNET_STAKE_POOL_PROGRAM_ID = new PublicKey(
15
- 'DPoo15wWDqpPJJtS2MUZ49aRxqz5ZaaJCJP4z8bLuib',
16
- )
14
+ export const DEVNET_STAKE_POOL_PROGRAM_ID = new PublicKey('DPoo15wWDqpPJJtS2MUZ49aRxqz5ZaaJCJP4z8bLuib')
17
15
 
18
16
  // Maximum number of validators to update during UpdateValidatorListBalance.
19
17
  export const MAX_VALIDATORS_TO_UPDATE = 4
package/src/index.ts CHANGED
@@ -309,23 +309,30 @@ export async function depositWsolWithSession(
309
309
  referrerTokenAccount?: PublicKey,
310
310
  depositAuthority?: PublicKey,
311
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,
312
317
  ) {
313
318
  const wsolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, userPubkey)
314
319
 
315
- const tokenAccountInfo = await connection.getTokenAccountBalance(
316
- wsolTokenAccount,
317
- 'confirmed',
318
- )
319
- const wsolBalance = tokenAccountInfo
320
- ? parseInt(tokenAccountInfo.value.amount)
321
- : 0
322
-
323
- if (wsolBalance < lamports) {
324
- throw new Error(
325
- `Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
326
- wsolBalance,
327
- )} WSOL.`,
320
+ if (!skipBalanceCheck) {
321
+ const tokenAccountInfo = await connection.getTokenAccountBalance(
322
+ wsolTokenAccount,
323
+ 'confirmed',
328
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
+ }
329
336
  }
330
337
 
331
338
  const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
@@ -927,16 +934,113 @@ export async function findNextUserStakeSeed(
927
934
  throw new Error(`No available user stake seed found between ${startSeed} and ${startSeed + maxSeed - 1}`)
928
935
  }
929
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
+
930
1033
  /**
931
1034
  * Withdraws stake from a stake pool using a Fogo session.
932
1035
  *
933
- * The on-chain program creates stake account PDAs, so no pre-creation step is needed.
934
- * Rent for the stake account PDAs is paid from the withdrawal amount.
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.
935
1038
  *
936
1039
  * @param connection - Solana connection
937
1040
  * @param stakePoolAddress - The stake pool to withdraw from
938
1041
  * @param signerOrSession - The session signer public key
939
1042
  * @param userPubkey - User's wallet (used for PDA derivation and token ownership)
1043
+ * @param payer - Payer for stake account rent (typically paymaster)
940
1044
  * @param amount - Amount of pool tokens to withdraw
941
1045
  * @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
942
1046
  * @param useReserve - Whether to withdraw from reserve (default: false)
@@ -949,6 +1053,7 @@ export async function withdrawStakeWithSession(
949
1053
  stakePoolAddress: PublicKey,
950
1054
  signerOrSession: PublicKey,
951
1055
  userPubkey: PublicKey,
1056
+ payer: PublicKey,
952
1057
  amount: number,
953
1058
  userStakeSeedStart: number = 0,
954
1059
  useReserve = false,
@@ -1064,7 +1169,7 @@ export async function withdrawStakeWithSession(
1064
1169
  stakeAccountPubkeys.push(stakeReceiverPubkey)
1065
1170
  userStakeSeeds.push(userStakeSeed)
1066
1171
 
1067
- // The on-chain program will create the stake account PDA
1172
+ // The on-chain program creates the stake account PDA and rent is paid by payer.
1068
1173
  instructions.push(
1069
1174
  StakePoolInstruction.withdrawStakeWithSession({
1070
1175
  programId: stakePoolProgramId,
@@ -1079,6 +1184,7 @@ export async function withdrawStakeWithSession(
1079
1184
  poolMint: stakePool.poolMint,
1080
1185
  tokenProgramId: stakePool.tokenProgramId,
1081
1186
  programSigner,
1187
+ payer,
1082
1188
  poolTokensIn: withdrawAccount.poolAmount.toNumber(),
1083
1189
  minimumLamportsOut,
1084
1190
  userStakeSeed,
@@ -42,6 +42,7 @@ export type StakePoolInstructionType
42
42
  | 'DepositWsolWithSession'
43
43
  | 'WithdrawWsolWithSession'
44
44
  | 'WithdrawStakeWithSession'
45
+ | 'WithdrawFromStakeAccountWithSession'
45
46
 
46
47
  // 'UpdateTokenMetadata' and 'CreateTokenMetadata' have dynamic layouts
47
48
 
@@ -237,6 +238,14 @@ export const STAKE_POOL_INSTRUCTION_LAYOUTS: {
237
238
  BufferLayout.ns64('userStakeSeed'),
238
239
  ]),
239
240
  },
241
+ WithdrawFromStakeAccountWithSession: {
242
+ index: 30,
243
+ layout: BufferLayout.struct<any>([
244
+ BufferLayout.u8('instruction'),
245
+ BufferLayout.ns64('lamports'),
246
+ BufferLayout.ns64('userStakeSeed'),
247
+ ]),
248
+ },
240
249
  })
241
250
 
242
251
  /**
@@ -417,12 +426,28 @@ export type WithdrawStakeWithSessionParams = {
417
426
  tokenProgramId: PublicKey
418
427
  /** The program signer PDA derived from PROGRAM_SIGNER_SEED */
419
428
  programSigner: PublicKey
429
+ /** The payer for stake account rent (typically the paymaster) */
430
+ payer: PublicKey
420
431
  poolTokensIn: number
421
432
  minimumLamportsOut: number
422
433
  /** Seed used to derive the user stake PDA */
423
434
  userStakeSeed: number
424
435
  }
425
436
 
437
+ export type WithdrawFromStakeAccountWithSessionParams = {
438
+ programId: PublicKey
439
+ /** The user stake account PDA to withdraw from */
440
+ userStakeAccount: PublicKey
441
+ /** The user's wallet to receive the withdrawn SOL */
442
+ userWallet: PublicKey
443
+ /** The session signer (user or session) */
444
+ sessionSigner: PublicKey
445
+ /** Seed used to derive the user stake PDA */
446
+ userStakeSeed: number
447
+ /** Lamports to withdraw (use BigInt max for full withdrawal) */
448
+ lamports: bigint
449
+ }
450
+
426
451
  /**
427
452
  * Deposit SOL directly into the pool's reserve account. The output is a "pool" token
428
453
  * representing ownership into the pool. Inputs are converted to the current ratio.
@@ -1197,7 +1222,7 @@ export class StakePoolInstruction {
1197
1222
 
1198
1223
  /**
1199
1224
  * Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
1200
- * The stake account is created as a PDA and rent is paid from the withdrawal amount.
1225
+ * The stake account is created as a PDA and rent is paid by the payer (typically paymaster).
1201
1226
  */
1202
1227
  static withdrawStakeWithSession(params: WithdrawStakeWithSessionParams): TransactionInstruction {
1203
1228
  const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStakeWithSession
@@ -1223,6 +1248,7 @@ export class StakePoolInstruction {
1223
1248
  { pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
1224
1249
  { pubkey: params.programSigner, isSigner: false, isWritable: false },
1225
1250
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1251
+ { pubkey: params.payer, isSigner: true, isWritable: true },
1226
1252
  ]
1227
1253
 
1228
1254
  return new TransactionInstruction({
@@ -1232,6 +1258,49 @@ export class StakePoolInstruction {
1232
1258
  })
1233
1259
  }
1234
1260
 
1261
+ /**
1262
+ * Creates a transaction instruction to withdraw SOL from a deactivated user stake account using a Fogo session.
1263
+ * The stake account must be fully deactivated (inactive).
1264
+ * User receives full stake balance (payer's rent contribution compensates for reduced split).
1265
+ */
1266
+ static withdrawFromStakeAccountWithSession(params: WithdrawFromStakeAccountWithSessionParams): TransactionInstruction {
1267
+ // For u64::MAX (full withdrawal), we need to manually encode since buffer-layout doesn't handle bigint well
1268
+ const U64_MAX = BigInt('18446744073709551615')
1269
+ const isFullWithdrawal = params.lamports >= U64_MAX
1270
+
1271
+ // Manually create the instruction data buffer using Uint8Array for browser compatibility
1272
+ // Layout: u8 instruction (1) + u64 lamports (8) + u64 userStakeSeed (8) = 17 bytes
1273
+ const data = new Uint8Array(17)
1274
+ data[0] = 30 // instruction discriminator (WithdrawFromStakeAccountWithSession = 30)
1275
+
1276
+ // Write lamports as u64 little-endian (bytes 1-8)
1277
+ const lamportsBigInt = isFullWithdrawal ? U64_MAX : params.lamports
1278
+ for (let i = 0; i < 8; i++) {
1279
+ data[1 + i] = Number((lamportsBigInt >> BigInt(i * 8)) & BigInt(0xff))
1280
+ }
1281
+
1282
+ // Write userStakeSeed as u64 little-endian (bytes 9-16)
1283
+ const seedBigInt = BigInt(params.userStakeSeed)
1284
+ for (let i = 0; i < 8; i++) {
1285
+ data[9 + i] = Number((seedBigInt >> BigInt(i * 8)) & BigInt(0xff))
1286
+ }
1287
+
1288
+ // Account order matches Rust: stake_account, recipient, clock, stake_history, session_signer
1289
+ const keys = [
1290
+ { pubkey: params.userStakeAccount, isSigner: false, isWritable: true },
1291
+ { pubkey: params.userWallet, isSigner: false, isWritable: true },
1292
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
1293
+ { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
1294
+ { pubkey: params.sessionSigner, isSigner: true, isWritable: false },
1295
+ ]
1296
+
1297
+ return new TransactionInstruction({
1298
+ programId: params.programId,
1299
+ keys,
1300
+ data: Buffer.from(data),
1301
+ })
1302
+ }
1303
+
1235
1304
  /**
1236
1305
  * Creates an instruction to create metadata
1237
1306
  * using the mpl token metadata program for the pool token
@@ -84,7 +84,10 @@ export async function prepareWithdrawAccounts(
84
84
  stakePoolAddress,
85
85
  )
86
86
 
87
- if (!validator.activeStakeLamports.isZero()) {
87
+ // For active stake accounts, subtract the minimum balance that must remain
88
+ // to allow for merges and maintain rent exemption
89
+ const availableActiveLamports = validator.activeStakeLamports.sub(minBalance)
90
+ if (availableActiveLamports.gt(new BN(0))) {
88
91
  const isPreferred = stakePool?.preferredWithdrawValidatorVoteAddress?.equals(
89
92
  validator.voteAccountAddress,
90
93
  )
@@ -92,7 +95,7 @@ export async function prepareWithdrawAccounts(
92
95
  type: isPreferred ? 'preferred' : 'active',
93
96
  voteAddress: validator.voteAccountAddress,
94
97
  stakeAddress: stakeAccountAddress,
95
- lamports: validator.activeStakeLamports,
98
+ lamports: availableActiveLamports,
96
99
  })
97
100
  }
98
101