@ignitionfi/fogo-stake-pool 1.0.2 → 1.1.0
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/codecs.d.ts +10 -0
- package/dist/index.browser.cjs.js +369 -208
- package/dist/index.browser.cjs.js.map +1 -1
- package/dist/index.browser.esm.js +369 -208
- package/dist/index.browser.esm.js.map +1 -1
- package/dist/index.cjs.js +369 -208
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.esm.js +369 -208
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +403 -242
- 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 +31 -22
- package/dist/utils/stake.d.ts +19 -2
- package/package.json +1 -1
- package/src/codecs.ts +59 -6
- package/src/constants.ts +1 -1
- package/src/index.ts +79 -26
- package/src/instructions.ts +140 -67
- package/src/utils/stake.ts +133 -75
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
|
|