@exodus/solana-lib 3.21.1 → 3.22.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.22.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.21.1...@exodus/solana-lib@3.22.0) (2026-03-13)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: solana add instruction shape validation (#7575)
13
+
14
+
15
+
6
16
  ## [3.21.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.21.0...@exodus/solana-lib@3.21.1) (2026-03-12)
7
17
 
8
18
  **Note:** Version bump only for package @exodus/solana-lib
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-lib",
3
- "version": "3.21.1",
3
+ "version": "3.22.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",
@@ -48,5 +48,5 @@
48
48
  "type": "git",
49
49
  "url": "git+https://github.com/ExodusMovement/assets.git"
50
50
  },
51
- "gitHead": "1c57290eed9f84bfd28f25948796f52f94e8a52f"
51
+ "gitHead": "cf39bddd95d88ea2e99809810172368a5d4adc97"
52
52
  }
@@ -4,9 +4,64 @@ import {
4
4
  TOKEN_2022_PROGRAM_ID,
5
5
  TOKEN_PROGRAM_ID,
6
6
  } from '../constants.js'
7
- import { TOKEN_INSTRUCTION_LAYOUTS, TRANSFER_FEE_SUB_INSTRUCTIONS } from '../vendor/index.js'
7
+ import {
8
+ SYSTEM_INSTRUCTION_LAYOUTS,
9
+ TOKEN_INSTRUCTION_LAYOUTS,
10
+ TRANSFER_FEE_SUB_INSTRUCTIONS,
11
+ } from '../vendor/index.js'
8
12
  import { toBuffer } from '../vendor/utils/to-buffer.js'
9
13
 
