@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 +20 -0
- package/package.json +2 -2
- package/src/tx/common.js +2 -0
- package/src/tx/parse-tx-buffer.js +24 -47
- package/src/tx/prepare-for-signing.js +5 -0
- package/src/vendor/index.js +1 -1
- package/src/vendor/system-program.js +26 -0
- package/src/vendor/token-program.js +88 -6
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.
|
|
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": "
|
|
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 {
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
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 } =
|
|
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
|
|
package/src/vendor/index.js
CHANGED
|
@@ -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
|
-
|
|
90
|
-
|
|
91
|
+
// Token-2022 Extension Instructions
|
|
92
|
+
InitializeMintCloseAuthority: {
|
|
93
|
+
index: 25,
|
|
91
94
|
},
|
|
92
|
-
|
|
93
|
-
index:
|
|
95
|
+
TransferFeeExtension: {
|
|
96
|
+
index: 26,
|
|
97
|
+
// Used for TransferCheckedWithFee operations
|
|
98
|
+
// Sub-instructions: InitializeTransferFeeConfig(0), TransferCheckedWithFee(1), etc.
|
|
94
99
|
},
|
|
95
|
-
|
|
96
|
-
index:
|
|
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
|
+
}
|