@ignitionfi/fogo-stake-pool 1.0.2 → 1.0.3

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/dist/index.cjs.js CHANGED
@@ -827,80 +827,84 @@ async function getValidatorListAccount(connection, pubkey) {
827
827
  },
828
828
  };
829
829
  }
830
- async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee) {
830
+ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee, allowPartial, prefetchedData) {
831
831
  var _a, _b;
832
832
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
833
- const validatorListAcc = await connection.getAccountInfo(stakePool.validatorList);
834
- const validatorList = ValidatorListLayout.decode(validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data);
835
- if (!(validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators) || (validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators.length) === 0) {
836
- throw new Error('No accounts found');
833
+ // Use prefetched data if available, otherwise fetch from RPC
834
+ let validatorListData;
835
+ let minBalanceForRentExemption;
836
+ let stakeMinimumDelegation;
837
+ if (prefetchedData) {
838
+ validatorListData = prefetchedData.validatorListData;
839
+ minBalanceForRentExemption = prefetchedData.minBalanceForRentExemption;
840
+ stakeMinimumDelegation = prefetchedData.stakeMinimumDelegation;
837
841
  }
838
- const minBalanceForRentExemption = await connection.getMinimumBalanceForRentExemption(web3_js.StakeProgram.space);
839
- const minBalance = new BN(minBalanceForRentExemption + MINIMUM_ACTIVE_STAKE);
840
- // First, collect all stake account addresses we need to check
841
- const accountsToFetch = [];
842
+ else {
843
+ const [validatorListAcc, rentExemption, stakeMinimumDelegationResponse] = await Promise.all([
844
+ connection.getAccountInfo(stakePool.validatorList),
845
+ connection.getMinimumBalanceForRentExemption(web3_js.StakeProgram.space),
846
+ connection.getStakeMinimumDelegation(),
847
+ ]);
848
+ validatorListData = (_a = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _a !== void 0 ? _a : null;
849
+ minBalanceForRentExemption = rentExemption;
850
+ stakeMinimumDelegation = Number(stakeMinimumDelegationResponse.value);
851
+ }
852
+ if (!validatorListData) {
853
+ throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
854
+ }
855
+ const validatorList = ValidatorListLayout.decode(validatorListData);
856
+ if (!(validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators) || (validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators.length) === 0) {
857
+ throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
858
+ }
859
+ // minBalance = rent + max(stake_minimum_delegation, MINIMUM_ACTIVE_STAKE)
860
+ const minimumDelegation = Math.max(stakeMinimumDelegation, MINIMUM_ACTIVE_STAKE);
861
+ const minBalance = new BN(minBalanceForRentExemption + minimumDelegation);
862
+ // Threshold for has_active_stake check (ceiling division for lamports_per_pool_token)
863
+ const lamportsPerPoolToken = stakePool.totalLamports
864
+ .add(stakePool.poolTokenSupply)
865
+ .sub(new BN(1))
866
+ .div(stakePool.poolTokenSupply);
867
+ const minimumLamportsWithTolerance = minBalance.add(lamportsPerPoolToken);
868
+ const hasActiveStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
869
+ && v.activeStakeLamports.gt(minimumLamportsWithTolerance));
870
+ const hasTransientStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
871
+ && v.transientStakeLamports.gt(minimumLamportsWithTolerance));
872
+ // ValidatorRemoval mode: no validator above threshold
873
+ const isValidatorRemovalMode = !hasActiveStake && !hasTransientStake;
874
+ let accounts = [];
842
875
  for (const validator of validatorList.validators) {
843
876
  if (validator.status !== ValidatorStakeInfoStatus.Active) {
844
877
  continue;
845
878
  }
846
879
  const stakeAccountAddress = await findStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress);
847
- const isPreferred = (_a = stakePool === null || stakePool === void 0 ? void 0 : stakePool.preferredWithdrawValidatorVoteAddress) === null || _a === void 0 ? void 0 : _a.equals(validator.voteAccountAddress);
848
- // Add active stake account if validator list indicates it has stake
849
- if (validator.activeStakeLamports.gt(new BN(0))) {
850
- accountsToFetch.push({
880
+ // ValidatorRemoval: full balance available; Normal: leave minBalance
881
+ const availableActiveLamports = isValidatorRemovalMode
882
+ ? validator.activeStakeLamports
883
+ : validator.activeStakeLamports.sub(minBalance);
884
+ if (availableActiveLamports.gt(new BN(0))) {
885
+ const isPreferred = (_b = stakePool === null || stakePool === void 0 ? void 0 : stakePool.preferredWithdrawValidatorVoteAddress) === null || _b === void 0 ? void 0 : _b.equals(validator.voteAccountAddress);
886
+ accounts.push({
851
887
  type: isPreferred ? 'preferred' : 'active',
852
888
  voteAddress: validator.voteAccountAddress,
853
889
  stakeAddress: stakeAccountAddress,
890
+ lamports: availableActiveLamports,
854
891
  });
855
892
  }
856
- // Add transient stake account if validator list indicates it has stake
857
- if (validator.transientStakeLamports.gt(new BN(0))) {
893
+ const availableTransientLamports = isValidatorRemovalMode
894
+ ? validator.transientStakeLamports
895
+ : validator.transientStakeLamports.sub(minBalance);
896
+ if (availableTransientLamports.gt(new BN(0))) {
858
897
  const transientStakeAccountAddress = await findTransientStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress, validator.transientSeedSuffixStart);
859
- accountsToFetch.push({
898
+ accounts.push({
860
899
  type: 'transient',
861
900
  voteAddress: validator.voteAccountAddress,
862
901
  stakeAddress: transientStakeAccountAddress,
863
- });
864
- }
865
- }
866
- // Fetch all stake accounts + reserve in one batch call
867
- const addressesToFetch = [
868
- ...accountsToFetch.map(a => a.stakeAddress),
869
- stakePool.reserveStake,
870
- ];
871
- const accountInfos = await connection.getMultipleAccountsInfo(addressesToFetch);
872
- // Build accounts list using actual on-chain balances
873
- let accounts = [];
874
- for (let i = 0; i < accountsToFetch.length; i++) {
875
- const { type, voteAddress, stakeAddress } = accountsToFetch[i];
876
- const accountInfo = accountInfos[i];
877
- if (!accountInfo) {
878
- continue;
879
- }
880
- // Use actual on-chain balance instead of validator list value
881
- const actualLamports = new BN(accountInfo.lamports);
882
- const availableLamports = actualLamports.sub(minBalance);
883
- if (availableLamports.gt(new BN(0))) {
884
- accounts.push({
885
- type,
886
- voteAddress,
887
- stakeAddress,
888
- lamports: availableLamports,
902
+ lamports: availableTransientLamports,
889
903
  });
890
904
  }
891
905
  }
892
906
  // Sort from highest to lowest balance
893
907
  accounts = accounts.sort(compareFn || ((a, b) => b.lamports.sub(a.lamports).toNumber()));
894
- // Add reserve stake using actual balance (last item in batch fetch)
895
- const reserveAccountInfo = accountInfos[accountInfos.length - 1];
896
- const reserveStakeBalance = new BN(((_b = reserveAccountInfo === null || reserveAccountInfo === void 0 ? void 0 : reserveAccountInfo.lamports) !== null && _b !== void 0 ? _b : 0) - minBalanceForRentExemption);
897
- if (reserveStakeBalance.gt(new BN(0))) {
898
- accounts.push({
899
- type: 'reserve',
900
- stakeAddress: stakePool.reserveStake,
901
- lamports: reserveStakeBalance,
902
- });
903
- }
904
908
  // Prepare the list of accounts to withdraw from
905
909
  const withdrawFrom = [];
906
910
  let remainingAmount = new BN(amount);
@@ -909,23 +913,24 @@ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress,
909
913
  numerator: fee.denominator.sub(fee.numerator),
910
914
  denominator: fee.denominator,
911
915
  };
912
- for (const type of ['preferred', 'active', 'transient', 'reserve']) {
916
+ for (const type of ['preferred', 'active', 'transient']) {
913
917
  const filteredAccounts = accounts.filter(a => a.type === type);
914
918
  for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
915
- if (lamports.lte(minBalance) && type === 'transient') {
916
- continue;
917
- }
918
919
  let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports);
919
920
  if (!skipFee && !inverseFee.numerator.isZero()) {
920
921
  availableForWithdrawal = availableForWithdrawal
921
922
  .mul(inverseFee.denominator)
922
923
  .div(inverseFee.numerator);
923
924
  }
925
+ // In ValidatorRemoval mode, must withdraw full validator balance (no partial)
926
+ // Skip if remaining amount is less than full validator balance
927
+ if (isValidatorRemovalMode && remainingAmount.lt(availableForWithdrawal)) {
928
+ continue;
929
+ }
924
930
  const poolAmount = BN.min(availableForWithdrawal, remainingAmount);
925
931
  if (poolAmount.lte(new BN(0))) {
926
932
  continue;
927
933
  }
928
- // Those accounts will be withdrawn completely with `claim` instruction
929
934
  withdrawFrom.push({ stakeAddress, voteAddress, poolAmount });
930
935
  remainingAmount = remainingAmount.sub(poolAmount);
931
936
  if (remainingAmount.isZero()) {
@@ -938,7 +943,23 @@ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress,
938
943
  }
939
944
  // Not enough stake to withdraw the specified amount
940
945
  if (remainingAmount.gt(new BN(0))) {
941
- throw new Error(`No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol(amount)} pool tokens.`);
946
+ if (allowPartial) {
947
+ const delayedAmount = amount.sub(remainingAmount);
948
+ return {
949
+ withdrawAccounts: withdrawFrom,
950
+ delayedAmount,
951
+ remainingAmount,
952
+ };
953
+ }
954
+ const availableAmount = amount.sub(remainingAmount);
955
+ throw new Error(`Not enough staked funds for delayed unstake. Requested ${lamportsToSol(amount)} iFOGO, but only ${lamportsToSol(availableAmount)} available. Use instant unstake for the remaining amount.`);
956
+ }
957
+ if (allowPartial) {
958
+ return {
959
+ withdrawAccounts: withdrawFrom,
960
+ delayedAmount: amount,
961
+ remainingAmount: new BN(0),
962
+ };
942
963
  }
943
964
  return withdrawFrom;
944
965
  }
@@ -1651,7 +1672,7 @@ class StakePoolInstruction {
1651
1672
  }
1652
1673
  /**
1653
1674
  * Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
1654
- * The stake account is created as a PDA and rent is paid by the payer (typically paymaster).
1675
+ * The stake account is created as a PDA and rent is funded from the reserve.
1655
1676
  */
1656
1677
  static withdrawStakeWithSession(params) {
1657
1678
  const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStakeWithSession;
@@ -1666,17 +1687,19 @@ class StakePoolInstruction {
1666
1687
  { pubkey: params.withdrawAuthority, isSigner: false, isWritable: false },
1667
1688
  { pubkey: params.stakeToSplit, isSigner: false, isWritable: true },
1668
1689
  { pubkey: params.stakeToReceive, isSigner: false, isWritable: true },
1669
- { pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info (signer_or_session)
1670
- { pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (not used in session path)
1690
+ { pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info
1691
+ { pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (unused in session path)
1671
1692
  { pubkey: params.burnFromPool, isSigner: false, isWritable: true },
1672
1693
  { pubkey: params.managerFeeAccount, isSigner: false, isWritable: true },
1673
1694
  { pubkey: params.poolMint, isSigner: false, isWritable: true },
1674
1695
  { pubkey: web3_js.SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
1675
1696
  { pubkey: params.tokenProgramId, isSigner: false, isWritable: false },
1676
1697
  { pubkey: web3_js.StakeProgram.programId, isSigner: false, isWritable: false },
1698
+ // Session-specific accounts
1677
1699
  { pubkey: params.programSigner, isSigner: false, isWritable: false },
1678
1700
  { pubkey: web3_js.SystemProgram.programId, isSigner: false, isWritable: false },
1679
- { pubkey: params.payer, isSigner: true, isWritable: true },
1701
+ { pubkey: params.reserveStake, isSigner: false, isWritable: true },
1702
+ { pubkey: web3_js.SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
1680
1703
  ];
1681
1704
  return new web3_js.TransactionInstruction({
1682
1705
  programId: params.programId,
@@ -2416,37 +2439,51 @@ async function getUserStakeAccounts(connection, programId, userPubkey, maxSeed =
2416
2439
  * Withdraws stake from a stake pool using a Fogo session.
2417
2440
  *
2418
2441
  * The on-chain program creates stake account PDAs. The rent for these accounts
2419
- * is paid by the payer (typically the paymaster), not deducted from the user's withdrawal.
2442
+ * is funded from the reserve stake.
2420
2443
  *
2421
2444
  * @param connection - Solana connection
2422
2445
  * @param stakePoolAddress - The stake pool to withdraw from
2423
2446
  * @param signerOrSession - The session signer public key
2424
2447
  * @param userPubkey - User's wallet (used for PDA derivation and token ownership)
2425
- * @param payer - Payer for stake account rent (typically paymaster)
2426
2448
  * @param amount - Amount of pool tokens to withdraw
2427
2449
  * @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
2428
2450
  * @param useReserve - Whether to withdraw from reserve (default: false)
2429
2451
  * @param voteAccountAddress - Optional specific validator to withdraw from
2430
2452
  * @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
2431
2453
  * @param validatorComparator - Optional comparator for validator selection
2454
+ * @param allowPartial - If true, returns partial results instead of throwing when not enough stake available
2432
2455
  */
2433
- async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, payer, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator) {
2434
- const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
2456
+ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator, allowPartial = false) {
2457
+ var _c;
2435
2458
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
2459
+ // First fetch: get stake pool to know other account addresses
2460
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
2436
2461
  const stakePool = stakePoolAccount.account.data;
2437
2462
  const poolTokens = solToLamports(amount);
2438
2463
  const poolAmount = new BN(poolTokens);
2439
2464
  const poolTokenAccount = splToken.getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey);
2440
- const tokenAccount = await splToken.getAccount(connection, poolTokenAccount);
2465
+ // Second fetch: get ALL remaining data in parallel
2466
+ const [tokenAccount, stakeAccountRentExemption, validatorListAcc, stakeMinimumDelegationResponse] = await Promise.all([
2467
+ splToken.getAccount(connection, poolTokenAccount),
2468
+ connection.getMinimumBalanceForRentExemption(web3_js.StakeProgram.space),
2469
+ connection.getAccountInfo(stakePool.validatorList),
2470
+ connection.getStakeMinimumDelegation(),
2471
+ ]);
2472
+ // Pre-fetch data to avoid duplicate RPC calls in prepareWithdrawAccounts
2473
+ const prefetchedData = {
2474
+ validatorListData: (_c = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _c !== void 0 ? _c : null,
2475
+ minBalanceForRentExemption: stakeAccountRentExemption,
2476
+ stakeMinimumDelegation: Number(stakeMinimumDelegationResponse.value),
2477
+ };
2441
2478
  if (tokenAccount.amount < poolTokens) {
2442
2479
  throw new Error(`Not enough token balance to withdraw ${amount} pool tokens.
2443
2480
  Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`);
2444
2481
  }
2445
2482
  const [programSigner] = web3_js.PublicKey.findProgramAddressSync([Buffer.from('fogo_session_program_signer')], stakePoolProgramId);
2446
2483
  const withdrawAuthority = await findWithdrawAuthorityProgramAddress(stakePoolProgramId, stakePoolAddress);
2447
- const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption(web3_js.StakeProgram.space);
2448
2484
  // Determine which stake accounts to withdraw from
2449
2485
  const withdrawAccounts = [];
2486
+ let partialRemainingAmount;
2450
2487
  if (useReserve) {
2451
2488
  withdrawAccounts.push({
2452
2489
  stakeAddress: stakePool.reserveStake,
@@ -2477,7 +2514,14 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
2477
2514
  }
2478
2515
  else {
2479
2516
  // Get the list of accounts to withdraw from automatically
2480
- withdrawAccounts.push(...(await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount))));
2517
+ if (allowPartial) {
2518
+ const result = await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), true, prefetchedData);
2519
+ withdrawAccounts.push(...result.withdrawAccounts);
2520
+ partialRemainingAmount = result.remainingAmount;
2521
+ }
2522
+ else {
2523
+ withdrawAccounts.push(...(await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), undefined, prefetchedData)));
2524
+ }
2481
2525
  }
2482
2526
  const instructions = [];
2483
2527
  const stakeAccountPubkeys = [];
@@ -2494,7 +2538,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
2494
2538
  const stakeReceiverPubkey = findUserStakeProgramAddress(stakePoolProgramId, userPubkey, userStakeSeed);
2495
2539
  stakeAccountPubkeys.push(stakeReceiverPubkey);
2496
2540
  userStakeSeeds.push(userStakeSeed);
2497
- // The on-chain program creates the stake account PDA and rent is paid by payer.
2541
+ // The on-chain program creates the stake account PDA and rent is funded from reserve.
2498
2542
  instructions.push(StakePoolInstruction.withdrawStakeWithSession({
2499
2543
  programId: stakePoolProgramId,
2500
2544
  stakePool: stakePoolAddress,
@@ -2508,7 +2552,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
2508
2552
  poolMint: stakePool.poolMint,
2509
2553
  tokenProgramId: stakePool.tokenProgramId,
2510
2554
  programSigner,
2511
- payer,
2555
+ reserveStake: stakePool.reserveStake,
2512
2556
  poolTokensIn: withdrawAccount.poolAmount.toNumber(),
2513
2557
  minimumLamportsOut,
2514
2558
  userStakeSeed,
@@ -2519,6 +2563,7 @@ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSe
2519
2563
  instructions,
2520
2564
  stakeAccountPubkeys,
2521
2565
  userStakeSeeds,
2566
+ remainingPoolTokens: partialRemainingAmount ? lamportsToSol(partialRemainingAmount) : 0,
2522
2567
  };
2523
2568
  }
2524
2569
  async function addValidatorToPool(connection, stakePoolAddress, validatorVote, seed) {