@ignitionfi/fogo-stake-pool 1.0.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/LICENSE +176 -0
- package/dist/codecs.d.ts +15 -0
- package/dist/constants.d.ts +13 -0
- package/dist/index.browser.cjs.js +2898 -0
- package/dist/index.browser.cjs.js.map +1 -0
- package/dist/index.browser.esm.js +2843 -0
- package/dist/index.browser.esm.js.map +1 -0
- package/dist/index.cjs.js +2898 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.esm.js +2843 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.iife.js +24083 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.iife.min.js +19 -0
- package/dist/index.iife.min.js.map +1 -0
- package/dist/instructions.d.ts +381 -0
- package/dist/layouts.d.ts +318 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/instruction.d.ts +21 -0
- package/dist/utils/math.d.ts +3 -0
- package/dist/utils/program-address.d.ts +31 -0
- package/dist/utils/stake.d.ts +29 -0
- package/package.json +90 -0
- package/src/codecs.ts +159 -0
- package/src/constants.ts +30 -0
- package/src/index.ts +1823 -0
- package/src/instructions.ts +1439 -0
- package/src/layouts.ts +248 -0
- package/src/types/buffer-layout.d.ts +29 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/instruction.ts +46 -0
- package/src/utils/math.ts +29 -0
- package/src/utils/program-address.ts +125 -0
- package/src/utils/stake.ts +233 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1823 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createApproveInstruction,
|
|
3
|
+
createAssociatedTokenAccountIdempotentInstruction,
|
|
4
|
+
getAccount,
|
|
5
|
+
getAssociatedTokenAddressSync,
|
|
6
|
+
NATIVE_MINT,
|
|
7
|
+
} from '@solana/spl-token'
|
|
8
|
+
import {
|
|
9
|
+
AccountInfo,
|
|
10
|
+
Connection,
|
|
11
|
+
Keypair,
|
|
12
|
+
PublicKey,
|
|
13
|
+
Signer,
|
|
14
|
+
StakeAuthorizationLayout,
|
|
15
|
+
StakeProgram,
|
|
16
|
+
SystemProgram,
|
|
17
|
+
TransactionInstruction,
|
|
18
|
+
} from '@solana/web3.js'
|
|
19
|
+
import BN from 'bn.js'
|
|
20
|
+
import { create } from 'superstruct'
|
|
21
|
+
import {
|
|
22
|
+
DEVNET_STAKE_POOL_PROGRAM_ID,
|
|
23
|
+
MAX_VALIDATORS_TO_UPDATE,
|
|
24
|
+
MINIMUM_ACTIVE_STAKE,
|
|
25
|
+
STAKE_POOL_PROGRAM_ID,
|
|
26
|
+
} from './constants'
|
|
27
|
+
import { StakePoolInstruction } from './instructions'
|
|
28
|
+
import {
|
|
29
|
+
StakeAccount,
|
|
30
|
+
StakePool,
|
|
31
|
+
StakePoolLayout,
|
|
32
|
+
ValidatorList,
|
|
33
|
+
ValidatorListLayout,
|
|
34
|
+
ValidatorStakeInfo,
|
|
35
|
+
} from './layouts'
|
|
36
|
+
import {
|
|
37
|
+
arrayChunk,
|
|
38
|
+
calcLamportsWithdrawAmount,
|
|
39
|
+
findEphemeralStakeProgramAddress,
|
|
40
|
+
findMetadataAddress,
|
|
41
|
+
findStakeProgramAddress,
|
|
42
|
+
findTransientStakeProgramAddress,
|
|
43
|
+
findUserStakeProgramAddress,
|
|
44
|
+
findWithdrawAuthorityProgramAddress,
|
|
45
|
+
findWsolTransientProgramAddress,
|
|
46
|
+
getValidatorListAccount,
|
|
47
|
+
lamportsToSol,
|
|
48
|
+
newStakeAccount,
|
|
49
|
+
prepareWithdrawAccounts,
|
|
50
|
+
solToLamports,
|
|
51
|
+
ValidatorAccount,
|
|
52
|
+
} from './utils'
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
DEVNET_STAKE_POOL_PROGRAM_ID,
|
|
56
|
+
STAKE_POOL_PROGRAM_ID,
|
|
57
|
+
} from './constants'
|
|
58
|
+
export * from './instructions'
|
|
59
|
+
export type {
|
|
60
|
+
AccountType,
|
|
61
|
+
StakePool,
|
|
62
|
+
ValidatorList,
|
|
63
|
+
ValidatorStakeInfo,
|
|
64
|
+
} from './layouts'
|
|
65
|
+
export {
|
|
66
|
+
StakePoolLayout,
|
|
67
|
+
ValidatorListLayout,
|
|
68
|
+
ValidatorStakeInfoLayout,
|
|
69
|
+
} from './layouts'
|
|
70
|
+
export {
|
|
71
|
+
findEphemeralStakeProgramAddress,
|
|
72
|
+
findStakeProgramAddress,
|
|
73
|
+
findTransientStakeProgramAddress,
|
|
74
|
+
findUserStakeProgramAddress,
|
|
75
|
+
findWithdrawAuthorityProgramAddress,
|
|
76
|
+
findWsolTransientProgramAddress,
|
|
77
|
+
} from './utils'
|
|
78
|
+
|
|
79
|
+
export interface ValidatorListAccount {
|
|
80
|
+
pubkey: PublicKey
|
|
81
|
+
account: AccountInfo<ValidatorList>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface StakePoolAccount {
|
|
85
|
+
pubkey: PublicKey
|
|
86
|
+
account: AccountInfo<StakePool>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface WithdrawAccount {
|
|
90
|
+
stakeAddress: PublicKey
|
|
91
|
+
voteAddress?: PublicKey
|
|
92
|
+
poolAmount: BN
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Wrapper class for a stake pool.
|
|
97
|
+
* Each stake pool has a stake pool account and a validator list account.
|
|
98
|
+
*/
|
|
99
|
+
export interface StakePoolAccounts {
|
|
100
|
+
stakePool: StakePoolAccount | undefined
|
|
101
|
+
validatorList: ValidatorListAccount | undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getStakePoolProgramId(rpcEndpoint: string): PublicKey {
|
|
105
|
+
if (rpcEndpoint.includes('devnet')) {
|
|
106
|
+
return DEVNET_STAKE_POOL_PROGRAM_ID
|
|
107
|
+
} else {
|
|
108
|
+
return STAKE_POOL_PROGRAM_ID
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Retrieves and deserializes a StakePool account using a web3js connection and the stake pool address.
|
|
114
|
+
* @param connection An active web3js connection.
|
|
115
|
+
* @param stakePoolAddress The public key (address) of the stake pool account.
|
|
116
|
+
*/
|
|
117
|
+
export async function getStakePoolAccount(
|
|
118
|
+
connection: Connection,
|
|
119
|
+
stakePoolAddress: PublicKey,
|
|
120
|
+
): Promise<StakePoolAccount> {
|
|
121
|
+
const account = await connection.getAccountInfo(stakePoolAddress)
|
|
122
|
+
|
|
123
|
+
if (!account) {
|
|
124
|
+
throw new Error('Invalid stake pool account')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
pubkey: stakePoolAddress,
|
|
129
|
+
account: {
|
|
130
|
+
data: StakePoolLayout.decode(account.data),
|
|
131
|
+
executable: account.executable,
|
|
132
|
+
lamports: account.lamports,
|
|
133
|
+
owner: account.owner,
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Retrieves and deserializes a Stake account using a web3js connection and the stake address.
|
|
140
|
+
* @param connection An active web3js connection.
|
|
141
|
+
* @param stakeAccount The public key (address) of the stake account.
|
|
142
|
+
*/
|
|
143
|
+
export async function getStakeAccount(
|
|
144
|
+
connection: Connection,
|
|
145
|
+
stakeAccount: PublicKey,
|
|
146
|
+
): Promise<StakeAccount> {
|
|
147
|
+
const result = (await connection.getParsedAccountInfo(stakeAccount)).value
|
|
148
|
+
if (!result || !('parsed' in result.data)) {
|
|
149
|
+
throw new Error('Invalid stake account')
|
|
150
|
+
}
|
|
151
|
+
const program = result.data.program
|
|
152
|
+
if (program !== 'stake') {
|
|
153
|
+
throw new Error('Not a stake account')
|
|
154
|
+
}
|
|
155
|
+
return create(result.data.parsed, StakeAccount)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Retrieves all StakePool and ValidatorList accounts that are running a particular StakePool program.
|
|
160
|
+
* @param connection An active web3js connection.
|
|
161
|
+
* @param stakePoolProgramAddress The public key (address) of the StakePool program.
|
|
162
|
+
*/
|
|
163
|
+
export async function getStakePoolAccounts(
|
|
164
|
+
connection: Connection,
|
|
165
|
+
stakePoolProgramAddress: PublicKey,
|
|
166
|
+
): Promise<
|
|
167
|
+
(StakePoolAccount | ValidatorListAccount | undefined)[] | undefined
|
|
168
|
+
> {
|
|
169
|
+
const response = await connection.getProgramAccounts(stakePoolProgramAddress)
|
|
170
|
+
|
|
171
|
+
return response
|
|
172
|
+
.map((a) => {
|
|
173
|
+
try {
|
|
174
|
+
if (a.account.data.readUInt8() === 1) {
|
|
175
|
+
const data = StakePoolLayout.decode(a.account.data)
|
|
176
|
+
return {
|
|
177
|
+
pubkey: a.pubkey,
|
|
178
|
+
account: {
|
|
179
|
+
data,
|
|
180
|
+
executable: a.account.executable,
|
|
181
|
+
lamports: a.account.lamports,
|
|
182
|
+
owner: a.account.owner,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
} else if (a.account.data.readUInt8() === 2) {
|
|
186
|
+
const data = ValidatorListLayout.decode(a.account.data)
|
|
187
|
+
return {
|
|
188
|
+
pubkey: a.pubkey,
|
|
189
|
+
account: {
|
|
190
|
+
data,
|
|
191
|
+
executable: a.account.executable,
|
|
192
|
+
lamports: a.account.lamports,
|
|
193
|
+
owner: a.account.owner,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
console.error(
|
|
198
|
+
`Could not decode. StakePoolAccount Enum is ${a.account.data.readUInt8()}, expected 1 or 2!`,
|
|
199
|
+
)
|
|
200
|
+
return undefined
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Could not decode account. Error:', error)
|
|
204
|
+
return undefined
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
.filter(a => a !== undefined)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Creates instructions required to deposit stake to stake pool.
|
|
212
|
+
*/
|
|
213
|
+
export async function depositStake(
|
|
214
|
+
connection: Connection,
|
|
215
|
+
stakePoolAddress: PublicKey,
|
|
216
|
+
authorizedPubkey: PublicKey,
|
|
217
|
+
validatorVote: PublicKey,
|
|
218
|
+
depositStake: PublicKey,
|
|
219
|
+
poolTokenReceiverAccount?: PublicKey,
|
|
220
|
+
) {
|
|
221
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
222
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
223
|
+
|
|
224
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
225
|
+
stakePoolProgramId,
|
|
226
|
+
stakePoolAddress,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const validatorStake = await findStakeProgramAddress(
|
|
230
|
+
stakePoolProgramId,
|
|
231
|
+
validatorVote,
|
|
232
|
+
stakePoolAddress,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const instructions: TransactionInstruction[] = []
|
|
236
|
+
const signers: Signer[] = []
|
|
237
|
+
|
|
238
|
+
const poolMint = stakePool.account.data.poolMint
|
|
239
|
+
|
|
240
|
+
// Create token account if not specified
|
|
241
|
+
if (!poolTokenReceiverAccount) {
|
|
242
|
+
const associatedAddress = getAssociatedTokenAddressSync(
|
|
243
|
+
poolMint,
|
|
244
|
+
authorizedPubkey,
|
|
245
|
+
)
|
|
246
|
+
instructions.push(
|
|
247
|
+
createAssociatedTokenAccountIdempotentInstruction(
|
|
248
|
+
authorizedPubkey,
|
|
249
|
+
associatedAddress,
|
|
250
|
+
authorizedPubkey,
|
|
251
|
+
poolMint,
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
poolTokenReceiverAccount = associatedAddress
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
instructions.push(
|
|
258
|
+
...StakeProgram.authorize({
|
|
259
|
+
stakePubkey: depositStake,
|
|
260
|
+
authorizedPubkey,
|
|
261
|
+
newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority,
|
|
262
|
+
stakeAuthorizationType: StakeAuthorizationLayout.Staker,
|
|
263
|
+
}).instructions,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
instructions.push(
|
|
267
|
+
...StakeProgram.authorize({
|
|
268
|
+
stakePubkey: depositStake,
|
|
269
|
+
authorizedPubkey,
|
|
270
|
+
newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority,
|
|
271
|
+
stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer,
|
|
272
|
+
}).instructions,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
instructions.push(
|
|
276
|
+
StakePoolInstruction.depositStake({
|
|
277
|
+
programId: stakePoolProgramId,
|
|
278
|
+
stakePool: stakePoolAddress,
|
|
279
|
+
validatorList: stakePool.account.data.validatorList,
|
|
280
|
+
depositAuthority: stakePool.account.data.stakeDepositAuthority,
|
|
281
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
282
|
+
managerFeeAccount: stakePool.account.data.managerFeeAccount,
|
|
283
|
+
referralPoolAccount: poolTokenReceiverAccount,
|
|
284
|
+
destinationPoolAccount: poolTokenReceiverAccount,
|
|
285
|
+
withdrawAuthority,
|
|
286
|
+
depositStake,
|
|
287
|
+
validatorStake,
|
|
288
|
+
poolMint,
|
|
289
|
+
}),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
instructions,
|
|
294
|
+
signers,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Creates instructions required to deposit sol to stake pool.
|
|
300
|
+
*/
|
|
301
|
+
export async function depositWsolWithSession(
|
|
302
|
+
connection: Connection,
|
|
303
|
+
stakePoolAddress: PublicKey,
|
|
304
|
+
signerOrSession: PublicKey,
|
|
305
|
+
userPubkey: PublicKey,
|
|
306
|
+
lamports: number,
|
|
307
|
+
minimumPoolTokensOut: number = 0,
|
|
308
|
+
destinationTokenAccount?: PublicKey,
|
|
309
|
+
referrerTokenAccount?: PublicKey,
|
|
310
|
+
depositAuthority?: PublicKey,
|
|
311
|
+
payer?: PublicKey,
|
|
312
|
+
/**
|
|
313
|
+
* Skip WSOL balance validation. Set to true when adding wrap instructions
|
|
314
|
+
* in the same transaction that will fund the WSOL account before deposit.
|
|
315
|
+
*/
|
|
316
|
+
skipBalanceCheck: boolean = false,
|
|
317
|
+
) {
|
|
318
|
+
const wsolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, userPubkey)
|
|
319
|
+
|
|
320
|
+
if (!skipBalanceCheck) {
|
|
321
|
+
const tokenAccountInfo = await connection.getTokenAccountBalance(
|
|
322
|
+
wsolTokenAccount,
|
|
323
|
+
'confirmed',
|
|
324
|
+
)
|
|
325
|
+
const wsolBalance = tokenAccountInfo
|
|
326
|
+
? parseInt(tokenAccountInfo.value.amount)
|
|
327
|
+
: 0
|
|
328
|
+
|
|
329
|
+
if (wsolBalance < lamports) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Not enough WSOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
|
|
332
|
+
wsolBalance,
|
|
333
|
+
)} WSOL.`,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
|
|
339
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
340
|
+
const stakePool = stakePoolAccount.account.data
|
|
341
|
+
|
|
342
|
+
const instructions: TransactionInstruction[] = []
|
|
343
|
+
|
|
344
|
+
// The program handles ATA creation internally using funds from the user's deposit
|
|
345
|
+
// This prevents rent drain attacks where paymaster pays for ATA and user reclaims rent
|
|
346
|
+
if (!destinationTokenAccount) {
|
|
347
|
+
destinationTokenAccount = getAssociatedTokenAddressSync(
|
|
348
|
+
stakePool.poolMint,
|
|
349
|
+
userPubkey,
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
354
|
+
stakePoolProgramId,
|
|
355
|
+
stakePoolAddress,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
const [programSigner] = PublicKey.findProgramAddressSync(
|
|
359
|
+
[Buffer.from('fogo_session_program_signer')],
|
|
360
|
+
stakePoolProgramId,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
const wsolTransientAccount = findWsolTransientProgramAddress(
|
|
364
|
+
stakePoolProgramId,
|
|
365
|
+
userPubkey,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
instructions.push(
|
|
369
|
+
StakePoolInstruction.depositWsolWithSession({
|
|
370
|
+
programId: stakePoolProgramId,
|
|
371
|
+
stakePool: stakePoolAddress,
|
|
372
|
+
reserveStake: stakePool.reserveStake,
|
|
373
|
+
fundingAccount: signerOrSession,
|
|
374
|
+
destinationPoolAccount: destinationTokenAccount,
|
|
375
|
+
managerFeeAccount: stakePool.managerFeeAccount,
|
|
376
|
+
referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount,
|
|
377
|
+
poolMint: stakePool.poolMint,
|
|
378
|
+
lamportsIn: lamports,
|
|
379
|
+
minimumPoolTokensOut,
|
|
380
|
+
withdrawAuthority,
|
|
381
|
+
depositAuthority,
|
|
382
|
+
wsolMint: NATIVE_MINT,
|
|
383
|
+
wsolTokenAccount,
|
|
384
|
+
wsolTransientAccount,
|
|
385
|
+
tokenProgramId: stakePool.tokenProgramId,
|
|
386
|
+
programSigner,
|
|
387
|
+
payer,
|
|
388
|
+
userWallet: userPubkey,
|
|
389
|
+
}),
|
|
390
|
+
)
|
|
391
|
+
return {
|
|
392
|
+
instructions,
|
|
393
|
+
signers: [],
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Creates instructions required to deposit sol to stake pool.
|
|
399
|
+
*/
|
|
400
|
+
export async function depositSol(
|
|
401
|
+
connection: Connection,
|
|
402
|
+
stakePoolAddress: PublicKey,
|
|
403
|
+
from: PublicKey,
|
|
404
|
+
lamports: number,
|
|
405
|
+
destinationTokenAccount?: PublicKey,
|
|
406
|
+
referrerTokenAccount?: PublicKey,
|
|
407
|
+
depositAuthority?: PublicKey,
|
|
408
|
+
) {
|
|
409
|
+
const fromBalance = await connection.getBalance(from, 'confirmed')
|
|
410
|
+
if (fromBalance < lamports) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Not enough SOL to deposit into pool. Maximum deposit amount is ${lamportsToSol(
|
|
413
|
+
fromBalance,
|
|
414
|
+
)} SOL.`,
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const stakePoolAccount = await getStakePoolAccount(
|
|
419
|
+
connection,
|
|
420
|
+
stakePoolAddress,
|
|
421
|
+
)
|
|
422
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
423
|
+
const stakePool = stakePoolAccount.account.data
|
|
424
|
+
|
|
425
|
+
// Ephemeral SOL account just to do the transfer
|
|
426
|
+
const userSolTransfer = new Keypair()
|
|
427
|
+
const signers: Signer[] = [userSolTransfer]
|
|
428
|
+
const instructions: TransactionInstruction[] = []
|
|
429
|
+
|
|
430
|
+
// Create the ephemeral SOL account
|
|
431
|
+
instructions.push(
|
|
432
|
+
SystemProgram.transfer({
|
|
433
|
+
fromPubkey: from,
|
|
434
|
+
toPubkey: userSolTransfer.publicKey,
|
|
435
|
+
lamports,
|
|
436
|
+
}),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
// Create token account if not specified
|
|
440
|
+
if (!destinationTokenAccount) {
|
|
441
|
+
const associatedAddress = getAssociatedTokenAddressSync(
|
|
442
|
+
stakePool.poolMint,
|
|
443
|
+
from,
|
|
444
|
+
)
|
|
445
|
+
instructions.push(
|
|
446
|
+
createAssociatedTokenAccountIdempotentInstruction(
|
|
447
|
+
from,
|
|
448
|
+
associatedAddress,
|
|
449
|
+
from,
|
|
450
|
+
stakePool.poolMint,
|
|
451
|
+
),
|
|
452
|
+
)
|
|
453
|
+
destinationTokenAccount = associatedAddress
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
457
|
+
stakePoolProgramId,
|
|
458
|
+
stakePoolAddress,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
instructions.push(
|
|
462
|
+
StakePoolInstruction.depositSol({
|
|
463
|
+
programId: stakePoolProgramId,
|
|
464
|
+
stakePool: stakePoolAddress,
|
|
465
|
+
reserveStake: stakePool.reserveStake,
|
|
466
|
+
fundingAccount: userSolTransfer.publicKey,
|
|
467
|
+
destinationPoolAccount: destinationTokenAccount,
|
|
468
|
+
managerFeeAccount: stakePool.managerFeeAccount,
|
|
469
|
+
referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount,
|
|
470
|
+
poolMint: stakePool.poolMint,
|
|
471
|
+
lamports,
|
|
472
|
+
withdrawAuthority,
|
|
473
|
+
depositAuthority,
|
|
474
|
+
}),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
instructions,
|
|
479
|
+
signers,
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Creates instructions required to withdraw stake from a stake pool.
|
|
485
|
+
*/
|
|
486
|
+
export async function withdrawStake(
|
|
487
|
+
connection: Connection,
|
|
488
|
+
stakePoolAddress: PublicKey,
|
|
489
|
+
tokenOwner: PublicKey,
|
|
490
|
+
amount: number,
|
|
491
|
+
useReserve = false,
|
|
492
|
+
voteAccountAddress?: PublicKey,
|
|
493
|
+
stakeReceiver?: PublicKey,
|
|
494
|
+
poolTokenAccount?: PublicKey,
|
|
495
|
+
validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
|
|
496
|
+
) {
|
|
497
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
498
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
499
|
+
const poolAmount = new BN(solToLamports(amount))
|
|
500
|
+
|
|
501
|
+
if (!poolTokenAccount) {
|
|
502
|
+
poolTokenAccount = getAssociatedTokenAddressSync(
|
|
503
|
+
stakePool.account.data.poolMint,
|
|
504
|
+
tokenOwner,
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const tokenAccount = await getAccount(connection, poolTokenAccount)
|
|
509
|
+
|
|
510
|
+
// Check withdrawFrom balance
|
|
511
|
+
if (tokenAccount.amount < poolAmount.toNumber()) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
|
|
514
|
+
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const stakeAccountRentExemption
|
|
519
|
+
= await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
|
|
520
|
+
|
|
521
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
522
|
+
stakePoolProgramId,
|
|
523
|
+
stakePoolAddress,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
let stakeReceiverAccount = null
|
|
527
|
+
if (stakeReceiver) {
|
|
528
|
+
stakeReceiverAccount = await getStakeAccount(connection, stakeReceiver)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const withdrawAccounts: WithdrawAccount[] = []
|
|
532
|
+
|
|
533
|
+
if (useReserve) {
|
|
534
|
+
withdrawAccounts.push({
|
|
535
|
+
stakeAddress: stakePool.account.data.reserveStake,
|
|
536
|
+
voteAddress: undefined,
|
|
537
|
+
poolAmount,
|
|
538
|
+
})
|
|
539
|
+
} else if (
|
|
540
|
+
stakeReceiverAccount
|
|
541
|
+
&& stakeReceiverAccount?.type === 'delegated'
|
|
542
|
+
) {
|
|
543
|
+
const voteAccount = stakeReceiverAccount.info?.stake?.delegation.voter
|
|
544
|
+
if (!voteAccount) {
|
|
545
|
+
throw new Error(`Invalid stake receiver ${stakeReceiver} delegation`)
|
|
546
|
+
}
|
|
547
|
+
const validatorListAccount = await connection.getAccountInfo(
|
|
548
|
+
stakePool.account.data.validatorList,
|
|
549
|
+
)
|
|
550
|
+
const validatorList = ValidatorListLayout.decode(
|
|
551
|
+
validatorListAccount?.data,
|
|
552
|
+
) as ValidatorList
|
|
553
|
+
const isValidVoter = validatorList.validators.find(val =>
|
|
554
|
+
val.voteAccountAddress.equals(voteAccount),
|
|
555
|
+
)
|
|
556
|
+
if (voteAccountAddress && voteAccountAddress !== voteAccount) {
|
|
557
|
+
throw new Error(`Provided withdrawal vote account ${voteAccountAddress} does not match delegation on stake receiver account ${voteAccount},
|
|
558
|
+
remove this flag or provide a different stake account delegated to ${voteAccountAddress}`)
|
|
559
|
+
}
|
|
560
|
+
if (isValidVoter) {
|
|
561
|
+
const stakeAccountAddress = await findStakeProgramAddress(
|
|
562
|
+
stakePoolProgramId,
|
|
563
|
+
voteAccount,
|
|
564
|
+
stakePoolAddress,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
|
|
568
|
+
if (!stakeAccount) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Preferred withdraw valdator's stake account is invalid`,
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const availableForWithdrawal = calcLamportsWithdrawAmount(
|
|
575
|
+
stakePool.account.data,
|
|
576
|
+
new BN(
|
|
577
|
+
stakeAccount.lamports
|
|
578
|
+
- MINIMUM_ACTIVE_STAKE
|
|
579
|
+
- stakeAccountRentExemption,
|
|
580
|
+
),
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
if (availableForWithdrawal.lt(poolAmount)) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`Not enough lamports available for withdrawal from ${stakeAccountAddress},
|
|
586
|
+
${poolAmount} asked, ${availableForWithdrawal} available.`,
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
withdrawAccounts.push({
|
|
590
|
+
stakeAddress: stakeAccountAddress,
|
|
591
|
+
voteAddress: voteAccount,
|
|
592
|
+
poolAmount,
|
|
593
|
+
})
|
|
594
|
+
} else {
|
|
595
|
+
throw new Error(
|
|
596
|
+
`Provided stake account is delegated to a vote account ${voteAccount} which does not exist in the stake pool`,
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
} else if (voteAccountAddress) {
|
|
600
|
+
const stakeAccountAddress = await findStakeProgramAddress(
|
|
601
|
+
stakePoolProgramId,
|
|
602
|
+
voteAccountAddress,
|
|
603
|
+
stakePoolAddress,
|
|
604
|
+
)
|
|
605
|
+
const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
|
|
606
|
+
if (!stakeAccount) {
|
|
607
|
+
throw new Error('Invalid Stake Account')
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const availableLamports = new BN(
|
|
611
|
+
stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
|
|
612
|
+
)
|
|
613
|
+
if (availableLamports.lt(new BN(0))) {
|
|
614
|
+
throw new Error('Invalid Stake Account')
|
|
615
|
+
}
|
|
616
|
+
const availableForWithdrawal = calcLamportsWithdrawAmount(
|
|
617
|
+
stakePool.account.data,
|
|
618
|
+
availableLamports,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
if (availableForWithdrawal.lt(poolAmount)) {
|
|
622
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
623
|
+
throw new Error(
|
|
624
|
+
`Not enough lamports available for withdrawal from ${stakeAccountAddress},
|
|
625
|
+
${poolAmount} asked, ${availableForWithdrawal} available.`,
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
withdrawAccounts.push({
|
|
629
|
+
stakeAddress: stakeAccountAddress,
|
|
630
|
+
voteAddress: voteAccountAddress,
|
|
631
|
+
poolAmount,
|
|
632
|
+
})
|
|
633
|
+
} else {
|
|
634
|
+
// Get the list of accounts to withdraw from
|
|
635
|
+
withdrawAccounts.push(
|
|
636
|
+
...(await prepareWithdrawAccounts(
|
|
637
|
+
connection,
|
|
638
|
+
stakePool.account.data,
|
|
639
|
+
stakePoolAddress,
|
|
640
|
+
poolAmount,
|
|
641
|
+
validatorComparator,
|
|
642
|
+
poolTokenAccount.equals(stakePool.account.data.managerFeeAccount),
|
|
643
|
+
)),
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Construct transaction to withdraw from withdrawAccounts account list
|
|
648
|
+
const instructions: TransactionInstruction[] = []
|
|
649
|
+
const userTransferAuthority = Keypair.generate()
|
|
650
|
+
|
|
651
|
+
const signers: Signer[] = [userTransferAuthority]
|
|
652
|
+
|
|
653
|
+
instructions.push(
|
|
654
|
+
createApproveInstruction(
|
|
655
|
+
poolTokenAccount,
|
|
656
|
+
userTransferAuthority.publicKey,
|
|
657
|
+
tokenOwner,
|
|
658
|
+
poolAmount.toNumber(),
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
let totalRentFreeBalances = 0
|
|
663
|
+
|
|
664
|
+
// Max 5 accounts to prevent an error: "Transaction too large"
|
|
665
|
+
const maxWithdrawAccounts = 5
|
|
666
|
+
let i = 0
|
|
667
|
+
|
|
668
|
+
// Go through prepared accounts and withdraw/claim them
|
|
669
|
+
for (const withdrawAccount of withdrawAccounts) {
|
|
670
|
+
if (i > maxWithdrawAccounts) {
|
|
671
|
+
break
|
|
672
|
+
}
|
|
673
|
+
// Convert pool tokens amount to lamports
|
|
674
|
+
const solWithdrawAmount = calcLamportsWithdrawAmount(
|
|
675
|
+
stakePool.account.data,
|
|
676
|
+
withdrawAccount.poolAmount,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
let infoMsg = `Withdrawing ◎${solWithdrawAmount},
|
|
680
|
+
from stake account ${withdrawAccount.stakeAddress?.toBase58()}`
|
|
681
|
+
|
|
682
|
+
if (withdrawAccount.voteAddress) {
|
|
683
|
+
infoMsg = `${infoMsg}, delegated to ${withdrawAccount.voteAddress?.toBase58()}`
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
console.info(infoMsg)
|
|
687
|
+
let stakeToReceive
|
|
688
|
+
|
|
689
|
+
if (
|
|
690
|
+
!stakeReceiver
|
|
691
|
+
|| (stakeReceiverAccount && stakeReceiverAccount.type === 'delegated')
|
|
692
|
+
) {
|
|
693
|
+
const stakeKeypair = newStakeAccount(
|
|
694
|
+
tokenOwner,
|
|
695
|
+
instructions,
|
|
696
|
+
stakeAccountRentExemption,
|
|
697
|
+
)
|
|
698
|
+
signers.push(stakeKeypair)
|
|
699
|
+
totalRentFreeBalances += stakeAccountRentExemption
|
|
700
|
+
stakeToReceive = stakeKeypair.publicKey
|
|
701
|
+
} else {
|
|
702
|
+
stakeToReceive = stakeReceiver
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
instructions.push(
|
|
706
|
+
StakePoolInstruction.withdrawStake({
|
|
707
|
+
programId: stakePoolProgramId,
|
|
708
|
+
stakePool: stakePoolAddress,
|
|
709
|
+
validatorList: stakePool.account.data.validatorList,
|
|
710
|
+
validatorStake: withdrawAccount.stakeAddress,
|
|
711
|
+
destinationStake: stakeToReceive,
|
|
712
|
+
destinationStakeAuthority: tokenOwner,
|
|
713
|
+
sourceTransferAuthority: userTransferAuthority.publicKey,
|
|
714
|
+
sourcePoolAccount: poolTokenAccount,
|
|
715
|
+
managerFeeAccount: stakePool.account.data.managerFeeAccount,
|
|
716
|
+
poolMint: stakePool.account.data.poolMint,
|
|
717
|
+
poolTokens: withdrawAccount.poolAmount.toNumber(),
|
|
718
|
+
withdrawAuthority,
|
|
719
|
+
}),
|
|
720
|
+
)
|
|
721
|
+
i++
|
|
722
|
+
}
|
|
723
|
+
if (
|
|
724
|
+
stakeReceiver
|
|
725
|
+
&& stakeReceiverAccount
|
|
726
|
+
&& stakeReceiverAccount.type === 'delegated'
|
|
727
|
+
) {
|
|
728
|
+
signers.forEach((newStakeKeypair) => {
|
|
729
|
+
instructions.concat(
|
|
730
|
+
StakeProgram.merge({
|
|
731
|
+
stakePubkey: stakeReceiver,
|
|
732
|
+
sourceStakePubKey: newStakeKeypair.publicKey,
|
|
733
|
+
authorizedPubkey: tokenOwner,
|
|
734
|
+
}).instructions,
|
|
735
|
+
)
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
instructions,
|
|
741
|
+
signers,
|
|
742
|
+
stakeReceiver,
|
|
743
|
+
totalRentFreeBalances,
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Creates instructions required to withdraw SOL directly from a stake pool.
|
|
749
|
+
*/
|
|
750
|
+
export async function withdrawSol(
|
|
751
|
+
connection: Connection,
|
|
752
|
+
stakePoolAddress: PublicKey,
|
|
753
|
+
tokenOwner: PublicKey,
|
|
754
|
+
solReceiver: PublicKey,
|
|
755
|
+
amount: number,
|
|
756
|
+
solWithdrawAuthority?: PublicKey,
|
|
757
|
+
) {
|
|
758
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
759
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
760
|
+
const poolAmount = solToLamports(amount)
|
|
761
|
+
|
|
762
|
+
const poolTokenAccount = getAssociatedTokenAddressSync(
|
|
763
|
+
stakePool.account.data.poolMint,
|
|
764
|
+
tokenOwner,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
const tokenAccount = await getAccount(connection, poolTokenAccount)
|
|
768
|
+
|
|
769
|
+
// Check withdrawFrom balance
|
|
770
|
+
if (tokenAccount.amount < poolAmount) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
|
|
773
|
+
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Construct transaction to withdraw from withdrawAccounts account list
|
|
778
|
+
const instructions: TransactionInstruction[] = []
|
|
779
|
+
const userTransferAuthority = Keypair.generate()
|
|
780
|
+
const signers: Signer[] = [userTransferAuthority]
|
|
781
|
+
|
|
782
|
+
instructions.push(
|
|
783
|
+
createApproveInstruction(
|
|
784
|
+
poolTokenAccount,
|
|
785
|
+
userTransferAuthority.publicKey,
|
|
786
|
+
tokenOwner,
|
|
787
|
+
poolAmount,
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
const poolWithdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
792
|
+
stakePoolProgramId,
|
|
793
|
+
stakePoolAddress,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if (solWithdrawAuthority) {
|
|
797
|
+
const expectedSolWithdrawAuthority
|
|
798
|
+
= stakePool.account.data.solWithdrawAuthority
|
|
799
|
+
if (!expectedSolWithdrawAuthority) {
|
|
800
|
+
throw new Error(
|
|
801
|
+
'SOL withdraw authority specified in arguments but stake pool has none',
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
if (
|
|
805
|
+
solWithdrawAuthority.toBase58() !== expectedSolWithdrawAuthority.toBase58()
|
|
806
|
+
) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Invalid deposit withdraw specified, expected ${expectedSolWithdrawAuthority.toBase58()}, received ${solWithdrawAuthority.toBase58()}`,
|
|
809
|
+
)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const withdrawTransaction = StakePoolInstruction.withdrawSol({
|
|
814
|
+
programId: stakePoolProgramId,
|
|
815
|
+
stakePool: stakePoolAddress,
|
|
816
|
+
withdrawAuthority: poolWithdrawAuthority,
|
|
817
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
818
|
+
sourcePoolAccount: poolTokenAccount,
|
|
819
|
+
sourceTransferAuthority: userTransferAuthority.publicKey,
|
|
820
|
+
destinationSystemAccount: solReceiver,
|
|
821
|
+
managerFeeAccount: stakePool.account.data.managerFeeAccount,
|
|
822
|
+
poolMint: stakePool.account.data.poolMint,
|
|
823
|
+
poolTokens: poolAmount,
|
|
824
|
+
solWithdrawAuthority,
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
instructions.push(withdrawTransaction)
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
instructions,
|
|
831
|
+
signers,
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Creates instructions required to withdraw wSOL from a stake pool.
|
|
837
|
+
* Rent for ATA creation (if needed) is paid from the withdrawal amount.
|
|
838
|
+
*/
|
|
839
|
+
export async function withdrawWsolWithSession(
|
|
840
|
+
connection: Connection,
|
|
841
|
+
stakePoolAddress: PublicKey,
|
|
842
|
+
signerOrSession: PublicKey,
|
|
843
|
+
userPubkey: PublicKey,
|
|
844
|
+
amount: number,
|
|
845
|
+
minimumLamportsOut: number = 0,
|
|
846
|
+
solWithdrawAuthority?: PublicKey,
|
|
847
|
+
) {
|
|
848
|
+
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
|
|
849
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
850
|
+
const stakePool = stakePoolAccount.account.data
|
|
851
|
+
const poolTokens = solToLamports(amount)
|
|
852
|
+
|
|
853
|
+
const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey)
|
|
854
|
+
const tokenAccount = await getAccount(connection, poolTokenAccount)
|
|
855
|
+
|
|
856
|
+
if (tokenAccount.amount < poolTokens) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`Not enough token balance to withdraw ${amount} pool tokens.
|
|
859
|
+
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const userWsolAccount = getAssociatedTokenAddressSync(NATIVE_MINT, userPubkey)
|
|
864
|
+
|
|
865
|
+
const instructions: TransactionInstruction[] = []
|
|
866
|
+
const signers: Signer[] = []
|
|
867
|
+
|
|
868
|
+
// The program handles wSOL ATA creation internally
|
|
869
|
+
// This prevents rent drain attacks where paymaster pays for ATA and user reclaims rent
|
|
870
|
+
|
|
871
|
+
const [programSigner] = PublicKey.findProgramAddressSync(
|
|
872
|
+
[Buffer.from('fogo_session_program_signer')],
|
|
873
|
+
stakePoolProgramId,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
877
|
+
stakePoolProgramId,
|
|
878
|
+
stakePoolAddress,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
instructions.push(
|
|
882
|
+
StakePoolInstruction.withdrawWsolWithSession({
|
|
883
|
+
programId: stakePoolProgramId,
|
|
884
|
+
stakePool: stakePoolAddress,
|
|
885
|
+
withdrawAuthority,
|
|
886
|
+
userTransferAuthority: signerOrSession,
|
|
887
|
+
poolTokensFrom: poolTokenAccount,
|
|
888
|
+
reserveStake: stakePool.reserveStake,
|
|
889
|
+
userWsolAccount,
|
|
890
|
+
managerFeeAccount: stakePool.managerFeeAccount,
|
|
891
|
+
poolMint: stakePool.poolMint,
|
|
892
|
+
tokenProgramId: stakePool.tokenProgramId,
|
|
893
|
+
solWithdrawAuthority,
|
|
894
|
+
wsolMint: NATIVE_MINT,
|
|
895
|
+
programSigner,
|
|
896
|
+
userWallet: userPubkey,
|
|
897
|
+
poolTokensIn: poolTokens,
|
|
898
|
+
minimumLamportsOut,
|
|
899
|
+
}),
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
instructions,
|
|
904
|
+
signers,
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Finds the next available seed for creating a user stake PDA.
|
|
910
|
+
* Scans from startSeed until an unused PDA is found.
|
|
911
|
+
*
|
|
912
|
+
* @param connection - Solana connection
|
|
913
|
+
* @param programId - The stake pool program ID
|
|
914
|
+
* @param userPubkey - User's wallet (used for PDA derivation)
|
|
915
|
+
* @param startSeed - Starting seed to search from (default: 0)
|
|
916
|
+
* @param maxSeed - Maximum seed to check before giving up (default: 1000)
|
|
917
|
+
* @returns The next available seed
|
|
918
|
+
* @throws Error if no available seed found within maxSeed
|
|
919
|
+
*/
|
|
920
|
+
export async function findNextUserStakeSeed(
|
|
921
|
+
connection: Connection,
|
|
922
|
+
programId: PublicKey,
|
|
923
|
+
userPubkey: PublicKey,
|
|
924
|
+
startSeed: number = 0,
|
|
925
|
+
maxSeed: number = 1000,
|
|
926
|
+
): Promise<number> {
|
|
927
|
+
for (let seed = startSeed; seed < startSeed + maxSeed; seed++) {
|
|
928
|
+
const pda = findUserStakeProgramAddress(programId, userPubkey, seed)
|
|
929
|
+
const account = await connection.getAccountInfo(pda)
|
|
930
|
+
if (!account) {
|
|
931
|
+
return seed
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
throw new Error(`No available user stake seed found between ${startSeed} and ${startSeed + maxSeed - 1}`)
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Represents a user stake account with its details
|
|
939
|
+
*/
|
|
940
|
+
export interface UserStakeAccount {
|
|
941
|
+
/** The stake account public key (PDA) */
|
|
942
|
+
pubkey: PublicKey
|
|
943
|
+
/** The seed used to derive this PDA */
|
|
944
|
+
seed: number
|
|
945
|
+
/** Lamports in the stake account */
|
|
946
|
+
lamports: number
|
|
947
|
+
/** Parsed stake state */
|
|
948
|
+
state: 'inactive' | 'activating' | 'active' | 'deactivating'
|
|
949
|
+
/** Validator vote account (if delegated) */
|
|
950
|
+
voter?: PublicKey
|
|
951
|
+
/** Activation epoch (if active/activating) */
|
|
952
|
+
activationEpoch?: number
|
|
953
|
+
/** Deactivation epoch (if deactivating) */
|
|
954
|
+
deactivationEpoch?: number
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Fetches all user stake accounts created via WithdrawStakeWithSession.
|
|
959
|
+
* These are PDAs derived from [b"user_stake", user_wallet, seed].
|
|
960
|
+
*
|
|
961
|
+
* @param connection - Solana connection
|
|
962
|
+
* @param programId - The stake pool program ID
|
|
963
|
+
* @param userPubkey - User's wallet address
|
|
964
|
+
* @param maxSeed - Maximum seed to check (default: 100)
|
|
965
|
+
* @returns Array of user stake accounts with their details
|
|
966
|
+
*/
|
|
967
|
+
export async function getUserStakeAccounts(
|
|
968
|
+
connection: Connection,
|
|
969
|
+
programId: PublicKey,
|
|
970
|
+
userPubkey: PublicKey,
|
|
971
|
+
maxSeed: number = 100,
|
|
972
|
+
): Promise<UserStakeAccount[]> {
|
|
973
|
+
const stakeAccounts: UserStakeAccount[] = []
|
|
974
|
+
const currentEpoch = (await connection.getEpochInfo()).epoch
|
|
975
|
+
|
|
976
|
+
for (let seed = 0; seed < maxSeed; seed++) {
|
|
977
|
+
const pda = findUserStakeProgramAddress(programId, userPubkey, seed)
|
|
978
|
+
const accountInfo = await connection.getAccountInfo(pda)
|
|
979
|
+
|
|
980
|
+
if (!accountInfo) {
|
|
981
|
+
continue // Skip empty slots, there might be gaps
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Check if owned by stake program
|
|
985
|
+
if (!accountInfo.owner.equals(StakeProgram.programId)) {
|
|
986
|
+
continue
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Parse stake account data
|
|
990
|
+
const stakeAccount: UserStakeAccount = {
|
|
991
|
+
pubkey: pda,
|
|
992
|
+
seed,
|
|
993
|
+
lamports: accountInfo.lamports,
|
|
994
|
+
state: 'inactive',
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
try {
|
|
998
|
+
// Parse the stake account to get delegation info
|
|
999
|
+
const parsedAccount = await connection.getParsedAccountInfo(pda)
|
|
1000
|
+
if (parsedAccount.value && 'parsed' in parsedAccount.value.data) {
|
|
1001
|
+
const parsed = parsedAccount.value.data.parsed
|
|
1002
|
+
if (parsed.type === 'delegated' && parsed.info?.stake?.delegation) {
|
|
1003
|
+
const delegation = parsed.info.stake.delegation
|
|
1004
|
+
stakeAccount.voter = new PublicKey(delegation.voter)
|
|
1005
|
+
stakeAccount.activationEpoch = Number(delegation.activationEpoch)
|
|
1006
|
+
stakeAccount.deactivationEpoch = Number(delegation.deactivationEpoch)
|
|
1007
|
+
|
|
1008
|
+
// Determine state based on epochs
|
|
1009
|
+
const activationEpoch = stakeAccount.activationEpoch
|
|
1010
|
+
const deactivationEpoch = stakeAccount.deactivationEpoch
|
|
1011
|
+
|
|
1012
|
+
if (deactivationEpoch !== undefined && deactivationEpoch < Number.MAX_SAFE_INTEGER && deactivationEpoch <= currentEpoch) {
|
|
1013
|
+
stakeAccount.state = 'inactive'
|
|
1014
|
+
} else if (deactivationEpoch !== undefined && deactivationEpoch < Number.MAX_SAFE_INTEGER) {
|
|
1015
|
+
stakeAccount.state = 'deactivating'
|
|
1016
|
+
} else if (activationEpoch !== undefined && activationEpoch <= currentEpoch) {
|
|
1017
|
+
stakeAccount.state = 'active'
|
|
1018
|
+
} else {
|
|
1019
|
+
stakeAccount.state = 'activating'
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
} catch {
|
|
1024
|
+
// If parsing fails, keep default 'inactive' state
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
stakeAccounts.push(stakeAccount)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return stakeAccounts
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Withdraws stake from a stake pool using a Fogo session.
|
|
1035
|
+
*
|
|
1036
|
+
* 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
|
+
*
|
|
1039
|
+
* @param connection - Solana connection
|
|
1040
|
+
* @param stakePoolAddress - The stake pool to withdraw from
|
|
1041
|
+
* @param signerOrSession - The session signer public key
|
|
1042
|
+
* @param userPubkey - User's wallet (used for PDA derivation and token ownership)
|
|
1043
|
+
* @param payer - Payer for stake account rent (typically paymaster)
|
|
1044
|
+
* @param amount - Amount of pool tokens to withdraw
|
|
1045
|
+
* @param userStakeSeedStart - Starting seed for user stake PDA derivation (default: 0)
|
|
1046
|
+
* @param useReserve - Whether to withdraw from reserve (default: false)
|
|
1047
|
+
* @param voteAccountAddress - Optional specific validator to withdraw from
|
|
1048
|
+
* @param minimumLamportsOut - Minimum lamports to receive (slippage protection)
|
|
1049
|
+
* @param validatorComparator - Optional comparator for validator selection
|
|
1050
|
+
*/
|
|
1051
|
+
export async function withdrawStakeWithSession(
|
|
1052
|
+
connection: Connection,
|
|
1053
|
+
stakePoolAddress: PublicKey,
|
|
1054
|
+
signerOrSession: PublicKey,
|
|
1055
|
+
userPubkey: PublicKey,
|
|
1056
|
+
payer: PublicKey,
|
|
1057
|
+
amount: number,
|
|
1058
|
+
userStakeSeedStart: number = 0,
|
|
1059
|
+
useReserve = false,
|
|
1060
|
+
voteAccountAddress?: PublicKey,
|
|
1061
|
+
minimumLamportsOut: number = 0,
|
|
1062
|
+
validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
|
|
1063
|
+
) {
|
|
1064
|
+
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress)
|
|
1065
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1066
|
+
const stakePool = stakePoolAccount.account.data
|
|
1067
|
+
const poolTokens = solToLamports(amount)
|
|
1068
|
+
const poolAmount = new BN(poolTokens)
|
|
1069
|
+
|
|
1070
|
+
const poolTokenAccount = getAssociatedTokenAddressSync(stakePool.poolMint, userPubkey)
|
|
1071
|
+
const tokenAccount = await getAccount(connection, poolTokenAccount)
|
|
1072
|
+
|
|
1073
|
+
if (tokenAccount.amount < poolTokens) {
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
`Not enough token balance to withdraw ${amount} pool tokens.
|
|
1076
|
+
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
|
|
1077
|
+
)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const [programSigner] = PublicKey.findProgramAddressSync(
|
|
1081
|
+
[Buffer.from('fogo_session_program_signer')],
|
|
1082
|
+
stakePoolProgramId,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1086
|
+
stakePoolProgramId,
|
|
1087
|
+
stakePoolAddress,
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
|
|
1091
|
+
|
|
1092
|
+
// Determine which stake accounts to withdraw from
|
|
1093
|
+
const withdrawAccounts: WithdrawAccount[] = []
|
|
1094
|
+
|
|
1095
|
+
if (useReserve) {
|
|
1096
|
+
withdrawAccounts.push({
|
|
1097
|
+
stakeAddress: stakePool.reserveStake,
|
|
1098
|
+
voteAddress: undefined,
|
|
1099
|
+
poolAmount,
|
|
1100
|
+
})
|
|
1101
|
+
} else if (voteAccountAddress) {
|
|
1102
|
+
const stakeAccountAddress = await findStakeProgramAddress(
|
|
1103
|
+
stakePoolProgramId,
|
|
1104
|
+
voteAccountAddress,
|
|
1105
|
+
stakePoolAddress,
|
|
1106
|
+
)
|
|
1107
|
+
const stakeAccount = await connection.getAccountInfo(stakeAccountAddress)
|
|
1108
|
+
if (!stakeAccount) {
|
|
1109
|
+
throw new Error(`Validator stake account not found for vote address ${voteAccountAddress.toBase58()}`)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const availableLamports = new BN(
|
|
1113
|
+
stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
|
|
1114
|
+
)
|
|
1115
|
+
if (availableLamports.lt(new BN(0))) {
|
|
1116
|
+
throw new Error('Invalid Stake Account')
|
|
1117
|
+
}
|
|
1118
|
+
const availableForWithdrawal = calcLamportsWithdrawAmount(
|
|
1119
|
+
stakePool,
|
|
1120
|
+
availableLamports,
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
if (availableForWithdrawal.lt(poolAmount)) {
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
`Not enough lamports available for withdrawal from ${stakeAccountAddress},
|
|
1126
|
+
${poolAmount} asked, ${availableForWithdrawal} available.`,
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
1129
|
+
withdrawAccounts.push({
|
|
1130
|
+
stakeAddress: stakeAccountAddress,
|
|
1131
|
+
voteAddress: voteAccountAddress,
|
|
1132
|
+
poolAmount,
|
|
1133
|
+
})
|
|
1134
|
+
} else {
|
|
1135
|
+
// Get the list of accounts to withdraw from automatically
|
|
1136
|
+
withdrawAccounts.push(
|
|
1137
|
+
...(await prepareWithdrawAccounts(
|
|
1138
|
+
connection,
|
|
1139
|
+
stakePool,
|
|
1140
|
+
stakePoolAddress,
|
|
1141
|
+
poolAmount,
|
|
1142
|
+
validatorComparator,
|
|
1143
|
+
poolTokenAccount.equals(stakePool.managerFeeAccount),
|
|
1144
|
+
)),
|
|
1145
|
+
)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const instructions: TransactionInstruction[] = []
|
|
1149
|
+
const stakeAccountPubkeys: PublicKey[] = []
|
|
1150
|
+
const userStakeSeeds: number[] = []
|
|
1151
|
+
|
|
1152
|
+
// Max 5 accounts to prevent an error: "Transaction too large"
|
|
1153
|
+
const maxWithdrawAccounts = 5
|
|
1154
|
+
let i = 0
|
|
1155
|
+
|
|
1156
|
+
for (const withdrawAccount of withdrawAccounts) {
|
|
1157
|
+
if (i >= maxWithdrawAccounts) {
|
|
1158
|
+
break
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Derive the stake account PDA for this withdrawal
|
|
1162
|
+
const userStakeSeed = userStakeSeedStart + i
|
|
1163
|
+
const stakeReceiverPubkey = findUserStakeProgramAddress(
|
|
1164
|
+
stakePoolProgramId,
|
|
1165
|
+
userPubkey,
|
|
1166
|
+
userStakeSeed,
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
stakeAccountPubkeys.push(stakeReceiverPubkey)
|
|
1170
|
+
userStakeSeeds.push(userStakeSeed)
|
|
1171
|
+
|
|
1172
|
+
// The on-chain program creates the stake account PDA and rent is paid by payer.
|
|
1173
|
+
instructions.push(
|
|
1174
|
+
StakePoolInstruction.withdrawStakeWithSession({
|
|
1175
|
+
programId: stakePoolProgramId,
|
|
1176
|
+
stakePool: stakePoolAddress,
|
|
1177
|
+
validatorList: stakePool.validatorList,
|
|
1178
|
+
withdrawAuthority,
|
|
1179
|
+
stakeToSplit: withdrawAccount.stakeAddress,
|
|
1180
|
+
stakeToReceive: stakeReceiverPubkey,
|
|
1181
|
+
sessionSigner: signerOrSession,
|
|
1182
|
+
burnFromPool: poolTokenAccount,
|
|
1183
|
+
managerFeeAccount: stakePool.managerFeeAccount,
|
|
1184
|
+
poolMint: stakePool.poolMint,
|
|
1185
|
+
tokenProgramId: stakePool.tokenProgramId,
|
|
1186
|
+
programSigner,
|
|
1187
|
+
payer,
|
|
1188
|
+
poolTokensIn: withdrawAccount.poolAmount.toNumber(),
|
|
1189
|
+
minimumLamportsOut,
|
|
1190
|
+
userStakeSeed,
|
|
1191
|
+
}),
|
|
1192
|
+
)
|
|
1193
|
+
i++
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return {
|
|
1197
|
+
instructions,
|
|
1198
|
+
stakeAccountPubkeys,
|
|
1199
|
+
userStakeSeeds,
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export async function addValidatorToPool(
|
|
1204
|
+
connection: Connection,
|
|
1205
|
+
stakePoolAddress: PublicKey,
|
|
1206
|
+
validatorVote: PublicKey,
|
|
1207
|
+
seed?: number,
|
|
1208
|
+
) {
|
|
1209
|
+
const stakePoolAccount = await getStakePoolAccount(
|
|
1210
|
+
connection,
|
|
1211
|
+
stakePoolAddress,
|
|
1212
|
+
)
|
|
1213
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1214
|
+
const stakePool = stakePoolAccount.account.data
|
|
1215
|
+
const { reserveStake, staker, validatorList } = stakePool
|
|
1216
|
+
|
|
1217
|
+
const validatorListAccount = await getValidatorListAccount(
|
|
1218
|
+
connection,
|
|
1219
|
+
validatorList,
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
const validatorInfo = validatorListAccount.account.data.validators.find(
|
|
1223
|
+
v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
if (validatorInfo) {
|
|
1227
|
+
throw new Error('Vote account is already in validator list')
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1231
|
+
stakePoolProgramId,
|
|
1232
|
+
stakePoolAddress,
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
const validatorStake = await findStakeProgramAddress(
|
|
1236
|
+
stakePoolProgramId,
|
|
1237
|
+
validatorVote,
|
|
1238
|
+
stakePoolAddress,
|
|
1239
|
+
seed,
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
const instructions: TransactionInstruction[] = [
|
|
1243
|
+
StakePoolInstruction.addValidatorToPool({
|
|
1244
|
+
programId: stakePoolProgramId,
|
|
1245
|
+
stakePool: stakePoolAddress,
|
|
1246
|
+
staker,
|
|
1247
|
+
reserveStake,
|
|
1248
|
+
withdrawAuthority,
|
|
1249
|
+
validatorList,
|
|
1250
|
+
validatorStake,
|
|
1251
|
+
validatorVote,
|
|
1252
|
+
}),
|
|
1253
|
+
]
|
|
1254
|
+
|
|
1255
|
+
return {
|
|
1256
|
+
instructions,
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export async function removeValidatorFromPool(
|
|
1261
|
+
connection: Connection,
|
|
1262
|
+
stakePoolAddress: PublicKey,
|
|
1263
|
+
validatorVote: PublicKey,
|
|
1264
|
+
seed?: number,
|
|
1265
|
+
) {
|
|
1266
|
+
const stakePoolAccount = await getStakePoolAccount(
|
|
1267
|
+
connection,
|
|
1268
|
+
stakePoolAddress,
|
|
1269
|
+
)
|
|
1270
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1271
|
+
const stakePool = stakePoolAccount.account.data
|
|
1272
|
+
const { staker, validatorList } = stakePool
|
|
1273
|
+
|
|
1274
|
+
const validatorListAccount = await getValidatorListAccount(
|
|
1275
|
+
connection,
|
|
1276
|
+
validatorList,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
const validatorInfo = validatorListAccount.account.data.validators.find(
|
|
1280
|
+
v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
if (!validatorInfo) {
|
|
1284
|
+
throw new Error('Vote account is not already in validator list')
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1288
|
+
stakePoolProgramId,
|
|
1289
|
+
stakePoolAddress,
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
const validatorStake = await findStakeProgramAddress(
|
|
1293
|
+
stakePoolProgramId,
|
|
1294
|
+
validatorVote,
|
|
1295
|
+
stakePoolAddress,
|
|
1296
|
+
seed,
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
const transientStakeSeed = validatorInfo.transientSeedSuffixStart
|
|
1300
|
+
|
|
1301
|
+
const transientStake = await findTransientStakeProgramAddress(
|
|
1302
|
+
stakePoolProgramId,
|
|
1303
|
+
validatorInfo.voteAccountAddress,
|
|
1304
|
+
stakePoolAddress,
|
|
1305
|
+
transientStakeSeed,
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
const instructions: TransactionInstruction[] = [
|
|
1309
|
+
StakePoolInstruction.removeValidatorFromPool({
|
|
1310
|
+
programId: stakePoolProgramId,
|
|
1311
|
+
stakePool: stakePoolAddress,
|
|
1312
|
+
staker,
|
|
1313
|
+
withdrawAuthority,
|
|
1314
|
+
validatorList,
|
|
1315
|
+
validatorStake,
|
|
1316
|
+
transientStake,
|
|
1317
|
+
}),
|
|
1318
|
+
]
|
|
1319
|
+
|
|
1320
|
+
return {
|
|
1321
|
+
instructions,
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Creates instructions required to increase validator stake.
|
|
1327
|
+
*/
|
|
1328
|
+
export async function increaseValidatorStake(
|
|
1329
|
+
connection: Connection,
|
|
1330
|
+
stakePoolAddress: PublicKey,
|
|
1331
|
+
validatorVote: PublicKey,
|
|
1332
|
+
lamports: number,
|
|
1333
|
+
ephemeralStakeSeed?: number,
|
|
1334
|
+
) {
|
|
1335
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
1336
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1337
|
+
|
|
1338
|
+
const validatorList = await getValidatorListAccount(
|
|
1339
|
+
connection,
|
|
1340
|
+
stakePool.account.data.validatorList,
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
const validatorInfo = validatorList.account.data.validators.find(
|
|
1344
|
+
v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
if (!validatorInfo) {
|
|
1348
|
+
throw new Error('Vote account not found in validator list')
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1352
|
+
stakePoolProgramId,
|
|
1353
|
+
stakePoolAddress,
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
// Bump transient seed suffix by one to avoid reuse when not using the increaseAdditionalStake instruction
|
|
1357
|
+
const transientStakeSeed
|
|
1358
|
+
= ephemeralStakeSeed === undefined
|
|
1359
|
+
? validatorInfo.transientSeedSuffixStart.addn(1)
|
|
1360
|
+
: validatorInfo.transientSeedSuffixStart
|
|
1361
|
+
|
|
1362
|
+
const transientStake = await findTransientStakeProgramAddress(
|
|
1363
|
+
stakePoolProgramId,
|
|
1364
|
+
validatorInfo.voteAccountAddress,
|
|
1365
|
+
stakePoolAddress,
|
|
1366
|
+
transientStakeSeed,
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
const validatorStake = await findStakeProgramAddress(
|
|
1370
|
+
stakePoolProgramId,
|
|
1371
|
+
validatorInfo.voteAccountAddress,
|
|
1372
|
+
stakePoolAddress,
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
const instructions: TransactionInstruction[] = []
|
|
1376
|
+
|
|
1377
|
+
if (ephemeralStakeSeed !== undefined) {
|
|
1378
|
+
const ephemeralStake = await findEphemeralStakeProgramAddress(
|
|
1379
|
+
stakePoolProgramId,
|
|
1380
|
+
stakePoolAddress,
|
|
1381
|
+
new BN(ephemeralStakeSeed),
|
|
1382
|
+
)
|
|
1383
|
+
instructions.push(
|
|
1384
|
+
StakePoolInstruction.increaseAdditionalValidatorStake({
|
|
1385
|
+
programId: stakePoolProgramId,
|
|
1386
|
+
stakePool: stakePoolAddress,
|
|
1387
|
+
staker: stakePool.account.data.staker,
|
|
1388
|
+
validatorList: stakePool.account.data.validatorList,
|
|
1389
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
1390
|
+
transientStakeSeed: transientStakeSeed.toNumber(),
|
|
1391
|
+
withdrawAuthority,
|
|
1392
|
+
transientStake,
|
|
1393
|
+
validatorStake,
|
|
1394
|
+
validatorVote,
|
|
1395
|
+
lamports,
|
|
1396
|
+
ephemeralStake,
|
|
1397
|
+
ephemeralStakeSeed,
|
|
1398
|
+
}),
|
|
1399
|
+
)
|
|
1400
|
+
} else {
|
|
1401
|
+
instructions.push(
|
|
1402
|
+
StakePoolInstruction.increaseValidatorStake({
|
|
1403
|
+
programId: stakePoolProgramId,
|
|
1404
|
+
stakePool: stakePoolAddress,
|
|
1405
|
+
staker: stakePool.account.data.staker,
|
|
1406
|
+
validatorList: stakePool.account.data.validatorList,
|
|
1407
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
1408
|
+
transientStakeSeed: transientStakeSeed.toNumber(),
|
|
1409
|
+
withdrawAuthority,
|
|
1410
|
+
transientStake,
|
|
1411
|
+
validatorStake,
|
|
1412
|
+
validatorVote,
|
|
1413
|
+
lamports,
|
|
1414
|
+
}),
|
|
1415
|
+
)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return {
|
|
1419
|
+
instructions,
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Creates instructions required to decrease validator stake.
|
|
1425
|
+
*/
|
|
1426
|
+
export async function decreaseValidatorStake(
|
|
1427
|
+
connection: Connection,
|
|
1428
|
+
stakePoolAddress: PublicKey,
|
|
1429
|
+
validatorVote: PublicKey,
|
|
1430
|
+
lamports: number,
|
|
1431
|
+
ephemeralStakeSeed?: number,
|
|
1432
|
+
) {
|
|
1433
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
1434
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1435
|
+
const validatorList = await getValidatorListAccount(
|
|
1436
|
+
connection,
|
|
1437
|
+
stakePool.account.data.validatorList,
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
const validatorInfo = validatorList.account.data.validators.find(
|
|
1441
|
+
v => v.voteAccountAddress.toBase58() === validatorVote.toBase58(),
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
if (!validatorInfo) {
|
|
1445
|
+
throw new Error('Vote account not found in validator list')
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1449
|
+
stakePoolProgramId,
|
|
1450
|
+
stakePoolAddress,
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
const validatorStake = await findStakeProgramAddress(
|
|
1454
|
+
stakePoolProgramId,
|
|
1455
|
+
validatorInfo.voteAccountAddress,
|
|
1456
|
+
stakePoolAddress,
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
// Bump transient seed suffix by one to avoid reuse when not using the decreaseAdditionalStake instruction
|
|
1460
|
+
const transientStakeSeed
|
|
1461
|
+
= ephemeralStakeSeed === undefined
|
|
1462
|
+
? validatorInfo.transientSeedSuffixStart.addn(1)
|
|
1463
|
+
: validatorInfo.transientSeedSuffixStart
|
|
1464
|
+
|
|
1465
|
+
const transientStake = await findTransientStakeProgramAddress(
|
|
1466
|
+
stakePoolProgramId,
|
|
1467
|
+
validatorInfo.voteAccountAddress,
|
|
1468
|
+
stakePoolAddress,
|
|
1469
|
+
transientStakeSeed,
|
|
1470
|
+
)
|
|
1471
|
+
|
|
1472
|
+
const instructions: TransactionInstruction[] = []
|
|
1473
|
+
|
|
1474
|
+
if (ephemeralStakeSeed !== undefined) {
|
|
1475
|
+
const ephemeralStake = await findEphemeralStakeProgramAddress(
|
|
1476
|
+
stakePoolProgramId,
|
|
1477
|
+
stakePoolAddress,
|
|
1478
|
+
new BN(ephemeralStakeSeed),
|
|
1479
|
+
)
|
|
1480
|
+
instructions.push(
|
|
1481
|
+
StakePoolInstruction.decreaseAdditionalValidatorStake({
|
|
1482
|
+
programId: stakePoolProgramId,
|
|
1483
|
+
stakePool: stakePoolAddress,
|
|
1484
|
+
staker: stakePool.account.data.staker,
|
|
1485
|
+
validatorList: stakePool.account.data.validatorList,
|
|
1486
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
1487
|
+
transientStakeSeed: transientStakeSeed.toNumber(),
|
|
1488
|
+
withdrawAuthority,
|
|
1489
|
+
validatorStake,
|
|
1490
|
+
transientStake,
|
|
1491
|
+
lamports,
|
|
1492
|
+
ephemeralStake,
|
|
1493
|
+
ephemeralStakeSeed,
|
|
1494
|
+
}),
|
|
1495
|
+
)
|
|
1496
|
+
} else {
|
|
1497
|
+
instructions.push(
|
|
1498
|
+
StakePoolInstruction.decreaseValidatorStakeWithReserve({
|
|
1499
|
+
programId: stakePoolProgramId,
|
|
1500
|
+
stakePool: stakePoolAddress,
|
|
1501
|
+
staker: stakePool.account.data.staker,
|
|
1502
|
+
validatorList: stakePool.account.data.validatorList,
|
|
1503
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
1504
|
+
transientStakeSeed: transientStakeSeed.toNumber(),
|
|
1505
|
+
withdrawAuthority,
|
|
1506
|
+
validatorStake,
|
|
1507
|
+
transientStake,
|
|
1508
|
+
lamports,
|
|
1509
|
+
}),
|
|
1510
|
+
)
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return {
|
|
1514
|
+
instructions,
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Creates instructions required to completely update a stake pool after epoch change.
|
|
1520
|
+
*/
|
|
1521
|
+
export async function updateStakePool(
|
|
1522
|
+
connection: Connection,
|
|
1523
|
+
stakePool: StakePoolAccount,
|
|
1524
|
+
noMerge = false,
|
|
1525
|
+
) {
|
|
1526
|
+
const stakePoolAddress = stakePool.pubkey
|
|
1527
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1528
|
+
|
|
1529
|
+
const validatorList = await getValidatorListAccount(
|
|
1530
|
+
connection,
|
|
1531
|
+
stakePool.account.data.validatorList,
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1535
|
+
stakePoolProgramId,
|
|
1536
|
+
stakePoolAddress,
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
const updateListInstructions: TransactionInstruction[] = []
|
|
1540
|
+
const instructions: TransactionInstruction[] = []
|
|
1541
|
+
|
|
1542
|
+
let startIndex = 0
|
|
1543
|
+
const validatorChunks: Array<ValidatorStakeInfo[]> = arrayChunk(
|
|
1544
|
+
validatorList.account.data.validators,
|
|
1545
|
+
MAX_VALIDATORS_TO_UPDATE,
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
for (const validatorChunk of validatorChunks) {
|
|
1549
|
+
const validatorAndTransientStakePairs: PublicKey[] = []
|
|
1550
|
+
|
|
1551
|
+
for (const validator of validatorChunk) {
|
|
1552
|
+
const validatorStake = await findStakeProgramAddress(
|
|
1553
|
+
stakePoolProgramId,
|
|
1554
|
+
validator.voteAccountAddress,
|
|
1555
|
+
stakePoolAddress,
|
|
1556
|
+
)
|
|
1557
|
+
validatorAndTransientStakePairs.push(validatorStake)
|
|
1558
|
+
|
|
1559
|
+
const transientStake = await findTransientStakeProgramAddress(
|
|
1560
|
+
stakePoolProgramId,
|
|
1561
|
+
validator.voteAccountAddress,
|
|
1562
|
+
stakePoolAddress,
|
|
1563
|
+
validator.transientSeedSuffixStart,
|
|
1564
|
+
)
|
|
1565
|
+
validatorAndTransientStakePairs.push(transientStake)
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
updateListInstructions.push(
|
|
1569
|
+
StakePoolInstruction.updateValidatorListBalance({
|
|
1570
|
+
programId: stakePoolProgramId,
|
|
1571
|
+
stakePool: stakePoolAddress,
|
|
1572
|
+
validatorList: stakePool.account.data.validatorList,
|
|
1573
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
1574
|
+
validatorAndTransientStakePairs,
|
|
1575
|
+
withdrawAuthority,
|
|
1576
|
+
startIndex,
|
|
1577
|
+
noMerge,
|
|
1578
|
+
}),
|
|
1579
|
+
)
|
|
1580
|
+
startIndex += MAX_VALIDATORS_TO_UPDATE
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
instructions.push(
|
|
1584
|
+
StakePoolInstruction.updateStakePoolBalance({
|
|
1585
|
+
programId: stakePoolProgramId,
|
|
1586
|
+
stakePool: stakePoolAddress,
|
|
1587
|
+
validatorList: stakePool.account.data.validatorList,
|
|
1588
|
+
reserveStake: stakePool.account.data.reserveStake,
|
|
1589
|
+
managerFeeAccount: stakePool.account.data.managerFeeAccount,
|
|
1590
|
+
poolMint: stakePool.account.data.poolMint,
|
|
1591
|
+
withdrawAuthority,
|
|
1592
|
+
}),
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
instructions.push(
|
|
1596
|
+
StakePoolInstruction.cleanupRemovedValidatorEntries({
|
|
1597
|
+
programId: stakePoolProgramId,
|
|
1598
|
+
stakePool: stakePoolAddress,
|
|
1599
|
+
validatorList: stakePool.account.data.validatorList,
|
|
1600
|
+
}),
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
return {
|
|
1604
|
+
updateListInstructions,
|
|
1605
|
+
finalInstructions: instructions,
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Retrieves detailed information about the StakePool.
|
|
1611
|
+
*/
|
|
1612
|
+
export async function stakePoolInfo(
|
|
1613
|
+
connection: Connection,
|
|
1614
|
+
stakePoolAddress: PublicKey,
|
|
1615
|
+
) {
|
|
1616
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
1617
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1618
|
+
const reserveAccountStakeAddress = stakePool.account.data.reserveStake
|
|
1619
|
+
const totalLamports = stakePool.account.data.totalLamports
|
|
1620
|
+
const lastUpdateEpoch = stakePool.account.data.lastUpdateEpoch
|
|
1621
|
+
|
|
1622
|
+
const validatorList = await getValidatorListAccount(
|
|
1623
|
+
connection,
|
|
1624
|
+
stakePool.account.data.validatorList,
|
|
1625
|
+
)
|
|
1626
|
+
|
|
1627
|
+
const maxNumberOfValidators = validatorList.account.data.maxValidators
|
|
1628
|
+
const currentNumberOfValidators
|
|
1629
|
+
= validatorList.account.data.validators.length
|
|
1630
|
+
|
|
1631
|
+
const epochInfo = await connection.getEpochInfo()
|
|
1632
|
+
const reserveStake = await connection.getAccountInfo(
|
|
1633
|
+
reserveAccountStakeAddress,
|
|
1634
|
+
)
|
|
1635
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1636
|
+
stakePoolProgramId,
|
|
1637
|
+
stakePoolAddress,
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
const minimumReserveStakeBalance
|
|
1641
|
+
= await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
|
|
1642
|
+
|
|
1643
|
+
const stakeAccounts = await Promise.all(
|
|
1644
|
+
validatorList.account.data.validators.map(async (validator) => {
|
|
1645
|
+
const stakeAccountAddress = await findStakeProgramAddress(
|
|
1646
|
+
stakePoolProgramId,
|
|
1647
|
+
validator.voteAccountAddress,
|
|
1648
|
+
stakePoolAddress,
|
|
1649
|
+
)
|
|
1650
|
+
const transientStakeAccountAddress
|
|
1651
|
+
= await findTransientStakeProgramAddress(
|
|
1652
|
+
stakePoolProgramId,
|
|
1653
|
+
validator.voteAccountAddress,
|
|
1654
|
+
stakePoolAddress,
|
|
1655
|
+
validator.transientSeedSuffixStart,
|
|
1656
|
+
)
|
|
1657
|
+
const updateRequired = !validator.lastUpdateEpoch.eqn(epochInfo.epoch)
|
|
1658
|
+
return {
|
|
1659
|
+
voteAccountAddress: validator.voteAccountAddress.toBase58(),
|
|
1660
|
+
stakeAccountAddress: stakeAccountAddress.toBase58(),
|
|
1661
|
+
validatorActiveStakeLamports: validator.activeStakeLamports.toString(),
|
|
1662
|
+
validatorLastUpdateEpoch: validator.lastUpdateEpoch.toString(),
|
|
1663
|
+
validatorLamports: validator.activeStakeLamports
|
|
1664
|
+
.add(validator.transientStakeLamports)
|
|
1665
|
+
.toString(),
|
|
1666
|
+
validatorTransientStakeAccountAddress:
|
|
1667
|
+
transientStakeAccountAddress.toBase58(),
|
|
1668
|
+
validatorTransientStakeLamports:
|
|
1669
|
+
validator.transientStakeLamports.toString(),
|
|
1670
|
+
updateRequired,
|
|
1671
|
+
}
|
|
1672
|
+
}),
|
|
1673
|
+
)
|
|
1674
|
+
|
|
1675
|
+
const totalPoolTokens = lamportsToSol(stakePool.account.data.poolTokenSupply)
|
|
1676
|
+
const updateRequired = !lastUpdateEpoch.eqn(epochInfo.epoch)
|
|
1677
|
+
|
|
1678
|
+
return {
|
|
1679
|
+
address: stakePoolAddress.toBase58(),
|
|
1680
|
+
poolWithdrawAuthority: withdrawAuthority.toBase58(),
|
|
1681
|
+
manager: stakePool.account.data.manager.toBase58(),
|
|
1682
|
+
staker: stakePool.account.data.staker.toBase58(),
|
|
1683
|
+
stakeDepositAuthority:
|
|
1684
|
+
stakePool.account.data.stakeDepositAuthority.toBase58(),
|
|
1685
|
+
stakeWithdrawBumpSeed: stakePool.account.data.stakeWithdrawBumpSeed,
|
|
1686
|
+
maxValidators: maxNumberOfValidators,
|
|
1687
|
+
validatorList: validatorList.account.data.validators.map((validator) => {
|
|
1688
|
+
return {
|
|
1689
|
+
activeStakeLamports: validator.activeStakeLamports.toString(),
|
|
1690
|
+
transientStakeLamports: validator.transientStakeLamports.toString(),
|
|
1691
|
+
lastUpdateEpoch: validator.lastUpdateEpoch.toString(),
|
|
1692
|
+
transientSeedSuffixStart: validator.transientSeedSuffixStart.toString(),
|
|
1693
|
+
transientSeedSuffixEnd: validator.transientSeedSuffixEnd.toString(),
|
|
1694
|
+
status: validator.status.toString(),
|
|
1695
|
+
voteAccountAddress: validator.voteAccountAddress.toString(),
|
|
1696
|
+
}
|
|
1697
|
+
}), // CliStakePoolValidator
|
|
1698
|
+
validatorListStorageAccount:
|
|
1699
|
+
stakePool.account.data.validatorList.toBase58(),
|
|
1700
|
+
reserveStake: stakePool.account.data.reserveStake.toBase58(),
|
|
1701
|
+
poolMint: stakePool.account.data.poolMint.toBase58(),
|
|
1702
|
+
managerFeeAccount: stakePool.account.data.managerFeeAccount.toBase58(),
|
|
1703
|
+
tokenProgramId: stakePool.account.data.tokenProgramId.toBase58(),
|
|
1704
|
+
totalLamports: stakePool.account.data.totalLamports.toString(),
|
|
1705
|
+
poolTokenSupply: stakePool.account.data.poolTokenSupply.toString(),
|
|
1706
|
+
lastUpdateEpoch: stakePool.account.data.lastUpdateEpoch.toString(),
|
|
1707
|
+
lockup: stakePool.account.data.lockup, // pub lockup: CliStakePoolLockup
|
|
1708
|
+
epochFee: stakePool.account.data.epochFee,
|
|
1709
|
+
nextEpochFee: stakePool.account.data.nextEpochFee,
|
|
1710
|
+
preferredDepositValidatorVoteAddress:
|
|
1711
|
+
stakePool.account.data.preferredDepositValidatorVoteAddress,
|
|
1712
|
+
preferredWithdrawValidatorVoteAddress:
|
|
1713
|
+
stakePool.account.data.preferredWithdrawValidatorVoteAddress,
|
|
1714
|
+
stakeDepositFee: stakePool.account.data.stakeDepositFee,
|
|
1715
|
+
stakeWithdrawalFee: stakePool.account.data.stakeWithdrawalFee,
|
|
1716
|
+
// CliStakePool the same
|
|
1717
|
+
nextStakeWithdrawalFee: stakePool.account.data.nextStakeWithdrawalFee,
|
|
1718
|
+
stakeReferralFee: stakePool.account.data.stakeReferralFee,
|
|
1719
|
+
solDepositAuthority: stakePool.account.data.solDepositAuthority?.toBase58(),
|
|
1720
|
+
solDepositFee: stakePool.account.data.solDepositFee,
|
|
1721
|
+
solReferralFee: stakePool.account.data.solReferralFee,
|
|
1722
|
+
solWithdrawAuthority:
|
|
1723
|
+
stakePool.account.data.solWithdrawAuthority?.toBase58(),
|
|
1724
|
+
solWithdrawalFee: stakePool.account.data.solWithdrawalFee,
|
|
1725
|
+
nextSolWithdrawalFee: stakePool.account.data.nextSolWithdrawalFee,
|
|
1726
|
+
lastEpochPoolTokenSupply:
|
|
1727
|
+
stakePool.account.data.lastEpochPoolTokenSupply.toString(),
|
|
1728
|
+
lastEpochTotalLamports:
|
|
1729
|
+
stakePool.account.data.lastEpochTotalLamports.toString(),
|
|
1730
|
+
details: {
|
|
1731
|
+
reserveStakeLamports: reserveStake?.lamports,
|
|
1732
|
+
reserveAccountStakeAddress: reserveAccountStakeAddress.toBase58(),
|
|
1733
|
+
minimumReserveStakeBalance,
|
|
1734
|
+
stakeAccounts,
|
|
1735
|
+
totalLamports,
|
|
1736
|
+
totalPoolTokens,
|
|
1737
|
+
currentNumberOfValidators,
|
|
1738
|
+
maxNumberOfValidators,
|
|
1739
|
+
updateRequired,
|
|
1740
|
+
}, // CliStakePoolDetails
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Creates instructions required to create pool token metadata.
|
|
1746
|
+
*/
|
|
1747
|
+
export async function createPoolTokenMetadata(
|
|
1748
|
+
connection: Connection,
|
|
1749
|
+
stakePoolAddress: PublicKey,
|
|
1750
|
+
payer: PublicKey,
|
|
1751
|
+
name: string,
|
|
1752
|
+
symbol: string,
|
|
1753
|
+
uri: string,
|
|
1754
|
+
) {
|
|
1755
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
1756
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1757
|
+
|
|
1758
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1759
|
+
stakePoolProgramId,
|
|
1760
|
+
stakePoolAddress,
|
|
1761
|
+
)
|
|
1762
|
+
const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint)
|
|
1763
|
+
const manager = stakePool.account.data.manager
|
|
1764
|
+
|
|
1765
|
+
const instructions: TransactionInstruction[] = []
|
|
1766
|
+
instructions.push(
|
|
1767
|
+
StakePoolInstruction.createTokenMetadata({
|
|
1768
|
+
programId: stakePoolProgramId,
|
|
1769
|
+
stakePool: stakePoolAddress,
|
|
1770
|
+
poolMint: stakePool.account.data.poolMint,
|
|
1771
|
+
payer,
|
|
1772
|
+
manager,
|
|
1773
|
+
tokenMetadata,
|
|
1774
|
+
withdrawAuthority,
|
|
1775
|
+
name,
|
|
1776
|
+
symbol,
|
|
1777
|
+
uri,
|
|
1778
|
+
}),
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
return {
|
|
1782
|
+
instructions,
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Creates instructions required to update pool token metadata.
|
|
1788
|
+
*/
|
|
1789
|
+
export async function updatePoolTokenMetadata(
|
|
1790
|
+
connection: Connection,
|
|
1791
|
+
stakePoolAddress: PublicKey,
|
|
1792
|
+
name: string,
|
|
1793
|
+
symbol: string,
|
|
1794
|
+
uri: string,
|
|
1795
|
+
) {
|
|
1796
|
+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress)
|
|
1797
|
+
const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
|
|
1798
|
+
|
|
1799
|
+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
|
|
1800
|
+
stakePoolProgramId,
|
|
1801
|
+
stakePoolAddress,
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint)
|
|
1805
|
+
|
|
1806
|
+
const instructions: TransactionInstruction[] = []
|
|
1807
|
+
instructions.push(
|
|
1808
|
+
StakePoolInstruction.updateTokenMetadata({
|
|
1809
|
+
programId: stakePoolProgramId,
|
|
1810
|
+
stakePool: stakePoolAddress,
|
|
1811
|
+
manager: stakePool.account.data.manager,
|
|
1812
|
+
tokenMetadata,
|
|
1813
|
+
withdrawAuthority,
|
|
1814
|
+
name,
|
|
1815
|
+
symbol,
|
|
1816
|
+
uri,
|
|
1817
|
+
}),
|
|
1818
|
+
)
|
|
1819
|
+
|
|
1820
|
+
return {
|
|
1821
|
+
instructions,
|
|
1822
|
+
}
|
|
1823
|
+
}
|