@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 +10 -0
- package/package.json +2 -2
- package/src/tx/parse-tx-buffer.js +220 -69
- package/src/vendor/index.js +1 -0
- package/src/vendor/token-program.js +98 -0
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.
|
|
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": "
|
|
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 {
|
|
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(
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
26
|
+
throw new Error(`Unsupported instruction type: ${instructionType}`)
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
function decodeSystemTransferData(
|
|
22
|
-
const buffer =
|
|
29
|
+
function decodeSystemTransferData(data) {
|
|
30
|
+
const buffer = Buffer.from(data)
|
|
23
31
|
|
|
24
|
-
// 4 bytes
|
|
25
|
-
if (buffer.readUInt32LE(0) !==
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
if (!Array.isArray(instructions) || instructions.length !== 3) return false
|
|
50
|
+
const programId = accountKeys[instruction.programIdIndex]
|
|
45
51
|
|
|
46
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
if (data
|
|
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
|
|
85
|
+
export function isTokenTransfer(tx) {
|
|
69
86
|
const { message } = tx
|
|
70
87
|
const { accountKeys, instructions } = message
|
|
71
88
|
|
|
72
|
-
if (!Array.isArray(accountKeys) ||
|
|
73
|
-
if (!Array.isArray(instructions) || instructions.length !== 3) return false
|
|
89
|
+
if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) return false
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
if (!accountKeys[ix.programIdIndex].equals(SYSTEM_PROGRAM_ID)) return false
|
|
101
|
+
if (!programId.equals(SYSTEM_PROGRAM_ID)) return false
|
|
84
102
|
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
if (data
|
|
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
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
269
|
+
if (parsedInstructions.length === 0) {
|
|
270
|
+
throw new Error('No supported instructions found in transaction')
|
|
271
|
+
}
|
|
123
272
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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('
|
|
283
|
+
console.log('Error getting mint address:', error)
|
|
133
284
|
}
|
|
134
285
|
|
|
135
|
-
|
|
286
|
+
return null
|
|
136
287
|
}
|
package/src/vendor/index.js
CHANGED
|
@@ -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
|
+
})
|