@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.
@@ -0,0 +1,233 @@
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
+ // For active stake accounts, subtract the minimum balance that must remain
88
+ // to allow for merges and maintain rent exemption
89
+ const availableActiveLamports = validator.activeStakeLamports.sub(minBalance)
90
+ if (availableActiveLamports.gt(new BN(0))) {
91
+ const isPreferred = stakePool?.preferredWithdrawValidatorVoteAddress?.equals(
92
+ validator.voteAccountAddress,
93
+ )
94
+ accounts.push({
95
+ type: isPreferred ? 'preferred' : 'active',
96
+ voteAddress: validator.voteAccountAddress,
97
+ stakeAddress: stakeAccountAddress,
98
+ lamports: availableActiveLamports,
99
+ })
100
+ }
101
+
102
+ const transientStakeLamports = validator.transientStakeLamports.sub(minBalance)
103
+ if (transientStakeLamports.gt(new BN(0))) {
104
+ const transientStakeAccountAddress = await findTransientStakeProgramAddress(
105
+ stakePoolProgramId,
106
+ validator.voteAccountAddress,
107
+ stakePoolAddress,
108
+ validator.transientSeedSuffixStart,
109
+ )
110
+ accounts.push({
111
+ type: 'transient',
112
+ voteAddress: validator.voteAccountAddress,
113
+ stakeAddress: transientStakeAccountAddress,
114
+ lamports: transientStakeLamports,
115
+ })
116
+ }
117
+ }
118
+
119
+ // Sort from highest to lowest balance
120
+ accounts = accounts.sort(compareFn || ((a, b) => b.lamports.sub(a.lamports).toNumber()))
121
+
122
+ const reserveStake = await connection.getAccountInfo(stakePool.reserveStake)
123
+ const reserveStakeBalance = new BN((reserveStake?.lamports ?? 0) - minBalanceForRentExemption)
124
+ if (reserveStakeBalance.gt(new BN(0))) {
125
+ accounts.push({
126
+ type: 'reserve',
127
+ stakeAddress: stakePool.reserveStake,
128
+ lamports: reserveStakeBalance,
129
+ })
130
+ }
131
+
132
+ // Prepare the list of accounts to withdraw from
133
+ const withdrawFrom: WithdrawAccount[] = []
134
+ let remainingAmount = new BN(amount)
135
+
136
+ const fee = stakePool.stakeWithdrawalFee
137
+ const inverseFee: Fee = {
138
+ numerator: fee.denominator.sub(fee.numerator),
139
+ denominator: fee.denominator,
140
+ }
141
+
142
+ for (const type of ['preferred', 'active', 'transient', 'reserve']) {
143
+ const filteredAccounts = accounts.filter(a => a.type === type)
144
+
145
+ for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
146
+ if (lamports.lte(minBalance) && type === 'transient') {
147
+ continue
148
+ }
149
+
150
+ let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports)
151
+
152
+ if (!skipFee && !inverseFee.numerator.isZero()) {
153
+ availableForWithdrawal = availableForWithdrawal
154
+ .mul(inverseFee.denominator)
155
+ .div(inverseFee.numerator)
156
+ }
157
+
158
+ const poolAmount = BN.min(availableForWithdrawal, remainingAmount)
159
+ if (poolAmount.lte(new BN(0))) {
160
+ continue
161
+ }
162
+
163
+ // Those accounts will be withdrawn completely with `claim` instruction
164
+ withdrawFrom.push({ stakeAddress, voteAddress, poolAmount })
165
+ remainingAmount = remainingAmount.sub(poolAmount)
166
+
167
+ if (remainingAmount.isZero()) {
168
+ break
169
+ }
170
+ }
171
+
172
+ if (remainingAmount.isZero()) {
173
+ break
174
+ }
175
+ }
176
+
177
+ // Not enough stake to withdraw the specified amount
178
+ if (remainingAmount.gt(new BN(0))) {
179
+ throw new Error(
180
+ `No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol(
181
+ amount,
182
+ )} pool tokens.`,
183
+ )
184
+ }
185
+
186
+ return withdrawFrom
187
+ }
188
+
189
+ /**
190
+ * Calculate the pool tokens that should be minted for a deposit of `stakeLamports`
191
+ */
192
+ export function calcPoolTokensForDeposit(stakePool: StakePool, stakeLamports: BN): BN {
193
+ if (stakePool.poolTokenSupply.isZero() || stakePool.totalLamports.isZero()) {
194
+ return stakeLamports
195
+ }
196
+ const numerator = stakeLamports.mul(stakePool.poolTokenSupply)
197
+ return numerator.div(stakePool.totalLamports)
198
+ }
199
+
200
+ /**
201
+ * Calculate lamports amount on withdrawal
202
+ */
203
+ export function calcLamportsWithdrawAmount(stakePool: StakePool, poolTokens: BN): BN {
204
+ const numerator = poolTokens.mul(stakePool.totalLamports)
205
+ const denominator = stakePool.poolTokenSupply
206
+ if (numerator.lt(denominator)) {
207
+ return new BN(0)
208
+ }
209
+ return numerator.div(denominator)
210
+ }
211
+
212
+ export function newStakeAccount(
213
+ feePayer: PublicKey,
214
+ instructions: TransactionInstruction[],
215
+ lamports: number,
216
+ ): Keypair {
217
+ // Account for tokens not specified, creating one
218
+ const stakeReceiverKeypair = Keypair.generate()
219
+ console.log(`Creating account to receive stake ${stakeReceiverKeypair.publicKey}`)
220
+
221
+ instructions.push(
222
+ // Creating new account
223
+ SystemProgram.createAccount({
224
+ fromPubkey: feePayer,
225
+ newAccountPubkey: stakeReceiverKeypair.publicKey,
226
+ lamports,
227
+ space: StakeProgram.space,
228
+ programId: StakeProgram.programId,
229
+ }),
230
+ )
231
+
232
+ return stakeReceiverKeypair
233
+ }