@exodus/solana-lib 3.19.0 → 3.19.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 +10 -0
- package/package.json +2 -2
- package/src/helpers/spl-token.js +75 -0
- package/src/helpers/tokenTransfer.js +46 -1
- package/src/transaction.js +6 -3
- package/src/tx/sign-hardware.js +78 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.19.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.19.0...@exodus/solana-lib@3.19.1) (2026-01-08)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/solana-lib
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## [3.19.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.18.4...@exodus/solana-lib@3.19.0) (2026-01-08)
|
|
7
15
|
|
|
8
16
|
|
|
@@ -11,6 +19,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
|
|
11
19
|
|
|
12
20
|
* feat: filter SOL poisoning txs (#7212)
|
|
13
21
|
|
|
22
|
+
* feat: use checked token transfer instruction (#7219)
|
|
23
|
+
|
|
14
24
|
|
|
15
25
|
|
|
16
26
|
## [3.18.4](https://github.com/ExodusMovement/assets/compare/@exodus/solana-lib@3.18.3...@exodus/solana-lib@3.18.4) (2026-01-06)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-lib",
|
|
3
|
-
"version": "3.19.
|
|
3
|
+
"version": "3.19.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": "99b6365c54f3b11fda999c2f89c82bdc354fc073"
|
|
51
51
|
}
|
package/src/helpers/spl-token.js
CHANGED
|
@@ -470,6 +470,10 @@ export const Token = {
|
|
|
470
470
|
/**
|
|
471
471
|
* Construct a Transfer instruction
|
|
472
472
|
*
|
|
473
|
+
* @deprecated Use createTransferCheckedInstruction instead. The Transfer instruction
|
|
474
|
+
* does not include mint and decimals, causing hardware wallets (like Trezor) to show
|
|
475
|
+
* warnings about unknown token decimals.
|
|
476
|
+
*
|
|
473
477
|
* @param programId SPL Token program account
|
|
474
478
|
* @param source Source account
|
|
475
479
|
* @param destination Destination account
|
|
@@ -520,6 +524,77 @@ export const Token = {
|
|
|
520
524
|
})
|
|
521
525
|
},
|
|
522
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Construct a TransferChecked instruction
|
|
529
|
+
*
|
|
530
|
+
* This is the recommended instruction for token transfers as it includes the mint
|
|
531
|
+
* address and decimals, which allows hardware wallets to properly verify and display
|
|
532
|
+
* token information.
|
|
533
|
+
*
|
|
534
|
+
* @param programId SPL Token program account
|
|
535
|
+
* @param source Source token account
|
|
536
|
+
* @param mint Token mint account
|
|
537
|
+
* @param destination Destination token account
|
|
538
|
+
* @param owner Owner of the source account
|
|
539
|
+
* @param multiSigners Signing accounts if `authority` is a multiSig
|
|
540
|
+
* @param amount Number of tokens to transfer
|
|
541
|
+
* @param decimals Number of decimals for the token
|
|
542
|
+
*/
|
|
543
|
+
createTransferCheckedInstruction(
|
|
544
|
+
programId,
|
|
545
|
+
source,
|
|
546
|
+
mint,
|
|
547
|
+
destination,
|
|
548
|
+
owner,
|
|
549
|
+
multiSigners,
|
|
550
|
+
amount,
|
|
551
|
+
decimals
|
|
552
|
+
) {
|
|
553
|
+
const dataLayout = BufferLayout.struct([
|
|
554
|
+
BufferLayout.u8('instruction'),
|
|
555
|
+
Layout.uint64('amount'),
|
|
556
|
+
BufferLayout.u8('decimals'),
|
|
557
|
+
])
|
|
558
|
+
|
|
559
|
+
const data = Buffer.alloc(dataLayout.span)
|
|
560
|
+
dataLayout.encode(
|
|
561
|
+
{
|
|
562
|
+
instruction: 12, // TransferChecked instruction
|
|
563
|
+
amount: new U64(amount).toBuffer(),
|
|
564
|
+
decimals,
|
|
565
|
+
},
|
|
566
|
+
data
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
const keys = [
|
|
570
|
+
{ pubkey: source, isSigner: false, isWritable: true },
|
|
571
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
572
|
+
{ pubkey: destination, isSigner: false, isWritable: true },
|
|
573
|
+
]
|
|
574
|
+
if (multiSigners.length === 0) {
|
|
575
|
+
keys.push({
|
|
576
|
+
pubkey: owner,
|
|
577
|
+
isSigner: true,
|
|
578
|
+
isWritable: false,
|
|
579
|
+
})
|
|
580
|
+
} else {
|
|
581
|
+
keys.push({ pubkey: owner, isSigner: false, isWritable: false })
|
|
582
|
+
multiSigners.forEach((signer) =>
|
|
583
|
+
keys.push({
|
|
584
|
+
pubkey: signer.publicKey,
|
|
585
|
+
isSigner: true,
|
|
586
|
+
isWritable: false,
|
|
587
|
+
})
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return new TransactionInstruction({
|
|
592
|
+
keys,
|
|
593
|
+
programId,
|
|
594
|
+
data,
|
|
595
|
+
})
|
|
596
|
+
},
|
|
597
|
+
|
|
523
598
|
decode(data) {
|
|
524
599
|
return BufferLayout.struct([
|
|
525
600
|
publicKey('mint'),
|
|
@@ -59,7 +59,11 @@ function createIx(
|
|
|
59
59
|
})
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated Use createTokenTransferCheckedInstruction instead. The Transfer instruction
|
|
64
|
+
* does not include mint and decimals, causing hardware wallets (like Trezor) to show
|
|
65
|
+
* warnings about unknown token decimals.
|
|
66
|
+
*/
|
|
63
67
|
export const createTokenTransferInstruction = (owner, fromTokenAddress, to, amount) => {
|
|
64
68
|
const sourcePubkey = new PublicKey(fromTokenAddress) // the token ADDRESS needed!
|
|
65
69
|
const destinationPubkey = new PublicKey(to)
|
|
@@ -75,6 +79,47 @@ export const createTokenTransferInstruction = (owner, fromTokenAddress, to, amou
|
|
|
75
79
|
)
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Create a token transfer instruction using TransferChecked.
|
|
84
|
+
* This is the recommended method as it includes mint and decimals, which allows
|
|
85
|
+
* hardware wallets to properly verify and display token information.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} owner - The owner of the source token account (native SOL address)
|
|
88
|
+
* @param {string} fromTokenAddress - The source token account address
|
|
89
|
+
* @param {string} mintAddress - The token mint address
|
|
90
|
+
* @param {string} to - The destination token account address
|
|
91
|
+
* @param {BN|string} amount - The amount to transfer (in base units)
|
|
92
|
+
* @param {number} decimals - The token decimals
|
|
93
|
+
* @param {string} [tokenProgram] - The token program ID (defaults to TOKEN_PROGRAM_ID)
|
|
94
|
+
* @returns {TransactionInstruction}
|
|
95
|
+
*/
|
|
96
|
+
export const createTokenTransferCheckedInstruction = (
|
|
97
|
+
owner,
|
|
98
|
+
fromTokenAddress,
|
|
99
|
+
mintAddress,
|
|
100
|
+
to,
|
|
101
|
+
amount,
|
|
102
|
+
decimals,
|
|
103
|
+
tokenProgram = TOKEN_PROGRAM_ID.toBase58()
|
|
104
|
+
) => {
|
|
105
|
+
const sourcePubkey = new PublicKey(fromTokenAddress)
|
|
106
|
+
const mintPubkey = new PublicKey(mintAddress)
|
|
107
|
+
const destinationPubkey = new PublicKey(to)
|
|
108
|
+
const ownerPubkey = new PublicKey(owner)
|
|
109
|
+
const tokenProgramId = new PublicKey(tokenProgram)
|
|
110
|
+
|
|
111
|
+
return Token.createTransferCheckedInstruction(
|
|
112
|
+
tokenProgramId,
|
|
113
|
+
sourcePubkey,
|
|
114
|
+
mintPubkey,
|
|
115
|
+
destinationPubkey,
|
|
116
|
+
ownerPubkey,
|
|
117
|
+
[],
|
|
118
|
+
amount.toString(),
|
|
119
|
+
decimals
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
78
123
|
export const createCloseAccountInstruction = ({
|
|
79
124
|
programId = TOKEN_PROGRAM_ID,
|
|
80
125
|
tokenPublicKey,
|
package/src/transaction.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createTransferCheckedWithFeeInstruction } from './helpers/spl-token-202
|
|
|
8
8
|
import {
|
|
9
9
|
createAssociatedTokenAccount,
|
|
10
10
|
createCloseAccountInstruction,
|
|
11
|
-
|
|
11
|
+
createTokenTransferCheckedInstruction,
|
|
12
12
|
} from './helpers/tokenTransfer.js'
|
|
13
13
|
import { MagicEdenEscrowProgram } from './magiceden/escrow-program.js'
|
|
14
14
|
import {
|
|
@@ -208,11 +208,14 @@ class Tx {
|
|
|
208
208
|
decimals // token decimals
|
|
209
209
|
)
|
|
210
210
|
} else {
|
|
211
|
-
tokenTransferInstruction =
|
|
211
|
+
tokenTransferInstruction = createTokenTransferCheckedInstruction(
|
|
212
212
|
from,
|
|
213
213
|
tokenAccountAddress,
|
|
214
|
+
tokenMintAddress,
|
|
214
215
|
dest,
|
|
215
|
-
amountToSend
|
|
216
|
+
amountToSend,
|
|
217
|
+
decimals,
|
|
218
|
+
tokenProgram
|
|
216
219
|
)
|
|
217
220
|
}
|
|
218
221
|
|
package/src/tx/sign-hardware.js
CHANGED
|
@@ -1,11 +1,84 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
+
import { TOKEN_PROGRAM_ID } from '../constants.js'
|
|
3
4
|
import { PublicKey } from '../vendor/index.js'
|
|
4
5
|
import { extractTransaction } from './common.js'
|
|
5
6
|
import { prepareForSigning } from './prepare-for-signing.js'
|
|
6
7
|
|
|
8
|
+
// Instruction types
|
|
9
|
+
const TOKEN_INSTRUCTION_TRANSFER_CHECKED = 12
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract token transfer info from a VersionedTransaction.
|
|
13
|
+
*/
|
|
14
|
+
const extractTransferInfo = (tx) => {
|
|
15
|
+
const message = tx.message
|
|
16
|
+
const accountKeys = message.staticAccountKeys.map((k) => k.toBase58())
|
|
17
|
+
|
|
18
|
+
for (const instruction of message.compiledInstructions) {
|
|
19
|
+
const programId = accountKeys[instruction.programIdIndex]
|
|
20
|
+
const accounts = instruction.accountKeyIndexes
|
|
21
|
+
const data = instruction.data
|
|
22
|
+
|
|
23
|
+
// TransferChecked: [source, mint, destination, owner]
|
|
24
|
+
if (
|
|
25
|
+
programId === TOKEN_PROGRAM_ID.toBase58() &&
|
|
26
|
+
data?.[0] === TOKEN_INSTRUCTION_TRANSFER_CHECKED &&
|
|
27
|
+
accounts.length >= 4
|
|
28
|
+
) {
|
|
29
|
+
return {
|
|
30
|
+
sourceTokenAccount: accountKeys[accounts[0]],
|
|
31
|
+
tokenMint: accountKeys[accounts[1]],
|
|
32
|
+
destTokenAccount: accountKeys[accounts[2]],
|
|
33
|
+
sourceOwner: accountKeys[accounts[3]],
|
|
34
|
+
tokenProgram: programId,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build tokenAccountsInfos by looking up token account owners via RPC.
|
|
44
|
+
*/
|
|
45
|
+
const buildTokenAccountsInfos = async (tx, api) => {
|
|
46
|
+
const transferInfo = extractTransferInfo(tx)
|
|
47
|
+
|
|
48
|
+
if (!transferInfo) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Look up destination token account owner via RPC
|
|
53
|
+
try {
|
|
54
|
+
const accountInfo = await api.getAccountInfo(transferInfo.destTokenAccount)
|
|
55
|
+
const destOwner = accountInfo?.data?.parsed?.info?.owner
|
|
56
|
+
|
|
57
|
+
if (!destOwner) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
baseAddress: transferInfo.sourceOwner,
|
|
64
|
+
tokenProgram: transferInfo.tokenProgram,
|
|
65
|
+
tokenMint: transferInfo.tokenMint,
|
|
66
|
+
tokenAccount: transferInfo.sourceTokenAccount,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
baseAddress: destOwner,
|
|
70
|
+
tokenProgram: transferInfo.tokenProgram,
|
|
71
|
+
tokenMint: transferInfo.tokenMint,
|
|
72
|
+
tokenAccount: transferInfo.destTokenAccount,
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
} catch {
|
|
76
|
+
// RPC lookup failed, continue without token account info
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
7
80
|
export const createSignHardwareFactory =
|
|
8
|
-
({ getKeyIdentifier }) =>
|
|
81
|
+
({ getKeyIdentifier, api }) =>
|
|
9
82
|
async ({ unsignedTx, hardwareDevice, accountIndex }) => {
|
|
10
83
|
assert(hardwareDevice, 'expected hardwareDevice to be defined')
|
|
11
84
|
assert(
|
|
@@ -21,10 +94,14 @@ export const createSignHardwareFactory =
|
|
|
21
94
|
accountIndex,
|
|
22
95
|
})
|
|
23
96
|
|
|
97
|
+
// Build token account infos with RPC lookup for token transfers
|
|
98
|
+
const tokenAccountsInfos = api ? await buildTokenAccountsInfos(tx, api) : undefined
|
|
99
|
+
|
|
24
100
|
const signatures = await hardwareDevice.signTransaction({
|
|
25
101
|
assetName: 'solana',
|
|
26
102
|
signableTransaction: Buffer.from(tx.message.serialize()),
|
|
27
103
|
derivationPaths: [derivationPath],
|
|
104
|
+
tokenAccountsInfos,
|
|
28
105
|
})
|
|
29
106
|
|
|
30
107
|
signatures.forEach(({ publicKey, signature }) => {
|