@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.
@@ -178,8 +178,8 @@ 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
+ /** Reserve stake account for rent funding */
182
+ reserveStake: PublicKey;
183
183
  poolTokensIn: number;
184
184
  minimumLamportsOut: number;
185
185
  /** Seed used to derive the user stake PDA */
@@ -344,7 +344,7 @@ export declare class StakePoolInstruction {
344
344
  static withdrawWsolWithSession(params: WithdrawWsolWithSessionParams): TransactionInstruction;
345
345
  /**
346
346
  * Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
347
- * The stake account is created as a PDA and rent is paid by the payer (typically paymaster).
347
+ * The stake account is created as a PDA and rent is funded from the reserve.
348
348
  */
349
349
  static withdrawStakeWithSession(params: WithdrawStakeWithSessionParams): TransactionInstruction;
350
350
  /**
@@ -12,12 +12,29 @@ export declare function getValidatorListAccount(connection: Connection, pubkey:
12
12
  };
13
13
  }>;
14
14
  export interface ValidatorAccount {
15
- type: 'preferred' | 'active' | 'transient' | 'reserve';
15
+ type: 'preferred' | 'active' | 'transient';
16
16
  voteAddress?: PublicKey | undefined;
17
17
  stakeAddress: PublicKey;
18
18
  lamports: BN;
19
19
  }
20
- export declare function prepareWithdrawAccounts(connection: Connection, stakePool: StakePool, stakePoolAddress: PublicKey, amount: BN, compareFn?: (a: ValidatorAccount, b: ValidatorAccount) => number, skipFee?: boolean): Promise<WithdrawAccount[]>;
20
+ export interface PrepareWithdrawResult {
21
+ withdrawAccounts: WithdrawAccount[];
22
+ /** Pool tokens that will be withdrawn via delayed unstake */
23
+ delayedAmount: BN;
24
+ /** Pool tokens remaining that need instant unstake */
25
+ remainingAmount: BN;
26
+ }
27
+ /** Pre-fetched data to avoid duplicate RPC calls */
28
+ export interface PrepareWithdrawPrefetchedData {
29
+ /** Raw validator list account data from getAccountInfo */
30
+ validatorListData: Buffer | null;
31
+ /** Rent exemption for stake accounts in lamports */
32
+ minBalanceForRentExemption: number;
33
+ /** Minimum stake delegation in lamports */
34
+ stakeMinimumDelegation: number;
35
+ }
36
+ export declare function prepareWithdrawAccounts(connection: Connection, stakePool: StakePool, stakePoolAddress: PublicKey, amount: BN, compareFn?: (a: ValidatorAccount, b: ValidatorAccount) => number, skipFee?: boolean, allowPartial?: boolean, prefetchedData?: PrepareWithdrawPrefetchedData): Promise<WithdrawAccount[]>;
37
+ export declare function prepareWithdrawAccounts(connection: Connection, stakePool: StakePool, stakePoolAddress: PublicKey, amount: BN, compareFn: ((a: ValidatorAccount, b: ValidatorAccount) => number) | undefined, skipFee: boolean | undefined, allowPartial: true, prefetchedData?: PrepareWithdrawPrefetchedData): Promise<PrepareWithdrawResult>;
21
38
  /**
22
39
  * Calculate the pool tokens that should be minted for a deposit of `stakeLamports`
23
40
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ignitionfi/fogo-stake-pool",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Fogo Stake Pool SDK",
5
5
  "contributors": [
6
6
  "Anza Maintainers <maintainers@anza.xyz>",
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from 'node:buffer'
2
- import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'
2
+ import { PublicKey } from '@solana/web3.js'
3
3
 
4
4
  // Public key that identifies the metadata program.
5
5
  export const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s')
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  lamportsToSol,
48
48
  newStakeAccount,
49
49
  prepareWithdrawAccounts,
50
+ PrepareWithdrawPrefetchedData,
50
51
  solToLamports,
51
52
  ValidatorAccount,
52
53
  } from './utils'
@@ -1034,41 +1035,57 @@ export async function getUserStakeAccounts(
1034
1035
  * Withdraws stake from a stake pool using a Fogo session.
1035
1036
  *
1036
1037
  * 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
+ * is funded from the reserve stake.
1038
1039
  *
1039
1040
  * @param connection - Solana connection
1040
1041
  * @param stakePoolAddress - The stake pool to withdraw from
1041
1042
  * @param signerOrSession - The session signer public key
1042
1043
  * @param userPubkey - User's wallet (used for PDA derivation and token ownership)
1043
- * @param payer - Payer for stake account rent (typically paymaster)
1044
1044
  * @param amount - Amount of pool tokens to withdraw
1045
1045
  * @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
1046
1046
  * @param useReserve - Whether to withdraw from reserve (default: false)
1047
1047
  * @param voteAccountAddress - Optional specific validator to withdraw from
1048
1048
  * @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
1049
1049
  * @param validatorComparator - Optional comparator for validator selection
1050
+ * @param allowPartial - If true, returns partial results instead of throwing when not enough stake available
1050
1051
  */
