@exodus/solana-lib 3.16.0 → 3.17.0

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,16 @@
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.17.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.15.3...@exodus/solana-lib@3.17.0) (2025-11-13)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: Solana support Close Authority Sponsorship in fee payer (#6767)
13
+
14
+
15
+
6
16
  ## [3.16.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.15.3...@exodus/solana-lib@3.16.0) (2025-11-11)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-lib",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
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": "e8b0a919196eda921b8bc532f5632180c2bef508"
50
+ "gitHead": "6a39e3cdbf3bf1849d6d4aae8e935588332d92ac"
51
51
  }
@@ -1,28 +1,36 @@
1
1
  import BN from 'bn.js'
2
2
  import bs58 from 'bs58'
3
3
 
4
- import { COMPUTE_BUDGET_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
4
+ 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'
5
6
  import { deserializeTransaction } from './common.js'
6
7
 
7
- function decodeSPLTransferData(base58Data) {
8
- const buffer = bs58.decode(base58Data)
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')
9
11
 
10
- // 1 byte
11
- if (buffer[0] !== 3) {
12
- throw new Error(`Unsupported instruction type: ${buffer[0]}`)
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' }
13
18
  }
14
19
 
15
- const amountBytes = buffer.slice(1, 9)
16
- const amountBN = new BN(amountBytes, 'le')
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
+ }
17
25
 
18
- return { amount: amountBN, method: 'transfer' }
26
+ throw new Error(`Unsupported instruction type: ${instructionType}`)
19
27
  }
20
28
 
