@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 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.0",
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": "273cd4583facb72bc145c5cdfe29063f78b13491"
50
+ "gitHead": "99b6365c54f3b11fda999c2f89c82bdc354fc073"
51
51
  }
@@ -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
- // https://github.com/paul-schaaf/spl-token-ui/blob/main/src/solana/token/editing.ts#L211
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,
@@ -8,7 +8,7 @@ import { createTransferCheckedWithFeeInstruction } from './helpers/spl-token-202
8
8
  import {
9
9
  createAssociatedTokenAccount,
10
10
  createCloseAccountInstruction,
11
- createTokenTransferInstruction,
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 = createTokenTransferInstruction(
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
 
@@ -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 }) => {