1051
1052
  export async function withdrawStakeWithSession(
1052
1053
  connection: Connection,
1053
1054
  stakePoolAddress: PublicKey,
1054
1055
  signerOrSession: PublicKey,
1055
1056
  userPubkey: PublicKey,
1056
- payer: PublicKey,
1057
1057
  amount: number,
1058
1058
  userStakeSeedStart: number = 0,
1059
1059
  useReserve = false,
1060
1060
  voteAccountAddress?: PublicKey,
1061
1061
  minimumLamportsOut: number = 0,
1062
1062
  validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
1063
+ allowPartial = false,
1063
1064
  ) {
1064
- const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
1065
1065
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
1066
+
1067
+ // First fetch: get stake pool to know other account addresses
1068
+ const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
1066
1069
  const stakePool = stakePoolAccount.account.data
1067
1070
  const poolTokens = solToLamports(amount)
1068
1071
  const poolAmount = new BN(poolTokens)
1069
1072
 
1070
1073
  const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey)
1071
- const tokenAccount = await getAccount(connection, poolTokenAccount)
1074
+
1075
+ // Second fetch: get ALL remaining data in parallel
1076
+ const [tokenAccount, stakeAccountRentExemption, validatorListAcc, stakeMinimumDelegationResponse] = await Promise.all([
1077
+ getAccount(connection, poolTokenAccount),
1078
+ connection.getMinimumBalanceForRentExemption(StakeProgram.space),
1079
+ connection.getAccountInfo(stakePool.validatorList),
1080
+ connection.getStakeMinimumDelegation(),
1081
+ ])
1082
+
1083
+ // Pre-fetch data to avoid duplicate RPC calls in prepareWithdrawAccounts
1084
+ const prefetchedData: PrepareWithdrawPrefetchedData = {
1085
+ validatorListData: validatorListAcc?.data ?? null,
1086
+ minBalanceForRentExemption: stakeAccountRentExemption,
1087
+ stakeMinimumDelegation: Number(stakeMinimumDelegationResponse.value),
1088
+ }
1072
1089
 
