@exodus/solana-lib 3.21.1 → 3.22.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 CHANGED
@@ -3,6 +3,24 @@
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.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.22.0...@exodus/solana-lib@3.22.1) (2026-03-18)
7
+
8
+ **Note:** Version bump only for package @exodus/solana-lib
9
+
10
+
11
+
12
+
13
+
14
+ ## [3.22.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.21.1...@exodus/solana-lib@3.22.0) (2026-03-13)
15
+
16
+
17
+ ### Features
18
+
19
+
20
+ * feat: solana add instruction shape validation (#7575)
21
+
22
+
23
+
6
24
  ## [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
25
 
8
26
  **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.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",
@@ -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": "e746a88ef78a0e7167587bfa14068ceb494d6fd7"
52
52
  }
package/src/keypair.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import {
2
+ // eslint-disable-next-line @exodus/import/no-deprecated
2
3
  edwardsToPublicSync,
4
+ // eslint-disable-next-line @exodus/import/no-deprecated
3
5
  signDetachedSync,
6
+ // eslint-disable-next-line @exodus/import/no-deprecated
4
7
  verifyDetachedSync,
5
8
  } from '@exodus/crypto/curve25519'
9
+ // eslint-disable-next-line @exodus/import/no-deprecated
6
10
  import { randomBytes } from '@exodus/crypto/randomBytes'
7
11
 
8
12
  import { PublicKey } from './vendor/publickey.js'
