@exodus/solana-lib 3.17.0 → 3.18.1

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,26 @@
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.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.18.0...@exodus/solana-lib@3.18.1) (2025-12-15)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: check SOL transactionBuffer type (#7111)
13
+
14
+
15
+
16
+ ## [3.18.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.15.3...@exodus/solana-lib@3.18.0) (2025-11-26)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat: Solana support Close Authority Sponsorship in fee payer (#6767)
23
+
24
+
25
+
6
26
  ## [3.17.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.15.3...@exodus/solana-lib@3.17.0) (2025-11-13)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-lib",
3
- "version": "3.17.0",
3
+ "version": "3.18.1",
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": "6a39e3cdbf3bf1849d6d4aae8e935588332d92ac"
50
+ "gitHead": "76314e6813c7b8c01a3c852cac1228be3820356d"
51
51
  }
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
 
@@ -1,48 +1,14 @@
1
- import BN from 'bn.js'
2
1
  import bs58 from 'bs58'
3
2
 
4
3
  import { SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
5
- import { SYSTEM_INSTRUCTION_LAYOUTS, TOKEN_INSTRUCTION_LAYOUTS } from '../vendor/index.js'
4
+ import {
5
+ SYSTEM_INSTRUCTION_LAYOUTS,
6
+ SystemInstruction,
7
+ TOKEN_INSTRUCTION_LAYOUTS,
8
+ TokenInstruction,
9
+ } from '../vendor/index.js'
6
10
  import { deserializeTransaction } from './common.js'
7
11
 
8
- function decodeSPLTransferData(data) {
9
- // Data is already normalized to Buffer in normalizeInstruction
10
- if (data.length < 9) throw new Error('Instruction data too short for SPL transfer')
11
-
12
- const instructionType = data[0]
13
- const amountBytes = data.slice(1, 9)
14
- const amountBN = new BN(amountBytes, 'le')
15
-
16
- if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.Transfer.index) {
17
- return { amount: amountBN, method: 'transfer' }
18
- }
19
-
20
- if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index) {
21
- if (data.length < 10) throw new Error('Instruction data too short for transferChecked')
22
- const decimals = data[9]
23
- return { amount: amountBN, method: 'transferChecked', decimals }
24
- }
25
-
26
- throw new Error(`Unsupported instruction type: ${instructionType}`)
27
- }
28
-
29
- function decodeSystemTransferData(data) {
30
- const buffer = Buffer.from(data)
31
-
32
- // 4 bytes - instruction type must be 2 (Transfer)
33
- if (buffer.readUInt32LE(0) !== SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index) {
34
- throw new Error(`Unsupported instruction type for SystemProgram: ${buffer.readUInt32LE(0)}`)
35
- }
36
-
37
- const amountBytes = buffer.slice(4, 12)
38
- const amountBN = new BN(amountBytes, 'le')
39
-
40
- return {
41
- amount: amountBN,
42
- method: 'systemTransfer',
43
- }
44
- }
45
-
46
12
  // Helper function to check if an instruction is a token transfer
47
13
  function isTokenTransferInstruction(instruction, accountKeys) {
48
14
  if (!instruction || instruction.programIdIndex === undefined) return false
@@ -58,7 +24,8 @@ function isTokenTransferInstruction(instruction, accountKeys) {
58
24
  return false
59
25
  }
60
26
 
61
- // Check if the instruction data is transfer (0x03) or transferChecked (0x0c/12)
27
+ // Check if the instruction data is transfer (0x03), transferChecked (0x0c/12),
28
+ // or TransferFeeExtension (0x1a/26) with TransferCheckedWithFee sub-instruction
62
29
  // Note: Must handle both formats because this is called from isTokenTransfer()
63
30
  // which uses non-normalized instructions
64
31
  try {
@@ -68,13 +35,22 @@ function isTokenTransferInstruction(instruction, accountKeys) {
68
35
  }
69
36
 
70
37
  const instructionType = data[0]
71
- // Accept both Transfer and TransferChecked
38
+
39
+ // Accept Transfer, TransferChecked, and TransferFeeExtension
72
40
  if (
73
41
  instructionType !== TOKEN_INSTRUCTION_LAYOUTS.Transfer.index &&
74
- instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index
42
+ instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index &&
43
+ instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index
75
44
  ) {
76
45
  return false
77
46
  }
47
+
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
+ }
78
54
  } catch {
79
55
  return false
80
56
  }
