@exodus/solana-lib 1.2.5 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-lib",
3
- "version": "1.2.5",
3
+ "version": "1.2.8",
4
4
  "description": "Exodus internal Solana low-level library",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -22,5 +22,5 @@
22
22
  "create-hash": "~1.1.3",
23
23
  "tweetnacl": "^1.0.3"
24
24
  },
25
- "gitHead": "791e305a718246460d5ac93ed2eb7808955c0f5d"
25
+ "gitHead": "0dc8adf4a7d4d6dc3d0785f5b5d39e0a6bfb23cd"
26
26
  }
package/src/constants.js CHANGED
@@ -1,8 +1,12 @@
1
- import { PublicKey, SystemProgram } from './vendor'
1
+ import { PublicKey, SystemProgram, StakeProgram } from './vendor'
2
2
 
3
3
  export const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey(
4
4
  'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'
5
5
  )
6
6
  export const SYSTEM_PROGRAM_ID = SystemProgram.programId
7
7
 
8
+ export const STAKE_PROGRAM_ID = StakeProgram.programId
9
+
8
10
  export const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
11
+
12
+ export const SEED = 'stake:0'
package/src/encode.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @flow
2
- import { PublicKey } from './vendor'
3
- import { SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants'
2
+ import { PublicKey, StakeProgram } from './vendor'
3
+ import { SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, TOKEN_PROGRAM_ID, SEED } from './constants'
4
4
  import { getPublicKey } from './keypair'
5
5
  import bs58 from 'bs58'
6
6
  import BN from 'bn.js'
@@ -42,3 +42,16 @@ export function findAssociatedTokenAddress(
42
42
  SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
43
43
  )[0].toBase58() // returns encoded PublicKey
44
44
  }
45
+
46
+ export function createStakeAddress(walletAddress: string, seed = SEED): string {
47
+ const fromPubkey = new PublicKey(walletAddress)
48
+
49
+ const newAccountPubkey = PublicKey.createWithSeed(
50
+ // HACK: refactored to sync
51
+ fromPubkey,
52
+ seed,
53
+ StakeProgram.programId
54
+ )
55
+
56
+ return newAccountPubkey.toBase58()
57
+ }
package/src/tokens.js CHANGED
@@ -7,6 +7,12 @@ const TOKENS = [
7
7
  tokenProperName: 'Serum',
8
8
  tokenSymbol: 'SRM',
9
9
  },
10
+ {
11
+ mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
12
+ tokenName: 'usdcoin_solana',
13
+ tokenProperName: 'USD Coin Solana',
14
+ tokenSymbol: 'USDC Solana',
15
+ },
10
16
  // ....
11
17
  ]
12
18
 
@@ -2,12 +2,21 @@
2
2
  import assert from 'assert'
3
3
 
4
4
  import { getKeyPairFromPrivateKey } from './keypair'
5
- import { findAssociatedTokenAddress } from './encode'
5
+ import { findAssociatedTokenAddress, createStakeAddress } from './encode'
6
6
  import {
7
7
  createAssociatedTokenAccount,
8
8
  createTokenTransferInstruction,
9
9
  } from './helpers/tokenTransfer'
10
- import { PublicKey, Account, Transaction, SystemProgram } from './vendor'
10
+ import {
11
+ PublicKey,
12
+ Account,
13
+ Transaction,
14
+ SystemProgram,
15
+ StakeProgram,
16
+ Authorized,
17
+ Lockup,
18
+ } from './vendor'
19
+ import { SEED } from './constants'
11
20
  import TOKENS from './tokens'
12
21
  import bs58 from 'bs58'
13
22
 
@@ -121,13 +130,81 @@ class Tx {
121
130
  assert(amountLeft === 0, `Not enough balance to send ${amount} ${token.tokenSymbol}`)
122
131
  }
123
132
 
