@exodus/solana-lib 3.22.6 → 3.23.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,22 @@
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.23.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.22.5...@exodus/solana-lib@3.23.0) (2026-04-21)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: add MPC signing from desktop (#7822)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+
18
+ * fix(solana-lib): pass missing fee argument to createTransferCheckedWithFeeInstruction (#7784)
19
+
20
+
21
+
6
22
  ## [3.22.6](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.22.5...@exodus/solana-lib@3.22.6) (2026-04-14)
7
23
 
8
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-lib",
3
- "version": "3.22.6",
3
+ "version": "3.23.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",
@@ -28,6 +28,7 @@
28
28
  "@exodus/currency": "^6.0.1",
29
29
  "@exodus/key-utils": "^3.7.0",
30
30
  "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc4",
31
+ "@noble/ed25519": "^1.7.5",
31
32
  "bn.js": "^5.2.1",
32
33
  "borsh": "^0.7.0",
33
34
  "bs58": "^4.0.1",
@@ -48,5 +49,5 @@
48
49
  "type": "git",
49
50
  "url": "git+https://github.com/ExodusMovement/assets.git"
50
51
  },
51
- "gitHead": "295b2329dc3925220d02e7c19c3eb21870276ba4"
52
+ "gitHead": "a500e7aa96e24bfc673f1bb354eb0f662f7d5d82"
52
53
  }
