@exodus/solana-lib 3.18.0 → 3.18.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.18.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.18.1...@exodus/solana-lib@3.18.2) (2025-12-16)
7
+
8
+ **Note:** Version bump only for package @exodus/solana-lib
9
+
10
+
11
+
12
+
13
+
14
+ ## [3.18.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.18.0...@exodus/solana-lib@3.18.1) (2025-12-15)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+
20
+ * fix: check SOL transactionBuffer type (#7111)
21
+
22
+
23
+
6
24
  ## [3.18.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.15.3...@exodus/solana-lib@3.18.0) (2025-11-26)
7
25
 
8
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-lib",
3
- "version": "3.18.0",
3
+ "version": "3.18.2",
4
4
  "description": "Solana utils, such as for cryptography, address encoding/decoding, transaction building, etc.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -47,5 +47,5 @@
47
47
  "type": "git",
48
48
  "url": "git+https://github.com/ExodusMovement/assets.git"
49
49
  },
50
- "gitHead": "4c60cc2d7f07229295b801d9f67a336d314711a0"
50
+ "gitHead": "e0c3dec90d1b6a5a1fac0ab54f3f7c551100ddb7"
51
51
  }
package/src/constants.js CHANGED
@@ -1,4 +1,10 @@
1
- import { PublicKey, StakeProgram, SystemProgram } from './vendor/index.js'
1
+ import {
2
+ PublicKey,
3
+ StakeProgram,
4
+ SystemProgram,
5
+ Token2022Program,
6
+ TokenProgram,
7
+ } from './vendor/index.js'
2
8
 
