@ignitionfi/fogo-stake-pool 1.0.1 → 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.
@@ -19024,7 +19024,7 @@ var solanaStakePool = (function (exports, node_buffer) {
19024
19024
  const USER_STAKE_SEED_PREFIX = node_buffer.Buffer.from('user_stake');
19025
19025
  // Minimum amount of staked SOL required in a validator stake account to allow
19026
19026
  // for merges without a mismatch on credits observed
19027
- const MINIMUM_ACTIVE_STAKE = LAMPORTS_PER_SOL;
19027
+ const MINIMUM_ACTIVE_STAKE = 1000000;
19028
19028
 
19029
19029
  /**
19030
19030
  * Populate a buffer of instruction data using an InstructionType
@@ -22008,80 +22008,84 @@ var solanaStakePool = (function (exports, node_buffer) {
22008
22008
  },
22009
22009
  };
22010
22010
  }
22011
- async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee) {
22011
+ async function prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, amount, compareFn, skipFee, allowPartial, prefetchedData) {
22012
22012
  var _a, _b;
22013
22013
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
22014
- const validatorListAcc = await connection.getAccountInfo(stakePool.validatorList);
22015
- const validatorList = ValidatorListLayout.decode(validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data);
22016
- if (!(validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators) || (validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators.length) === 0) {
22017
- throw new Error('No accounts found');
22014
+ // Use prefetched data if available, otherwise fetch from RPC
22015
+ let validatorListData;
22016
+ let minBalanceForRentExemption;
22017
+ let stakeMinimumDelegation;
22018
+ if (prefetchedData) {
22019
+ validatorListData = prefetchedData.validatorListData;
22020
+ minBalanceForRentExemption = prefetchedData.minBalanceForRentExemption;
22021
+ stakeMinimumDelegation = prefetchedData.stakeMinimumDelegation;
22018
22022
  }
22019
- const minBalanceForRentExemption = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);
22020
- const minBalance = new BN(minBalanceForRentExemption + MINIMUM_ACTIVE_STAKE);
22021
- // First, collect all stake account addresses we need to check
22022
- const accountsToFetch = [];
22023
+ else {
22024
+ const [validatorListAcc, rentExemption, stakeMinimumDelegationResponse] = await Promise.all([
22025
+ connection.getAccountInfo(stakePool.validatorList),
22026
+ connection.getMinimumBalanceForRentExemption(StakeProgram.space),
22027
+ connection.getStakeMinimumDelegation(),
22028
+ ]);
22029
+ validatorListData = (_a = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _a !== void 0 ? _a : null;
22030
+ minBalanceForRentExemption = rentExemption;
22031
+ stakeMinimumDelegation = Number(stakeMinimumDelegationResponse.value);
22032
+ }
22033
+ if (!validatorListData) {
22034
+ throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
22035
+ }
22036
+ const validatorList = ValidatorListLayout.decode(validatorListData);
22037
+ if (!(validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators) || (validatorList === null || validatorList === void 0 ? void 0 : validatorList.validators.length) === 0) {
22038
+ throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.');
22039
+ }
22040
+ // minBalance = rent + max(stake_minimum_delegation, MINIMUM_ACTIVE_STAKE)
22041
+ const minimumDelegation = Math.max(stakeMinimumDelegation, MINIMUM_ACTIVE_STAKE);
22042
+ const minBalance = new BN(minBalanceForRentExemption + minimumDelegation);
22043
+ // Threshold for has_active_stake check (ceiling division for lamports_per_pool_token)
22044
+ const lamportsPerPoolToken = stakePool.totalLamports
22045
+ .add(stakePool.poolTokenSupply)
22046
+ .sub(new BN(1))
22047
+ .div(stakePool.poolTokenSupply);
22048
+ const minimumLamportsWithTolerance = minBalance.add(lamportsPerPoolToken);
22049
+ const hasActiveStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
22050
+ && v.activeStakeLamports.gt(minimumLamportsWithTolerance));
22051
+ const hasTransientStake = validatorList.validators.some(v => v.status === ValidatorStakeInfoStatus.Active
22052
+ && v.transientStakeLamports.gt(minimumLamportsWithTolerance));
22053
+ // ValidatorRemoval mode: no validator above threshold
22054
+ const isValidatorRemovalMode = !hasActiveStake && !hasTransientStake;
22055
+ let accounts = [];
22023
22056
  for (const validator of validatorList.validators) {
22024
22057
  if (validator.status !== ValidatorStakeInfoStatus.Active) {
22025
22058
  continue;
22026
22059
  }
22027
22060
  const stakeAccountAddress = await findStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress);
22028
- const isPreferred = (_a = stakePool === null || stakePool === void 0 ? void 0 : stakePool.preferredWithdrawValidatorVoteAddress) === null || _a === void 0 ? void 0 : _a.equals(validator.voteAccountAddress);
22029
- // Add active stake account if validator list indicates it has stake
22030
- if (validator.activeStakeLamports.gt(new BN(0))) {
22031
- accountsToFetch.push({
22061
+ // ValidatorRemoval: full balance available; Normal: leave minBalance
22062
+ const availableActiveLamports = isValidatorRemovalMode
22063
+ ? validator.activeStakeLamports
22064
+ : validator.activeStakeLamports.sub(minBalance);
22065
+ if (availableActiveLamports.gt(new BN(0))) {
22066
+ const isPreferred = (_b = stakePool === null || stakePool === void 0 ? void 0 : stakePool.preferredWithdrawValidatorVoteAddress) === null || _b === void 0 ? void 0 : _b.equals(validator.voteAccountAddress);
22067
+ accounts.push({
22032
22068
  type: isPreferred ? 'preferred' : 'active',
22033
22069
  voteAddress: validator.voteAccountAddress,
22034
22070
  stakeAddress: stakeAccountAddress,
22071
+ lamports: availableActiveLamports,
22035
22072
  });
22036
22073
  }
22037
- // Add transient stake account if validator list indicates it has stake
22038
- if (validator.transientStakeLamports.gt(new BN(0))) {
22074
+ const availableTransientLamports = isValidatorRemovalMode
22075
+ ? validator.transientStakeLamports
22076
+ : validator.transientStakeLamports.sub(minBalance);
22077
+ if (availableTransientLamports.gt(new BN(0))) {
22039
22078
  const transientStakeAccountAddress = await findTransientStakeProgramAddress(stakePoolProgramId, validator.voteAccountAddress, stakePoolAddress, validator.transientSeedSuffixStart);
22040
- accountsToFetch.push({
22079
+ accounts.push({
22041
22080
  type: 'transient',
22042
22081
  voteAddress: validator.voteAccountAddress,
22043
22082
  stakeAddress: transientStakeAccountAddress,
22044
- });
22045
- }
22046
- }
22047
- // Fetch all stake accounts + reserve in one batch call
22048
- const addressesToFetch = [
22049
- ...accountsToFetch.map(a => a.stakeAddress),
22050
- stakePool.reserveStake,
22051
- ];
22052
- const accountInfos = await connection.getMultipleAccountsInfo(addressesToFetch);
22053
- // Build accounts list using actual on-chain balances
22054
- let accounts = [];
22055
- for (let i = 0; i < accountsToFetch.length; i++) {
22056
- const { type, voteAddress, stakeAddress } = accountsToFetch[i];
22057
- const accountInfo = accountInfos[i];
22058
- if (!accountInfo) {
22059
- continue;
22060
- }
22061
- // Use actual on-chain balance instead of validator list value
22062
- const actualLamports = new BN(accountInfo.lamports);
22063
- const availableLamports = actualLamports.sub(minBalance);
22064
- if (availableLamports.gt(new BN(0))) {
22065
- accounts.push({
22066
- type,
22067
- voteAddress,
22068
- stakeAddress,
22069
- lamports: availableLamports,
22083
+ lamports: availableTransientLamports,
22070
22084
  });
22071
22085
  }
22072
22086
  }
22073
22087
  // Sort from highest to lowest balance
22074
22088
  accounts = accounts.sort(compareFn || ((a, b) => b.lamports.sub(a.lamports).toNumber()));
22075
- // Add reserve stake using actual balance (last item in batch fetch)
22076
- const reserveAccountInfo = accountInfos[accountInfos.length - 1];
22077
- const reserveStakeBalance = new BN(((_b = reserveAccountInfo === null || reserveAccountInfo === void 0 ? void 0 : reserveAccountInfo.lamports) !== null && _b !== void 0 ? _b : 0) - minBalanceForRentExemption);
22078
- if (reserveStakeBalance.gt(new BN(0))) {
22079
- accounts.push({
22080
- type: 'reserve',
22081
- stakeAddress: stakePool.reserveStake,
22082
- lamports: reserveStakeBalance,
22083
- });
22084
- }
22085
22089
  // Prepare the list of accounts to withdraw from
22086
22090
  const withdrawFrom = [];
22087
22091
  let remainingAmount = new BN(amount);
@@ -22090,23 +22094,24 @@ var solanaStakePool = (function (exports, node_buffer) {
22090
22094
  numerator: fee.denominator.sub(fee.numerator),
22091
22095
  denominator: fee.denominator,
22092
22096
  };
22093
- for (const type of ['preferred', 'active', 'transient', 'reserve']) {
22097
+ for (const type of ['preferred', 'active', 'transient']) {
22094
22098
  const filteredAccounts = accounts.filter(a => a.type === type);
22095
22099
  for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
22096
- if (lamports.lte(minBalance) && type === 'transient') {
22097
- continue;
22098
- }
22099
22100
  let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports);
22100
22101
  if (!skipFee && !inverseFee.numerator.isZero()) {
22101
22102
  availableForWithdrawal = availableForWithdrawal
22102
22103
  .mul(inverseFee.denominator)
22103
22104
  .div(inverseFee.numerator);
22104
22105
  }
22106
+ // In ValidatorRemoval mode, must withdraw full validator balance (no partial)
22107
+ // Skip if remaining amount is less than full validator balance
22108
+ if (isValidatorRemovalMode && remainingAmount.lt(availableForWithdrawal)) {
22109
+ continue;
22110
+ }
22105
22111
  const poolAmount = BN.min(availableForWithdrawal, remainingAmount);
22106
22112
  if (poolAmount.lte(new BN(0))) {
22107
22113
  continue;
22108
22114
  }
22109
- // Those accounts will be withdrawn completely with `claim` instruction
22110
22115
  withdrawFrom.push({ stakeAddress, voteAddress, poolAmount });
22111
22116
  remainingAmount = remainingAmount.sub(poolAmount);
22112
22117
  if (remainingAmount.isZero()) {
@@ -22119,7 +22124,23 @@ var solanaStakePool = (function (exports, node_buffer) {
22119
22124
  }
22120
22125
  // Not enough stake to withdraw the specified amount
22121
22126
  if (remainingAmount.gt(new BN(0))) {
22122
- throw new Error(`No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol(amount)} pool tokens.`);
22127
+ if (allowPartial) {
22128
+ const delayedAmount = amount.sub(remainingAmount);
22129
+ return {
22130
+ withdrawAccounts: withdrawFrom,
22131
+ delayedAmount,
22132
+ remainingAmount,
22133
+ };
22134
+ }
22135
+ const availableAmount = amount.sub(remainingAmount);
22136
+ 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.`);
22137
+ }
22138
+ if (allowPartial) {
22139
+ return {
22140
+ withdrawAccounts: withdrawFrom,
22141
+ delayedAmount: amount,
22142
+ remainingAmount: new BN(0),
22143
+ };
22123
22144
  }
22124
22145
  return withdrawFrom;
22125
22146
  }
@@ -22832,7 +22853,7 @@ var solanaStakePool = (function (exports, node_buffer) {
22832
22853
  }
22833
22854
  /**
22834
22855
  * Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
22835
- * The stake account is created as a PDA and rent is paid by the payer (typically paymaster).
22856
+ * The stake account is created as a PDA and rent is funded from the reserve.
22836
22857
  */
22837
22858
  static withdrawStakeWithSession(params) {
22838
22859
  const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStakeWithSession;
@@ -22847,17 +22868,19 @@ var solanaStakePool = (function (exports, node_buffer) {
22847
22868
  { pubkey: params.withdrawAuthority, isSigner: false, isWritable: false },
22848
22869
  { pubkey: params.stakeToSplit, isSigner: false, isWritable: true },
22849
22870
  { pubkey: params.stakeToReceive, isSigner: false, isWritable: true },
22850
- { pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info (signer_or_session)
22851
- { pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (not used in session path)
22871
+ { pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info
22872
+ { pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (unused in session path)
22852
22873
  { pubkey: params.burnFromPool, isSigner: false, isWritable: true },
22853
22874
  { pubkey: params.managerFeeAccount, isSigner: false, isWritable: true },
22854
22875
  { pubkey: params.poolMint, isSigner: false, isWritable: true },
22855
22876
  { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
22856
22877
  { pubkey: params.tokenProgramId, isSigner: false, isWritable: false },
22857
22878
  { pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
22879
+ // Session-specific accounts
22858
22880
  { pubkey: params.programSigner, isSigner: false, isWritable: false },
22859
22881
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
22860
- { pubkey: params.payer, isSigner: true, isWritable: true },
22882
+ { pubkey: params.reserveStake, isSigner: false, isWritable: true },
22883
+ { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
22861
22884
  ];
22862
22885
  return new TransactionInstruction({
22863
22886
  programId: params.programId,
@@ -23597,37 +23620,51 @@ var solanaStakePool = (function (exports, node_buffer) {
23597
23620
  * Withdraws stake from a stake pool using a Fogo session.
23598
23621
  *
23599
23622
  * The on-chain program creates stake account PDAs. The rent for these accounts
23600
- * is paid by the payer (typically the paymaster), not deducted from the user's withdrawal.
23623
+ * is funded from the reserve stake.
23601
23624
  *
23602
23625
  * @param connection - Solana connection
23603
23626
  * @param stakePoolAddress - The stake pool to withdraw from
23604
23627
  * @param signerOrSession - The session signer public key
23605
23628
  * @param userPubkey - User's wallet (used for PDA derivation and token ownership)
23606
- * @param payer - Payer for stake account rent (typically paymaster)
23607
23629
  * @param amount - Amount of pool tokens to withdraw
23608
23630
  * @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
23609
23631
  * @param useReserve - Whether to withdraw from reserve (default: false)
23610
23632
  * @param voteAccountAddress - Optional specific validator to withdraw from
23611
23633
  * @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
23612
23634
  * @param validatorComparator - Optional comparator for validator selection
23635
+ * @param allowPartial - If true, returns partial results instead of throwing when not enough stake available
23613
23636
  */
23614
- async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, payer, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator) {
23615
- const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
23637
+ async function withdrawStakeWithSession(connection, stakePoolAddress, signerOrSession, userPubkey, amount, userStakeSeedStart = 0, useReserve = false, voteAccountAddress, minimumLamportsOut = 0, validatorComparator, allowPartial = false) {
23638
+ var _c;
23616
23639
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint);
23640
+ // First fetch: get stake pool to know other account addresses
23641
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
23617
23642
  const stakePool = stakePoolAccount.account.data;
23618
23643
  const poolTokens = solToLamports(amount);
23619
23644
  const poolAmount = new BN(poolTokens);
23620
23645
  const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey);
23621
- const tokenAccount = await getAccount(connection, poolTokenAccount);
23646
+ // Second fetch: get ALL remaining data in parallel
23647
+ const [tokenAccount, stakeAccountRentExemption, validatorListAcc, stakeMinimumDelegationResponse] = await Promise.all([
23648
+ getAccount(connection, poolTokenAccount),
23649
+ connection.getMinimumBalanceForRentExemption(StakeProgram.space),
23650
+ connection.getAccountInfo(stakePool.validatorList),
23651
+ connection.getStakeMinimumDelegation(),
23652
+ ]);
23653
+ // Pre-fetch data to avoid duplicate RPC calls in prepareWithdrawAccounts
23654
+ const prefetchedData = {
23655
+ validatorListData: (_c = validatorListAcc === null || validatorListAcc === void 0 ? void 0 : validatorListAcc.data) !== null && _c !== void 0 ? _c : null,
23656
+ minBalanceForRentExemption: stakeAccountRentExemption,
23657
+ stakeMinimumDelegation: Number(stakeMinimumDelegationResponse.value),
23658
+ };
23622
23659
  if (tokenAccount.amount < poolTokens) {
23623
23660
  throw new Error(`Not enough token balance to withdraw ${amount} pool tokens.
23624
23661
  Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`);
23625
23662
  }
23626
23663
  const [programSigner] = PublicKey.findProgramAddressSync([Buffer.from('fogo_session_program_signer')], stakePoolProgramId);
23627
23664
  const withdrawAuthority = await findWithdrawAuthorityProgramAddress(stakePoolProgramId, stakePoolAddress);
23628
- const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);
23629
23665
  // Determine which stake accounts to withdraw from
23630
23666
  const withdrawAccounts = [];
23667
+ let partialRemainingAmount;
23631
23668
  if (useReserve) {
23632
23669
  withdrawAccounts.push({
23633
23670
  stakeAddress: stakePool.reserveStake,
@@ -23658,7 +23695,14 @@ var solanaStakePool = (function (exports, node_buffer) {
23658
23695
  }
23659
23696
  else {
23660
23697
  // Get the list of accounts to withdraw from automatically
23661
- withdrawAccounts.push(...(await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount))));
23698
+ if (allowPartial) {
23699
+ const result = await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), true, prefetchedData);
23700
+ withdrawAccounts.push(...result.withdrawAccounts);
23701
+ partialRemainingAmount = result.remainingAmount;
23702
+ }
23703
+ else {
23704
+ withdrawAccounts.push(...(await prepareWithdrawAccounts(connection, stakePool, stakePoolAddress, poolAmount, validatorComparator, poolTokenAccount.equals(stakePool.managerFeeAccount), undefined, prefetchedData)));
23705
+ }
23662
23706
  }
23663
23707
  const instructions = [];
23664
23708
  const stakeAccountPubkeys = [];
@@ -23675,7 +23719,7 @@ var solanaStakePool = (function (exports, node_buffer) {
23675
23719
  const stakeReceiverPubkey = findUserStakeProgramAddress(stakePoolProgramId, userPubkey, userStakeSeed);
23676
23720
  stakeAccountPubkeys.push(stakeReceiverPubkey);
23677
23721
  userStakeSeeds.push(userStakeSeed);
23678
- // The on-chain program creates the stake account PDA and rent is paid by payer.
23722
+ // The on-chain program creates the stake account PDA and rent is funded from reserve.
23679
23723
  instructions.push(StakePoolInstruction.withdrawStakeWithSession({
23680
23724
  programId: stakePoolProgramId,
23681
23725
  stakePool: stakePoolAddress,
@@ -23689,7 +23733,7 @@ var solanaStakePool = (function (exports, node_buffer) {
23689
23733
  poolMint: stakePool.poolMint,
23690
23734
  tokenProgramId: stakePool.tokenProgramId,
23691
23735
  programSigner,
23692
- payer,
23736
+ reserveStake: stakePool.reserveStake,
23693
23737
  poolTokensIn: withdrawAccount.poolAmount.toNumber(),
23694
23738
  minimumLamportsOut,
23695
23739
  userStakeSeed,
@@ -23700,6 +23744,7 @@ var solanaStakePool = (function (exports, node_buffer) {
23700
23744
  instructions,
23701
23745
  stakeAccountPubkeys,
23702
23746
  userStakeSeeds,
23747
+ remainingPoolTokens: partialRemainingAmount ? lamportsToSol(partialRemainingAmount) : 0,
23703
23748
  };
23704
23749
  }
23705
23750
  async function addValidatorToPool(connection, stakePoolAddress, validatorVote, seed) {