21
- function decodeSystemTransferData(base58Data) {
22
- const buffer = bs58.decode(base58Data)
29
+ function decodeSystemTransferData(data) {
30
+ const buffer = Buffer.from(data)
23
31
 
24
- // 4 bytes
25
- if (buffer.readUInt32LE(0) !== 2) {
32
+ // 4 bytes - instruction type must be 2 (Transfer)
33
+ if (buffer.readUInt32LE(0) !== SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index) {
26
34
  throw new Error(`Unsupported instruction type for SystemProgram: ${buffer.readUInt32LE(0)}`)
27
35
  }
28
36
 
@@ -35,29 +43,38 @@ function decodeSystemTransferData(base58Data) {
35
43
  }
36
44
  }
37
45
 
38
- // TODO support more tx types/options
39
- export function isTokenTransfer(tx) {
40
- const { message } = tx
41
- const { accountKeys, instructions } = message
46
+ // Helper function to check if an instruction is a token transfer
47
+ function isTokenTransferInstruction(instruction, accountKeys) {
48
+ if (!instruction || instruction.programIdIndex === undefined) return false
42
49
 
43
- if (!Array.isArray(accountKeys) || accountKeys.length !== 6) return false
44
- if (!Array.isArray(instructions) || instructions.length !== 3) return false
50
+ const programId = accountKeys[instruction.programIdIndex]
45
51
 
46
- const [ix1, ix2, ix3] = instructions
47
- if (
48
- !accountKeys[ix1.programIdIndex].equals(COMPUTE_BUDGET_PROGRAM_ID) ||
49
- !accountKeys[ix2.programIdIndex].equals(COMPUTE_BUDGET_PROGRAM_ID)
50
- ) {
52
+ if (!programId.equals(TOKEN_PROGRAM_ID) && !programId.equals(TOKEN_2022_PROGRAM_ID)) {
51
53
  return false
52
54
  }
53
55
 
54
- if (!accountKeys[ix3.programIdIndex].equals(TOKEN_PROGRAM_ID)) return false
55
-
56
- if (!Array.isArray(ix3.accounts) || ix3.accounts.length !== 3) return false
56
+ // Must have at least 3 accounts (from, to, authority, and potentially more for delegate transfers)
57
+ if (!Array.isArray(instruction.accounts) || instruction.accounts.length < 3) {
58
+ return false
59
+ }
57
60
 
61
+ // Check if the instruction data is transfer (0x03) or transferChecked (0x0c/12)
62
+ // Note: Must handle both formats because this is called from isTokenTransfer()
63
+ // which uses non-normalized instructions
58
64
  try {
59
- const data = bs58.decode(ix3.data)
60
- if (data[0] !== 0x03) return false
65
+ let data = instruction.data
66
+ if (typeof data === 'string') {
67
+ data = bs58.decode(data)
68
+ }
69
+
70
+ const instructionType = data[0]
71
+ // Accept both Transfer and TransferChecked
72
+ if (
73
+ instructionType !== TOKEN_INSTRUCTION_LAYOUTS.Transfer.index &&
74
+ instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index
75
+ ) {
76
+ return false
77
+ }
61
78
  } catch {
62
79
  return false
63
80
  }
@@ -65,28 +82,40 @@ export function isTokenTransfer(tx) {
65
82
  return true
66
83
  }
67
84
 
68
- export function isSolanaTransfer(tx) {
85
+ export function isTokenTransfer(tx) {
69
86
  const { message } = tx
70
87
  const { accountKeys, instructions } = message
71
88
 
72
- if (!Array.isArray(accountKeys) || accountKeys.length !== 5) return false
73
- if (!Array.isArray(instructions) || instructions.length !== 3) return false
89
+ if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) return false
74
90
 
75
- if (
76
- !accountKeys[instructions[0].programIdIndex].equals(COMPUTE_BUDGET_PROGRAM_ID) ||
77
- !accountKeys[instructions[1].programIdIndex].equals(COMPUTE_BUDGET_PROGRAM_ID)
78
- ) {
79
- return false
80
- }
91
+ // Search for any token transfer instruction
92
+ return instructions.some((instruction) => isTokenTransferInstruction(instruction, accountKeys))
93
+ }
94
+
95
+ // Helper function to check if an instruction is a SOL transfer
96
+ function isSolTransferInstruction(instruction, accountKeys) {
97
+ if (!instruction || instruction.programIdIndex === undefined) return false
98
+
99
+ const programId = accountKeys[instruction.programIdIndex]
81
100
 
82
- const ix = instructions[2]
83
- if (!accountKeys[ix.programIdIndex].equals(SYSTEM_PROGRAM_ID)) return false
101
+ if (!programId.equals(SYSTEM_PROGRAM_ID)) return false
84
102
 
85
- if (!Array.isArray(ix.accounts) || ix.accounts.length !== 2) return false
103
+ // Must have exactly 2 accounts (from, to)
104
+ if (!Array.isArray(instruction.accounts) || instruction.accounts.length !== 2) {
105
+ return false
106
+ }
86
107
 
108
+ // Check if the instruction data starts with 0x02 (System Transfer)
109
+ // Note: Must handle both formats because this is called from isSolanaTransfer()
110
+ // which uses non-normalized instructions
87
111
  try {
88
- const data = bs58.decode(ix.data)
89
- if (data[0] !== 0x02) return false
112
+ let data = instruction.data
113
+ if (typeof data === 'string') {
114
+ data = bs58.decode(data)
115
+ }
116
+
117
+ const buffer = Buffer.from(data)
118
+ if (buffer.readUInt32LE(0) !== SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index) return false
90
119
  } catch {
91
120
  return false
92
121
  }
@@ -94,43 +123,165 @@ export function isSolanaTransfer(tx) {
94
123
  return true
95
124
  }
96
125
 
126
+ export function isSolanaTransfer(tx) {
127
+ const { message } = tx
128
+ const { accountKeys, instructions } = message
129
+
130
+ if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) return false
131
+
132
+ return instructions.some((instruction) => isSolTransferInstruction(instruction, accountKeys))
133
+ }
134
+
135
+ // Helper to normalize account keys for both versioned and legacy transactions
136
+ function getAccountKeys(message) {
137
+ // Versioned transactions use staticAccountKeys
138
+ if (message.staticAccountKeys) {
139
+ return message.staticAccountKeys
140
+ }
141
+
142
+ // Legacy transactions use accountKeys
143
+ return message.accountKeys
144
+ }
145
+
146
+ // Helper to normalize a single instruction (handles both compiled and regular instructions)
147
+ function normalizeInstruction(instruction) {
148
+ let normalizedData = instruction.data
149
+ if (typeof instruction.data === 'string') {
150
+ normalizedData = bs58.decode(instruction.data)
151
+ }
152
+
153
+ return {
154
+ programIdIndex: instruction.programIdIndex,
155
+ accounts: instruction.accountKeyIndexes || instruction.accounts,
156
+ data: normalizedData,
157
+ }
158
+ }
159
+
160
+ // Helper to normalize instructions for both versioned and legacy transactions
161
+ function getInstructions(message) {
162
+ let instructions
163
+ // Versioned transactions use compiledInstructions
164
+ if (message.compiledInstructions) {
165
+ instructions = message.compiledInstructions
166
+ } else if (message.instructions) {
167
+ // Legacy transactions use instructions
168
+ instructions = message.instructions
169
+ } else {
170
+ return []
171
+ }
172
+
173
+ return instructions.map((inst) => normalizeInstruction(inst))
174
+ }
175
+
97
176
  // TODO: Unify with parseTransaction in solana-api and use there as well?
98
- // TODO: support more tx types.
177
+ // TODO: add support for swap and stake instructions
99
178
  export async function parseTxBuffer(buffer, api) {
100
179
  const transaction = deserializeTransaction(buffer)
180
+ const { message } = transaction
181
+
182
+ // Normalize account keys and instructions (versioned vs legacy)
183
+ const accountKeys = getAccountKeys(message)
184
+ const instructions = getInstructions(message)
185
+
186
+ if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) {
187
+ throw new TypeError('Invalid transaction structure')
188
+ }
189
+
190
+ const parsedInstructions = []
101
191
 
102
192
  try {
103
- if (isTokenTransfer(transaction)) {
104
- const mainInstruction = transaction.message.instructions[2]
105
- const { amount, method } = decodeSPLTransferData(mainInstruction.data)
106
-
107
- const fromTokenAddress =
108
- transaction.message.accountKeys[mainInstruction.accounts[0]].toBase58()
109
- const toTokenAddress = transaction.message.accountKeys[mainInstruction.accounts[1]].toBase58()
110
- const from = await api.getTokenAddressOwner(fromTokenAddress)
111
- const to = await api.getTokenAddressOwner(toTokenAddress)
112
- return {
113
- method,
114
- from,
115
- to,
116
- amount,
193
+ for (const [index, instruction] of instructions.entries()) {
194
+ if (isTokenTransferInstruction(instruction, accountKeys)) {
195
+ const decoded = decodeSPLTransferData(instruction.data)
196
+ const programId = accountKeys[instruction.programIdIndex]
197
+
198
+ // Transfer (3): [source, destination, authority]
199
+ // TransferChecked (12): [source, mint, destination, authority, ...]
200
+ let fromTokenAccount, toTokenAccount, mintAddress
201
+
202
+ if (decoded.method === 'transferChecked') {
203
+ // TransferChecked: mint is at index 1, destination at index 2
204
+ fromTokenAccount = accountKeys[instruction.accounts[0]].toBase58()
205
+ mintAddress = accountKeys[instruction.accounts[1]].toBase58() // Mint directly from accounts
206
+ toTokenAccount = accountKeys[instruction.accounts[2]].toBase58()
207
+ } else {
208
+ // Transfer: destination is at index 1
209
+ fromTokenAccount = accountKeys[instruction.accounts[0]].toBase58()
210
+ toTokenAccount = accountKeys[instruction.accounts[1]].toBase58()
211
+ mintAddress = null
212
+ }
213
+
214
+ // Get token account owners in parallel (requires API calls)
215
+ let fromOwner, toOwner
216
+ if (api) {
217
+ ;[fromOwner, toOwner, mintAddress] = await Promise.all([
218
+ api.getTokenAddressOwner(fromTokenAccount),
219
+ api.getTokenAddressOwner(toTokenAccount),
220
+ mintAddress
221
+ ? Promise.resolve(mintAddress)
222
+ : getMintAddressFromTokenAccount(fromTokenAccount, api),
223
+ ])
224
+ // Fallback to token account address if owner lookup fails
225
+ fromOwner = fromOwner || fromTokenAccount
226
+ toOwner = toOwner || toTokenAccount
227
+ } else {
228
+ fromOwner = fromTokenAccount
229
+ toOwner = toTokenAccount
230
+ }
231
+
232
+ const parsedInstruction = {
233
+ method: decoded.method,
234
+ from: fromOwner,
235
+ to: toOwner,
236
+ amount: decoded.amount,
237
+ programId: programId.toBase58(),
238
+ instructionIndex: index,
239
+ fromTokenAccount,
240
+ toTokenAccount,
241
+ mintAddress,
242
+ }
243
+
244
+ // Include decimals if it's a transferChecked instruction
245
+ if (decoded.decimals !== undefined) {
246
+ parsedInstruction.decimals = decoded.decimals
247
+ }
248
+
249
+ parsedInstructions.push(parsedInstruction)
250
+ } else if (isSolTransferInstruction(instruction, accountKeys)) {
251
+ const { amount, method } = decodeSystemTransferData(instruction.data)
252
+ const programId = accountKeys[instruction.programIdIndex]
253
+
254
+ parsedInstructions.push({
255
+ method,
256
+ from: accountKeys[instruction.accounts[0]].toBase58(),
257
+ to: accountKeys[instruction.accounts[1]].toBase58(),
258
+ amount,
259
+ programId: programId.toBase58(),
260
+ instructionIndex: index,
261
+ })
117
262
  }
118
263
  }
264
+ } catch (error) {
265
+ console.log('instruction parsing error', error)
266
+ throw error
267
+ }
119
268
 
120
- if (isSolanaTransfer(transaction)) {
121
- const mainInstruction = transaction.message.instructions[2]
122
- const { amount, method } = decodeSystemTransferData(mainInstruction.data)
269
+ if (parsedInstructions.length === 0) {
270
+ throw new Error('No supported instructions found in transaction')
271
+ }
123
272
 
124
- return {
125
- method,
126
- from: transaction.message.accountKeys[mainInstruction.accounts[0]],
127
- to: transaction.message.accountKeys[mainInstruction.accounts[1]],
128
- amount,
129
- }
273
+ return parsedInstructions
274
+ }
275
+
276
+ async function getMintAddressFromTokenAccount(tokenAccountAddress, api) {
277
+ try {
278
+ const accountInfo = await api.getAccountInfo(tokenAccountAddress)
279
+ if (accountInfo?.data?.parsed?.info?.mint) {
280
+ return accountInfo.data.parsed.info.mint
130
281
  }
131
282
  } catch (error) {
132
- console.log('transaction check error', error)
283
+ console.log('Error getting mint address:', error)
133
284
  }
134
285
 
135
- throw new Error('Transaction not supported for buffer parsing')
286
+ return null
136
287
  }
@@ -9,3 +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'
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Token Program instruction layouts
3
+ * Token-2022 (Token Extensions) is backward compatible with these instruction types
4
+ *
5
+ * Reference: https://github.com/solana-program/token/blob/81ba155af8684c224c943af16ac3d70f5cad5e93/pinocchio/interface/src/instruction.rs
6
+ */
7
+
8
+ /**
9
+ * An enumeration of valid SPL Token InstructionType's
10
+ */
11
+ export const TOKEN_INSTRUCTION_LAYOUTS = Object.freeze({
12
+ InitializeMint: {
13
+ index: 0,
14
+ },
15
+ InitializeAccount: {
16
+ index: 1,
17
+ },
18
+ InitializeMultisig: {
19
+ index: 2,
20
+ },
21
+ Transfer: {
22
+ index: 3,
23
+ // Accounts: [source, destination, authority]
24
+ },
25
+ Approve: {
26
+ index: 4,
27
+ },
28
+ Revoke: {
29
+ index: 5,
30
+ },
31
+ SetAuthority: {
32
+ index: 6,
33
+ },
34
+ MintTo: {
35
+ index: 7,
36
+ },
37
+ Burn: {
38
+ index: 8,
39
+ },
40
+ CloseAccount: {
41
+ index: 9,
42
+ },
43
+ FreezeAccount: {
44
+ index: 10,
45
+ },
46
+ ThawAccount: {
47
+ index: 11,
48
+ },
49
+ TransferChecked: {
50
+ index: 12,
51
+ // Accounts: [source, mint, destination, authority]
52
+ },
53
+ ApproveChecked: {
54
+ index: 13,
55
+ },
56
+ MintToChecked: {
57
+ index: 14,
58
+ },
59
+ BurnChecked: {
60
+ index: 15,
61
+ },
62
+ InitializeAccount2: {
63
+ index: 16,
64
+ },
65
+ SyncNative: {
66
+ index: 17,
67
+ },
68
+ InitializeAccount3: {
69
+ index: 18,
70
+ },
71
+ InitializeMultisig2: {
72
+ index: 19,
73
+ },
74
+ InitializeMint2: {
75
+ index: 20,
76
+ },
77
+ GetAccountDataSize: {
78
+ index: 21,
79
+ },
80
+ InitializeImmutableOwner: {
81
+ index: 22,
82
+ },
83
+ AmountToUiAmount: {
84
+ index: 23,
85
+ },
86
+ UiAmountToAmount: {
87
+ index: 24,
88
+ },
89
+ WithdrawExcessLamports: {
90
+ index: 38,
91
+ },
92
+ UnwrapLamports: {
93
+ index: 45,
94
+ },
95
+ Batch: {
96
+ index: 255,
97
+ },
98
+ })