3
9
  export const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey(
4
10
  'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'
@@ -7,13 +13,13 @@ export const SYSTEM_PROGRAM_ID = SystemProgram.programId
7
13
 
8
14
  export const STAKE_PROGRAM_ID = StakeProgram.programId
9
15
 
10
- export const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
16
+ export const TOKEN_PROGRAM_ID = TokenProgram.programId
11
17
 
12
18
  export const COMPUTE_BUDGET_PROGRAM_ID = new PublicKey(
13
19
  'ComputeBudget111111111111111111111111111111'
14
20
  )
15
21
 
16
- export const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb')
22
+ export const TOKEN_2022_PROGRAM_ID = Token2022Program.programId
17
23
 
18
24
  export const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr')
19
25
 
package/src/tx/common.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { VersionedTransaction } from '@exodus/solana-web3.js'
2
2
  import base58 from 'bs58'
3
+ import assert from 'minimalistic-assert'
3
4
 
4
5
  export function isVersionedTransaction(tx) {
5
6
  return Number.isInteger(tx.version)
@@ -14,6 +15,7 @@ export function transactionToBase58(tx) {
14
15
  }
15
16
 
16
17
  export function deserializeTransaction(tx) {
18
+ assert(tx instanceof Uint8Array, 'tx must be a Buffer or Uint8Array')
17
19
  return VersionedTransaction.deserialize(tx)
18
20
  }
19
21
 
@@ -0,0 +1,28 @@
1
+ import bs58 from 'bs58'
2
+
3
+ export function getAccountKeys(message) {
4
+ // versioned: staticAccountKeys, legacy: accountKeys
5
+ return message.staticAccountKeys || message.accountKeys
6
+ }
7
+
8
+ export function normalizeInstruction(instruction) {
9
+ // legacy: bs58 string, versioned: Uint8Array
10
+ let normalizedData = instruction.data
11
+ if (typeof instruction.data === 'string') {
12
+ normalizedData = bs58.decode(instruction.data)
13
+ }
14
+
15
+ return {
16
+ programIdIndex: instruction.programIdIndex,
17
+ // versioned: accountKeyIndexes, legacy: accounts
18
+ accounts: instruction.accountKeyIndexes || instruction.accounts,
19
+ data: normalizedData,
20
+ }
21
+ }
22
+
23
+ export function getNormalizedInstructions(message) {
24
+ // versioned: compiledInstructions, legacy: instructions
25
+ const instructions = message.compiledInstructions || message.instructions || []
26
+
27
+ return instructions.map((instruction) => normalizeInstruction(instruction))
28
+ }
@@ -1,264 +1,217 @@
1
- import bs58 from 'bs58'
2
-
3
- import { SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
1
+ import { SYSTEM_PROGRAM_ID } from '../constants.js'
4
2
  import {
3
+ StakeInstruction,
5
4
  SYSTEM_INSTRUCTION_LAYOUTS,
6
5
  SystemInstruction,
7
- TOKEN_INSTRUCTION_LAYOUTS,
8
6
  TokenInstruction,
9
7
  } from '../vendor/index.js'
8
+ import { toBuffer } from '../vendor/utils/to-buffer.js'
10
9
  import { deserializeTransaction } from './common.js'
10
+ import { getAccountKeys, getNormalizedInstructions } from './instruction-normalizer.js'
11
11
 
12
- // Helper function to check if an instruction is a token transfer
13
- function isTokenTransferInstruction(instruction, accountKeys) {
14
- if (!instruction || instruction.programIdIndex === undefined) return false
15
-
16
- const programId = accountKeys[instruction.programIdIndex]
17
-
18
- if (!programId.equals(TOKEN_PROGRAM_ID) && !programId.equals(TOKEN_2022_PROGRAM_ID)) {
19
- return false
20
- }
21
-
22
- // Must have at least 3 accounts (from, to, authority, and potentially more for delegate transfers)
23
- if (!Array.isArray(instruction.accounts) || instruction.accounts.length < 3) {
24
- return false
25
- }
26
-
27
- // Check if the instruction data is transfer (0x03), transferChecked (0x0c/12),
28
- // or TransferFeeExtension (0x1a/26) with TransferCheckedWithFee sub-instruction
29
- // Note: Must handle both formats because this is called from isTokenTransfer()
30
- // which uses non-normalized instructions
31
- try {
32
- let data = instruction.data
33
- if (typeof data === 'string') {
34
- data = bs58.decode(data)
35
- }
12
+ const InstructionKind = {
13
+ TOKEN: 'token',
14
+ SYSTEM_TRANSFER: 'systemTransfer',
15
+ STAKE: 'stake',
16
+ }
36
17
 
37
- const instructionType = data[0]
18
+ // Get staked amount from CreateWithSeed instruction that created the stake account
19
+ function getStakedAmountFromCreateWithSeed(stakeAddress, instructions, accountKeys) {
20
+ for (const instruction of instructions) {
21
+ const programId = accountKeys[instruction.programIdIndex]
22
+ if (!programId.equals(SYSTEM_PROGRAM_ID)) continue
38
23
 
39
- // Accept Transfer, TransferChecked, and TransferFeeExtension
24
+ const buffer = toBuffer(instruction.data)
40
25
  if (
41
- instructionType !== TOKEN_INSTRUCTION_LAYOUTS.Transfer.index &&
42
- instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index &&
43
- instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index
26
+ buffer.length < 4 ||
27
+ buffer.readUInt32LE(0) !== SYSTEM_INSTRUCTION_LAYOUTS.CreateWithSeed.index
44
28
  ) {
45
- return false
29
+ continue
46
30
  }
47
31
 
48
- // For TransferFeeExtension, check if it's specifically TransferCheckedWithFee (sub-instruction 1)
49
- if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index) {
50
- if (data.length < 2) return false
51
- const subInstruction = data[1]
52
- if (subInstruction !== 1) return false // Only support TransferCheckedWithFee
53
- }
54
- } catch {
55
- return false
56
- }
32
+ // Check if this instruction created our stake account
33
+ const newAccountPubkey = accountKeys[instruction.accounts[1]].toBase58()
34
+ if (newAccountPubkey !== stakeAddress) continue
57
35
 
58
- return true
36
+ const { lamports } = SystemInstruction.decodeCreateWithSeedData(instruction.data)
37
+ return lamports
38
+ }
59
39
  }
60
40
 
61
- export function isTokenTransfer(tx) {
62
- const { message } = tx
63
- const { accountKeys, instructions } = message
41
+ // TODO: Unify with parseTransaction in solana-api and use there as well?
42
+ // TODO: add support for swap instructions
43
+ export async function parseTxBuffer(buffer, api) {
44
+ const transaction = deserializeTransaction(buffer)
45
+ const { message } = transaction
64
46
 
65
- if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) return false
47
+ const accountKeys = getAccountKeys(message)
48
+ const instructions = getNormalizedInstructions(message)
66
49
 
67
- // Search for any token transfer instruction
68
- return instructions.some((instruction) => isTokenTransferInstruction(instruction, accountKeys))
69
- }
50
+ if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) {
51
+ throw new TypeError('Invalid transaction structure')
52
+ }
70
53
 
71
- // Helper function to check if an instruction is a SOL transfer
72
- function isSolTransferInstruction(instruction, accountKeys) {
73
- if (!instruction || instruction.programIdIndex === undefined) return false
54
+ const decodedEntries = []
74
55
 
75
- const programId = accountKeys[instruction.programIdIndex]
56
+ for (const [index, instruction] of instructions.entries()) {
57
+ try {
58
+ const decoded = decodeInstructionEntry({ instruction, accountKeys })
59
+ if (decoded) {
60
+ decodedEntries.push({
61
+ ...decoded,
62
+ instruction,
63
+ index,
64
+ })
65
+ }
66
+ } catch {
67
+ // Skip unsupported instructions
68
+ }
69
+ }
76
70
 
77
- if (!programId.equals(SYSTEM_PROGRAM_ID)) return false
71
+ const parsedInstructions = []
72
+ for (const entry of decodedEntries) {
73
+ const parsed = await parseDecodedInstruction(entry, {
74
+ accountKeys,
75
+ api,
76
+ instructions,
77
+ })
78
+ if (parsed) parsedInstructions.push(parsed)
79
+ }
78
80
 
79
- // Must have exactly 2 accounts (from, to)
80
- if (!Array.isArray(instruction.accounts) || instruction.accounts.length !== 2) {
81
- return false
81
+ if (parsedInstructions.length === 0) {
82
+ throw new Error('No supported instructions found in transaction')
82
83
  }
83
84
 
84
- // Check if the instruction data starts with 0x02 (System Transfer)
85
- // Note: Must handle both formats because this is called from isSolanaTransfer()
86
- // which uses non-normalized instructions
85
+ return parsedInstructions
86
+ }
87
+
88
+ async function getMintAddressFromTokenAccount(tokenAccountAddress, api) {
87
89
  try {
88
- let data = instruction.data
89
- if (typeof data === 'string') {
90
- data = bs58.decode(data)
90
+ const accountInfo = await api.getAccountInfo(tokenAccountAddress)
91
+ if (accountInfo?.data?.parsed?.info?.mint) {
92
+ return accountInfo.data.parsed.info.mint
91
93
  }
92
-
93
- const buffer = Buffer.from(data)
94
- if (buffer.readUInt32LE(0) !== SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index) return false
95
94
  } catch {
96
- return false
95
+ // Ignore errors when fetching mint address
97
96
  }
98
97
 
99
- return true
98
+ return null
100
99
  }
101
100
 
102
- export function isSolanaTransfer(tx) {
103
- const { message } = tx
104
- const { accountKeys, instructions } = message
105
-
106
- if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) return false
107
-
108
- return instructions.some((instruction) => isSolTransferInstruction(instruction, accountKeys))
109
- }
101
+ function decodeInstructionEntry({ instruction, accountKeys }) {
102
+ const { accounts } = instruction
103
+ const buffer = toBuffer(instruction.data)
110
104
 
111
- // Helper to normalize account keys for both versioned and legacy transactions
112
- function getAccountKeys(message) {
113
- // Versioned transactions use staticAccountKeys
114
- if (message.staticAccountKeys) {
115
- return message.staticAccountKeys
105
+ if (TokenInstruction.isProgramInstruction(instruction, accountKeys)) {
106
+ TokenInstruction.validateInstruction(instruction)
107
+ return {
108
+ kind: InstructionKind.TOKEN,
109
+ decoded: TokenInstruction.decodeInstructionData(buffer, accounts, accountKeys),
110
+ }
116
111
  }
117
112
 
118
- // Legacy transactions use accountKeys
119
- return message.accountKeys
120
- }
121
-
122
- // Helper to normalize a single instruction (handles both compiled and regular instructions)
123
- function normalizeInstruction(instruction) {
124
- let normalizedData = instruction.data
125
- if (typeof instruction.data === 'string') {
126
- normalizedData = bs58.decode(instruction.data)
113
+ if (SystemInstruction.isProgramInstruction(instruction, accountKeys)) {
114
+ SystemInstruction.validateInstruction(instruction)
115
+ return {
116
+ kind: InstructionKind.SYSTEM_TRANSFER,
117
+ decoded: SystemInstruction.decodeInstructionData(buffer, accounts, accountKeys),
118
+ }
127
119
  }
128
120
 
129
- return {
130
- programIdIndex: instruction.programIdIndex,
131
- accounts: instruction.accountKeyIndexes || instruction.accounts,
132
- data: normalizedData,
121
+ if (StakeInstruction.isProgramInstruction(instruction, accountKeys)) {
122
+ StakeInstruction.validateInstruction(instruction)
123
+ return {
124
+ kind: InstructionKind.STAKE,
125
+ decoded: StakeInstruction.decodeInstructionData(buffer, accounts, accountKeys),
126
+ }
133
127
  }
128
+
129
+ return null
134
130
  }
135
131
 
136
- // Helper to normalize instructions for both versioned and legacy transactions
137
- function getInstructions(message) {
138
- let instructions
139
- // Versioned transactions use compiledInstructions
140
- if (message.compiledInstructions) {
141
- instructions = message.compiledInstructions
142
- } else if (message.instructions) {
143
- // Legacy transactions use instructions
144
- instructions = message.instructions
145
- } else {
146
- return []
132
+ async function parseDecodedInstruction(entry, context) {
133
+ switch (entry.kind) {
134
+ case InstructionKind.TOKEN:
135
+ return parseTokenInstruction(entry, context)
136
+ case InstructionKind.SYSTEM_TRANSFER:
137
+ return parseSystemInstruction(entry, context)
138
+ case InstructionKind.STAKE:
139
+ return parseStakeInstruction(entry, context)
140
+ default:
141
+ return null
147
142
  }
148
-
149
- return instructions.map((inst) => normalizeInstruction(inst))
150
143
  }
151
144
 
152
- // TODO: Unify with parseTransaction in solana-api and use there as well?
153
- // TODO: add support for swap and stake instructions
154
- export async function parseTxBuffer(buffer, api) {
155
- const transaction = deserializeTransaction(buffer)
156
- const { message } = transaction
145
+ async function parseTokenInstruction({ decoded, index, instruction }, { accountKeys, api }) {
146
+ const programId = accountKeys[instruction.programIdIndex]
147
+ const { fromTokenAccount, toTokenAccount, mintAddress } = decoded
148
+
149
+ let fromOwner = fromTokenAccount
150
+ let toOwner = toTokenAccount
151
+ let resolvedMint = mintAddress
152
+
153
+ if (api) {
154
+ const ownerPromises = [
155
+ api.getTokenAddressOwner(fromTokenAccount),
156
+ api.getTokenAddressOwner(toTokenAccount),
157
+ ]
158
+ if (!resolvedMint) {
159
+ ownerPromises.push(getMintAddressFromTokenAccount(fromTokenAccount, api))
160
+ }
157
161
 
158
- // Normalize account keys and instructions (versioned vs legacy)
159
- const accountKeys = getAccountKeys(message)
160
- const instructions = getInstructions(message)
162
+ const [fromOwnerRaw, toOwnerRaw, fetchedMint] = await Promise.all(ownerPromises)
163
+ fromOwner = fromOwnerRaw || fromTokenAccount
164
+ toOwner = toOwnerRaw || toTokenAccount
165
+ if (!resolvedMint) resolvedMint = fetchedMint
166
+ }
161
167
 
162
- if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) {
163
- throw new TypeError('Invalid transaction structure')
168
+ const parsedInstruction = {
169
+ method: decoded.method,
170
+ from: fromOwner,
171
+ to: toOwner,
172
+ amount: decoded.amount,
173
+ programId: programId.toBase58(),
174
+ instructionIndex: index,
175
+ fromTokenAccount,
176
+ toTokenAccount,
177
+ mintAddress: resolvedMint,
164
178
  }
165
179
 
166
- const parsedInstructions = []
180
+ if (decoded.decimals !== undefined) parsedInstruction.decimals = decoded.decimals
181
+ if (decoded.fee !== undefined) parsedInstruction.fee = decoded.fee
167
182
 
168
- try {
169
- for (const [index, instruction] of instructions.entries()) {
170
- if (isTokenTransferInstruction(instruction, accountKeys)) {
171
- const decoded = TokenInstruction.decodeInstructionData(instruction.data)
172
- const programId = accountKeys[instruction.programIdIndex]
173
-
174
- // Transfer (3): [source, destination, authority]
175
- // TransferChecked (12): [source, mint, destination, authority, ...]
176
- // TransferCheckedWithFee (26,1): [source, mint, destination, authority, ...]
177
- let fromTokenAccount, toTokenAccount, mintAddress
178
-
179
- if (decoded.method === 'transferChecked' || decoded.method === 'transferCheckedWithFee') {
180
- // TransferChecked and TransferCheckedWithFee: mint is at index 1, destination at index 2
181
- fromTokenAccount = accountKeys[instruction.accounts[0]].toBase58()
182
- mintAddress = accountKeys[instruction.accounts[1]].toBase58() // Mint directly from accounts
183
- toTokenAccount = accountKeys[instruction.accounts[2]].toBase58()
184
- } else {
185
- // Transfer: destination is at index 1
186
- fromTokenAccount = accountKeys[instruction.accounts[0]].toBase58()
187
- toTokenAccount = accountKeys[instruction.accounts[1]].toBase58()
188
- mintAddress = null
189
- }
190
-
191
- // Get token account owners in parallel (requires API calls)
192
- let fromOwner, toOwner
193
- if (api) {
194
- ;[fromOwner, toOwner, mintAddress] = await Promise.all([
195
- api.getTokenAddressOwner(fromTokenAccount),
196
- api.getTokenAddressOwner(toTokenAccount),
197
- mintAddress
198
- ? Promise.resolve(mintAddress)
199
- : getMintAddressFromTokenAccount(fromTokenAccount, api),
200
- ])
201
- // Fallback to token account address if owner lookup fails
202
- fromOwner = fromOwner || fromTokenAccount
203
- toOwner = toOwner || toTokenAccount
204
- } else {
205
- fromOwner = fromTokenAccount
206
- toOwner = toTokenAccount
207
- }
208
-
209
- const parsedInstruction = {
210
- method: decoded.method,
211
- from: fromOwner,
212
- to: toOwner,
213
- amount: decoded.amount,
214
- programId: programId.toBase58(),
215
- instructionIndex: index,
216
- fromTokenAccount,
217
- toTokenAccount,
218
- mintAddress,
219
- }
220
-
221
- // Include decimals if it's a transferChecked instruction
222
- if (decoded.decimals !== undefined) {
223
- parsedInstruction.decimals = decoded.decimals
224
- }
225
-
226
- parsedInstructions.push(parsedInstruction)
227
- } else if (isSolTransferInstruction(instruction, accountKeys)) {
228
- const { amount, method } = SystemInstruction.decodeInstructionData(instruction.data)
229
- const programId = accountKeys[instruction.programIdIndex]
230
-
231
- parsedInstructions.push({
232
- method,
233
- from: accountKeys[instruction.accounts[0]].toBase58(),
234
- to: accountKeys[instruction.accounts[1]].toBase58(),
235
- amount,
236
- programId: programId.toBase58(),
237
- instructionIndex: index,
238
- })
239
- }
240
- }
241
- } catch (error) {
242
- console.log('instruction parsing error', error)
243
- throw error
244
- }
183
+ return parsedInstruction
184
+ }
245
185
 
246
- if (parsedInstructions.length === 0) {
247
- throw new Error('No supported instructions found in transaction')
248
- }
186
+ function parseSystemInstruction({ decoded, index, instruction }, { accountKeys }) {
187
+ const programId = accountKeys[instruction.programIdIndex]
249
188
 
250
- return parsedInstructions
189
+ return {
190
+ method: decoded.method,
191
+ from: decoded.from,
192
+ to: decoded.to,
193
+ amount: decoded.amount,
194
+ programId: programId.toBase58(),
195
+ instructionIndex: index,
196
+ }
251
197
  }
252
198
 
253
- async function getMintAddressFromTokenAccount(tokenAccountAddress, api) {
254
- try {
255
- const accountInfo = await api.getAccountInfo(tokenAccountAddress)
256
- if (accountInfo?.data?.parsed?.info?.mint) {
257
- return accountInfo.data.parsed.info.mint
258
- }
259
- } catch (error) {
260
- console.log('Error getting mint address:', error)
199
+ function parseStakeInstruction({ decoded, index, instruction }, { accountKeys, instructions }) {
200
+ const programId = accountKeys[instruction.programIdIndex]
201
+ const { stakeAccount: stakeAddress, ...rest } = decoded
202
+
203
+ const parsedInstruction = {
204
+ method: decoded.method,
205
+ stakeAddress,
206
+ programId: programId.toBase58(),
207
+ instructionIndex: index,
208
+ ...rest,
261
209
  }
262
210
 
263
- return null
211
+ if (decoded.method === 'delegate' && parsedInstruction.amount === undefined) {
212
+ const amount = getStakedAmountFromCreateWithSeed(stakeAddress, instructions, accountKeys)
213
+ if (amount !== undefined) parsedInstruction.amount = amount
214
+ }
215
+
216
+ return parsedInstruction
264
217
  }
@@ -1,6 +1,7 @@
1
1
  import { isNumberUnit } from '@exodus/currency'
2
2
  import { VersionedTransaction } from '@exodus/solana-web3.js'
3
3
  import BN from 'bn.js'
4
+ import assert from 'minimalistic-assert'
4
5
 
5
6
  import { createMetaplexTransferTransaction } from '../helpers/metaplex-transfer.js'
6
7
  import Transaction from '../transaction.js'
@@ -42,6 +43,10 @@ export function prepareForSigning(unsignedTx, { checkBalances = true } = {}) {
42
43
 
43
44
  // Recreate a Web3.js Transaction instance if the buffer provided.
44
45
  if (transactionBuffer) {
46
+ assert(
47
+ unsignedTx.txData.transactionBuffer instanceof Uint8Array,
48
+ 'transactionBuffer must be a Buffer or Uint8Array'
49
+ )
45
50
  return deserializeTransactionBytes(transactionBuffer)
46
51
  }
47
52
 
@@ -9,4 +9,10 @@ export * from './stake-program.js'
9
9
  export * from './sysvar.js'
10
10
  export * from './transaction.js'
11
11
  export * from './constants.js'
12
- export { TOKEN_INSTRUCTION_LAYOUTS, TokenInstruction } from './token-program.js'
12
+ export {
13
+ Token2022Program,
14
+ TOKEN_INSTRUCTION_LAYOUTS,
15
+ TokenInstruction,
16
+ TokenProgram,
17
+ TRANSFER_FEE_SUB_INSTRUCTIONS,
18
+ } from './token-program.js'
@@ -1,4 +1,5 @@
1
1
  import * as BufferLayout from '@exodus/buffer-layout'
2
+ import BN from 'bn.js'
2
3
 
3
4
  import { decodeData, encodeData } from './instruction.js'
4
5
  import { PublicKey } from './publickey.js'
@@ -177,6 +178,52 @@ export const STAKE_INSTRUCTION_LAYOUTS = Object.freeze({
177
178
  * Stake Instruction class
178
179
  */
179
180
  export const StakeInstruction = {
181
+ /**
182
+ * Check if an instruction belongs to the Stake program (program ID check only)
183
+ * @param {Object} instruction - Normalized instruction object
184
+ * @param {Array} accountKeys - Account public keys array
185
+ * @returns {boolean} True if this is a stake program instruction
186
+ */
187
+ isProgramInstruction(instruction, accountKeys) {
188
+ if (!instruction || instruction.programIdIndex === undefined) return false
189
+ const programId = accountKeys[instruction.programIdIndex]
190
+ return programId.equals(StakeProgram.programId)
191
+ },
192
+
193
+ /**
194
+ * Validate instruction data for decoding (length, type, accounts)
195
+ * Only supports fund movement instructions: Delegate, Deactivate, Withdraw, Split
196
+ * @param {Object} instruction - Normalized instruction object
197
+ * @throws {Error} If instruction is invalid for decoding
198
+ */
199
+ validateInstruction(instruction) {
200
+ if (!Array.isArray(instruction.accounts) || instruction.accounts.length === 0) {
201
+ throw new Error('Stake instruction requires at least 1 account')
202
+ }
203
+
204
+ const buffer = Buffer.from(instruction.data)
205
+ if (buffer.length < 4) {
206
+ throw new Error('Stake instruction data too short')
207
+ }
208
+
209
+ const instructionType = buffer.readUInt32LE(0)
210
+
211
+ switch (instructionType) {
212
+ case STAKE_INSTRUCTION_LAYOUTS.Delegate.index:
213
+ case STAKE_INSTRUCTION_LAYOUTS.Deactivate.index:
214
+ return
215
+ case STAKE_INSTRUCTION_LAYOUTS.Split.index:
216
+ case STAKE_INSTRUCTION_LAYOUTS.Withdraw.index:
217
+ if (buffer.length < 12) {
218
+ throw new Error('Split/Withdraw instruction data too short')
219
+ }
220
+
221
+ return
222
+ default:
223
+ throw new Error(`Unsupported stake instruction type: ${instructionType}`)
224
+ }
225
+ },
226
+
180
227
  /**
181
228
  * Decode a stake instruction and retrieve the instruction type.
182
229
  */
@@ -286,6 +333,67 @@ export const StakeInstruction = {
286
333
  )
287
334
  }
288
335
  },
336
+
337
+ /**
338
+ * Decode raw staking instruction data.
339
+ * Only supports fund movement instructions: Delegate, Deactivate, Withdraw, Split
340
+ * IMPORTANT: Call isProgramInstruction() and validateInstruction() first.
341
+ * @param {Buffer} buffer - Instruction data buffer
342
+ * @param {Array} accounts - Account indices array
343
+ * @param {Array} accountKeys - Account public keys array
344
+ * @returns {Object} Decoded staking data with method and relevant fields
345
+ */
346
+ decodeInstructionData(buffer, accounts, accountKeys) {
347
+ const getAccountAddress = (index) => {
348
+ const accountIndex = accounts[index]
349
+ if (accountIndex === undefined) return null
350
+ const key = accountKeys[accountIndex]
351
+ return key ? key.toBase58() : null
352
+ }
353
+
354
+ const instructionType = buffer.readUInt32LE(0)
355
+
356
+ switch (instructionType) {
357
+ case STAKE_INSTRUCTION_LAYOUTS.Delegate.index:
358
+ // Accounts: [stakeAccount, voteAccount, clock, stakeHistory, stakeConfig, stakeAuthority]
359
+ return {
360
+ method: 'delegate',
361
+ stakeAccount: getAccountAddress(0),
362
+ validator: getAccountAddress(1),
363
+ stakeAuthority: getAccountAddress(5),
364
+ }
365
+
366
+ case STAKE_INSTRUCTION_LAYOUTS.Deactivate.index:
367
+ // Accounts: [stakeAccount, clock, stakeAuthority]
368
+ return {
369
+ method: 'deactivate',
370
+ stakeAccount: getAccountAddress(0),
371
+ stakeAuthority: getAccountAddress(2),
372
+ }
373
+
374
+ case STAKE_INSTRUCTION_LAYOUTS.Withdraw.index:
375
+ // Accounts: [stakeAccount, to, clock, stakeHistory, withdrawAuthority]
376
+ return {
377
+ method: 'withdraw',
378
+ stakeAccount: getAccountAddress(0),
379
+ amount: new BN(buffer.slice(4, 12), 'le'),
380
+ to: getAccountAddress(1),
381
+ withdrawAuthority: getAccountAddress(4),
382
+ }
383
+
384
+ case STAKE_INSTRUCTION_LAYOUTS.Split.index:
385
+ // Accounts: [stakeAccount, splitStakeAccount, stakeAuthority]
386
+ return {
387
+ method: 'split',
388
+ stakeAccount: getAccountAddress(0),
389
+ splitStakeAccount: getAccountAddress(1),
390
+ amount: new BN(buffer.slice(4, 12), 'le'),
391
+ }
392
+
393
+ default:
394
+ throw new Error(`Unsupported stake instruction type: ${instructionType}`)
395
+ }
396
+ },
289
397
  }
290
398
 
291
399
  /**
@@ -7,6 +7,7 @@ import { PublicKey } from './publickey.js'
7
7
  import { SYSVAR_RECENT_BLOCKHASHES_PUBKEY, SYSVAR_RENT_PUBKEY } from './sysvar.js'
8
8
  import { Transaction, TransactionInstruction } from './transaction.js'
9
9
  import * as Layout from './utils/layout.js'
10
+ import { toBuffer } from './utils/to-buffer.js'
10
11
 
11
12
  /**
12
13
  * Create account system transaction params
@@ -199,6 +200,40 @@ export const SYSTEM_INSTRUCTION_LAYOUTS = Object.freeze({
199
200
  * System Instruction class
200
201
  */
201
202
  export const SystemInstruction = {
203
+ /**
204
+ * Check if an instruction belongs to the System program (program ID check only)
205
+ * @param {Object} instruction - Normalized instruction object
206
+ * @param {Array} accountKeys - Account public keys array
207
+ * @returns {boolean} True if this is a system program instruction
208
+ */
209
+ isProgramInstruction(instruction, accountKeys) {
210
+ if (!instruction || instruction.programIdIndex === undefined) return false
211
+ const programId = accountKeys[instruction.programIdIndex]
212
+ return programId.equals(SystemProgram.programId)
213
+ },
214
+
215
+ /**
216
+ * Validate instruction data for transfer decoding (length, type, accounts)
217
+ * @param {Object} instruction - Normalized instruction object
218
+ * @throws {Error} If instruction is invalid for decoding
219
+ */
220
+ validateInstruction(instruction) {
221
+ if (!Array.isArray(instruction.accounts) || instruction.accounts.length !== 2) {
222
+ throw new Error('System transfer instruction requires exactly 2 accounts')
223
+ }
224
+
225
+ const buffer = toBuffer(instruction.data)
226
+ // Transfer layout: u32 instruction + u64 lamports = 12 bytes
227
+ if (buffer.length < 12) {
228
+ throw new Error('System transfer instruction data too short')
229
+ }
230
+
231
+ const instructionType = buffer.readUInt32LE(0)
232
+ if (instructionType !== SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index) {
233
+ throw new Error(`Unsupported system instruction type: ${instructionType}`)
234
+ }
235
+ },
236
+
202
237
  /**
203
238
  * Decode a system instruction and retrieve the instruction type.
204
239
  */
@@ -260,27 +295,40 @@ export const SystemInstruction = {
260
295
  },
261
296
 
262
297
  /**
263
- * Decode raw instruction data (for use in parseTxBuffer)
298
+ * Decode raw instruction data.
299
+ * IMPORTANT: Call isProgramInstruction() and validateInstruction() first.
300
+ * @param {Buffer} buffer - Instruction data buffer
301
+ * @param {Array} accounts - Account indices array
302
+ * @param {Array} accountKeys - Account public keys array
303
+ * @returns {Object} Decoded instruction data with amount, method, and accounts
304
+ */
305
+ decodeInstructionData(buffer, accounts, accountKeys) {
306
+ const { lamports } = decodeData(SYSTEM_INSTRUCTION_LAYOUTS.Transfer, buffer)
307
+ return {
308
+ method: 'systemTransfer',
309
+ amount: new BN(lamports),
310
+ from: accountKeys[accounts[0]].toBase58(),
311
+ to: accountKeys[accounts[1]].toBase58(),
312
+ }
313
+ },
314
+
315
+ /**
316
+ * Decode raw CreateWithSeed instruction data
264
317
  * @param {Buffer} data - Raw instruction data buffer
265
- * @returns {Object} Decoded instruction data with amount and method
318
+ * @returns {Object} Decoded instruction data with lamports, seed, space, programId
266
319
  */
267
- decodeInstructionData(data) {
320
+ decodeCreateWithSeedData(data) {
268
321
  const buffer = Buffer.from(data)
269
-
270
- try {
271
- const { lamports } = decodeData(SYSTEM_INSTRUCTION_LAYOUTS.Transfer, buffer)
272
- return {
273
- amount: new BN(lamports),
274
- method: 'systemTransfer',
275
- }
276
- } catch (err) {
277
- // Provide more specific error for non-transfer instructions
278
- if (err.message.includes('instruction index mismatch')) {
279
- const instructionType = buffer.readUInt32LE(0)
280
- throw new Error(`Unsupported instruction type for SystemProgram: ${instructionType}`)
281
- }
282
-
283
- throw err
322
+ const { base, seed, lamports, space, programId } = decodeData(
323
+ SYSTEM_INSTRUCTION_LAYOUTS.CreateWithSeed,
324
+ buffer
325
+ )
326
+ return {
327
+ basePubkey: new PublicKey(base),
328
+ seed,
329
+ lamports: new BN(lamports),
330
+ space,
331
+ programId: new PublicKey(programId),
284
332
  }
285
333
  },
286
334
 
@@ -1,5 +1,32 @@
1
1
  import BN from 'bn.js'
2
2
 
3
+ import { PublicKey } from './publickey.js'
4
+ import { toBuffer } from './utils/to-buffer.js'
5
+
6
+ /**
7
+ * Token Program
8
+ */
9
+ export const TokenProgram = {
10
+ /**
11
+ * Public key that identifies the Token program
12
+ */
13
+ get programId() {
14
+ return new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
15
+ },
16
+ }
17
+
18
+ /**
19
+ * Token-2022 (Token Extensions) Program
20
+ */
21
+ export const Token2022Program = {
22
+ /**
23
+ * Public key that identifies the Token-2022 program
24
+ */
25
+ get programId() {
26
+ return new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb')
27
+ },
28
+ }
29
+
3
30
  /**
4
31
  * Token Program instruction layouts
5
32
  * Token-2022 (Token Extensions) is backward compatible with these instruction types
@@ -119,62 +146,110 @@ export const TRANSFER_FEE_SUB_INSTRUCTIONS = Object.freeze({
119
146
  */
120
147
  export const TokenInstruction = {
121
148
  /**
122
- * Decode raw SPL Token instruction data (for use in parseTxBuffer)
123
- * @param {Buffer} data - Instruction data buffer
124
- * @returns {Object} Decoded instruction data with amount, method, and optional decimals/fee
149
+ * Check if an instruction belongs to the Token program (program ID check only)
150
+ * @param {Object} instruction - Normalized instruction object
151
+ * @param {Array} accountKeys - Account public keys array
152
+ * @returns {boolean} True if this is a token program instruction
125
153
  */
126
- decodeInstructionData(data) {
127
- // Ensure we have a Buffer to work with
128
- const buffer = Buffer.from(data)
154
+ isProgramInstruction(instruction, accountKeys) {
155
+ if (!instruction || instruction.programIdIndex === undefined) return false
156
+ const programId = accountKeys[instruction.programIdIndex]
157
+ return programId.equals(TokenProgram.programId) || programId.equals(Token2022Program.programId)
158
+ },
159
+
160
+ /**
161
+ * Validate instruction data for transfer decoding (length, type, accounts)
162
+ * @param {Object} instruction - Normalized instruction object
163
+ * @throws {Error} If instruction is invalid for decoding
164
+ */
165
+ validateInstruction(instruction) {
166
+ if (!Array.isArray(instruction.accounts) || instruction.accounts.length < 3) {
167
+ throw new Error('Token instruction requires at least 3 accounts')
168
+ }
169
+
170
+ const data = toBuffer(instruction.data)
171
+ if (data.length < 9) {
172
+ throw new Error('Token instruction data too short')
173
+ }
174
+
175
+ const instructionType = data[0]
129
176
 
130
- if (buffer.length < 9) throw new Error('Instruction data too short for SPL transfer')
177
+ if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.Transfer.index) {
178
+ return
179
+ }
180
+
181
+ if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index) {
182
+ if (data.length < 10) {
183
+ throw new Error('TransferChecked instruction data too short')
184
+ }
185
+
186
+ return
187
+ }
188
+
189
+ if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index) {
190
+ if (data.length < 19) {
191
+ throw new Error('TransferCheckedWithFee instruction data too short')
192
+ }
193
+
194
+ if (data[1] !== TRANSFER_FEE_SUB_INSTRUCTIONS.TransferCheckedWithFee) {
195
+ throw new Error(`Unsupported TransferFeeExtension sub-instruction: ${data[1]}`)
196
+ }
131
197
 
198
+ return
199
+ }
200
+
201
+ throw new Error(`Unsupported token instruction type: ${instructionType}`)
202
+ },
203
+
204
+ /**
205
+ * Decode raw SPL Token instruction data.
206
+ * IMPORTANT: Call isProgramInstruction() and validateInstruction() first.
207
+ * @param {Buffer} buffer - Instruction data buffer
208
+ * @param {Array} accounts - Account indices array
209
+ * @param {Array} accountKeys - Account public keys array
210
+ * @returns {Object} Decoded instruction data with amount, method, accounts, and optional decimals/fee
211
+ */
212
+ decodeInstructionData(buffer, accounts, accountKeys) {
132
213
  const instructionType = buffer[0]
214
+ const getAddress = (index) => accountKeys[accounts[index]].toBase58()
133
215
 
134
216
  // Handle TransferFeeExtension (Token-2022 instruction 26)
135
217
  if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index) {
136
- // TransferFeeExtension has a sub-instruction type at byte 1
137
- if (buffer.length < 2) throw new Error('Instruction data too short for TransferFeeExtension')
138
-
139
- const subInstructionType = buffer[1]
140
- // TransferCheckedWithFee sub-instruction (1)
141
- if (subInstructionType === TRANSFER_FEE_SUB_INSTRUCTIONS.TransferCheckedWithFee) {
142
- // Format: [26, 1, amount(8), decimals(1), fee(8)]
143
- if (buffer.length < 18) {
144
- throw new Error('Instruction data too short for transferCheckedWithFee')
145
- }
146
-
147
- const amountBytes = buffer.slice(2, 10)
148
- const amountBN = new BN(amountBytes, 'le')
149
- const decimals = buffer[10]
150
- const feeBytes = buffer.slice(11, 19)
151
- const feeBN = new BN(feeBytes, 'le')
152
-
153
- return {
154
- amount: amountBN,
155
- method: 'transferCheckedWithFee',
156
- decimals,
157
- fee: feeBN,
158
- }
218
+ // Format: [26, 1, amount(8), decimals(1), fee(8)]
219
+ return {
220
+ method: 'transferCheckedWithFee',
221
+ amount: new BN(buffer.slice(2, 10), 'le'),
222
+ decimals: buffer[10],
223
+ fee: new BN(buffer.slice(11, 19), 'le'),
224
+ // Accounts: [source, mint, destination, authority]
225
+ fromTokenAccount: getAddress(0),
226
+ mintAddress: getAddress(1),
227
+ toTokenAccount: getAddress(2),
159
228
  }
160
-
161
- // Other TransferFeeExtension sub-instructions not supported yet
162
- throw new Error(`Unsupported TransferFeeExtension sub-instruction: ${subInstructionType}`)
163
229
  }
164
230
 
165
- const amountBytes = buffer.slice(1, 9)
166
- const amountBN = new BN(amountBytes, 'le')
231
+ const amount = new BN(buffer.slice(1, 9), 'le')
167
232
 
168
233
  if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.Transfer.index) {
169
- return { amount: amountBN, method: 'transfer' }
234
+ // Accounts: [source, destination, authority]
235
+ return {
236
+ method: 'transfer',
237
+ amount,
238
+ fromTokenAccount: getAddress(0),
239
+ mintAddress: null,
240
+ toTokenAccount: getAddress(1),
241
+ }
170
242
  }
171
243
 
172
- if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index) {
173
- if (buffer.length < 10) throw new Error('Instruction data too short for transferChecked')
174
- const decimals = buffer[9]
175
- return { amount: amountBN, method: 'transferChecked', decimals }
244
+ // TransferChecked
245
+ // Accounts: [source, mint, destination, authority]
246
+ return {
247
+ method: 'transferChecked',
248
+ amount,
249
+ decimals: buffer[9],
250
+ fromTokenAccount: getAddress(0),
251
+ mintAddress: getAddress(1),
252
+ toTokenAccount: getAddress(2),
176
253
  }
177
-
178
- throw new Error(`Unsupported instruction type: ${instructionType}`)
179
254
  },
180
255
  }