14
+ /** Token instruction type index → { minBytes, minAccounts } for hasValidShape. Uses TOKEN_INSTRUCTION_LAYOUTS indices. */
15
+ export const TOKEN_INSTRUCTION_REQUIREMENTS = Object.freeze({
16
+ [TOKEN_INSTRUCTION_LAYOUTS.InitializeAccount.index]: { minBytes: 1, minAccounts: 4 },
17
+ [TOKEN_INSTRUCTION_LAYOUTS.InitializeAccount2.index]: { minBytes: 33, minAccounts: 3 },
18
+ [TOKEN_INSTRUCTION_LAYOUTS.InitializeAccount3.index]: { minBytes: 33, minAccounts: 2 },
19
+ [TOKEN_INSTRUCTION_LAYOUTS.Transfer.index]: { minBytes: 9, minAccounts: 3 },
20
+ [TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index]: { minBytes: 10, minAccounts: 4 },
21
+ [TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index]: { minBytes: 19, minAccounts: 4 },
22
+ [TOKEN_INSTRUCTION_LAYOUTS.CloseAccount.index]: { minBytes: 1, minAccounts: 3 },
23
+ [TOKEN_INSTRUCTION_LAYOUTS.Approve.index]: { minBytes: 9, minAccounts: 3 },
24
+ [TOKEN_INSTRUCTION_LAYOUTS.ApproveChecked.index]: { minBytes: 10, minAccounts: 4 },
25
+ [TOKEN_INSTRUCTION_LAYOUTS.Revoke.index]: { minBytes: 1, minAccounts: 2 },
26
+ [TOKEN_INSTRUCTION_LAYOUTS.SyncNative.index]: { minBytes: 1, minAccounts: 1 },
27
+ [TOKEN_INSTRUCTION_LAYOUTS.SetAuthority.index]: { minBytes: 35, minAccounts: 2 },
28
+ })
29
+
30
+ /** System program instruction type index → { minBytes, minAccounts } for hasValidShape. */
31
+ export const SYSTEM_INSTRUCTION_REQUIREMENTS = Object.freeze({
32
+ [SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index]: { minBytes: 12, minAccounts: 2 },
33
+ })
34
+
35
+ /**
36
+ * Checks that an instruction has the required shape (data length and account count).
37
+ * Works with both legacy shape (keys/data) and normalized shape (accounts/data).
38
+ *
39
+ * @param {Object} instruction - Instruction object with `data` and either `keys` or `accounts`
40
+ * @param {Object} reqs - Requirements: `{ minBytes?: number, minAccounts?: number }`
41
+ * @returns {boolean}
42
+ */
43
+ export function hasValidShape(instruction, reqs) {
44
+ if (!reqs) return false
45
+ let data = null
46
+ if (instruction?.data != null) {
47
+ try {
48
+ data = toBuffer(instruction.data)
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ const minBytes = reqs.minBytes ?? 0
55
+ const minAccounts = reqs.minAccounts ?? 0
56
+ const accounts = instruction?.keys ?? instruction?.accounts
57
+ return (
58
+ data != null &&
59
+ data.length >= minBytes &&
60
+ Array.isArray(accounts) &&
61
+ accounts.length >= minAccounts
62
+ )
63
+ }
64
+
10
65
  export function isTokenProgramInstruction(instruction) {
11
66
  const programId = instruction?.programId
12
67
  if (!programId) return false
@@ -16,12 +71,11 @@ export function isTokenProgramInstruction(instruction) {
16
71
  export function isSystemTransferInstruction(instruction) {
17
72
  const programId = instruction?.programId
18
73
  if (!programId || !programId.equals(SYSTEM_PROGRAM_ID)) return false
19
- if (!Array.isArray(instruction.keys) || instruction.keys.length !== 2) return false
20
- if (!instruction.data) return false
74
+ const reqs = SYSTEM_INSTRUCTION_REQUIREMENTS[SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index]
75
+ if (!hasValidShape(instruction, reqs)) return false
21
76
  try {
22
77
  const buffer = toBuffer(instruction.data)
23
- if (buffer.length < 12) return false
24
- return buffer.readUInt32LE(0) === 2
78
+ return buffer.readUInt32LE(0) === SYSTEM_INSTRUCTION_LAYOUTS.Transfer.index
25
79
  } catch {
26
80
  return false
27
81
  }
@@ -35,31 +89,34 @@ export function isComputeBudgetInstruction(instruction) {
35
89
 
36
90
  export function isSetAuthorityInstruction(instruction) {
37
91
  if (!isTokenProgramInstruction(instruction)) return false
38
- if (!instruction.data) return false
92
+ const reqs = TOKEN_INSTRUCTION_REQUIREMENTS[TOKEN_INSTRUCTION_LAYOUTS.SetAuthority.index]
93
+ if (!hasValidShape(instruction, reqs)) return false
39
94
  const buffer = toBuffer(instruction.data)
40
- return buffer.length > 0 && buffer[0] === TOKEN_INSTRUCTION_LAYOUTS.SetAuthority.index
95
+ return buffer[0] === TOKEN_INSTRUCTION_LAYOUTS.SetAuthority.index
41
96
  }
42
97
 
43
98
  export function isTransferInstruction(instruction) {
44
99
  if (!isTokenProgramInstruction(instruction)) return false
45
- if (!instruction.data) return false
100
+ const reqs = TOKEN_INSTRUCTION_REQUIREMENTS[TOKEN_INSTRUCTION_LAYOUTS.Transfer.index]
101
+ if (!hasValidShape(instruction, reqs)) return false
46
102
  const buffer = toBuffer(instruction.data)
47
- return buffer.length > 0 && buffer[0] === TOKEN_INSTRUCTION_LAYOUTS.Transfer.index
103
+ return buffer[0] === TOKEN_INSTRUCTION_LAYOUTS.Transfer.index
48
104
  }
49
105
 
50
106
  export function isTransferCheckedInstruction(instruction) {
51
107
  if (!isTokenProgramInstruction(instruction)) return false
52
- if (!instruction.data) return false
108
+ const reqs = TOKEN_INSTRUCTION_REQUIREMENTS[TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index]
109
+ if (!hasValidShape(instruction, reqs)) return false
53
110
  const buffer = toBuffer(instruction.data)
54
- return buffer.length > 0 && buffer[0] === TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index
111
+ return buffer[0] === TOKEN_INSTRUCTION_LAYOUTS.TransferChecked.index
55
112
  }
56
113
 
57
114
  export function isTransferCheckedWithFeeInstruction(instruction) {
58
115
  if (!isTokenProgramInstruction(instruction)) return false
59
- if (!instruction.data) return false
116
+ const reqs = TOKEN_INSTRUCTION_REQUIREMENTS[TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index]
117
+ if (!hasValidShape(instruction, reqs)) return false
60
118
  const buffer = toBuffer(instruction.data)
61
119
  return (
62
- buffer.length >= 2 &&
63
120
  buffer[0] === TOKEN_INSTRUCTION_LAYOUTS.TransferFeeExtension.index &&
64
121
  buffer[1] === TRANSFER_FEE_SUB_INSTRUCTIONS.TransferCheckedWithFee
65
122
  )