@@ -10,6 +14,7 @@ import { PublicKey } from './vendor/publickey.js'
10
14
  export function getKeyPairFromPrivateKey(seed) {
11
15
  const pair = Buffer.from(seed, 'hex')
12
16
  const privateKey = pair.subarray(0, 32)
17
+ // eslint-disable-next-line @exodus/import/no-deprecated
13
18
  const publicKey = edwardsToPublicSync({ privateKey, format: 'buffer' })
14
19
 
15
20
  // Recheck just in case
@@ -23,6 +28,7 @@ export function getKeyPairFromPrivateKey(seed) {
23
28
  }
24
29
 
25
30
  export function generateKeyPair() {
31
+ // eslint-disable-next-line @exodus/import/no-deprecated
26
32
  const { publicKey, privateKey } = getKeyPairFromPrivateKey(randomBytes(32))
27
33
 
28
34
  return {
@@ -36,6 +42,7 @@ export function getPublicKey(privateKey) {
36
42
  }
37
43
 
38
44
  export function sign(data, privateKey) {
45
+ // eslint-disable-next-line @exodus/import/no-deprecated
39
46
  return signDetachedSync({
40
47
  message: data,
41
48
  privateKey: Buffer.from(privateKey, 'hex'),
@@ -44,5 +51,6 @@ export function sign(data, privateKey) {
44
51
  }
45
52
 
46
53
  export function verifySignature(data, signature, publicKey) {
54
+ // eslint-disable-next-line @exodus/import/no-deprecated
47
55
  return verifyDetachedSync({ message: data, signature, publicKey: Buffer.from(publicKey, 'hex') })
48
56
  }
@@ -1,4 +1,5 @@
1
1
  import * as BufferLayout from '@exodus/buffer-layout'
2
+ // eslint-disable-next-line @exodus/import/no-deprecated
2
3
  import { hashSync } from '@exodus/crypto/hash'
3
4
 
4
5
  import { bnAmountU64, publicKey } from '../vendor/utils/layout.js'
@@ -21,6 +22,7 @@ const idl = {
21
22
  export function sighash(nameSpace, ixName) {
22
23
  const preimage = `${nameSpace}:${ixName}`
23
24
 
25
+ // eslint-disable-next-line @exodus/import/no-deprecated
24
26
  return hashSync('sha256', preimage).slice(0, 8)
25
27
  }
26
28
 
@@ -1,3 +1,4 @@
1
+ // eslint-disable-next-line @exodus/import/no-deprecated
1
2
  import { signDetachedSync } from '@exodus/crypto/curve25519'
2
3
  import bs58 from 'bs58'
3
4
  import assert from 'minimalistic-assert'
@@ -13,6 +14,7 @@ export function signMessage({ message, privateKey }) {
13
14
  !isTransactionMessage(messageBuffer),
14
15
  'attempted to sign transaction using message signing'
15
16
  )
17
+ // eslint-disable-next-line @exodus/import/no-deprecated
16
18
  const signature = signDetachedSync({
17
19
  message: messageBuffer,
18
20
  privateKey: Buffer.from(privateKey, 'hex').subarray(0, 32),
@@ -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
  )
@@ -1,3 +1,4 @@
1
+ // eslint-disable-next-line @exodus/import/no-deprecated
1
2
  import { isNumberUnit } from '@exodus/currency'
2
3
  import { VersionedTransaction } from '@exodus/solana-web3.js'
3
4
  import BN from 'bn.js'
@@ -55,11 +56,13 @@ export function prepareForSigning(unsignedTx, { checkBalances = true } = {}) {
55
56
  const address = from
56
57
 
57
58
  const amount = unitAmount
58
- ? new BN(isNumberUnit(unitAmount) ? unitAmount.toBaseString() : unitAmount).toString()
59
+ ? // eslint-disable-next-line @exodus/import/no-deprecated
60
+ new BN(isNumberUnit(unitAmount) ? unitAmount.toBaseString() : unitAmount).toString()
59
61
  : unitAmount
60
62
 
61
63
  const fee = feeAmount
62
- ? new BN(isNumberUnit(feeAmount) ? feeAmount.toBaseString() : feeAmount).toNumber()
64
+ ? // eslint-disable-next-line @exodus/import/no-deprecated
65
+ new BN(isNumberUnit(feeAmount) ? feeAmount.toBaseString() : feeAmount).toNumber()
63
66
  : feeAmount
64
67
 
65
68
  const txData = { ...unsignedTx.txData, address, amount, fee }
@@ -1,5 +1,7 @@
1
1
  import { typedView } from '@exodus/bytes/array.js'
2
+ // eslint-disable-next-line @exodus/import/no-deprecated
2
3
  import { edwardsToMontgomeryPublicSync } from '@exodus/crypto/curve25519'
4
+ // eslint-disable-next-line @exodus/import/no-deprecated
3
5
  import { hashSync } from '@exodus/crypto/hash'
4
6
  import BN from 'bn.js'
5
7
  import bs58 from 'bs58'
@@ -121,6 +123,7 @@ export class PublicKey {
121
123
  Buffer.from(seed),
122
124
  programId.toBuffer(),
123
125
  ])
126
+ // eslint-disable-next-line @exodus/import/no-deprecated
124
127
  const hash = hashSync('sha256', buffer)
125
128
  return new PublicKey(hash)
126
129
  }
@@ -138,6 +141,7 @@ export class PublicKey {
138
141
  buffer = Buffer.concat([buffer, Buffer.from(seed)])
139
142
  })
140
143
  buffer = Buffer.concat([buffer, programId.toBuffer(), Buffer.from('ProgramDerivedAddress')])
144
+ // eslint-disable-next-line @exodus/import/no-deprecated
141
145
  const hash = hashSync('sha256', buffer, 'hex')
142
146
  const publicKeyBytes = new BN(hash, 16).toArray(null, 32)
143
147
  if (isOnCurve(publicKeyBytes)) {
@@ -152,6 +156,7 @@ export class PublicKey {
152
156
  function isOnCurve(p) {
153
157
  try {
154
158
  // This tries to parse edwards public key and validates it
159
+ // eslint-disable-next-line @exodus/import/no-deprecated
155
160
  edwardsToMontgomeryPublicSync({ publicKey: Uint8Array.from(p) })
156
161
  return true
157
162
  } catch {}
@@ -1,5 +1,6 @@
1
1
  // https://github.com/solana-labs/solana-web3.js/blob/master/src/transaction.js
2
2
 
3
+ // eslint-disable-next-line @exodus/import/no-deprecated
3
4
  import { signDetachedSync, verifyDetachedSync } from '@exodus/crypto/curve25519'
4
5
  import bs58 from 'bs58'
5
6
  import invariant from 'minimalistic-assert'
@@ -436,6 +437,7 @@ export class Transaction {
436
437
 
437
438
  const signData = message.serialize()
438
439
  signers.forEach((signer) => {
440
+ // eslint-disable-next-line @exodus/import/no-deprecated
439
441
  const signature = signDetachedSync({ message: signData, privateKey: signer.privateKey })
440
442
  this.addSignature(signer.publicKey, signature)
441
443
  })
@@ -474,6 +476,7 @@ export class Transaction {
474
476
  }
475
477
  } else {
476
478
  if (
479
+ // eslint-disable-next-line @exodus/import/no-deprecated
477
480
  !verifyDetachedSync({ message: signData, signature, publicKey: publicKey.toBuffer() })
478
481
  ) {
479
482
  return false