@@ -192,15 +168,16 @@ export async function parseTxBuffer(buffer, api) {
192
168
  try {
193
169
  for (const [index, instruction] of instructions.entries()) {
194
170
  if (isTokenTransferInstruction(instruction, accountKeys)) {
195
- const decoded = decodeSPLTransferData(instruction.data)
171
+ const decoded = TokenInstruction.decodeInstructionData(instruction.data)
196
172
  const programId = accountKeys[instruction.programIdIndex]
197
173
 
198
174
  // Transfer (3): [source, destination, authority]
199
175
  // TransferChecked (12): [source, mint, destination, authority, ...]
176
+ // TransferCheckedWithFee (26,1): [source, mint, destination, authority, ...]
200
177
  let fromTokenAccount, toTokenAccount, mintAddress
201
178
 
202
- if (decoded.method === 'transferChecked') {
203
- // TransferChecked: mint is at index 1, destination at index 2
179
+ if (decoded.method === 'transferChecked' || decoded.method === 'transferCheckedWithFee') {
180
+ // TransferChecked and TransferCheckedWithFee: mint is at index 1, destination at index 2
204
181
  fromTokenAccount = accountKeys[instruction.accounts[0]].toBase58()
205
182
  mintAddress = accountKeys[instruction.accounts[1]].toBase58() // Mint directly from accounts
206
183
  toTokenAccount = accountKeys[instruction.accounts[2]].toBase58()
@@ -248,7 +225,7 @@ export async function parseTxBuffer(buffer, api) {
248
225
 
249
226
  parsedInstructions.push(parsedInstruction)
250
227
  } else if (isSolTransferInstruction(instruction, accountKeys)) {
251
- const { amount, method } = decodeSystemTransferData(instruction.data)
228
+ const { amount, method } = SystemInstruction.decodeInstructionData(instruction.data)
252
229
  const programId = accountKeys[instruction.programIdIndex]
253
230
 
254
231
  parsedInstructions.push({
@@ -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,4 @@ 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 } from './token-program.js'
12
+ export { TOKEN_INSTRUCTION_LAYOUTS, TokenInstruction } 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 { NONCE_ACCOUNT_LENGTH } from './nonce-account.js'
@@ -258,6 +259,31 @@ export const SystemInstruction = {
258
259
  }
259
260
  },
260
261
 
262
+ /**
263
+ * Decode raw instruction data (for use in parseTxBuffer)
264
+ * @param {Buffer} data - Raw instruction data buffer
265
+ * @returns {Object} Decoded instruction data with amount and method
266
+ */
267
+ decodeInstructionData(data) {
268
+ 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
284
+ }
285
+ },
286
+
261
287
  /**
262
288
  * Decode an allocate system instruction and retrieve the instruction params.
263
289
  */
@@ -1,3 +1,5 @@
1
+ import BN from 'bn.js'
2
+
1
3
  /**
2
4
  * Token Program instruction layouts
3
5
  * Token-2022 (Token Extensions) is backward compatible with these instruction types
@@ -86,13 +88,93 @@ export const TOKEN_INSTRUCTION_LAYOUTS = Object.freeze({
86
88
  UiAmountToAmount: {
87
89
  index: 24,
88
90
  },
89
- WithdrawExcessLamports: {
90
- index: 38,
91
+ // Token-2022 Extension Instructions
92
+ InitializeMintCloseAuthority: {
93
+ index: 25,
91
94
  },
92
- UnwrapLamports: {
93
- index: 45,
95
+ TransferFeeExtension: {
96
+ index: 26,
97
+ // Used for TransferCheckedWithFee operations
98
+ // Sub-instructions: InitializeTransferFeeConfig(0), TransferCheckedWithFee(1), etc.
94
99
  },
95
- Batch: {
96
- index: 255,
100
+ WithdrawExcessLamports: {
101
+ index: 38,
97
102
  },
98
103
  })
104
+
105
+ /**
106
+ * TransferFeeExtension sub-instruction types
107
+ */
108
+ export const TRANSFER_FEE_SUB_INSTRUCTIONS = Object.freeze({
109
+ InitializeTransferFeeConfig: 0,
110
+ TransferCheckedWithFee: 1,
111
+ WithdrawWithheldTokensFromMint: 2,
112
+ WithdrawWithheldTokensFromAccounts: 3,
113
+ HarvestWithheldTokensToMint: 4,
114
+ SetTransferFee: 5,
115
+ })
116
+
117
+ /**
118
+ * Token Instruction class
119
+ */
120
+ export const TokenInstruction = {
121
+ /**
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
125
+ */
126
+ decodeInstructionData(data) {
127
+ // Ensure we have a Buffer to work with
128
+ const buffer = Buffer.from(data)
129
+
130
+ if (buffer.length < 9) throw new Error('Instruction data too short for SPL transfer')
131
+
132
+ const instructionType = buffer[0]
133
+
134
+ // Handle TransferFeeExtension (Token-2022 instruction 26)
135
+ 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
+ }
159
+ }
160
+
161
+ // Other TransferFeeExtension sub-instructions not supported yet
162
+ throw new Error(`Unsupported TransferFeeExtension sub-instruction: ${subInstructionType}`)
163
+ }
164
+
165
+ const amountBytes = buffer.slice(1, 9)
166
+ const amountBN = new BN(amountBytes, 'le')
167
+
168
+ if (instructionType === TOKEN_INSTRUCTION_LAYOUTS.Transfer.index) {
169
+ return { amount: amountBN, method: 'transfer' }
170
+ }
171
+
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 }
176
+ }
177
+
178
+ throw new Error(`Unsupported instruction type: ${instructionType}`)
179
+ },
180
+ }