@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/LICENSE +176 -0
- package/dist/codecs.d.ts +15 -0
- package/dist/constants.d.ts +12 -0
- package/dist/index.browser.cjs.js +2570 -0
- package/dist/index.browser.cjs.js.map +1 -0
- package/dist/index.browser.esm.js +2524 -0
- package/dist/index.browser.esm.js.map +1 -0
- package/dist/index.cjs.js +2570 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.esm.js +2524 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.iife.js +23755 -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 +329 -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 +26 -0
- package/dist/utils/stake.d.ts +29 -0
- package/package.json +90 -0
- package/src/codecs.ts +159 -0
- package/src/constants.ts +29 -0
- package/src/index.ts +1522 -0
- package/src/instructions.ts +1293 -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 +103 -0
- package/src/utils/stake.ts +230 -0
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
|
+
}
|