@exodus/solana-lib 3.22.5 → 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 +26 -0
- package/package.json +3 -2
- package/src/helpers/spl-token-2022.js +19 -0
- package/src/index.js +1 -0
- package/src/mpc.js +76 -0
- package/src/transaction.js +9 -2
- package/src/tx/prepare-for-signing.js +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
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
|
+
|
|
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)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
* fix(solana-lib): rename account to tokenPublicKey in createCloseAccountTransaction (#7783)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
## [3.22.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.22.4...@exodus/solana-lib@3.22.5) (2026-04-04)
|
|
7
33
|
|
|
8
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-lib",
|
|
3
|
-
"version": "3.
|
|
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": "
|
|
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
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
|
+
}
|
package/src/transaction.js
CHANGED
|
@@ -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 {
|
|
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
|
|
222
|
+
decimals,
|
|
223
|
+
transferFee
|
|
217
224
|
)
|
|
218
225
|
} else {
|
|
219
226
|
tokenTransferInstruction = createTokenTransferCheckedInstruction(
|
|
@@ -255,10 +255,15 @@ const createTokenTransaction = (
|
|
|
255
255
|
options
|
|
256
256
|
).transaction
|
|
257
257
|
|
|
258
|
-
const createCloseAccountTransaction = ({
|
|
258
|
+
const createCloseAccountTransaction = ({
|
|
259
|
+
account: tokenPublicKey,
|
|
260
|
+
programId,
|
|
261
|
+
recentBlockhash,
|
|
262
|
+
walletPublicKey,
|
|
263
|
+
}) =>
|
|
259
264
|
Transaction.createCloseAccount({
|
|
260
|
-
account,
|
|
261
265
|
programId,
|
|
262
|
-
|
|
266
|
+
tokenPublicKey,
|
|
263
267
|
walletPublicKey,
|
|
268
|
+
recentBlockhash,
|
|
264
269
|
})
|