@@ -190,6 +190,25 @@ export function decodeTransferCheckedWithFeeInstructionUnchecked({
190
190
 
191
191
  // utils
192
192
 
193
+ const ONE_IN_BASIS_POINTS = new BN(10_000)
194
+
195
+ /**
196
+ * Calculate the transfer fee for a Token-2022 transfer.
197
+ * Uses the same ceiling-division formula as the on-chain program.
198
+ *
199
+ * @param amount BN — the pre-fee transfer amount
200
+ * @param feeBasisPoints number — transfer fee in basis points (0 or absent means no fee)
201
+ * @param maximumFee number|string — maximum fee cap
202
+ * @returns BN — the calculated fee
203
+ */
204
+ export function calculateTransferFee(amount, feeBasisPoints, maximumFee) {
205
+ if (!feeBasisPoints) return new BN(0)
206
+ const numerator = amount.mul(new BN(feeBasisPoints))
207
+ const rawFee = numerator.add(ONE_IN_BASIS_POINTS).subn(1).div(ONE_IN_BASIS_POINTS)
208
+ if (maximumFee == null) return rawFee
209
+ return BN.min(rawFee, new BN(maximumFee))
210
+ }
211
+
193
212
  export function addSigners(
194
213
  keys, // AccountMeta[],
195
214
  ownerOrAuthority, // PublicKey,
package/src/index.js CHANGED
@@ -22,3 +22,4 @@ export {
22
22
  createApproveDelegationTx,
23
23
  createRevokeDelegationTx,
24
24
  } from './helpers/token-delegation.js'
25
+ export { createAndSignTxWithMpcKey, getAddressFromMpcKey, isMpcKey } from './mpc.js'
package/src/mpc.js ADDED
@@ -0,0 +1,76 @@
1
+ import { hash } from '@exodus/crypto/hash'
2
+ import { CURVE, getPublicKey, Point, utils } from '@noble/ed25519'
3
+ import bs58 from 'bs58'
4
+
5
+ import { getAddressFromPublicKey } from './encode.js'
6
+ import { createUnsignedTx, signUnsignedTxWithSigner } from './tx/index.js'
7
+
8
+ const N = CURVE.n
9
+ const G = Point.BASE
10
+
11
+ const bytesToNumberLE = (buf) => BigInt('0x' + Buffer.from(buf).reverse().toString('hex'))
12
+ const pad = (num, pad) => num.toString(16).padStart(pad, '0')
13
+ const hexToBytesPaddedLE = (num) => utils.hexToBytes(pad(num, 32 * 2)).reverse()
14
+ const modLE = (hash) => utils.mod(bytesToNumberLE(hash), N)
15
+ const sha512 = (data) => hash('sha512', data)
16
+
17
+ const getExtendedPublicKey = async (priv) => {
18
+ const hashed = await sha512(priv)
19
+ const prefix = hashed.slice(32, 64) // ignore the first 32 bytes generally used to generate scalar
20
+ const scalar = modLE(priv) // interpret private key bytes directly as scalar
21
+ const point = G.multiply(scalar) // public key point
22
+ const pointBytes = point.toRawBytes() // point serialized to Uint8Array
23
+ return { prefix, scalar, point, pointBytes }
24
+ }
25
+
26
+ const sign = async (data, privKey) => {
27
+ const { pointBytes: P, scalar: s, prefix } = await getExtendedPublicKey(privKey)
28
+ const rBytes = await sha512(utils.concatBytes(prefix, data)) // r = SHA512(dom2(F, C) || prefix || PH(M))
29
+ const r = modLE(rBytes)
30
+ const R = G.multiply(r).toRawBytes() // R = [r]B
31
+ const hashable = utils.concatBytes(R, P, data) // dom2(F, C) || R || A || PH(M)
32
+ const hashed = await sha512(hashable)
33
+ const signature = utils.mod(r + modLE(hashed) * s, N) // S = (r + k * s) mod L; 0 <= s < l
34
+ return utils.concatBytes(R, hexToBytesPaddedLE(signature))
35
+ }
36
+
37
+ const multiplyWithBase = (scalar) => {
38
+ const scalarNumber = bytesToNumberLE(scalar)
39
+ return G.multiply(scalarNumber)
40
+ }
41
+
42
+ export async function createAndSignTxWithMpcKey(input, key) {
43
+ const unsignedTx = createUnsignedTx(input)
44
+ const decoded = bs58.decode(key)
45
+ const publicKey = decoded.slice(32)
46
+ const privateScalar = decoded.slice(0, 32)
47
+
48
+ return signUnsignedTxWithSigner(unsignedTx, {
49
+ getPublicKey: async () => publicKey,
50
+ sign: async ({ data }) => sign(data, privateScalar),
51
+ })
52
+ }
53
+
54
+ export const getAddressFromMpcKey = (key) => {
55
+ const decoded = bs58.decode(key)
56
+ return getAddressFromPublicKey(decoded.slice(32))
57
+ }
58
+
59
+ export const isMpcKey = async (key) => {
60
+ try {
61
+ const decoded = bs58.decode(key)
62
+ if (decoded.length !== 64) return false
63
+ const seedOrScalar = decoded.slice(0, 32)
64
+ const publicKey = decoded.slice(32)
65
+ const derivedPublicKey = await getPublicKey(seedOrScalar)
66
+
67
+ if (Buffer.compare(derivedPublicKey, publicKey) === 0) {
68
+ return false // PK is a seed not a scalar
69
+ }
70
+
71
+ const directlyDerived = await multiplyWithBase(seedOrScalar)
72
+ return Buffer.compare(directlyDerived.toRawBytes(), publicKey) === 0
73
+ } catch {
74
+ return false
75
+ }
76
+ }
@@ -4,7 +4,10 @@ import assert from 'minimalistic-assert'
4
4
 
5
5
  import { SEED, STAKE_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from './constants.js'
6
6
  import { createStakeAddress, findAssociatedTokenAddress } from './encode.js'
7
- import { createTransferCheckedWithFeeInstruction } from './helpers/spl-token-2022.js'
7
+ import {
8
+ calculateTransferFee,
9
+ createTransferCheckedWithFeeInstruction,
10
+ } from './helpers/spl-token-2022.js'
8
11
  import {
9
12
  createAssociatedTokenAccount,
10
13
  createCloseAccountInstruction,
@@ -175,6 +178,8 @@ class Tx {
175
178
  decimals,
176
179
  isDelegated,
177
180
  delegatedAmount,
181
+ feeBasisPoints,
182
+ maximumFee,
178
183
  } of fromTokenAddresses) {
179
184
  // need to add more of this instruction until we reach the desired balance (amount) to send
180
185
  assert(mintAddress === tokenMintAddress, `Got unexpected mintAddress ${mintAddress}`)
@@ -207,13 +212,15 @@ class Tx {
207
212
  : to
208
213
  let tokenTransferInstruction
209
214
  if (tokenProgram === TOKEN_2022_PROGRAM_ID.toBase58()) {
215
+ const transferFee = calculateTransferFee(amountToSend, feeBasisPoints, maximumFee)
210
216
  tokenTransferInstruction = createTransferCheckedWithFeeInstruction(
211
217
  tokenAccountAddress,
212
218
  tokenMintAddress,
213
219
  dest,
214
220
  from,
215
221
  amountToSend,
216
- decimals // token decimals
222
+ decimals,
223
+ transferFee
217
224
  )
218
225
  } else {
219
226
  tokenTransferInstruction = createTokenTransferCheckedInstruction(