124
- static sign(tx: Tx, privateKey: Buffer | string) {
133
+ static createStakeAccountTransaction({ address, amount, seed = SEED, pool, recentBlockhash }) {
134
+ const fromPubkey = new PublicKey(address)
135
+ const stakeAddress = createStakeAddress(address, seed)
136
+ const stakePublicKey = new PublicKey(stakeAddress)
137
+ const poolKey = new PublicKey(pool)
138
+
139
+ const authorized = new Authorized(fromPubkey, fromPubkey) // staker, withdrawer
140
+ const lockup = new Lockup(0, 0, fromPubkey) // no lockup
141
+
142
+ // create account instruction
143
+ const programTx = StakeProgram.createAccountWithSeed({
144
+ fromPubkey,
145
+ stakePubkey: stakePublicKey,
146
+ basePubkey: fromPubkey,
147
+ seed,
148
+ authorized,
149
+ lockup,
150
+ lamports: amount, // number
151
+ })
152
+
153
+ // delegate funds instruction
154
+ const delegateTx = StakeProgram.delegate({
155
+ stakePubkey: stakePublicKey,
156
+ authorizedPubkey: fromPubkey,
157
+ votePubkey: poolKey, // pool vote key
158
+ })
159
+
160
+ const transaction = new Transaction({ recentBlockhash }).add(programTx).add(delegateTx)
161
+ return transaction
162
+ }
163
+
164
+ static undelegate({ address, stakeAddresses, recentBlockhash }) {
165
+ // undelegate all stake addresses
166
+ assert(Array.isArray(stakeAddresses), 'stakeAddresses Array is required')
167
+
168
+ const fromPubkey = new PublicKey(address)
169
+ const transaction = new Transaction({ recentBlockhash })
170
+
171
+ stakeAddresses.forEach((stakeAddress) => {
172
+ const stakePublicKey = new PublicKey(stakeAddress)
173
+ const programTx = StakeProgram.deactivate({
174
+ stakePubkey: stakePublicKey,
175
+ authorizedPubkey: fromPubkey,
176
+ })
177
+ transaction.add(programTx)
178
+ })
179
+
180
+ return transaction
181
+ }
182
+
183
+ static withdraw({ address, stakeAddresses, amount, recentBlockhash }) {
184
+ const fromPubkey = new PublicKey(address)
185
+ const stakeAddress = Array.isArray(stakeAddresses) ? stakeAddresses[0] : stakeAddresses
186
+ const stakePublicKey = new PublicKey(stakeAddress)
187
+
188
+ const transaction = StakeProgram.withdraw({
189
+ stakePubkey: stakePublicKey,
190
+ authorizedPubkey: fromPubkey,
191
+ toPubkey: fromPubkey,
192
+ lamports: amount,
193
+ })
194
+ transaction.recentBlockhash = recentBlockhash
195
+ return transaction
196
+ }
197
+
198
+ static sign(tx, privateKey: Buffer | string) {
125
199
  if (!privateKey) throw new Error('Please provide a secretKey')
126
200
  const { secretKey } = getKeyPairFromPrivateKey(privateKey)
127
201
  const signers = [new Account(secretKey)]
128
202
 
129
- tx.transaction.sign(...signers)
130
- if (!tx.transaction.signature) {
203
+ let transaction = tx
204
+ if (tx instanceof Tx) transaction = tx.transaction
205
+
206
+ transaction.sign(...signers)
207
+ if (!transaction.signature) {
131
208
  throw new Error('!signature') // should never happen
132
209
  }
133
210
  }
@@ -137,12 +214,27 @@ class Tx {
137
214
  return wireTransaction.toString('base64')
138
215
  }
139
216
 
217
+ static serialize(tx) {
218
+ let transaction = tx
219
+ if (tx instanceof Tx) transaction = tx.transaction
220
+ return transaction.serialize().toString('base64')
221
+ }
222
+
140
223
  getTxId() {
141
224
  if (!this.transaction.signature) {
142
225
  throw new Error('Cannot get txId, tx is not signed')
143
226
  }
144
227
  return bs58.encode(this.transaction.signature)
145
228
  }
229
+
230
+ static getTxId(tx) {
231
+ let transaction = tx
232
+ if (tx instanceof Tx) transaction = tx.transaction
233
+ if (!transaction.signature) {
234
+ throw new Error('Cannot get txId, tx is not signed')
235
+ }
236
+ return bs58.encode(transaction.signature)
237
+ }
146
238
  }
147
239
 
148
240
  export default Tx
@@ -11,6 +11,11 @@ export function createUnsignedTx({
11
11
  destinationAddressType,
12
12
  isAssociatedTokenAccountActive, // true when recipient balance !== 0
13
13
  fromTokenAddresses, // sender token addresses
14
+ // Staking related:
15
+ method,
16
+ stakeAddresses,
17
+ seed,
18
+ pool,
14
19
  }: ParsedTransaction): UnsignedTransaction {
15
20
  return {
16
21
  txData: {
@@ -23,6 +28,11 @@ export function createUnsignedTx({
23
28
  destinationAddressType,
24
29
  isAssociatedTokenAccountActive,
25
30
  fromTokenAddresses,
31
+ // Staking related:
32
+ method,
33
+ stakeAddresses,
34
+ seed,
35
+ pool,
26
36
  },
27
37
  txMeta: {
28
38
  assetName: asset.name,
@@ -11,6 +11,10 @@ export function parseUnsignedTx(unsignedTx: UnsignedTransaction): ParsedTransact
11
11
  destinationAddressType,
12
12
  isAssociatedTokenAccountActive,
13
13
  fromTokenAddresses,
14
+ method,
15
+ stakeAddresses,
16
+ seed,
17
+ pool,
14
18
  ...txData
15
19
  } = unsignedTx.txData
16
20
 
@@ -26,5 +30,10 @@ export function parseUnsignedTx(unsignedTx: UnsignedTransaction): ParsedTransact
26
30
  destinationAddressType,
27
31
  isAssociatedTokenAccountActive,
28
32
  fromTokenAddresses,
33
+ // staking related
34
+ method,
35
+ stakeAddresses,
36
+ seed,
37
+ pool,
29
38
  }
30
39
  }
@@ -9,30 +9,70 @@ export function signUnsignedTx(
9
9
  const {
10
10
  from,
11
11
  to,
12
- amount,
12
+ amount: unitAmount,
13
13
  recentBlockhash,
14
+ // tokens related
14
15
  tokenName,
15
16
  destinationAddressType,
16
17
  isAssociatedTokenAccountActive,
17
18
  fromTokenAddresses,
19
+ // staking related
20
+ stakeAddresses,
21
+ method,
22
+ seed,
23
+ pool,
18
24
  } = unsignedTx.txData
19
25
 
20
26
  const asset = assets.solana
21
- const tx = new Transaction({
22
- from,
23
- to,
24
- amount: asset.currency.baseUnit(amount).toNumber(),
25
- recentBlockhash,
26
- tokenName,
27
- destinationAddressType,
28
- isAssociatedTokenAccountActive,
29
- fromTokenAddresses,
30
- })
27
+
28
+ const address = from
29
+ const amount = unitAmount ? asset.currency.baseUnit(unitAmount).toNumber() : unitAmount
30
+
31
+ let tx
32
+ switch (method) {
33
+ case 'delegate':
34
+ tx = Transaction.createStakeAccountTransaction({
35
+ address,
36
+ amount,
37
+ recentBlockhash,
38
+ seed,
39
+ pool,
40
+ })
41
+ break
42
+ case 'undelegate':
43
+ tx = Transaction.undelegate({
44
+ address,
45
+ stakeAddresses,
46
+ recentBlockhash,
47
+ })
48
+ break
49
+ case 'withdraw':
50
+ tx = Transaction.withdraw({
51
+ address,
52
+ stakeAddresses,
53
+ amount,
54
+ recentBlockhash,
55
+ })
56
+ break
57
+ default:
58
+ // SOL and Token tx
59
+ tx = new Transaction({
60
+ from,
61
+ to,
62
+ amount,
63
+ recentBlockhash,
64
+ tokenName,
65
+ destinationAddressType,
66
+ isAssociatedTokenAccountActive,
67
+ fromTokenAddresses,
68
+ })
69
+ break
70
+ }
31
71
 
32
72
  // sign plain tx
33
73
  Transaction.sign(tx, privateKey)
34
- const rawTx = tx.serialize()
35
- const txId = tx.getTxId()
74
+ const rawTx = Transaction.serialize(tx)
75
+ const txId = Transaction.getTxId(tx)
36
76
 
37
77
  return { txId, rawTx }
38
78
  }
@@ -4,5 +4,6 @@ export * from './message'
4
4
  export * from './nonce-account'
5
5
  export * from './publickey'
6
6
  export * from './system-program'
7
+ export * from './stake-program'
7
8
  export * from './sysvar'
8
9
  export * from './transaction'
@@ -97,6 +97,26 @@ export class PublicKey {
97
97
  throw new Error('Unable to find a viable program address nonce')
98
98
  }
99
99
 
100
+ /**
101
+ * Derive a public key from another key, a seed, and a program ID.
102
+ * HACK: transformed in sync function
103
+ */
104
+ static createWithSeed(
105
+ fromPublicKey: PublicKey,
106
+ seed: string,
107
+ programId: PublicKey
108
+ ): Promise<PublicKey> {
109
+ const buffer = Buffer.concat([
110
+ fromPublicKey.toBuffer(),
111
+ Buffer.from(seed),
112
+ programId.toBuffer(),
113
+ ])
114
+ const hash = createHash('sha256')
115
+ .update(buffer)
116
+ .digest('hex')
117
+ return new PublicKey(Buffer.from(hash, 'hex'))
118
+ }
119
+
100
120
  /**
101
121
  * Derive a program address from seeds and a program ID.
102
122
  */
@@ -0,0 +1,527 @@
1
+ // @flow
2
+
3
+ import * as BufferLayout from '@exodus/buffer-layout'
4
+
5
+ import { encodeData, decodeData } from './instruction'
6
+ import * as Layout from './utils/layout'
7
+ import { PublicKey } from './publickey'
8
+ import { SystemProgram } from './system-program'
9
+ import { SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY } from './sysvar'
10
+ import { Transaction, TransactionInstruction } from './transaction'
11
+
12
+ export const STAKE_CONFIG_ID = new PublicKey('StakeConfig11111111111111111111111111111111')
13
+
14
+ export class Authorized {
15
+ staker: PublicKey
16
+ withdrawer: PublicKey
17
+
18
+ /**
19
+ * Create a new Authorized object
20
+ */
21
+ constructor(staker: PublicKey, withdrawer: PublicKey) {
22
+ this.staker = staker
23
+ this.withdrawer = withdrawer
24
+ }
25
+ }
26
+
27
+ export class Lockup {
28
+ unixTimestamp: number
29
+ epoch: number
30
+ custodian: PublicKey
31
+
32
+ /**
33
+ * Create a new Lockup object
34
+ */
35
+ constructor(unixTimestamp: number, epoch: number, custodian: PublicKey) {
36
+ this.unixTimestamp = unixTimestamp
37
+ this.epoch = epoch
38
+ this.custodian = custodian
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Create stake account transaction params
44
+ * @typedef {Object} CreateStakeAccountParams
45
+ * @property {PublicKey} fromPubkey
46
+ * @property {PublicKey} stakePubkey
47
+ * @property {Authorized} authorized
48
+ * @property {Lockup} lockup
49
+ * @property {number} lamports
50
+ */
51
+ export type CreateStakeAccountParams = {|
52
+ fromPubkey: PublicKey,
53
+ stakePubkey: PublicKey,
54
+ authorized: Authorized,
55
+ lockup: Lockup,
56
+ lamports: number,
57
+ |}
58
+
59
+ /**
60
+ * Create stake account with seed transaction params
61
+ * @typedef {Object} CreateStakeAccountWithSeedParams
62
+ * @property {PublicKey} fromPubkey
63
+ * @property {PublicKey} stakePubkey
64
+ * @property {PublicKey} basePubkey
65
+ * @property {string} seed
66
+ * @property {Authorized} authorized
67
+ * @property {Lockup} lockup
68
+ * @property {number} lamports
69
+ */
70
+ export type CreateStakeAccountWithSeedParams = {|
71
+ fromPubkey: PublicKey,
72
+ stakePubkey: PublicKey,
73
+ basePubkey: PublicKey,
74
+ seed: string,
75
+ authorized: Authorized,
76
+ lockup: Lockup,
77
+ lamports: number,
78
+ |}
79
+
80
+ /**
81
+ * Initialize stake instruction params
82
+ * @typedef {Object} InitializeStakeParams
83
+ * @property {PublicKey} stakePubkey
84
+ * @property {Authorized} authorized
85
+ * @property {Lockup} lockup
86
+ */
87
+ export type InitializeStakeParams = {|
88
+ stakePubkey: PublicKey,
89
+ authorized: Authorized,
90
+ lockup: Lockup,
91
+ |}
92
+
93
+ /**
94
+ * Delegate stake instruction params
95
+ * @typedef {Object} DelegateStakeParams
96
+ * @property {PublicKey} stakePubkey
97
+ * @property {PublicKey} authorizedPubkey
98
+ * @property {PublicKey} votePubkey
99
+ */
100
+ export type DelegateStakeParams = {|
101
+ stakePubkey: PublicKey,
102
+ authorizedPubkey: PublicKey,
103
+ votePubkey: PublicKey,
104
+ |}
105
+
106
+ /**
107
+ * @typedef {Object} StakeAuthorizationType
108
+ * @property (index} The Stake Authorization index (from solana-stake-program)
109
+ */
110
+ export type StakeAuthorizationType = {|
111
+ index: number,
112
+ |}
113
+
114
+ /**
115
+ * Authorize stake instruction params
116
+ * @typedef {Object} AuthorizeStakeParams
117
+ * @property {PublicKey} stakePubkey
118
+ * @property {PublicKey} authorizedPubkey
119
+ * @property {PublicKey} newAuthorizedPubkey
120
+ * @property {StakeAuthorizationType} stakeAuthorizationType
121
+ */
122
+ export type AuthorizeStakeParams = {|
123
+ stakePubkey: PublicKey,
124
+ authorizedPubkey: PublicKey,
125
+ newAuthorizedPubkey: PublicKey,
126
+ stakeAuthorizationType: StakeAuthorizationType,
127
+ |}
128
+
129
+ /**
130
+ * Authorize stake instruction params using a derived key
131
+ * @typedef {Object} AuthorizeWithSeedStakeParams
132
+ * @property {PublicKey} stakePubkey
133
+ * @property {PublicKey} authorityBase
134
+ * @property {string} authoritySeed
135
+ * @property {PublicKey} authorityOwner
136
+ * @property {PublicKey} newAuthorizedPubkey
137
+ * @property {StakeAuthorizationType} stakeAuthorizationType
138
+ */
139
+ export type AuthorizeWithSeedStakeParams = {|
140
+ stakePubkey: PublicKey,
141
+ authorityBase: PublicKey,
142
+ authoritySeed: string,
143
+ authorityOwner: PublicKey,
144
+ newAuthorizedPubkey: PublicKey,
145
+ stakeAuthorizationType: StakeAuthorizationType,
146
+ |}
147
+
148
+ /**
149
+ * Split stake instruction params
150
+ * @typedef {Object} SplitStakeParams
151
+ * @property {PublicKey} stakePubkey
152
+ * @property {PublicKey} authorizedPubkey
153
+ * @property {PublicKey} splitStakePubkey
154
+ * @property {number} lamports
155
+ */
156
+ export type SplitStakeParams = {|
157
+ stakePubkey: PublicKey,
158
+ authorizedPubkey: PublicKey,
159
+ splitStakePubkey: PublicKey,
160
+ lamports: number,
161
+ |}
162
+
163
+ /**
164
+ * Withdraw stake instruction params
165
+ * @typedef {Object} WithdrawStakeParams
166
+ * @property {PublicKey} stakePubkey
167
+ * @property {PublicKey} authorizedPubkey
168
+ * @property {PublicKey} toPubkey
169
+ * @property {number} lamports
170
+ */
171
+ export type WithdrawStakeParams = {|
172
+ stakePubkey: PublicKey,
173
+ authorizedPubkey: PublicKey,
174
+ toPubkey: PublicKey,
175
+ lamports: number,
176
+ |}
177
+
178
+ /**
179
+ * Deactivate stake instruction params
180
+ * @typedef {Object} DeactivateStakeParams
181
+ * @property {PublicKey} stakePubkey
182
+ * @property {PublicKey} authorizedPubkey
183
+ */
184
+ export type DeactivateStakeParams = {|
185
+ stakePubkey: PublicKey,
186
+ authorizedPubkey: PublicKey,
187
+ |}
188
+
189
+ /**
190
+ * An enumeration of valid stake InstructionType's
191
+ */
192
+ export const STAKE_INSTRUCTION_LAYOUTS = Object.freeze({
193
+ Initialize: {
194
+ index: 0,
195
+ layout: BufferLayout.struct([
196
+ BufferLayout.u32('instruction'),
197
+ Layout.authorized(),
198
+ Layout.lockup(),
199
+ ]),
200
+ },
201
+ Authorize: {
202
+ index: 1,
203
+ layout: BufferLayout.struct([
204
+ BufferLayout.u32('instruction'),
205
+ Layout.publicKey('newAuthorized'),
206
+ BufferLayout.u32('stakeAuthorizationType'),
207
+ ]),
208
+ },
209
+ Delegate: {
210
+ index: 2,
211
+ layout: BufferLayout.struct([BufferLayout.u32('instruction')]),
212
+ },
213
+ Split: {
214
+ index: 3,
215
+ layout: BufferLayout.struct([BufferLayout.u32('instruction'), BufferLayout.ns64('lamports')]),
216
+ },
217
+ Withdraw: {
218
+ index: 4,
219
+ layout: BufferLayout.struct([BufferLayout.u32('instruction'), BufferLayout.ns64('lamports')]),
220
+ },
221
+ Deactivate: {
222
+ index: 5,
223
+ layout: BufferLayout.struct([BufferLayout.u32('instruction')]),
224
+ },
225
+ AuthorizeWithSeed: {
226
+ index: 8,
227
+ layout: BufferLayout.struct([
228
+ BufferLayout.u32('instruction'),
229
+ Layout.publicKey('newAuthorized'),
230
+ BufferLayout.u32('stakeAuthorizationType'),
231
+ Layout.rustString('authoritySeed'),
232
+ Layout.publicKey('authorityOwner'),
233
+ ]),
234
+ },
235
+ })
236
+
237
+ /**
238
+ * Stake Instruction class
239
+ */
240
+ export class StakeInstruction {
241
+ /**
242
+ * Decode a stake instruction and retrieve the instruction type.
243
+ */
244
+ static decodeInstructionType(instruction: TransactionInstruction) {
245
+ this.checkProgramId(instruction.programId)
246
+
247
+ const instructionTypeLayout = BufferLayout.u32('instruction')
248
+ const typeIndex = instructionTypeLayout.decode(instruction.data)
249
+
250
+ let type
251
+ for (const t of Object.keys(STAKE_INSTRUCTION_LAYOUTS)) {
252
+ if (STAKE_INSTRUCTION_LAYOUTS[t].index === typeIndex) {
253
+ type = t
254
+ }
255
+ }
256
+
257
+ if (!type) {
258
+ throw new Error('Instruction type incorrect; not a StakeInstruction')
259
+ }
260
+
261
+ return type
262
+ }
263
+
264
+ /**
265
+ * Decode a initialize stake instruction and retrieve the instruction params.
266
+ */
267
+ static decodeInitialize(instruction: TransactionInstruction): InitializeStakeParams {
268
+ this.checkProgramId(instruction.programId)
269
+ this.checkKeyLength(instruction.keys, 2)
270
+
271
+ const { authorized, lockup } = decodeData(
272
+ STAKE_INSTRUCTION_LAYOUTS.Initialize,
273
+ instruction.data
274
+ )
275
+
276
+ return {
277
+ stakePubkey: instruction.keys[0].pubkey,
278
+ authorized: new Authorized(
279
+ new PublicKey(authorized.staker),
280
+ new PublicKey(authorized.withdrawer)
281
+ ),
282
+ lockup: new Lockup(lockup.unixTimestamp, lockup.epoch, new PublicKey(lockup.custodian)),
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Decode a delegate stake instruction and retrieve the instruction params.
288
+ */
289
+ static decodeDelegate(instruction: TransactionInstruction): DelegateStakeParams {
290
+ this.checkProgramId(instruction.programId)
291
+ this.checkKeyLength(instruction.keys, 6)
292
+ decodeData(STAKE_INSTRUCTION_LAYOUTS.Delegate, instruction.data)
293
+
294
+ return {
295
+ stakePubkey: instruction.keys[0].pubkey,
296
+ votePubkey: instruction.keys[1].pubkey,
297
+ authorizedPubkey: instruction.keys[5].pubkey,
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Decode a withdraw stake instruction and retrieve the instruction params.
303
+ */
304
+ static decodeWithdraw(instruction: TransactionInstruction): WithdrawStakeParams {
305
+ this.checkProgramId(instruction.programId)
306
+ this.checkKeyLength(instruction.keys, 5)
307
+ const { lamports } = decodeData(STAKE_INSTRUCTION_LAYOUTS.Withdraw, instruction.data)
308
+
309
+ return {
310
+ stakePubkey: instruction.keys[0].pubkey,
311
+ toPubkey: instruction.keys[1].pubkey,
312
+ authorizedPubkey: instruction.keys[4].pubkey,
313
+ lamports,
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Decode a deactivate stake instruction and retrieve the instruction params.
319
+ */
320
+ static decodeDeactivate(instruction: TransactionInstruction): DeactivateStakeParams {
321
+ this.checkProgramId(instruction.programId)
322
+ this.checkKeyLength(instruction.keys, 3)
323
+ decodeData(STAKE_INSTRUCTION_LAYOUTS.Deactivate, instruction.data)
324
+
325
+ return {
326
+ stakePubkey: instruction.keys[0].pubkey,
327
+ authorizedPubkey: instruction.keys[2].pubkey,
328
+ }
329
+ }
330
+
331
+ /**
332
+ * @private
333
+ */
334
+ static checkProgramId(programId: PublicKey) {
335
+ if (!programId.equals(StakeProgram.programId)) {
336
+ throw new Error('invalid instruction; programId is not StakeProgram')
337
+ }
338
+ }
339
+
340
+ /**
341
+ * @private
342
+ */
343
+ static checkKeyLength(keys: Array<any>, expectedLength: number) {
344
+ if (keys.length < expectedLength) {
345
+ throw new Error(
346
+ `invalid instruction; found ${keys.length} keys, expected at least ${expectedLength}`
347
+ )
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * An enumeration of valid StakeAuthorizationLayout's
354
+ */
355
+ export const StakeAuthorizationLayout = Object.freeze({
356
+ Staker: {
357
+ index: 0,
358
+ },
359
+ Withdrawer: {
360
+ index: 1,
361
+ },
362
+ })
363
+
364
+ /**
365
+ * Factory class for transactions to interact with the Stake program
366
+ */
367
+ export class StakeProgram {
368
+ /**
369
+ * Public key that identifies the Stake program
370
+ */
371
+ static get programId(): PublicKey {
372
+ return new PublicKey('Stake11111111111111111111111111111111111111')
373
+ }
374
+
375
+ /**
376
+ * Max space of a Stake account
377
+ *
378
+ * This is generated from the solana-stake-program StakeState struct as
379
+ * `std::mem::size_of::<StakeState>()`:
380
+ * https://docs.rs/solana-stake-program/1.4.4/solana_stake_program/stake_state/enum.StakeState.html
381
+ */
382
+ static get space(): number {
383
+ return 200
384
+ }
385
+
386
+ /**
387
+ * Generate an Initialize instruction to add to a Stake Create transaction
388
+ */
389
+ static initialize(params: InitializeStakeParams): TransactionInstruction {
390
+ const { stakePubkey, authorized, lockup } = params
391
+ const type = STAKE_INSTRUCTION_LAYOUTS.Initialize
392
+ const data = encodeData(type, {
393
+ authorized: {
394
+ staker: authorized.staker.toBuffer(),
395
+ withdrawer: authorized.withdrawer.toBuffer(),
396
+ },
397
+ lockup: {
398
+ unixTimestamp: lockup.unixTimestamp,
399
+ epoch: lockup.epoch,
400
+ custodian: lockup.custodian.toBuffer(),
401
+ },
402
+ })
403
+ const instructionData = {
404
+ keys: [
405
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
406
+ { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
407
+ ],
408
+ programId: this.programId,
409
+ data,
410
+ }
411
+ return new TransactionInstruction(instructionData)
412
+ }
413
+
414
+ /**
415
+ * Generate a Transaction that creates a new Stake account at
416
+ * an address generated with `from`, a seed, and the Stake programId
417
+ */
418
+ static createAccountWithSeed(params: CreateStakeAccountWithSeedParams): Transaction {
419
+ const transaction = new Transaction()
420
+ transaction.add(
421
+ SystemProgram.createAccountWithSeed({
422
+ fromPubkey: params.fromPubkey,
423
+ newAccountPubkey: params.stakePubkey,
424
+ basePubkey: params.basePubkey,
425
+ seed: params.seed,
426
+ lamports: params.lamports,
427
+ space: this.space,
428
+ programId: this.programId,
429
+ })
430
+ )
431
+
432
+ const { stakePubkey, authorized, lockup } = params
433
+ return transaction.add(this.initialize({ stakePubkey, authorized, lockup }))
434
+ }
435
+
436
+ /**
437
+ * Generate a Transaction that creates a new Stake account
438
+ */
439
+ static createAccount(params: CreateStakeAccountParams): Transaction {
440
+ const transaction = new Transaction()
441
+ transaction.add(
442
+ SystemProgram.createAccount({
443
+ fromPubkey: params.fromPubkey,
444
+ newAccountPubkey: params.stakePubkey,
445
+ lamports: params.lamports,
446
+ space: this.space,
447
+ programId: this.programId,
448
+ })
449
+ )
450
+
451
+ const { stakePubkey, authorized, lockup } = params
452
+ return transaction.add(this.initialize({ stakePubkey, authorized, lockup }))
453
+ }
454
+
455
+ /**
456
+ * Generate a Transaction that delegates Stake tokens to a validator
457
+ * Vote PublicKey. This transaction can also be used to redelegate Stake
458
+ * to a new validator Vote PublicKey.
459
+ */
460
+ static delegate(params: DelegateStakeParams): Transaction {
461
+ const { stakePubkey, authorizedPubkey, votePubkey } = params
462
+
463
+ const type = STAKE_INSTRUCTION_LAYOUTS.Delegate
464
+ const data = encodeData(type)
465
+
466
+ return new Transaction().add({
467
+ keys: [
468
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
469
+ { pubkey: votePubkey, isSigner: false, isWritable: false },
470
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
471
+ {
472
+ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY,
473
+ isSigner: false,
474
+ isWritable: false,
475
+ },
476
+ { pubkey: STAKE_CONFIG_ID, isSigner: false, isWritable: false },
477
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
478
+ ],
479
+ programId: this.programId,
480
+ data,
481
+ })
482
+ }
483
+
484
+ /**
485
+ * Generate a Transaction that withdraws deactivated Stake tokens.
486
+ */
487
+ static withdraw(params: WithdrawStakeParams): Transaction {
488
+ const { stakePubkey, authorizedPubkey, toPubkey, lamports } = params
489
+ const type = STAKE_INSTRUCTION_LAYOUTS.Withdraw
490
+ const data = encodeData(type, { lamports })
491
+
492
+ return new Transaction().add({
493
+ keys: [
494
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
495
+ { pubkey: toPubkey, isSigner: false, isWritable: true },
496
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
497
+ {
498
+ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY,
499
+ isSigner: false,
500
+ isWritable: false,
501
+ },
502
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
503
+ ],
504
+ programId: this.programId,
505
+ data,
506
+ })
507
+ }
508
+
509
+ /**
510
+ * Generate a Transaction that deactivates Stake tokens.
511
+ */
512
+ static deactivate(params: DeactivateStakeParams): Transaction {
513
+ const { stakePubkey, authorizedPubkey } = params
514
+ const type = STAKE_INSTRUCTION_LAYOUTS.Deactivate
515
+ const data = encodeData(type)
516
+
517
+ return new Transaction().add({
518
+ keys: [
519
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
520
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
521
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
522
+ ],
523
+ programId: this.programId,
524
+ data,
525
+ })
526
+ }
527
+ }