@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.
@@ -844,13 +844,16 @@ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress,
844
844
  continue;
845
845
  }
846
846
  const stakeAccountAddress = await findStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress);
847
- if (!validator.activeStakeLamports.isZero()) {
847
+ // For active stake accounts, subtract the minimum balance that must remain
848
+ // to allow for merges and maintain rent exemption
849
+ const availableActiveLamports = validator.activeStakeLamports.sub(minBalance);
850
+ if (availableActiveLamports.gt(new BN(0))) {
848
851
  const isPreferred = (_a = stakePool === null || stakePool === void 0 ? void 0 : stakePool.preferredWithdrawValidatorVoteAddress) === null || _a === void 0 ? void 0 : _a.equals(validator.voteAccountAddress);
849
852
  accounts.push({
850
853
  type: isPreferred ? 'preferred' : 'active',
851
854
  voteAddress: validator.voteAccountAddress,
852
855
  stakeAddress: stakeAccountAddress,
853
- lamports: validator.activeStakeLamports,
856
+ lamports: availableActiveLamports,
854
857
  });
855
858
  }
856
859
  const transientStakeLamports = validator.transientStakeLamports.sub(minBalance);
@@ -1141,6 +1144,14 @@ const STAKE_POOL_INSTRUCTION_LAYOUTS = Object.freeze({
1141
1144
  BufferLayout__namespace.ns64('userStakeSeed'),
1142
1145
  ]),
1143
1146
  },
1147
+ WithdrawFromStakeAccountWithSession: {
1148
+ index: 30,
1149
+ layout: BufferLayout__namespace.struct([
1150
+ BufferLayout__namespace.u8('instruction'),
1151
+ BufferLayout__namespace.ns64('lamports'),
1152
+ BufferLayout__namespace.ns64('userStakeSeed'),
1153
+ ]),
1154
+ },
1144
1155
  });
1145
1156
  /**
1146
1157
  * Stake Pool Instruction class
@@ -1617,7 +1628,7 @@ class StakePoolInstruction {
1617
1628
  }
1618
1629
  /**
1619
1630
  * Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
1620
- * The stake account is created as a PDA and rent is paid from the withdrawal amount.
1631
+ * The stake account is created as a PDA and rent is paid by the payer (typically paymaster).
1621
1632
  */
