@exodus/solana-lib 1.2.4 → 1.2.7

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.4",
3
+ "version": "1.2.7",
4
4
  "description": "Exodus internal Solana low-level library",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@exodus/asset-lib": "^3.4.3",
17
- "@exodus/assets": "^8.0.0",
17
+ "@exodus/assets": "^8.0.2",
18
18
  "@exodus/buffer-layout": "^1.2.0-exodus1",
19
19
  "@exodus/models": "^8.4.0",
20
20
  "bn.js": "~4.11.0",
@@ -22,5 +22,5 @@
22
22
  "create-hash": "~1.1.3",
23
23
  "tweetnacl": "^1.0.3"
24
24
  },
25
- "gitHead": "f5a2d444a2789ca118671566f32805dba061ce5a"
25
+ "gitHead": "892c55db784616256e065a2c931a5e5611f5404f"
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
+ }
@@ -52,17 +52,11 @@ function createIx(
52
52
  })
53
53
  }
54
54
 
55
- // https://github.com/paul-schaaf/spl-token-ui/blob/main/src/solana/token/editing.ts#L207
56
- export const createTokenTransferInstruction = (
57
- owner,
58
- fromTokenAddress,
59
- tokenMintAddress,
60
- to,
61
- amount
62
- ) => {
55
+ // https://github.com/paul-schaaf/spl-token-ui/blob/main/src/solana/token/editing.ts#L211
56
+ export const createTokenTransferInstruction = (owner, fromTokenAddress, to, amount) => {
63
57
  const sourcePubkey = new PublicKey(fromTokenAddress) // the token ADDRESS needed!
64
- const destinationPubkey = new PublicKey(findAssociatedTokenAddress(to, tokenMintAddress))
65
- console.log(`destination associatedTokenAddress: ${destinationPubkey.toBase58()}`)
58
+ const destinationPubkey = new PublicKey(to)
59
+ console.log(`destination token address: ${destinationPubkey.toBase58()}`)
66
60
  const ownerAccountOrWalletPublicKey = new PublicKey(owner) // the only native SOL address
67
61
 
68
62
  const transferIx = Token.createTransferInstruction(
package/src/tokens.js CHANGED
@@ -1,26 +1,19 @@
1
- // reference https://github.com/project-serum/spl-token-wallet/blob/master/src/utils/tokens/names.js
1
+ // adapted from https://github.com/project-serum/spl-token-wallet/blob/master/src/utils/tokens/names.js
2
2
 
3
3
  const TOKENS = [
4
4
  {
5
5
  mintAddress: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt',
6
- tokenName: 'Serum',
6
+ tokenName: 'serum',
7
+ tokenProperName: 'Serum',
7
8
  tokenSymbol: 'SRM',
8
9
  },
9
10
  {
10
- tokenSymbol: 'BTC',
11
- mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E',
12
- tokenName: 'Wrapped Bitcoin',
13
- icon:
14
- 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/bitcoin/info/logo.png',
15
- },
16
- {
17
- tokenSymbol: 'USDC',
18
11
  mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
19
- tokenName: 'USD Coin',
20
- icon:
21
- 'https://raw.githubusercontent.com/trustwallet/assets/f3ffd0b9ae2165336279ce2f8db1981a55ce30f8/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
12
+ tokenName: 'usdcoin_solana',
13
+ tokenProperName: 'USD Coin Solana',
14
+ tokenSymbol: 'USDC Solana',
22
15
  },
23
- // ... TODO: do we want to add decimals for every token? (Serum SRM is 6)
16
+ // ....
24
17
  ]
25
18
 
26
19
  export default TOKENS
@@ -2,11 +2,21 @@
2
2
  import assert from 'assert'
3
3
 
4
4
  import { getKeyPairFromPrivateKey } from './keypair'
5
+ import { findAssociatedTokenAddress, createStakeAddress } from './encode'
5
6
  import {
6
7
  createAssociatedTokenAccount,
7
8
  createTokenTransferInstruction,
8
9
  } from './helpers/tokenTransfer'
9
- 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'
10
20
  import TOKENS from './tokens'
11
21
  import bs58 from 'bs58'
12
22
 
@@ -18,7 +28,8 @@ class Tx {
18
28
  recentBlockhash,
19
29
  // fee, // (Fee per Signature: 5000 lamports)
20
30
  // Tokens related:
21
- tokenTicker,
31
+ tokenName,
32
+ destinationAddressType,
22
33
  isAssociatedTokenAccountActive, // true when recipient balance !== 0
23
34
  fromTokenAddresses, // sender token addresses
24
35
  } = {}) {
@@ -27,7 +38,11 @@ class Tx {
27
38
  assert(amount, 'amount is required')
28
39
  assert(typeof amount === 'number', 'amount must be a number')
29
40
  assert(recentBlockhash, 'recentBlockhash is required')
30
- if (tokenTicker) {
41
+ if (tokenName) {
42
+ assert(
43
+ typeof destinationAddressType !== 'undefined',
44
+ 'destinationAddressType is required when sending tokens'
45
+ )
31
46
  assert(
32
47
  typeof isAssociatedTokenAccountActive !== 'undefined',
33
48
  'isAssociatedTokenAccountActive is required when sending tokens'
@@ -35,13 +50,14 @@ class Tx {
35
50
  assert(Array.isArray(fromTokenAddresses), 'fromTokenAddresses Array is required')
36
51
  }
37
52
 
38
- const token = TOKENS.find(({ tokenSymbol }) => tokenSymbol === tokenTicker)
53
+ const token = TOKENS.find(({ tokenName: name }) => name === tokenName)
39
54
  this.txObj = {
40
55
  from,
41
56
  to,
42
57
  amount,
43
58
  recentBlockhash,
44
59
  token,
60
+ destinationAddressType,
45
61
  isAssociatedTokenAccountActive,
46
62
  fromTokenAddresses,
47
63
  }
@@ -74,14 +90,18 @@ class Tx {
74
90
  amount,
75
91
  recentBlockhash,
76
92
  token,
93
+ destinationAddressType,
77
94
  isAssociatedTokenAccountActive,
78
95
  fromTokenAddresses,
79
96
  }) {
80
97
  this.transaction = new Transaction({
81
98
  recentBlockhash,
82
99
  })
100
+ const isUnknown = destinationAddressType === null
101
+ const isSOLaddress = destinationAddressType === 'solana'
83
102
  // crete account instruction
84
- if (!isAssociatedTokenAccountActive)
103
+ if (isUnknown) throw new Error('Destination SOL balance cannot be zero (address not active)') // cannot initialize without knowing the owner
104
+ if (isSOLaddress && !isAssociatedTokenAccountActive)
85
105
  this.transaction.add(createAssociatedTokenAccount(from, token.mintAddress, to))
86
106
 
87
107
  let amountLeft = amount
@@ -100,28 +120,47 @@ class Tx {
100
120
  amountLeft -= amountToSend
101
121
  }
102
122
 
123
+ const dest = isSOLaddress ? findAssociatedTokenAddress(to, token.mintAddress) : to
103
124
  // add transfer token instruction
104
125
  this.transaction.add(
105
- createTokenTransferInstruction(
106
- from,
107
- tokenAccountAddress,
108
- token.mintAddress,
109
- to,
110
- amountToSend
111
- )
126
+ createTokenTransferInstruction(from, tokenAccountAddress, dest, amountToSend)
112
127
  )
113
128
  }
114
129
 
115
130
  assert(amountLeft === 0, `Not enough balance to send ${amount} ${token.tokenSymbol}`)
116
131
  }
117
132
 
118
- static sign(tx: Tx, privateKey: Buffer | string) {
133
+ static createStakeAccountTransaction({ address, amount, seed = SEED, recentBlockhash }) {
134
+ const fromPubkey = new PublicKey(address)
135
+ const stakeAddress = createStakeAddress(address, seed)
136
+ const stakePublicKey = new PublicKey(stakeAddress)
137
+
138
+ const authorized = new Authorized(fromPubkey, fromPubkey) // staker, withdrawer
139
+ const lockup = new Lockup(0, 0, fromPubkey) // no lockup
140
+
141
+ const transaction = StakeProgram.createAccountWithSeed({
142
+ fromPubkey,
143
+ stakePubkey: stakePublicKey,
144
+ basePubkey: fromPubkey,
145
+ seed,
146
+ authorized,
147
+ lockup,
148
+ lamports: amount, // number
149
+ })
150
+ transaction.recentBlockhash = recentBlockhash
151
+ return transaction
152
+ }
153
+
154
+ static sign(tx, privateKey: Buffer | string) {
119
155
  if (!privateKey) throw new Error('Please provide a secretKey')
120
156
  const { secretKey } = getKeyPairFromPrivateKey(privateKey)
121
157
  const signers = [new Account(secretKey)]
122
158
 
123
- tx.transaction.sign(...signers)
124
- if (!tx.transaction.signature) {
159
+ let transaction = tx
160
+ if (tx instanceof Tx) transaction = tx.transaction
161
+
162
+ transaction.sign(...signers)
163
+ if (!transaction.signature) {
125
164
  throw new Error('!signature') // should never happen
126
165
  }
127
166
  }
@@ -131,6 +170,12 @@ class Tx {
131
170
  return wireTransaction.toString('base64')
132
171
  }
133
172
 
173
+ static serialize(tx) {
174
+ let transaction = tx
175
+ if (tx instanceof Tx) transaction = tx.transaction
176
+ return transaction.serialize().toString('base64')
177
+ }
178
+
134
179
  getTxId() {
135
180
  if (!this.transaction.signature) {
136
181
  throw new Error('Cannot get txId, tx is not signed')
@@ -6,6 +6,11 @@ export function createUnsignedTx({
6
6
  to,
7
7
  amount,
8
8
  recentBlockhash,
9
+ // Tokens related:
10
+ tokenName,
11
+ destinationAddressType,
12
+ isAssociatedTokenAccountActive, // true when recipient balance !== 0
13
+ fromTokenAddresses, // sender token addresses
9
14
  }: ParsedTransaction): UnsignedTransaction {
10
15
  return {
11
16
  txData: {
@@ -13,6 +18,11 @@ export function createUnsignedTx({
13
18
  to,
14
19
  amount: amount.toBase().toNumber(),
15
20
  recentBlockhash,
21
+ // Tokens related:
22
+ tokenName,
23
+ destinationAddressType,
24
+ isAssociatedTokenAccountActive,
25
+ fromTokenAddresses,
16
26
  },
17
27
  txMeta: {
18
28
  assetName: asset.name,
@@ -3,7 +3,16 @@ import type { UnsignedTransaction, ParsedTransaction } from '@exodus/models/lib/
3
3
 
4
4
  export function parseUnsignedTx(unsignedTx: UnsignedTransaction): ParsedTransaction {
5
5
  const asset = assets.solana
6
- const { from, to, recentBlockhash, ...txData } = unsignedTx.txData
6
+ const {
7
+ from,
8
+ to,
9
+ recentBlockhash,
10
+ tokenName,
11
+ destinationAddressType,
12
+ isAssociatedTokenAccountActive,
13
+ fromTokenAddresses,
14
+ ...txData
15
+ } = unsignedTx.txData
7
16
 
8
17
  const amount = asset.currency.baseUnit(txData.amount)
9
18
  return {
@@ -12,5 +21,10 @@ export function parseUnsignedTx(unsignedTx: UnsignedTransaction): ParsedTransact
12
21
  to,
13
22
  amount,
14
23
  recentBlockhash,
24
+ // token related
25
+ tokenName,
26
+ destinationAddressType,
27
+ isAssociatedTokenAccountActive,
28
+ fromTokenAddresses,
15
29
  }
16
30
  }
@@ -6,7 +6,16 @@ export function signUnsignedTx(
6
6
  unsignedTx: UnsignedTransaction,
7
7
  privateKey: Buffer
8
8
  ): SignedTransaction {
9
- const { from, to, amount, recentBlockhash } = unsignedTx.txData
9
+ const {
10
+ from,
11
+ to,
12
+ amount,
13
+ recentBlockhash,
14
+ tokenName,
15
+ destinationAddressType,
16
+ isAssociatedTokenAccountActive,
17
+ fromTokenAddresses,
18
+ } = unsignedTx.txData
10
19
 
11
20
  const asset = assets.solana
12
21
  const tx = new Transaction({
@@ -14,6 +23,10 @@ export function signUnsignedTx(
14
23
  to,
15
24
  amount: asset.currency.baseUnit(amount).toNumber(),
16
25
  recentBlockhash,
26
+ tokenName,
27
+ destinationAddressType,
28
+ isAssociatedTokenAccountActive,
29
+ fromTokenAddresses,
17
30
  })
18
31
 
19
32
  // sign plain tx
@@ -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,675 @@
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 an authorize stake instruction and retrieve the instruction params.
303
+ */
304
+ static decodeAuthorize(instruction: TransactionInstruction): AuthorizeStakeParams {
305
+ this.checkProgramId(instruction.programId)
306
+ this.checkKeyLength(instruction.keys, 3)
307
+ const { newAuthorized, stakeAuthorizationType } = decodeData(
308
+ STAKE_INSTRUCTION_LAYOUTS.Authorize,
309
+ instruction.data
310
+ )
311
+
312
+ return {
313
+ stakePubkey: instruction.keys[0].pubkey,
314
+ authorizedPubkey: instruction.keys[2].pubkey,
315
+ newAuthorizedPubkey: new PublicKey(newAuthorized),
316
+ stakeAuthorizationType: {
317
+ index: stakeAuthorizationType,
318
+ },
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Decode an authorize-with-seed stake instruction and retrieve the instruction params.
324
+ */
325
+ static decodeAuthorizeWithSeed(
326
+ instruction: TransactionInstruction
327
+ ): AuthorizeWithSeedStakeParams {
328
+ this.checkProgramId(instruction.programId)
329
+ this.checkKeyLength(instruction.keys, 2)
330
+ const { newAuthorized, stakeAuthorizationType, authoritySeed, authorityOwner } = decodeData(
331
+ STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed,
332
+ instruction.data
333
+ )
334
+
335
+ return {
336
+ stakePubkey: instruction.keys[0].pubkey,
337
+ authorityBase: instruction.keys[1].pubkey,
338
+ authoritySeed: authoritySeed,
339
+ authorityOwner: new PublicKey(authorityOwner),
340
+ newAuthorizedPubkey: new PublicKey(newAuthorized),
341
+ stakeAuthorizationType: {
342
+ index: stakeAuthorizationType,
343
+ },
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Decode a split stake instruction and retrieve the instruction params.
349
+ */
350
+ static decodeSplit(instruction: TransactionInstruction): SplitStakeParams {
351
+ this.checkProgramId(instruction.programId)
352
+ this.checkKeyLength(instruction.keys, 3)
353
+ const { lamports } = decodeData(STAKE_INSTRUCTION_LAYOUTS.Split, instruction.data)
354
+
355
+ return {
356
+ stakePubkey: instruction.keys[0].pubkey,
357
+ splitStakePubkey: instruction.keys[1].pubkey,
358
+ authorizedPubkey: instruction.keys[2].pubkey,
359
+ lamports,
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Decode a withdraw stake instruction and retrieve the instruction params.
365
+ */
366
+ static decodeWithdraw(instruction: TransactionInstruction): WithdrawStakeParams {
367
+ this.checkProgramId(instruction.programId)
368
+ this.checkKeyLength(instruction.keys, 5)
369
+ const { lamports } = decodeData(STAKE_INSTRUCTION_LAYOUTS.Withdraw, instruction.data)
370
+
371
+ return {
372
+ stakePubkey: instruction.keys[0].pubkey,
373
+ toPubkey: instruction.keys[1].pubkey,
374
+ authorizedPubkey: instruction.keys[4].pubkey,
375
+ lamports,
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Decode a deactivate stake instruction and retrieve the instruction params.
381
+ */
382
+ static decodeDeactivate(instruction: TransactionInstruction): DeactivateStakeParams {
383
+ this.checkProgramId(instruction.programId)
384
+ this.checkKeyLength(instruction.keys, 3)
385
+ decodeData(STAKE_INSTRUCTION_LAYOUTS.Deactivate, instruction.data)
386
+
387
+ return {
388
+ stakePubkey: instruction.keys[0].pubkey,
389
+ authorizedPubkey: instruction.keys[2].pubkey,
390
+ }
391
+ }
392
+
393
+ /**
394
+ * @private
395
+ */
396
+ static checkProgramId(programId: PublicKey) {
397
+ if (!programId.equals(StakeProgram.programId)) {
398
+ throw new Error('invalid instruction; programId is not StakeProgram')
399
+ }
400
+ }
401
+
402
+ /**
403
+ * @private
404
+ */
405
+ static checkKeyLength(keys: Array<any>, expectedLength: number) {
406
+ if (keys.length < expectedLength) {
407
+ throw new Error(
408
+ `invalid instruction; found ${keys.length} keys, expected at least ${expectedLength}`
409
+ )
410
+ }
411
+ }
412
+ }
413
+
414
+ /**
415
+ * An enumeration of valid StakeAuthorizationLayout's
416
+ */
417
+ export const StakeAuthorizationLayout = Object.freeze({
418
+ Staker: {
419
+ index: 0,
420
+ },
421
+ Withdrawer: {
422
+ index: 1,
423
+ },
424
+ })
425
+
426
+ /**
427
+ * Factory class for transactions to interact with the Stake program
428
+ */
429
+ export class StakeProgram {
430
+ /**
431
+ * Public key that identifies the Stake program
432
+ */
433
+ static get programId(): PublicKey {
434
+ return new PublicKey('Stake11111111111111111111111111111111111111')
435
+ }
436
+
437
+ /**
438
+ * Max space of a Stake account
439
+ *
440
+ * This is generated from the solana-stake-program StakeState struct as
441
+ * `std::mem::size_of::<StakeState>()`:
442
+ * https://docs.rs/solana-stake-program/1.4.4/solana_stake_program/stake_state/enum.StakeState.html
443
+ */
444
+ static get space(): number {
445
+ return 200
446
+ }
447
+
448
+ /**
449
+ * Generate an Initialize instruction to add to a Stake Create transaction
450
+ */
451
+ static initialize(params: InitializeStakeParams): TransactionInstruction {
452
+ const { stakePubkey, authorized, lockup } = params
453
+ const type = STAKE_INSTRUCTION_LAYOUTS.Initialize
454
+ const data = encodeData(type, {
455
+ authorized: {
456
+ staker: authorized.staker.toBuffer(),
457
+ withdrawer: authorized.withdrawer.toBuffer(),
458
+ },
459
+ lockup: {
460
+ unixTimestamp: lockup.unixTimestamp,
461
+ epoch: lockup.epoch,
462
+ custodian: lockup.custodian.toBuffer(),
463
+ },
464
+ })
465
+ const instructionData = {
466
+ keys: [
467
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
468
+ { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
469
+ ],
470
+ programId: this.programId,
471
+ data,
472
+ }
473
+ return new TransactionInstruction(instructionData)
474
+ }
475
+
476
+ /**
477
+ * Generate a Transaction that creates a new Stake account at
478
+ * an address generated with `from`, a seed, and the Stake programId
479
+ */
480
+ static createAccountWithSeed(params: CreateStakeAccountWithSeedParams): Transaction {
481
+ const transaction = new Transaction()
482
+ transaction.add(
483
+ SystemProgram.createAccountWithSeed({
484
+ fromPubkey: params.fromPubkey,
485
+ newAccountPubkey: params.stakePubkey,
486
+ basePubkey: params.basePubkey,
487
+ seed: params.seed,
488
+ lamports: params.lamports,
489
+ space: this.space,
490
+ programId: this.programId,
491
+ })
492
+ )
493
+
494
+ const { stakePubkey, authorized, lockup } = params
495
+ return transaction.add(this.initialize({ stakePubkey, authorized, lockup }))
496
+ }
497
+
498
+ /**
499
+ * Generate a Transaction that creates a new Stake account
500
+ */
501
+ static createAccount(params: CreateStakeAccountParams): Transaction {
502
+ const transaction = new Transaction()
503
+ transaction.add(
504
+ SystemProgram.createAccount({
505
+ fromPubkey: params.fromPubkey,
506
+ newAccountPubkey: params.stakePubkey,
507
+ lamports: params.lamports,
508
+ space: this.space,
509
+ programId: this.programId,
510
+ })
511
+ )
512
+
513
+ const { stakePubkey, authorized, lockup } = params
514
+ return transaction.add(this.initialize({ stakePubkey, authorized, lockup }))
515
+ }
516
+
517
+ /**
518
+ * Generate a Transaction that delegates Stake tokens to a validator
519
+ * Vote PublicKey. This transaction can also be used to redelegate Stake
520
+ * to a new validator Vote PublicKey.
521
+ */
522
+ static delegate(params: DelegateStakeParams): Transaction {
523
+ const { stakePubkey, authorizedPubkey, votePubkey } = params
524
+
525
+ const type = STAKE_INSTRUCTION_LAYOUTS.Delegate
526
+ const data = encodeData(type)
527
+
528
+ return new Transaction().add({
529
+ keys: [
530
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
531
+ { pubkey: votePubkey, isSigner: false, isWritable: false },
532
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
533
+ {
534
+ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY,
535
+ isSigner: false,
536
+ isWritable: false,
537
+ },
538
+ { pubkey: STAKE_CONFIG_ID, isSigner: false, isWritable: false },
539
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
540
+ ],
541
+ programId: this.programId,
542
+ data,
543
+ })
544
+ }
545
+
546
+ /**
547
+ * Generate a Transaction that authorizes a new PublicKey as Staker
548
+ * or Withdrawer on the Stake account.
549
+ */
550
+ static authorize(params: AuthorizeStakeParams): Transaction {
551
+ const { stakePubkey, authorizedPubkey, newAuthorizedPubkey, stakeAuthorizationType } = params
552
+
553
+ const type = STAKE_INSTRUCTION_LAYOUTS.Authorize
554
+ const data = encodeData(type, {
555
+ newAuthorized: newAuthorizedPubkey.toBuffer(),
556
+ stakeAuthorizationType: stakeAuthorizationType.index,
557
+ })
558
+
559
+ return new Transaction().add({
560
+ keys: [
561
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
562
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: true },
563
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
564
+ ],
565
+ programId: this.programId,
566
+ data,
567
+ })
568
+ }
569
+
570
+ /**
571
+ * Generate a Transaction that authorizes a new PublicKey as Staker
572
+ * or Withdrawer on the Stake account.
573
+ */
574
+ static authorizeWithSeed(params: AuthorizeWithSeedStakeParams): Transaction {
575
+ const {
576
+ stakePubkey,
577
+ authorityBase,
578
+ authoritySeed,
579
+ authorityOwner,
580
+ newAuthorizedPubkey,
581
+ stakeAuthorizationType,
582
+ } = params
583
+
584
+ const type = STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed
585
+ const data = encodeData(type, {
586
+ newAuthorized: newAuthorizedPubkey.toBuffer(),
587
+ stakeAuthorizationType: stakeAuthorizationType.index,
588
+ authoritySeed: authoritySeed,
589
+ authorityOwner: authorityOwner.toBuffer(),
590
+ })
591
+
592
+ return new Transaction().add({
593
+ keys: [
594
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
595
+ { pubkey: authorityBase, isSigner: true, isWritable: false },
596
+ ],
597
+ programId: this.programId,
598
+ data,
599
+ })
600
+ }
601
+
602
+ /**
603
+ * Generate a Transaction that splits Stake tokens into another stake account
604
+ */
605
+ static split(params: SplitStakeParams): Transaction {
606
+ const { stakePubkey, authorizedPubkey, splitStakePubkey, lamports } = params
607
+
608
+ const transaction = new Transaction()
609
+ transaction.add(
610
+ SystemProgram.createAccount({
611
+ fromPubkey: authorizedPubkey,
612
+ newAccountPubkey: splitStakePubkey,
613
+ lamports: 0,
614
+ space: this.space,
615
+ programId: this.programId,
616
+ })
617
+ )
618
+ const type = STAKE_INSTRUCTION_LAYOUTS.Split
619
+ const data = encodeData(type, { lamports })
620
+
621
+ return transaction.add({
622
+ keys: [
623
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
624
+ { pubkey: splitStakePubkey, isSigner: false, isWritable: true },
625
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
626
+ ],
627
+ programId: this.programId,
628
+ data,
629
+ })
630
+ }
631
+
632
+ /**
633
+ * Generate a Transaction that withdraws deactivated Stake tokens.
634
+ */
635
+ static withdraw(params: WithdrawStakeParams): Transaction {
636
+ const { stakePubkey, authorizedPubkey, toPubkey, lamports } = params
637
+ const type = STAKE_INSTRUCTION_LAYOUTS.Withdraw
638
+ const data = encodeData(type, { lamports })
639
+
640
+ return new Transaction().add({
641
+ keys: [
642
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
643
+ { pubkey: toPubkey, isSigner: false, isWritable: true },
644
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
645
+ {
646
+ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY,
647
+ isSigner: false,
648
+ isWritable: false,
649
+ },
650
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
651
+ ],
652
+ programId: this.programId,
653
+ data,
654
+ })
655
+ }
656
+
657
+ /**
658
+ * Generate a Transaction that deactivates Stake tokens.
659
+ */
660
+ static deactivate(params: DeactivateStakeParams): Transaction {
661
+ const { stakePubkey, authorizedPubkey } = params
662
+ const type = STAKE_INSTRUCTION_LAYOUTS.Deactivate
663
+ const data = encodeData(type)
664
+
665
+ return new Transaction().add({
666
+ keys: [
667
+ { pubkey: stakePubkey, isSigner: false, isWritable: true },
668
+ { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
669
+ { pubkey: authorizedPubkey, isSigner: true, isWritable: false },
670
+ ],
671
+ programId: this.programId,
672
+ data,
673
+ })
674
+ }
675
+ }
@@ -316,7 +316,9 @@ export class Transaction {
316
316
  })
317
317
 
318
318
  if (numRequiredSignatures !== this.signatures.length) {
319
- throw new Error('missing signer(s)')
319
+ throw new Error(
320
+ `missing signer(s) - required signatures: ${numRequiredSignatures}, got ${this.signatures.length}`
321
+ )
320
322
  }
321
323
 
322
324
  const accountKeys = signedKeys.concat(unsignedKeys)