@exodus/solana-lib 3.16.0 → 3.18.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 +20 -0
- package/package.json +2 -2
- package/src/tx/parse-tx-buffer.js +207 -79
- package/src/vendor/index.js +1 -0
- package/src/vendor/system-program.js +26 -0
- package/src/vendor/token-program.js +180 -0
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.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.15.3...@exodus/solana-lib@3.18.0) (2025-11-26)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: Solana support Close Authority Sponsorship in fee payer (#6767)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.17.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.15.3...@exodus/solana-lib@3.17.0) (2025-11-13)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat: Solana support Close Authority Sponsorship in fee payer (#6767)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [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
27
|
|
|
8
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-lib",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.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": "4c60cc2d7f07229295b801d9f67a336d314711a0"
|
|
51
51
|
}
|
|
@@ -1,63 +1,97 @@
|
|
|
1
|
-
import BN from 'bn.js'
|
|
2
1
|
import bs58 from 'bs58'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
|
|
4
|
+
import {
|
|
5
|
+
SYSTEM_INSTRUCTION_LAYOUTS,
|
|
6
|
+
SystemInstruction,
|
|
7
|
+
TOKEN_INSTRUCTION_LAYOUTS,
|
|
8
|
+
TokenInstruction,
|
|
9
|
+
} from '../vendor/index.js'
|
|
5
10
|
import { deserializeTransaction } from './common.js'
|
|
6
11
|
|
|
7
|
-
function
|
|
8
|
-
|
|
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
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
if (buffer[0] !== 3) {
|
|
12
|
-
throw new Error(`Unsupported instruction type: ${buffer[0]}`)
|
|
13
|
-
}
|
|
16
|
+
const programId = accountKeys[instruction.programIdIndex]
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
if (!programId.equals(TOKEN_PROGRAM_ID) && !programId.equals(TOKEN_2022_PROGRAM_ID)) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
if (buffer.readUInt32LE(0) !== 2) {
|
|
26
|
-
throw new Error(`Unsupported instruction type for SystemProgram: ${buffer.readUInt32LE(0)}`)
|
|
27
|
-
}
|
|
37
|
+
const instructionType = data[0]
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
// Accept Transfer, TransferChecked, and TransferFeeExtension
|
|
40
|
+
if (
|
|
41
|
+
instructionType !== TOKEN_INSTRUCTION_LAYOUTS.Transfer.index &&
|
|
42
|
+
instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index &&
|
|
43
|
+
instructionType !== TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index
|
|
44
|
+
) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
56
|
}
|
|
57
|
+
|
|
58
|
+
return true
|
|
36
59
|
}
|
|
37
60
|
|
|
38
|
-
// TODO support more tx types/options
|
|
39
61
|
export function isTokenTransfer(tx) {
|
|
40
62
|
const { message } = tx
|
|
41
63
|
const { accountKeys, instructions } = message
|
|
42
64
|
|
|
43
|
-
if (!Array.isArray(accountKeys) ||
|
|
44
|
-
if (!Array.isArray(instructions) || instructions.length !== 3) return false
|
|
65
|
+
if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) return false
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
// Search for any token transfer instruction
|
|
68
|
+
return instructions.some((instruction) => isTokenTransferInstruction(instruction, accountKeys))
|
|
69
|
+
}
|
|
70
|
+
|
|
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
|
|
74
|
+
|
|
75
|
+
const programId = accountKeys[instruction.programIdIndex]
|
|
53
76
|
|
|
54
|
-
if (!
|
|
77
|
+
if (!programId.equals(SYSTEM_PROGRAM_ID)) return false
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
// Must have exactly 2 accounts (from, to)
|
|
80
|
+
if (!Array.isArray(instruction.accounts) || instruction.accounts.length !== 2) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
57
83
|
|
|
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
|
|
58
87
|
try {
|
|
59
|
-
|
|
60
|
-
if (data
|
|
88
|
+
let data = instruction.data
|
|
89
|
+
if (typeof data === 'string') {
|
|
90
|
+
data = bs58.decode(data)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const buffer = Buffer.from(data)
|
|
94
|
+
if (buffer.readUInt32LE(0) !== SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index) return false
|
|
61
95
|
} catch {
|
|
62
96
|
return false
|
|
63
97
|
}
|
|
@@ -69,68 +103,162 @@ export function isSolanaTransfer(tx) {
|
|
|
69
103
|
const { message } = tx
|
|
70
104
|
const { accountKeys, instructions } = message
|
|
71
105
|
|
|
72
|
-
if (!Array.isArray(accountKeys) ||
|
|
73
|
-
if (!Array.isArray(instructions) || instructions.length !== 3) return false
|
|
106
|
+
if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) return false
|
|
74
107
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
return instructions.some((instruction) => isSolTransferInstruction(instruction, accountKeys))
|
|
109
|
+
}
|
|
110
|
+
|
|
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
|
|
80
116
|
}
|
|
81
117
|
|
|
82
|
-
|
|
83
|
-
|
|
118
|
+
// Legacy transactions use accountKeys
|
|
119
|
+
return message.accountKeys
|
|
120
|
+
}
|
|
84
121
|
|
|
85
|
-
|
|
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)
|
|
127
|
+
}
|
|
86
128
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
return {
|
|
130
|
+
programIdIndex: instruction.programIdIndex,
|
|
131
|
+
accounts: instruction.accountKeyIndexes || instruction.accounts,
|
|
132
|
+
data: normalizedData,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
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 []
|
|
92
147
|
}
|
|
93
148
|
|
|
94
|
-
return
|
|
149
|
+
return instructions.map((inst) => normalizeInstruction(inst))
|
|
95
150
|
}
|
|
96
151
|
|
|
97
152
|
// TODO: Unify with parseTransaction in solana-api and use there as well?
|
|
98
|
-
// TODO: support
|
|
153
|
+
// TODO: add support for swap and stake instructions
|
|
99
154
|
export async function parseTxBuffer(buffer, api) {
|
|
100
155
|
const transaction = deserializeTransaction(buffer)
|
|
156
|
+
const { message } = transaction
|
|
157
|
+
|
|
158
|
+
// Normalize account keys and instructions (versioned vs legacy)
|
|
159
|
+
const accountKeys = getAccountKeys(message)
|
|
160
|
+
const instructions = getInstructions(message)
|
|
161
|
+
|
|
162
|
+
if (!Array.isArray(accountKeys) || !Array.isArray(instructions)) {
|
|
163
|
+
throw new TypeError('Invalid transaction structure')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const parsedInstructions = []
|
|
101
167
|
|
|
102
168
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
method
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
})
|
|
117
239
|
}
|
|
118
240
|
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.log('instruction parsing error', error)
|
|
243
|
+
throw error
|
|
244
|
+
}
|
|
119
245
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
246
|
+
if (parsedInstructions.length === 0) {
|
|
247
|
+
throw new Error('No supported instructions found in transaction')
|
|
248
|
+
}
|
|
123
249
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
250
|
+
return parsedInstructions
|
|
251
|
+
}
|
|
252
|
+
|
|
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
|
|
130
258
|
}
|
|
131
259
|
} catch (error) {
|
|
132
|
-
console.log('
|
|
260
|
+
console.log('Error getting mint address:', error)
|
|
133
261
|
}
|
|
134
262
|
|
|
135
|
-
|
|
263
|
+
return null
|
|
136
264
|
}
|
package/src/vendor/index.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import BN from 'bn.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token Program instruction layouts
|
|
5
|
+
* Token-2022 (Token Extensions) is backward compatible with these instruction types
|
|
6
|
+
*
|
|
7
|
+
* Reference: https://github.com/solana-program/token/blob/81ba155af8684c224c943af16ac3d70f5cad5e93/pinocchio/interface/src/instruction.rs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An enumeration of valid SPL Token InstructionType's
|
|
12
|
+
*/
|
|
13
|
+
export const TOKEN_INSTRUCTION_LAYOUTS = Object.freeze({
|
|
14
|
+
InitializeMint: {
|
|
15
|
+
index: 0,
|
|
16
|
+
},
|
|
17
|
+
InitializeAccount: {
|
|
18
|
+
index: 1,
|
|
19
|
+
},
|
|
20
|
+
InitializeMultisig: {
|
|
21
|
+
index: 2,
|
|
22
|
+
},
|
|
23
|
+
Transfer: {
|
|
24
|
+
index: 3,
|
|
25
|
+
// Accounts: [source, destination, authority]
|
|
26
|
+
},
|
|
27
|
+
Approve: {
|
|
28
|
+
index: 4,
|
|
29
|
+
},
|
|
30
|
+
Revoke: {
|
|
31
|
+
index: 5,
|
|
32
|
+
},
|
|
33
|
+
SetAuthority: {
|
|
34
|
+
index: 6,
|
|
35
|
+
},
|
|
36
|
+
MintTo: {
|
|
37
|
+
index: 7,
|
|
38
|
+
},
|
|
39
|
+
Burn: {
|
|
40
|
+
index: 8,
|
|
41
|
+
},
|
|
42
|
+
CloseAccount: {
|
|
43
|
+
index: 9,
|
|
44
|
+
},
|
|
45
|
+
FreezeAccount: {
|
|
46
|
+
index: 10,
|
|
47
|
+
},
|
|
48
|
+
ThawAccount: {
|
|
49
|
+
index: 11,
|
|
50
|
+
},
|
|
51
|
+
TransferChecked: {
|
|
52
|
+
index: 12,
|
|
53
|
+
// Accounts: [source, mint, destination, authority]
|
|
54
|
+
},
|
|
55
|
+
ApproveChecked: {
|
|
56
|
+
index: 13,
|
|
57
|
+
},
|
|
58
|
+
MintToChecked: {
|
|
59
|
+
index: 14,
|
|
60
|
+
},
|
|
61
|
+
BurnChecked: {
|
|
62
|
+
index: 15,
|
|
63
|
+
},
|
|
64
|
+
InitializeAccount2: {
|
|
65
|
+
index: 16,
|
|
66
|
+
},
|
|
67
|
+
SyncNative: {
|
|
68
|
+
index: 17,
|
|
69
|
+
},
|
|
70
|
+
InitializeAccount3: {
|
|
71
|
+
index: 18,
|
|
72
|
+
},
|
|
73
|
+
InitializeMultisig2: {
|
|
74
|
+
index: 19,
|
|
75
|
+
},
|
|
76
|
+
InitializeMint2: {
|
|
77
|
+
index: 20,
|
|
78
|
+
},
|
|
79
|
+
GetAccountDataSize: {
|
|
80
|
+
index: 21,
|
|
81
|
+
},
|
|
82
|
+
InitializeImmutableOwner: {
|
|
83
|
+
index: 22,
|
|
84
|
+
},
|
|
85
|
+
AmountToUiAmount: {
|
|
86
|
+
index: 23,
|
|
87
|
+
},
|
|
88
|
+
UiAmountToAmount: {
|
|
89
|
+
index: 24,
|
|
90
|
+
},
|
|
91
|
+
// Token-2022 Extension Instructions
|
|
92
|
+
InitializeMintCloseAuthority: {
|
|
93
|
+
index: 25,
|
|
94
|
+
},
|
|
95
|
+
TransferFeeExtension: {
|
|
96
|
+
index: 26,
|
|
97
|
+
// Used for TransferCheckedWithFee operations
|
|
98
|
+
// Sub-instructions: InitializeTransferFeeConfig(0), TransferCheckedWithFee(1), etc.
|
|
99
|
+
},
|
|
100
|
+
WithdrawExcessLamports: {
|
|
101
|
+
index: 38,
|
|
102
|
+
},
|
|
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
|
+
}
|