@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.browser.cjs.js +116 -71
- package/dist/index.browser.cjs.js.map +1 -1
- package/dist/index.browser.esm.js +116 -71
- package/dist/index.browser.esm.js.map +1 -1
- package/dist/index.cjs.js +116 -71
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.esm.js +116 -71
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +116 -71
- package/dist/index.iife.js.map +1 -1
- package/dist/index.iife.min.js +1 -1
- package/dist/index.iife.min.js.map +1 -1
- package/dist/instructions.d.ts +3 -3
- package/dist/utils/stake.d.ts +19 -2
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/index.ts +47 -13
- package/src/instructions.ts +8 -6
- package/src/utils/stake.ts +133 -75
package/dist/instructions.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
182
|
-
|
|
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
|
|
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
|
/**
|
package/dist/utils/stake.d.ts
CHANGED
|
@@ -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'
|
|
15
|
+
type: 'preferred' | 'active' | 'transient';
|
|
16
16
|
voteAddress?: PublicKey | undefined;
|
|
17
17
|
stakeAddress: PublicKey;
|
|
18
18
|
lamports: BN;
|
|
19
19
|
}
|
|
20
|
-
export
|
|
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
package/src/constants.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/src/instructions.ts
CHANGED
|
@@ -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
|
-
/**
|
|
430
|
-
|
|
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
|
|
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
|
|
1242
|
-
{ pubkey: params.sessionSigner, isSigner: false, isWritable: false }, // user_transfer_authority_info (
|
|
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.
|
|
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({
|
package/src/utils/stake.ts
CHANGED
|
@@ -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'
|
|
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
|
-
|
|
92
|
+
allowPartial?: boolean,
|
|
93
|
+
prefetchedData?: PrepareWithdrawPrefetchedData,
|
|
94
|
+
): Promise<WithdrawAccount[] | PrepareWithdrawResult> {
|
|
55
95
|
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
124
|
+
throw new Error('No staked funds available for delayed unstake. Use instant unstake instead.')
|
|
61
125
|
}
|
|
62
126
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
163
|
+
// ValidatorRemoval: full balance available; Normal: leave minBalance
|
|
164
|
+
const availableActiveLamports = isValidatorRemovalMode
|
|
165
|
+
? validator.activeStakeLamports
|
|
166
|
+
: validator.activeStakeLamports.sub(minBalance)
|
|
89
167
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
`
|
|
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
|
|