1073
1090
  if (tokenAccount.amount < poolTokens) {
1074
1091
  throw new Error(
@@ -1087,10 +1104,9 @@ export async function withdrawStakeWithSession(
1087
1104
  stakePoolAddress,
1088
1105
  )
1089
1106
 
1090
- const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
1091
-
1092
1107
  // Determine which stake accounts to withdraw from
1093
1108
  const withdrawAccounts: WithdrawAccount[] = []
1109
+ let partialRemainingAmount: BN | undefined
1094
1110
 
1095
1111
  if (useReserve) {
1096
1112
  withdrawAccounts.push({
@@ -1133,16 +1149,33 @@ export async function withdrawStakeWithSession(
1133
1149
  })
1134
1150
  } else {
1135
1151
  // Get the list of accounts to withdraw from automatically
1136
- withdrawAccounts.push(
1137
- ...(await prepareWithdrawAccounts(
1152
+ if (allowPartial) {
1153
+ const result = await prepareWithdrawAccounts(
1138
1154
  connection,
1139
1155
  stakePool,
1140
1156
  stakePoolAddress,
1141
1157
  poolAmount,
1142
1158
  validatorComparator,
1143
1159
  poolTokenAccount.equals(stakePool.managerFeeAccount),
1144
- )),
1145
- )
1160
+ true,
1161
+ prefetchedData,
1162
+ )
1163
+ withdrawAccounts.push(...result.withdrawAccounts)
1164
+ partialRemainingAmount = result.remainingAmount
1165
+ } else {
1166
+ withdrawAccounts.push(
1167
+ ...(await prepareWithdrawAccounts(
1168
+ connection,
1169
+ stakePool,
1170
+ stakePoolAddress,
1171
+ poolAmount,
1172
+ validatorComparator,
1173
+ poolTokenAccount.equals(stakePool.managerFeeAccount),
1174
+ undefined,
1175
+ prefetchedData,
1176
+ )),
1177
+ )
1178
+ }
1146
1179
  }
1147
1180
 
1148
1181
  const instructions: TransactionInstruction[] = []
@@ -1169,7 +1202,7 @@ export async function withdrawStakeWithSession(
1169
1202
  stakeAccountPubkeys.push(stakeReceiverPubkey)
1170
1203
  userStakeSeeds.push(userStakeSeed)
1171
1204
 
1172
- // The on-chain program creates the stake account PDA and rent is paid by payer.
1205
+ // The on-chain program creates the stake account PDA and rent is funded from reserve.
1173
1206
  instructions.push(
1174
1207
  StakePoolInstruction.withdrawStakeWithSession({
1175
1208
  programId: stakePoolProgramId,
@@ -1184,7 +1217,7 @@ export async function withdrawStakeWithSession(
1184
1217
  poolMint: stakePool.poolMint,
1185
1218
  tokenProgramId: stakePool.tokenProgramId,
1186
1219
  programSigner,
1187
- payer,
1220
+ reserveStake: stakePool.reserveStake,
1188
1221
  poolTokensIn: withdrawAccount.poolAmount.toNumber(),
1189
1222
  minimumLamportsOut,
1190
1223
  userStakeSeed,
@@ -1197,6 +1230,7 @@ export async function withdrawStakeWithSession(
1197
1230
  instructions,
1198
1231
  stakeAccountPubkeys,
1199
1232
  userStakeSeeds,
1233
+ remainingPoolTokens: partialRemainingAmount ? lamportsToSol(partialRemainingAmount) : 0,
1200
1234
  }
1201
1235
  }
1202
1236
 
@@ -426,8 +426,8 @@ export type WithdrawStakeWithSessionParams = {
426
426
  tokenProgramId: PublicKey
427
427
  /** The program signer PDA derived from PROGRAM_SIGNER_SEED */
428
428
  programSigner: PublicKey
429
- /** The payer for stake account rent (typically the paymaster) */
430
- payer: PublicKey
429
+ /** Reserve stake account for rent funding */
430
+ reserveStake: PublicKey
431
431
  poolTokensIn: number
432
432
  minimumLamportsOut: number
433
433
  /** Seed used to derive the user stake PDA */
@@ -1222,7 +1222,7 @@ export class StakePoolInstruction {
1222
1222
 
1223
1223
  /**
1224
1224
  * Creates a transaction instruction to withdraw stake from a stake pool using a Fogo session.
1225
- * The stake account is created as a PDA and rent is paid by the payer (typically paymaster).
1225
+ * The stake account is created as a PDA and rent is funded from the reserve.
1226
1226
  */
1227
1227
  static withdrawStakeWithSession(params: WithdrawStakeWithSessionParams): TransactionInstruction {
1228
1228
  const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStakeWithSession
@@ -1238,17 +1238,19 @@ export class StakePoolInstruction {
1238
1238
  { pubkey: params.withdrawAuthority, isSigner: false, isWritable: false },
1239
1239
  { pubkey: params.stakeToSplit, isSigner: false, isWritable: true },
1240
1240
  { pubkey: params.stakeToReceive, isSigner: false, isWritable: true },
1241
- { pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info (signer_or_session)
1242
- { pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (not used in session path)
1241
+ { pubkey: params.sessionSigner, isSigner: true, isWritable: false }, // user_stake_authority_info
1242
+ { pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (unused in session path)
1243
1243
  { pubkey: params.burnFromPool, isSigner: false, isWritable: true },
1244
1244
  { pubkey: params.managerFeeAccount, isSigner: false, isWritable: true },
1245
1245
  { pubkey: params.poolMint, isSigner: false, isWritable: true },
1246
1246
  { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
1247
1247
  { pubkey: params.tokenProgramId, isSigner: false, isWritable: false },
1248
1248
  { pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
1249
+ // Session-specific accounts
1249
1250
  { pubkey: params.programSigner, isSigner: false, isWritable: false },
1250
1251
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1251
- { pubkey: params.payer, isSigner: true, isWritable: true },
1252
+ { pubkey: params.reserveStake, isSigner: false, isWritable: true },
1253
+ { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false },
1252
1254
  ]
1253
1255
 
1254
1256
  return new TransactionInstruction({
@@ -38,12 +38,50 @@ export async function getValidatorListAccount(connection: Connection, pubkey: Pu
38
38
  }
39
39
 
40
40
  export interface ValidatorAccount {
41
- type: 'preferred' | 'active' | 'transient' | 'reserve'
41
+ type: 'preferred' | 'active' | 'transient'
42
42
  voteAddress?: PublicKey | undefined
43
43
  stakeAddress: PublicKey
44
44
  lamports: BN
45
45
  }
46
46
 
47
+ export interface PrepareWithdrawResult {
48
+ withdrawAccounts: WithdrawAccount[]
49
+ /** Pool tokens that will be withdrawn via delayed unstake */
50
+ delayedAmount: BN
51
+ /** Pool tokens remaining that need instant unstake */
52
+ remainingAmount: BN
53
+ }
54
+
55
+ /** Pre-fetched data to avoid duplicate RPC calls */
56
+ export interface PrepareWithdrawPrefetchedData {
57
+ /** Raw validator list account data from getAccountInfo */
58
+ validatorListData: Buffer | null
59
+ /** Rent exemption for stake accounts in lamports */
60
+ minBalanceForRentExemption: number
61
+ /** Minimum stake delegation in lamports */
62
+ stakeMinimumDelegation: number
63
+ }
64
+
65
+ export async function prepareWithdrawAccounts(
66
+ connection: Connection,
67
+ stakePool: StakePool,
68
+ stakePoolAddress: PublicKey,
69
+ amount: BN,
70
+ compareFn?: (a: ValidatorAccount, b: ValidatorAccount) => number,
71
+ skipFee?: boolean,
72
+ allowPartial?: boolean,
73
+ prefetchedData?: PrepareWithdrawPrefetchedData,
74
+ ): Promise<WithdrawAccount[]>
75
+ export async function prepareWithdrawAccounts(
76
+ connection: Connection,
77
+ stakePool: StakePool,
78
+ stakePoolAddress: PublicKey,
79
+ amount: BN,
80
+ compareFn: ((a: ValidatorAccount, b: ValidatorAccount) => number) | undefined,
81
+ skipFee: boolean | undefined,
82
+ allowPartial: true,
83
+ prefetchedData?: PrepareWithdrawPrefetchedData,
84
+ ): Promise<PrepareWithdrawResult>
47
85
  export async function prepareWithdrawAccounts(
48
86
  connection: Connection,
49
87
  stakePool: StakePool,
@@ -51,26 +89,65 @@ export async function prepareWithdrawAccounts(
51
89
  amount: BN,
52
90
  compareFn?: (a: ValidatorAccount, b: ValidatorAccount) => number,
53
91
  skipFee?: boolean,
54
- ): Promise<WithdrawAccount[]> {
92
+ allowPartial?: boolean,
93
+ prefetchedData?: PrepareWithdrawPrefetchedData,
94
+ ): Promise<WithdrawAccount[] | PrepareWithdrawResult> {
55
95
  const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
56
- const validatorListAcc = await connection.getAccountInfo(stakePool.validatorList)
57
- const validatorList = ValidatorListLayout.decode(validatorListAcc?.data) as ValidatorList
96
+
97
+ // Use prefetched data if available, otherwise fetch from RPC
98
+ let validatorListData: Buffer | null
99
+ let minBalanceForRentExemption: number
100
+ let stakeMinimumDelegation: number
101
+
102
+ if (prefetchedData) {
103
+ validatorListData = prefetchedData.validatorListData
104
+ minBalanceForRentExemption = prefetchedData.minBalanceForRentExemption
105
+ stakeMinimumDelegation = prefetchedData.stakeMinimumDelegation
106
+ } else {
107
+ const [validatorListAcc, rentExemption, stakeMinimumDelegationResponse] = await Promise.all([
108
+ connection.getAccountInfo(stakePool.validatorList),
109
+ connection.getMinimumBalanceForRentExemption(StakeProgram.space),
110
+ connection.getStakeMinimumDelegation(),
111
+ ])
112
+ validatorListData = validatorListAcc?.data ?? null
113
+ minBalanceForRentExemption = rentExemption
114
+ stakeMinimumDelegation = Number(stakeMinimumDelegationResponse.value)
115
+ }
116
+
117
+ if (!validatorListData) {
118
+ throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.')
119
+ }
120
+
121
+ const validatorList = ValidatorListLayout.decode(validatorListData) as ValidatorList
58
122
 
59
123
  if (!validatorList?.validators || validatorList?.validators.length === 0) {
60
- throw new Error('No accounts found')
124
+ throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.')
61
125
  }
62
126
 
63
- const minBalanceForRentExemption = await connection.getMinimumBalanceForRentExemption(
64
- StakeProgram.space,
127
+ // minBalance = rent + max(stake_minimum_delegation, MINIMUM_ACTIVE_STAKE)
128
+ const minimumDelegation = Math.max(stakeMinimumDelegation, MINIMUM_ACTIVE_STAKE)
129
+ const minBalance = new BN(minBalanceForRentExemption + minimumDelegation)
130
+
131
+ // Threshold for has_active_stake check (ceiling division for lamports_per_pool_token)
132
+ const lamportsPerPoolToken = stakePool.totalLamports
133
+ .add(stakePool.poolTokenSupply)
134
+ .sub(new BN(1))
135
+ .div(stakePool.poolTokenSupply)
136
+ const minimumLamportsWithTolerance = minBalance.add(lamportsPerPoolToken)
137
+
138
+ const hasActiveStake = validatorList.validators.some(
139
+ v => v.status === ValidatorStakeInfoStatus.Active
140
+ && v.activeStakeLamports.gt(minimumLamportsWithTolerance),
65
141
  )
66
- const minBalance = new BN(minBalanceForRentExemption + MINIMUM_ACTIVE_STAKE)
142
+ const hasTransientStake = validatorList.validators.some(
143
+ v => v.status === ValidatorStakeInfoStatus.Active
144
+ && v.transientStakeLamports.gt(minimumLamportsWithTolerance),
145
+ )
146
+
147
+ // ValidatorRemoval mode: no validator above threshold
148
+ const isValidatorRemovalMode = !hasActiveStake && !hasTransientStake
67
149
 
68
- // First, collect all stake account addresses we need to check
69
- const accountsToFetch: Array<{
70
- type: 'preferred' | 'active' | 'transient'
71
- voteAddress: PublicKey
72
- stakeAddress: PublicKey
73
- }> = []
150
+ let accounts: ValidatorAccount[] = []
74
151
 
75
152
  for (const validator of validatorList.validators) {
76
153
  if (validator.status !== ValidatorStakeInfoStatus.Active) {
@@ -83,63 +160,39 @@ export async function prepareWithdrawAccounts(
83
160
  stakePoolAddress,
84
161
  )
85
162
 
86
- const isPreferred = stakePool?.preferredWithdrawValidatorVoteAddress?.equals(
87
- validator.voteAccountAddress,
88
- )
163
+ // ValidatorRemoval: full balance available; Normal: leave minBalance
164
+ const availableActiveLamports = isValidatorRemovalMode
165
+ ? validator.activeStakeLamports
166
+ : validator.activeStakeLamports.sub(minBalance)
89
167
 
90
- // Add active stake account if validator list indicates it has stake
91
- if (validator.activeStakeLamports.gt(new BN(0))) {
92
- accountsToFetch.push({
168
+ if (availableActiveLamports.gt(new BN(0))) {
169
+ const isPreferred = stakePool?.preferredWithdrawValidatorVoteAddress?.equals(
170
+ validator.voteAccountAddress,
171
+ )
172
+ accounts.push({
93
173
  type: isPreferred ? 'preferred' : 'active',
94
174
  voteAddress: validator.voteAccountAddress,
95
175
  stakeAddress: stakeAccountAddress,
176
+ lamports: availableActiveLamports,
96
177
  })
97
178
  }
98
179
 
99
- // Add transient stake account if validator list indicates it has stake
100
- if (validator.transientStakeLamports.gt(new BN(0))) {
180
+ const availableTransientLamports = isValidatorRemovalMode
181
+ ? validator.transientStakeLamports
182
+ : validator.transientStakeLamports.sub(minBalance)
183
+
184
+ if (availableTransientLamports.gt(new BN(0))) {
101
185
  const transientStakeAccountAddress = await findTransientStakeProgramAddress(
102
186
  stakePoolProgramId,
103
187
  validator.voteAccountAddress,
104
188
  stakePoolAddress,
105
189
  validator.transientSeedSuffixStart,
106
190
  )
107
- accountsToFetch.push({
191
+ accounts.push({
108
192
  type: 'transient',
109
193
  voteAddress: validator.voteAccountAddress,
110
194
  stakeAddress: transientStakeAccountAddress,
111
- })
112
- }
113
- }
114
-
115
- // Fetch all stake accounts + reserve in one batch call
116
- const addressesToFetch = [
117
- ...accountsToFetch.map(a => a.stakeAddress),
118
- stakePool.reserveStake,
119
- ]
120
- const accountInfos = await connection.getMultipleAccountsInfo(addressesToFetch)
121
-
122
- // Build accounts list using actual on-chain balances
123
- let accounts: ValidatorAccount[] = []
124
-
125
- for (let i = 0; i < accountsToFetch.length; i++) {
126
- const { type, voteAddress, stakeAddress } = accountsToFetch[i]
127
- const accountInfo = accountInfos[i]
128
-
129
- if (!accountInfo) {
130
- continue
131
- }
132
-
133
- // Use actual on-chain balance instead of validator list value
134
- const actualLamports = new BN(accountInfo.lamports)
135
- const availableLamports = actualLamports.sub(minBalance)
136
-
137
- if (availableLamports.gt(new BN(0))) {
138
- accounts.push({
139
- type,
140
- voteAddress,
141
- stakeAddress,
142
- lamports: availableLamports,
195
+ lamports: availableTransientLamports,
143
196
  })
144
197
  }
145
198
  }
@@ -147,17 +200,6 @@ export async function prepareWithdrawAccounts(
147
200
  // Sort from highest to lowest balance
148
201
  accounts = accounts.sort(compareFn || ((a, b) => b.lamports.sub(a.lamports).toNumber()))
149
202
 
150
- // Add reserve stake using actual balance (last item in batch fetch)
151
- const reserveAccountInfo = accountInfos[accountInfos.length - 1]
152
- const reserveStakeBalance = new BN((reserveAccountInfo?.lamports ?? 0) - minBalanceForRentExemption)
153
- if (reserveStakeBalance.gt(new BN(0))) {
154
- accounts.push({
155
- type: 'reserve',
156
- stakeAddress: stakePool.reserveStake,
157
- lamports: reserveStakeBalance,
158
- })
159
- }
160
-
161
203
  // Prepare the list of accounts to withdraw from
162
204
  const withdrawFrom: WithdrawAccount[] = []
163
205
  let remainingAmount = new BN(amount)
@@ -168,14 +210,10 @@ export async function prepareWithdrawAccounts(
168
210
  denominator: fee.denominator,
169
211
  }
170
212
 
171
- for (const type of ['preferred', 'active', 'transient', 'reserve']) {
213
+ for (const type of ['preferred', 'active', 'transient']) {
172
214
  const filteredAccounts = accounts.filter(a => a.type === type)
173
215
 
174
216
  for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
175
- if (lamports.lte(minBalance) && type === 'transient') {
176
- continue
177
- }
178
-
179
217
  let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports)
180
218
 
181
219
  if (!skipFee && !inverseFee.numerator.isZero()) {
@@ -184,12 +222,17 @@ export async function prepareWithdrawAccounts(
184
222
  .div(inverseFee.numerator)
185
223
  }
186
224
 
225
+ // In ValidatorRemoval mode, must withdraw full validator balance (no partial)
226
+ // Skip if remaining amount is less than full validator balance
227
+ if (isValidatorRemovalMode && remainingAmount.lt(availableForWithdrawal)) {
228
+ continue
229
+ }
230
+
187
231
  const poolAmount = BN.min(availableForWithdrawal, remainingAmount)
188
232
  if (poolAmount.lte(new BN(0))) {
189
233
  continue
190
234
  }
191
235
 
192
- // Those accounts will be withdrawn completely with `claim` instruction
193
236
  withdrawFrom.push({ stakeAddress, voteAddress, poolAmount })
194
237
  remainingAmount = remainingAmount.sub(poolAmount)
195
238
 
@@ -205,13 +248,28 @@ export async function prepareWithdrawAccounts(
205
248
 
206
249
  // Not enough stake to withdraw the specified amount
207
250
  if (remainingAmount.gt(new BN(0))) {
251
+ if (allowPartial) {
252
+ const delayedAmount = amount.sub(remainingAmount)
253
+ return {
254
+ withdrawAccounts: withdrawFrom,
255
+ delayedAmount,
256
+ remainingAmount,
257
+ }
258
+ }
259
+ const availableAmount = amount.sub(remainingAmount)
208
260
  throw new Error(
209
- `No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol(
210
- amount,
211
- )} pool tokens.`,
261
+ `Not enough staked funds for delayed unstake. Requested ${lamportsToSol(amount)} iFOGO, but only ${lamportsToSol(availableAmount)} available. Use instant unstake for the remaining amount.`,
212
262
  )
213
263
  }
214
264
 
265
+ if (allowPartial) {
266
+ return {
267
+ withdrawAccounts: withdrawFrom,
268
+ delayedAmount: amount,
269
+ remainingAmount: new BN(0),
270
+ }
271
+ }
272
+
215
273
  return withdrawFrom
216
274
  }
217
275