@ignitionfi/spl-stake-pool 1.1.8

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/src/layouts.ts ADDED
@@ -0,0 +1,248 @@
1
+ import { PublicKey } from '@solana/web3.js'
2
+ import BN from 'bn.js'
3
+ import { Layout as LayoutCls, struct, u8, u32 } from 'buffer-layout'
4
+ import {
5
+ coerce,
6
+ enums,
7
+ Infer,
8
+ instance,
9
+ nullable,
10
+ number,
11
+ optional,
12
+ string,
13
+ type,
14
+ } from 'superstruct'
15
+ import { Layout, option, publicKey, u64, vec } from './codecs'
16
+
17
+ export interface Fee {
18
+ denominator: BN
19
+ numerator: BN
20
+ }
21
+
22
+ const feeFields = [u64('denominator'), u64('numerator')]
23
+
24
+ export enum AccountType {
25
+ Uninitialized,
26
+ StakePool,
27
+ ValidatorList,
28
+ }
29
+
30
+ export const BigNumFromString = coerce(instance(BN), string(), (value) => {
31
+ if (typeof value === 'string') {
32
+ return new BN(value, 10)
33
+ }
34
+ throw new Error('invalid big num')
35
+ })
36
+
37
+ export const PublicKeyFromString = coerce(
38
+ instance(PublicKey),
39
+ string(),
40
+ value => new PublicKey(value),
41
+ )
42
+
43
+ export class FutureEpochLayout<T> extends LayoutCls<T | null> {
44
+ layout: Layout<T>
45
+ discriminator: Layout<number>
46
+
47
+ constructor(layout: Layout<T>, property?: string) {
48
+ super(-1, property)
49
+ this.layout = layout
50
+ this.discriminator = u8()
51
+ }
52
+
53
+ encode(src: T | null, b: Buffer, offset = 0): number {
54
+ if (src === null || src === undefined) {
55
+ return this.discriminator.encode(0, b, offset)
56
+ }
57
+ // This isn't right, but we don't typically encode outside of tests
58
+ this.discriminator.encode(2, b, offset)
59
+ return this.layout.encode(src, b, offset + 1) + 1
60
+ }
61
+
62
+ decode(b: Buffer, offset = 0): T | null {
63
+ const discriminator = this.discriminator.decode(b, offset)
64
+ if (discriminator === 0) {
65
+ return null
66
+ } else if (discriminator === 1 || discriminator === 2) {
67
+ return this.layout.decode(b, offset + 1)
68
+ }
69
+ throw new Error(`Invalid future epoch ${this.property}`)
70
+ }
71
+
72
+ getSpan(b: Buffer, offset = 0): number {
73
+ const discriminator = this.discriminator.decode(b, offset)
74
+ if (discriminator === 0) {
75
+ return 1
76
+ } else if (discriminator === 1 || discriminator === 2) {
77
+ return this.layout.getSpan(b, offset + 1) + 1
78
+ }
79
+ throw new Error(`Invalid future epoch ${this.property}`)
80
+ }
81
+ }
82
+
83
+ export function futureEpoch<T>(layout: Layout<T>, property?: string): LayoutCls<T | null> {
84
+ return new FutureEpochLayout<T>(layout, property)
85
+ }
86
+
87
+ export type StakeAccountType = Infer<typeof StakeAccountType>
88
+ export const StakeAccountType = enums(['uninitialized', 'initialized', 'delegated', 'rewardsPool'])
89
+
90
+ export type StakeMeta = Infer<typeof StakeMeta>
91
+ export const StakeMeta = type({
92
+ rentExemptReserve: BigNumFromString,
93
+ authorized: type({
94
+ staker: PublicKeyFromString,
95
+ withdrawer: PublicKeyFromString,
96
+ }),
97
+ lockup: type({
98
+ unixTimestamp: number(),
99
+ epoch: number(),
100
+ custodian: PublicKeyFromString,
101
+ }),
102
+ })
103
+
104
+ export type StakeAccountInfo = Infer<typeof StakeAccountInfo>
105
+ export const StakeAccountInfo = type({
106
+ meta: StakeMeta,
107
+ stake: nullable(
108
+ type({
109
+ delegation: type({
110
+ voter: PublicKeyFromString,
111
+ stake: BigNumFromString,
112
+ activationEpoch: BigNumFromString,
113
+ deactivationEpoch: BigNumFromString,
114
+ warmupCooldownRate: number(),
115
+ }),
116
+ creditsObserved: number(),
117
+ }),
118
+ ),
119
+ })
120
+
121
+ export type StakeAccount = Infer<typeof StakeAccount>
122
+ export const StakeAccount = type({
123
+ type: StakeAccountType,
124
+ info: optional(StakeAccountInfo),
125
+ })
126
+ export interface Lockup {
127
+ unixTimestamp: BN
128
+ epoch: BN
129
+ custodian: PublicKey
130
+ }
131
+
132
+ export interface StakePool {
133
+ accountType: AccountType
134
+ manager: PublicKey
135
+ staker: PublicKey
136
+ stakeDepositAuthority: PublicKey
137
+ stakeWithdrawBumpSeed: number
138
+ validatorList: PublicKey
139
+ reserveStake: PublicKey
140
+ poolMint: PublicKey
141
+ managerFeeAccount: PublicKey
142
+ tokenProgramId: PublicKey
143
+ totalLamports: BN
144
+ poolTokenSupply: BN
145
+ lastUpdateEpoch: BN
146
+ lockup: Lockup
147
+ epochFee: Fee
148
+ nextEpochFee?: Fee | undefined
149
+ preferredDepositValidatorVoteAddress?: PublicKey | undefined
150
+ preferredWithdrawValidatorVoteAddress?: PublicKey | undefined
151
+ stakeDepositFee: Fee
152
+ stakeWithdrawalFee: Fee
153
+ nextStakeWithdrawalFee?: Fee | undefined
154
+ stakeReferralFee: number
155
+ solDepositAuthority?: PublicKey | undefined
156
+ solDepositFee: Fee
157
+ solReferralFee: number
158
+ solWithdrawAuthority?: PublicKey | undefined
159
+ solWithdrawalFee: Fee
160
+ nextSolWithdrawalFee?: Fee | undefined
161
+ lastEpochPoolTokenSupply: BN
162
+ lastEpochTotalLamports: BN
163
+ }
164
+
165
+ export const StakePoolLayout = struct<StakePool>([
166
+ u8('accountType'),
167
+ publicKey('manager'),
168
+ publicKey('staker'),
169
+ publicKey('stakeDepositAuthority'),
170
+ u8('stakeWithdrawBumpSeed'),
171
+ publicKey('validatorList'),
172
+ publicKey('reserveStake'),
173
+ publicKey('poolMint'),
174
+ publicKey('managerFeeAccount'),
175
+ publicKey('tokenProgramId'),
176
+ u64('totalLamports'),
177
+ u64('poolTokenSupply'),
178
+ u64('lastUpdateEpoch'),
179
+ struct([u64('unixTimestamp'), u64('epoch'), publicKey('custodian')], 'lockup'),
180
+ struct(feeFields, 'epochFee'),
181
+ futureEpoch(struct(feeFields), 'nextEpochFee'),
182
+ option(publicKey(), 'preferredDepositValidatorVoteAddress'),
183
+ option(publicKey(), 'preferredWithdrawValidatorVoteAddress'),
184
+ struct(feeFields, 'stakeDepositFee'),
185
+ struct(feeFields, 'stakeWithdrawalFee'),
186
+ futureEpoch(struct(feeFields), 'nextStakeWithdrawalFee'),
187
+ u8('stakeReferralFee'),
188
+ option(publicKey(), 'solDepositAuthority'),
189
+ struct(feeFields, 'solDepositFee'),
190
+ u8('solReferralFee'),
191
+ option(publicKey(), 'solWithdrawAuthority'),
192
+ struct(feeFields, 'solWithdrawalFee'),
193
+ futureEpoch(struct(feeFields), 'nextSolWithdrawalFee'),
194
+ u64('lastEpochPoolTokenSupply'),
195
+ u64('lastEpochTotalLamports'),
196
+ ])
197
+
198
+ export enum ValidatorStakeInfoStatus {
199
+ Active,
200
+ DeactivatingTransient,
201
+ ReadyForRemoval,
202
+ }
203
+
204
+ export interface ValidatorStakeInfo {
205
+ status: ValidatorStakeInfoStatus
206
+ voteAccountAddress: PublicKey
207
+ activeStakeLamports: BN
208
+ transientStakeLamports: BN
209
+ transientSeedSuffixStart: BN
210
+ transientSeedSuffixEnd: BN
211
+ lastUpdateEpoch: BN
212
+ }
213
+
214
+ export const ValidatorStakeInfoLayout = struct<ValidatorStakeInfo>([
215
+ /// Amount of active stake delegated to this validator
216
+ /// Note that if `last_update_epoch` does not match the current epoch then
217
+ /// this field may not be accurate
218
+ u64('activeStakeLamports'),
219
+ /// Amount of transient stake delegated to this validator
220
+ /// Note that if `last_update_epoch` does not match the current epoch then
221
+ /// this field may not be accurate
222
+ u64('transientStakeLamports'),
223
+ /// Last epoch the active and transient stake lamports fields were updated
224
+ u64('lastUpdateEpoch'),
225
+ /// Start of the validator transient account seed suffixes
226
+ u64('transientSeedSuffixStart'),
227
+ /// End of the validator transient account seed suffixes
228
+ u64('transientSeedSuffixEnd'),
229
+ /// Status of the validator stake account
230
+ u8('status'),
231
+ /// Validator vote account address
232
+ publicKey('voteAccountAddress'),
233
+ ])
234
+
235
+ export interface ValidatorList {
236
+ /// Account type, must be ValidatorList currently
237
+ accountType: number
238
+ /// Maximum allowable number of validators
239
+ maxValidators: number
240
+ /// List of stake info for each validator in the pool
241
+ validators: ValidatorStakeInfo[]
242
+ }
243
+
244
+ export const ValidatorListLayout = struct<ValidatorList>([
245
+ u8('accountType'),
246
+ u32('maxValidators'),
247
+ vec(ValidatorStakeInfoLayout, 'validators'),
248
+ ])
@@ -0,0 +1,29 @@
1
+ declare module 'buffer-layout' {
2
+ export class Layout<T = any> {
3
+ span: number
4
+ property?: string
5
+ constructor(span: number, property?: string)
6
+ decode(b: Buffer | undefined, offset?: number): T
7
+ encode(src: T, b: Buffer, offset?: number): number
8
+ getSpan(b: Buffer, offset?: number): number
9
+ replicate(name: string): this
10
+ }
11
+ export function struct<T>(
12
+ fields: Layout<any>[],
13
+ property?: string,
14
+ decodePrefixes?: boolean,
15
+ ): Layout<T>
16
+ export function seq<T>(
17
+ elementLayout: Layout<T>,
18
+ count: number | Layout<number>,
19
+ property?: string,
20
+ ): Layout<T[]>
21
+ export function offset<T>(layout: Layout<T>, offset?: number, property?: string): Layout<T>
22
+ export function blob(length: number | Layout<number>, property?: string): Layout<Buffer>
23
+ export function s32(property?: string): Layout<number>
24
+ export function u32(property?: string): Layout<number>
25
+ export function s16(property?: string): Layout<number>
26
+ export function u16(property?: string): Layout<number>
27
+ export function s8(property?: string): Layout<number>
28
+ export function u8(property?: string): Layout<number>
29
+ }
@@ -0,0 +1,12 @@
1
+ export * from './instruction'
2
+ export * from './math'
3
+ export * from './program-address'
4
+ export * from './stake'
5
+
6
+ export function arrayChunk(array: any[], size: number): any[] {
7
+ const result = []
8
+ for (let i = 0; i < array.length; i += size) {
9
+ result.push(array.slice(i, i + size))
10
+ }
11
+ return result
12
+ }
@@ -0,0 +1,46 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import * as BufferLayout from '@solana/buffer-layout'
3
+
4
+ /**
5
+ * @internal
6
+ */
7
+ export type InstructionType = {
8
+ /** The Instruction index (from solana upstream program) */
9
+ index: number
10
+ /** The BufferLayout to use to build data */
11
+ layout: BufferLayout.Layout<any>
12
+ }
13
+
14
+ /**
15
+ * Populate a buffer of instruction data using an InstructionType
16
+ * @internal
17
+ */
18
+ export function encodeData(type: InstructionType, fields?: any): Buffer {
19
+ const allocLength = type.layout.span
20
+ const data = Buffer.alloc(allocLength)
21
+ const layoutFields = Object.assign({ instruction: type.index }, fields)
22
+ type.layout.encode(layoutFields, data)
23
+
24
+ return data
25
+ }
26
+
27
+ /**
28
+ * Decode instruction data buffer using an InstructionType
29
+ * @internal
30
+ */
31
+ export function decodeData(type: InstructionType, buffer: Buffer): any {
32
+ let data
33
+ try {
34
+ data = type.layout.decode(buffer)
35
+ } catch (err) {
36
+ throw new Error(`invalid instruction; ${err}`)
37
+ }
38
+
39
+ if (data.instruction !== type.index) {
40
+ throw new Error(
41
+ `invalid instruction; instruction index mismatch ${data.instruction} != ${type.index}`,
42
+ )
43
+ }
44
+
45
+ return data
46
+ }
@@ -0,0 +1,29 @@
1
+ import { LAMPORTS_PER_SOL } from '@solana/web3.js'
2
+ import BN from 'bn.js'
3
+
4
+ export function solToLamports(amount: number): number {
5
+ if (isNaN(amount)) {
6
+ return Number(0)
7
+ }
8
+ return Number(amount * LAMPORTS_PER_SOL)
9
+ }
10
+
11
+ export function lamportsToSol(lamports: number | BN | bigint): number {
12
+ if (typeof lamports === 'number') {
13
+ return Math.abs(lamports) / LAMPORTS_PER_SOL
14
+ }
15
+ if (typeof lamports === 'bigint') {
16
+ return Math.abs(Number(lamports)) / LAMPORTS_PER_SOL
17
+ }
18
+
19
+ let signMultiplier = 1
20
+ if (lamports.isNeg()) {
21
+ signMultiplier = -1
22
+ }
23
+
24
+ const absLamports = lamports.abs()
25
+ const lamportsString = absLamports.toString(10).padStart(10, '0')
26
+ const splitIndex = lamportsString.length - 9
27
+ const solString = `${lamportsString.slice(0, splitIndex)}.${lamportsString.slice(splitIndex)}`
28
+ return signMultiplier * Number.parseFloat(solString)
29
+ }
@@ -0,0 +1,103 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { PublicKey } from '@solana/web3.js'
3
+ import BN from 'bn.js'
4
+ import {
5
+ EPHEMERAL_STAKE_SEED_PREFIX,
6
+ METADATA_PROGRAM_ID,
7
+ TRANSIENT_STAKE_SEED_PREFIX,
8
+ } from '../constants'
9
+
10
+ /**
11
+ * Generates the wSOL transient program address for the stake pool
12
+ */
13
+ export function findWsolTransientProgramAddress(
14
+ programId: PublicKey,
15
+ userPubkey: PublicKey,
16
+ ) {
17
+ const [publicKey] = PublicKey.findProgramAddressSync(
18
+ [Buffer.from('transient_wsol'), userPubkey.toBuffer()],
19
+ programId,
20
+ )
21
+ return publicKey
22
+ }
23
+
24
+ /**
25
+ * Generates the withdraw authority program address for the stake pool
26
+ */
27
+ export async function findWithdrawAuthorityProgramAddress(
28
+ programId: PublicKey,
29
+ stakePoolAddress: PublicKey,
30
+ ) {
31
+ const [publicKey] = PublicKey.findProgramAddressSync(
32
+ [stakePoolAddress.toBuffer(), Buffer.from('withdraw')],
33
+ programId,
34
+ )
35
+ return publicKey
36
+ }
37
+
38
+ /**
39
+ * Generates the stake program address for a validator's vote account
40
+ */
41
+ export async function findStakeProgramAddress(
42
+ programId: PublicKey,
43
+ voteAccountAddress: PublicKey,
44
+ stakePoolAddress: PublicKey,
45
+ seed?: number,
46
+ ) {
47
+ const [publicKey] = PublicKey.findProgramAddressSync(
48
+ [
49
+ voteAccountAddress.toBuffer(),
50
+ stakePoolAddress.toBuffer(),
51
+ seed ? new BN(seed).toArrayLike(Buffer, 'le', 4) : Buffer.alloc(0),
52
+ ],
53
+ programId,
54
+ )
55
+ return publicKey
56
+ }
57
+
58
+ /**
59
+ * Generates the stake program address for a validator's vote account
60
+ */
61
+ export async function findTransientStakeProgramAddress(
62
+ programId: PublicKey,
63
+ voteAccountAddress: PublicKey,
64
+ stakePoolAddress: PublicKey,
65
+ seed: BN,
66
+ ) {
67
+ const [publicKey] = PublicKey.findProgramAddressSync(
68
+ [
69
+ TRANSIENT_STAKE_SEED_PREFIX,
70
+ voteAccountAddress.toBuffer(),
71
+ stakePoolAddress.toBuffer(),
72
+ seed.toArrayLike(Buffer, 'le', 8),
73
+ ],
74
+ programId,
75
+ )
76
+ return publicKey
77
+ }
78
+
79
+ /**
80
+ * Generates the ephemeral program address for stake pool redelegation
81
+ */
82
+ export async function findEphemeralStakeProgramAddress(
83
+ programId: PublicKey,
84
+ stakePoolAddress: PublicKey,
85
+ seed: BN,
86
+ ) {
87
+ const [publicKey] = PublicKey.findProgramAddressSync(
88
+ [EPHEMERAL_STAKE_SEED_PREFIX, stakePoolAddress.toBuffer(), seed.toArrayLike(Buffer, 'le', 8)],
89
+ programId,
90
+ )
91
+ return publicKey
92
+ }
93
+
94
+ /**
95
+ * Generates the metadata program address for the stake pool
96
+ */
97
+ export function findMetadataAddress(stakePoolMintAddress: PublicKey) {
98
+ const [publicKey] = PublicKey.findProgramAddressSync(
99
+ [Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), stakePoolMintAddress.toBuffer()],
100
+ METADATA_PROGRAM_ID,
101
+ )
102
+ return publicKey
103
+ }
@@ -0,0 +1,230 @@
1
+ import {
2
+ Connection,
3
+ Keypair,
4
+ PublicKey,
5
+ StakeProgram,
6
+ SystemProgram,
7
+ TransactionInstruction,
8
+ } from '@solana/web3.js'
9
+ import BN from 'bn.js'
10
+ import { MINIMUM_ACTIVE_STAKE } from '../constants'
11
+
12
+ import { getStakePoolProgramId, WithdrawAccount } from '../index'
13
+ import {
14
+ Fee,
15
+ StakePool,
16
+ ValidatorList,
17
+ ValidatorListLayout,
18
+ ValidatorStakeInfoStatus,
19
+ } from '../layouts'
20
+ import { lamportsToSol } from './math'
21
+ import { findStakeProgramAddress, findTransientStakeProgramAddress } from './program-address'
22
+
23
+ export async function getValidatorListAccount(connection: Connection, pubkey: PublicKey) {
24
+ const account = await connection.getAccountInfo(pubkey)
25
+ if (!account) {
26
+ throw new Error('Invalid validator list account')
27
+ }
28
+
29
+ return {
30
+ pubkey,
31
+ account: {
32
+ data: ValidatorListLayout.decode(account?.data) as ValidatorList,
33
+ executable: account.executable,
34
+ lamports: account.lamports,
35
+ owner: account.owner,
36
+ },
37
+ }
38
+ }
39
+
40
+ export interface ValidatorAccount {
41
+ type: 'preferred' | 'active' | 'transient' | 'reserve'
42
+ voteAddress?: PublicKey | undefined
43
+ stakeAddress: PublicKey
44
+ lamports: BN
45
+ }
46
+
47
+ export async function prepareWithdrawAccounts(
48
+ connection: Connection,
49
+ stakePool: StakePool,
50
+ stakePoolAddress: PublicKey,
51
+ amount: BN,
52
+ compareFn?: (a: ValidatorAccount, b: ValidatorAccount) => number,
53
+ skipFee?: boolean,
54
+ ): Promise<WithdrawAccount[]> {
55
+ const stakePoolProgramId = getStakePoolProgramId(connection.rpcEndpoint)
56
+ const validatorListAcc = await connection.getAccountInfo(stakePool.validatorList)
57
+ const validatorList = ValidatorListLayout.decode(validatorListAcc?.data) as ValidatorList
58
+
59
+ if (!validatorList?.validators || validatorList?.validators.length == 0) {
60
+ throw new Error('No accounts found')
61
+ }
62
+
63
+ const minBalanceForRentExemption = await connection.getMinimumBalanceForRentExemption(
64
+ StakeProgram.space,
65
+ )
66
+ const minBalance = new BN(minBalanceForRentExemption + MINIMUM_ACTIVE_STAKE)
67
+
68
+ let accounts = [] as Array<{
69
+ type: 'preferred' | 'active' | 'transient' | 'reserve'
70
+ voteAddress?: PublicKey | undefined
71
+ stakeAddress: PublicKey
72
+ lamports: BN
73
+ }>
74
+
75
+ // Prepare accounts
76
+ for (const validator of validatorList.validators) {
77
+ if (validator.status !== ValidatorStakeInfoStatus.Active) {
78
+ continue
79
+ }
80
+
81
+ const stakeAccountAddress = await findStakeProgramAddress(
82
+ stakePoolProgramId,
83
+ validator.voteAccountAddress,
84
+ stakePoolAddress,
85
+ )
86
+
87
+ if (!validator.activeStakeLamports.isZero()) {
88
+ const isPreferred = stakePool?.preferredWithdrawValidatorVoteAddress?.equals(
89
+ validator.voteAccountAddress,
90
+ )
91
+ accounts.push({
92
+ type: isPreferred ? 'preferred' : 'active',
93
+ voteAddress: validator.voteAccountAddress,
94
+ stakeAddress: stakeAccountAddress,
95
+ lamports: validator.activeStakeLamports,
96
+ })
97
+ }
98
+
99
+ const transientStakeLamports = validator.transientStakeLamports.sub(minBalance)
100
+ if (transientStakeLamports.gt(new BN(0))) {
101
+ const transientStakeAccountAddress = await findTransientStakeProgramAddress(
102
+ stakePoolProgramId,
103
+ validator.voteAccountAddress,
104
+ stakePoolAddress,
105
+ validator.transientSeedSuffixStart,
106
+ )
107
+ accounts.push({
108
+ type: 'transient',
109
+ voteAddress: validator.voteAccountAddress,
110
+ stakeAddress: transientStakeAccountAddress,
111
+ lamports: transientStakeLamports,
112
+ })
113
+ }
114
+ }
115
+
116
+ // Sort from highest to lowest balance
117
+ accounts = accounts.sort(compareFn || ((a, b) => b.lamports.sub(a.lamports).toNumber()))
118
+
119
+ const reserveStake = await connection.getAccountInfo(stakePool.reserveStake)
120
+ const reserveStakeBalance = new BN((reserveStake?.lamports ?? 0) - minBalanceForRentExemption)
121
+ if (reserveStakeBalance.gt(new BN(0))) {
122
+ accounts.push({
123
+ type: 'reserve',
124
+ stakeAddress: stakePool.reserveStake,
125
+ lamports: reserveStakeBalance,
126
+ })
127
+ }
128
+
129
+ // Prepare the list of accounts to withdraw from
130
+ const withdrawFrom: WithdrawAccount[] = []
131
+ let remainingAmount = new BN(amount)
132
+
133
+ const fee = stakePool.stakeWithdrawalFee
134
+ const inverseFee: Fee = {
135
+ numerator: fee.denominator.sub(fee.numerator),
136
+ denominator: fee.denominator,
137
+ }
138
+
139
+ for (const type of ['preferred', 'active', 'transient', 'reserve']) {
140
+ const filteredAccounts = accounts.filter(a => a.type == type)
141
+
142
+ for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
143
+ if (lamports.lte(minBalance) && type == 'transient') {
144
+ continue
145
+ }
146
+
147
+ let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports)
148
+
149
+ if (!skipFee && !inverseFee.numerator.isZero()) {
150
+ availableForWithdrawal = availableForWithdrawal
151
+ .mul(inverseFee.denominator)
152
+ .div(inverseFee.numerator)
153
+ }
154
+
155
+ const poolAmount = BN.min(availableForWithdrawal, remainingAmount)
156
+ if (poolAmount.lte(new BN(0))) {
157
+ continue
158
+ }
159
+
160
+ // Those accounts will be withdrawn completely with `claim` instruction
161
+ withdrawFrom.push({ stakeAddress, voteAddress, poolAmount })
162
+ remainingAmount = remainingAmount.sub(poolAmount)
163
+
164
+ if (remainingAmount.isZero()) {
165
+ break
166
+ }
167
+ }
168
+
169
+ if (remainingAmount.isZero()) {
170
+ break
171
+ }
172
+ }
173
+
174
+ // Not enough stake to withdraw the specified amount
175
+ if (remainingAmount.gt(new BN(0))) {
176
+ throw new Error(
177
+ `No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol(
178
+ amount,
179
+ )} pool tokens.`,
180
+ )
181
+ }
182
+
183
+ return withdrawFrom
184
+ }
185
+
186
+ /**
187
+ * Calculate the pool tokens that should be minted for a deposit of `stakeLamports`
188
+ */
189
+ export function calcPoolTokensForDeposit(stakePool: StakePool, stakeLamports: BN): BN {
190
+ if (stakePool.poolTokenSupply.isZero() || stakePool.totalLamports.isZero()) {
191
+ return stakeLamports
192
+ }
193
+ const numerator = stakeLamports.mul(stakePool.poolTokenSupply)
194
+ return numerator.div(stakePool.totalLamports)
195
+ }
196
+
197
+ /**
198
+ * Calculate lamports amount on withdrawal
199
+ */
200
+ export function calcLamportsWithdrawAmount(stakePool: StakePool, poolTokens: BN): BN {
201
+ const numerator = poolTokens.mul(stakePool.totalLamports)
202
+ const denominator = stakePool.poolTokenSupply
203
+ if (numerator.lt(denominator)) {
204
+ return new BN(0)
205
+ }
206
+ return numerator.div(denominator)
207
+ }
208
+
209
+ export function newStakeAccount(
210
+ feePayer: PublicKey,
211
+ instructions: TransactionInstruction[],
212
+ lamports: number,
213
+ ): Keypair {
214
+ // Account for tokens not specified, creating one
215
+ const stakeReceiverKeypair = Keypair.generate()
216
+ console.log(`Creating account to receive stake ${stakeReceiverKeypair.publicKey}`)
217
+
218
+ instructions.push(
219
+ // Creating new account
220
+ SystemProgram.createAccount({
221
+ fromPubkey: feePayer,
222
+ newAccountPubkey: stakeReceiverKeypair.publicKey,
223
+ lamports,
224
+ space: StakeProgram.space,
225
+ programId: StakeProgram.programId,
226
+ }),
227
+ )
228
+
229
+ return stakeReceiverKeypair
230
+ }