1622
1633
  static withdrawStakeWithSession(params) {
1623
1634
  const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStakeWithSession;
@@ -1642,6 +1653,7 @@ class StakePoolInstruction {
1642
1653
  { pubkey: web3_js.StakeProgram.programId, isSigner: false, isWritable: false },
1643
1654
  { pubkey: params.programSigner, isSigner: false, isWritable: false },
1644
1655
  { pubkey: web3_js.SystemProgram.programId, isSigner: false, isWritable: false },
1656
+ { pubkey: params.payer, isSigner: true, isWritable: true },
1645
1657
  ];
1646
1658
  return new web3_js.TransactionInstruction({
1647
1659
  programId: params.programId,
@@ -1649,6 +1661,43 @@ class StakePoolInstruction {
1649
1661
  data,
1650
1662
  });
1651
1663
  }
1664
+ /**
1665
+ * Creates a transaction instruction to withdraw SOL from a deactivated user stake account using a Fogo session.
1666
+ * The stake account must be fully deactivated (inactive).
1667
+ * User receives full stake balance (payer's rent contribution compensates for reduced split).
1668
+ */
1669
+ static withdrawFromStakeAccountWithSession(params) {
1670
+ // For u64::MAX (full withdrawal), we need to manually encode since buffer-layout doesn't handle bigint well
1671
+ const U64_MAX = BigInt('18446744073709551615');
1672
+ const isFullWithdrawal = params.lamports >= U64_MAX;
1673
+ // Manually create the instruction data buffer using Uint8Array for browser compatibility
1674
+ // Layout: u8 instruction (1) + u64 lamports (8) + u64 userStakeSeed (8) = 17 bytes
1675
+ const data = new Uint8Array(17);
1676
+ data[0] = 30; // instruction discriminator (WithdrawFromStakeAccountWithSession = 30)
1677
+ // Write lamports as u64 little-endian (bytes 1-8)
1678
+ const lamportsBigInt = isFullWithdrawal ? U64_MAX : params.lamports;
1679
+ for (let i = 0; i < 8; i++) {
1680
+ data[1 + i] = Number((lamportsBigInt >> BigInt(i * 8)) & BigInt(0xff));
1681
+ }
1682
+ // Write userStakeSeed as u64 little-endian (bytes 9-16)
1683
+ const seedBigInt = BigInt(params.userStakeSeed);
1684
+ for (let i = 0; i < 8; i++) {
1685
+ data[9 + i] = Number((seedBigInt >> BigInt(i * 8)) & BigInt(0xff));
1686
+ }
1687
+ // Account order matches Rust: stake_account, recipient, clock, stake_history, session_signer
1688
+ const keys = [
1689
+ { pubkey: params.userStakeAccount, isSigner: false, isWritable: true },
1690
+ { pubkey: params.userWallet, isSigner: false, isWritable: true },
1691
+ { pubkey: web3_js.SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
1692
+ { pubkey: web3_js.SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
1693
+ { pubkey: params.sessionSigner, isSigner: true, isWritable: false },
1694
+ ];
1695
+ return new web3_js.TransactionInstruction({
1696
+ programId: params.programId,
1697
+ keys,
1698
+ data: Buffer.from(data),
1699
+ });
1700
+ }
1652
1701
  /**
1653
1702
  * Creates an instruction to create metadata
1654
1703
  * using the mpl token metadata program for the pool token
@@ -1912,14 +1961,21 @@ async function depositStake(connection, stakePoolAddress, authorizedPubkey, vali
1912
1961
  /**
1913
1962
  * Creates instructions required to deposit sol to stake pool.
1914
1963
  */
1915
- async function depositWsolWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, lamports, minimumPoolTokensOut = 0, destinationTokenAccount, referrerTokenAccount, depositAuthority, payer) {
1964
+ async function depositWsolWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, lamports, minimumPoolTokensOut = 0, destinationTokenAccount, referrerTokenAccount, depositAuthority, payer,
1965
+ /**
1966
+ * Skip WSOL balance validation. Set to true when adding wrap instructions
1967
+ * in the same transaction that will fund the WSOL account before deposit.
1968
+ */
1969
+ skipBalanceCheck = false) {
1916
1970
  const wsolTokenAccount = splToken.getAssociatedTokenAddressSync(splToken.NATIVE_MINT, userPubkey);
1917
- const tokenAccountInfo = await connection.getTokenAccountBalance(wsolTokenAccount, 'confirmed');
1918
- const wsolBalance = tokenAccountInfo
1919
- ? parseInt(tokenAccountInfo.value.amount)
1920
- : 0;
1921
- if (wsolBalance < lamports) {
1922
- throw new Error(`Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(wsolBalance)} WSOL.`);
1971
+ if (!skipBalanceCheck) {
1972
+ const tokenAccountInfo = await connection.getTokenAccountBalance(wsolTokenAccount, 'confirmed');
1973
+ const wsolBalance = tokenAccountInfo
1974
+ ? parseInt(tokenAccountInfo.value.amount)
1975
+ : 0;
1976
+ if (wsolBalance < lamports) {
1977
+ throw new Error(`Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(wsolBalance)} WSOL.`);
1978
+ }
1923
1979
  }
1924
1980
  const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
1925
1981
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
@@ -2280,16 +2336,83 @@ async function findNextUserStakeSeed(connection, programId, userPubkey, startSee
2280
2336
  }
2281
2337
  throw new Error(`No available user stake seed found between ${startSeed} and ${startSeed + maxSeed - 1}`);
2282
2338
  }
2339
+ /**
2340
+ * Fetches all user stake accounts created via WithdrawStakeWithSession.
2341
+ * These are PDAs derived from [b"user_stake", user_wallet, seed].
2342
+ *
2343
+ * @param connection - Solana connection
2344
+ * @param programId - The stake pool program ID
2345
+ * @param userPubkey - User's wallet address
2346
+ * @param maxSeed - Maximum seed to check (default: 100)
2347
+ * @returns Array of user stake accounts with their details
2348
+ */
2349
+ async function getUserStakeAccounts(connection, programId, userPubkey, maxSeed = 100) {
2350
+ var _c, _d;
2351
+ const stakeAccounts = [];
2352
+ const currentEpoch = (await connection.getEpochInfo()).epoch;
2353
+ for (let seed = 0; seed < maxSeed; seed++) {
2354
+ const pda = findUserStakeProgramAddress(programId, userPubkey, seed);
2355
+ const accountInfo = await connection.getAccountInfo(pda);
2356
+ if (!accountInfo) {
2357
+ continue; // Skip empty slots, there might be gaps
2358
+ }
2359
+ // Check if owned by stake program
2360
+ if (!accountInfo.owner.equals(web3_js.StakeProgram.programId)) {
2361
+ continue;
2362
+ }
2363
+ // Parse stake account data
2364
+ const stakeAccount = {
2365
+ pubkey: pda,
2366
+ seed,
2367
+ lamports: accountInfo.lamports,
2368
+ state: 'inactive',
2369
+ };
2370
+ try {
2371
+ // Parse the stake account to get delegation info
2372
+ const parsedAccount = await connection.getParsedAccountInfo(pda);
2373
+ if (parsedAccount.value && 'parsed' in parsedAccount.value.data) {
2374
+ const parsed = parsedAccount.value.data.parsed;
2375
+ if (parsed.type === 'delegated' && ((_d = (_c = parsed.info) === null || _c === void 0 ? void 0 : _c.stake) === null || _d === void 0 ? void 0 : _d.delegation)) {
2376
+ const delegation = parsed.info.stake.delegation;
2377
+ stakeAccount.voter = new web3_js.PublicKey(delegation.voter);
2378
+ stakeAccount.activationEpoch = Number(delegation.activationEpoch);
2379
+ stakeAccount.deactivationEpoch = Number(delegation.deactivationEpoch);
2380
+ // Determine state based on epochs
2381
+ const activationEpoch = stakeAccount.activationEpoch;
2382
+ const deactivationEpoch = stakeAccount.deactivationEpoch;
2383
+ if (deactivationEpoch !== undefined && deactivationEpoch < Number.MAX_SAFE_INTEGER && deactivationEpoch <= currentEpoch) {
2384
+ stakeAccount.state = 'inactive';
2385
+ }
2386
+ else if (deactivationEpoch !== undefined && deactivationEpoch < Number.MAX_SAFE_INTEGER) {
2387
+ stakeAccount.state = 'deactivating';
2388
+ }
2389
+ else if (activationEpoch !== undefined && activationEpoch <= currentEpoch) {
2390
+ stakeAccount.state = 'active';
2391
+ }
2392
+ else {
2393
+ stakeAccount.state = 'activating';
2394
+ }
2395
+ }
2396
+ }
2397
+ }
2398
+ catch {
2399
+ // If parsing fails, keep default 'inactive' state
2400
+ }
2401
+ stakeAccounts.push(stakeAccount);
2402
+ }
2403
+ return stakeAccounts;
2404
+ }
2283
2405
  /**
2284
2406
  * Withdraws stake from a stake pool using a Fogo session.
2285
2407
  *
2286
- * The on-chain program creates stake account PDAs, so no pre-creation step is needed.
2287
- * Rent for the stake account PDAs is paid from the withdrawal amount.
2408
+ * The on-chain program creates stake account PDAs. The rent for these accounts
2409
+ * is paid by the payer (typically the paymaster), not deducted from the user's withdrawal.
2288
2410
  *
2289
2411
  * @param connection - Solana connection
2290
2412
  * @param stakePoolAddress - The stake pool to withdraw from
2291
2413
  * @param signerOrSession - The session signer public key
2292
2414
  * @param userPubkey - User's wallet (used for PDA derivation and token ownership)
2415
+ * @param payer - Payer for stake account rent (typically paymaster)
2293
2416
  * @param amount - Amount of pool tokens to withdraw
2294
2417
  * @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
2295
2418
  * @param useReserve - Whether to withdraw from reserve (default: false)
@@ -2297,7 +2420,7 @@ async function findNextUserStakeSeed(connection, programId, userPubkey, startSee
2297
2420
  * @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
2298
2421
  * @param validatorComparator - Optional comparator for validator selection
2299
2422
  */
2300
- async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator) {
2423
+ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, payer, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator) {
2301
2424
  const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
2302
2425
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
2303
2426
  const stakePool = stakePoolAccount.account.data;
@@ -2361,7 +2484,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
2361
2484
  const stakeReceiverPubkey = findUserStakeProgramAddress(stakePoolProgramId, userPubkey, userStakeSeed);
2362
2485
  stakeAccountPubkeys.push(stakeReceiverPubkey);
2363
2486
  userStakeSeeds.push(userStakeSeed);
2364
- // The on-chain program will create the stake account PDA
2487
+ // The on-chain program creates the stake account PDA and rent is paid by payer.
2365
2488
  instructions.push(StakePoolInstruction.withdrawStakeWithSession({
2366
2489
  programId: stakePoolProgramId,
2367
2490
  stakePool: stakePoolAddress,
@@ -2375,6 +2498,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
2375
2498
  poolMint: stakePool.poolMint,
2376
2499
  tokenProgramId: stakePool.tokenProgramId,
2377
2500
  programSigner,
2501
+ payer,
2378
2502
  poolTokensIn: withdrawAccount.poolAmount.toNumber(),
2379
2503
  minimumLamportsOut,
2380
2504
  userStakeSeed,
@@ -2773,6 +2897,7 @@ exports.getStakeAccount = getStakeAccount;
2773
2897
  exports.getStakePoolAccount = getStakePoolAccount;
2774
2898
  exports.getStakePoolAccounts = getStakePoolAccounts;
2775
2899
  exports.getStakePoolProgramId = getStakePoolProgramId;
2900
+ exports.getUserStakeAccounts = getUserStakeAccounts;
2776
2901
  exports.increaseValidatorStake = increaseValidatorStake;
2777
2902
  exports.removeValidatorFromPool = removeValidatorFromPool;
2778
2903
  exports.stakePoolInfo = stakePoolInfo;