@chainlink/ccip-cli 0.94.0 → 0.95.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.
Files changed (37) hide show
  1. package/README.md +96 -40
  2. package/dist/commands/manual-exec.d.ts.map +1 -1
  3. package/dist/commands/manual-exec.js +11 -11
  4. package/dist/commands/manual-exec.js.map +1 -1
  5. package/dist/commands/send.d.ts +5 -9
  6. package/dist/commands/send.d.ts.map +1 -1
  7. package/dist/commands/send.js +56 -52
  8. package/dist/commands/send.js.map +1 -1
  9. package/dist/commands/show.d.ts.map +1 -1
  10. package/dist/commands/show.js +2 -4
  11. package/dist/commands/show.js.map +1 -1
  12. package/dist/commands/supported-tokens.d.ts +2 -2
  13. package/dist/commands/supported-tokens.d.ts.map +1 -1
  14. package/dist/commands/supported-tokens.js +22 -10
  15. package/dist/commands/supported-tokens.js.map +1 -1
  16. package/dist/commands/token.d.ts +26 -0
  17. package/dist/commands/token.d.ts.map +1 -0
  18. package/dist/commands/token.js +105 -0
  19. package/dist/commands/token.js.map +1 -0
  20. package/dist/commands/utils.d.ts.map +1 -1
  21. package/dist/commands/utils.js +21 -7
  22. package/dist/commands/utils.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/providers/index.d.ts.map +1 -1
  27. package/dist/providers/index.js +3 -3
  28. package/dist/providers/index.js.map +1 -1
  29. package/package.json +10 -13
  30. package/src/commands/manual-exec.ts +13 -23
  31. package/src/commands/send.ts +58 -66
  32. package/src/commands/show.ts +1 -4
  33. package/src/commands/supported-tokens.ts +21 -10
  34. package/src/commands/token.ts +132 -0
  35. package/src/commands/utils.ts +23 -6
  36. package/src/index.ts +2 -2
  37. package/src/providers/index.ts +7 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainlink/ccip-cli",
3
- "version": "0.94.0",
3
+ "version": "0.95.0",
4
4
  "description": "CCIP Command Line Interface, based on @chainlink/ccip-sdk",
5
5
  "author": "Chainlink devs",
6
6
  "license": "MIT",
@@ -32,37 +32,34 @@
32
32
  ],
33
33
  "devDependencies": {
34
34
  "@eslint/js": "^9.39.2",
35
- "@types/node": "25.0.8",
35
+ "@types/node": "25.0.10",
36
36
  "@types/yargs": "17.0.35",
37
37
  "eslint": "^9.39.2",
38
38
  "eslint-config-prettier": "10.1.8",
39
39
  "eslint-import-resolver-typescript": "4.4.4",
40
40
  "eslint-plugin-import": "^2.32.0",
41
- "eslint-plugin-jsdoc": "^62.0.0",
41
+ "eslint-plugin-jsdoc": "^62.4.1",
42
42
  "eslint-plugin-prettier": "^5.5.5",
43
43
  "eslint-plugin-tsdoc": "^0.5.0",
44
- "prettier": "^3.7.4",
44
+ "prettier": "^3.8.1",
45
45
  "tsx": "4.21.0",
46
46
  "typescript": "5.9.3",
47
- "typescript-eslint": "8.53.0"
47
+ "typescript-eslint": "8.54.0"
48
48
  },
49
49
  "dependencies": {
50
50
  "@aptos-labs/ts-sdk": "^5.2.1",
51
- "@chainlink/ccip-sdk": "^0.94.0",
51
+ "@chainlink/ccip-sdk": "^0.95.0",
52
52
  "@coral-xyz/anchor": "^0.29.0",
53
53
  "@ethers-ext/signer-ledger": "^6.0.0-beta.1",
54
54
  "@inquirer/prompts": "8.2.0",
55
- "@ledgerhq/hw-app-aptos": "^6.34.12",
56
- "@ledgerhq/hw-app-solana": "^7.6.3",
57
- "@ledgerhq/hw-transport-node-hid": "^6.29.17",
55
+ "@ledgerhq/hw-app-aptos": "^6.35.0",
56
+ "@ledgerhq/hw-app-solana": "^7.7.0",
57
+ "@ledgerhq/hw-transport-node-hid": "^6.30.0",
58
58
  "@solana/web3.js": "^1.98.4",
59
59
  "@ton-community/ton-ledger": "^7.3.0",
60
60
  "bs58": "^6.0.0",
61
61
  "ethers": "6.16.0",
62
- "type-fest": "^5.4.1",
62
+ "type-fest": "^5.4.2",
63
63
  "yargs": "18.0.0"
64
- },
65
- "overrides": {
66
- "@ledgerhq/types-live": "6.90.0"
67
64
  }
68
65
  }
@@ -1,14 +1,9 @@
1
1
  import {
2
- type CCIPRequest,
3
- type CCIPVersion,
4
- type EVMChain,
5
2
  type ExecutionReport,
6
- CCIPChainFamilyUnsupportedError,
7
- ChainFamily,
8
3
  bigIntReplacer,
9
4
  calculateManualExecProof,
10
5
  discoverOffRamp,
11
- estimateExecGasForRequest,
6
+ estimateReceiveExecution,
12
7
  isSupportedTxHash,
13
8
  } from '@chainlink/ccip-sdk/src/index.ts'
14
9
  import type { Argv } from 'yargs'
@@ -180,31 +175,26 @@ async function manualExec(
180
175
  offchainTokenData,
181
176
  }
182
177
 
183
- if (
184
- argv.estimateGasLimit != null &&
185
- 'gasLimit' in request.message &&
186
- 'extraArgs' in request.message
187
- ) {
188
- if (dest.network.family !== ChainFamily.EVM)
189
- throw new CCIPChainFamilyUnsupportedError(dest.network.family, {
190
- context: { feature: 'gas estimation' },
191
- })
192
-
193
- let estimated = await estimateExecGasForRequest(
178
+ if (argv.estimateGasLimit != null) {
179
+ let estimated = await estimateReceiveExecution({
194
180
  source,
195
- dest as unknown as EVMChain,
196
- request as CCIPRequest<typeof CCIPVersion.V1_5 | typeof CCIPVersion.V1_6>,
197
- )
181
+ dest,
182
+ routerOrRamp: offRamp,
183
+ message: request.message,
184
+ })
198
185
  logger.info('Estimated gasLimit override:', estimated)
199
186
  estimated += Math.ceil((estimated * argv.estimateGasLimit) / 100)
200
- if (request.message.gasLimit >= estimated) {
187
+ const origLimit = Number(
188
+ 'gasLimit' in request.message ? request.message.gasLimit : request.message.computeUnits,
189
+ )
190
+ if (origLimit >= estimated) {
201
191
  logger.warn(
202
192
  'Estimated +',
203
193
  argv.estimateGasLimit,
204
- '% margin =',
194
+ '% =',
205
195
  estimated,
206
196
  '< original gasLimit =',
207
- request.message.gasLimit,
197
+ origLimit,
208
198
  '. Leaving unchanged.',
209
199
  )
210
200
  } else {
@@ -1,17 +1,13 @@
1
1
  import {
2
- type CCIPVersion,
3
2
  type ChainStatic,
4
- type EVMChain,
5
3
  type ExtraArgs,
6
4
  type MessageInput,
7
5
  CCIPArgumentInvalidError,
8
- CCIPChainFamilyUnsupportedError,
9
6
  CCIPTokenNotFoundError,
10
7
  ChainFamily,
11
- estimateExecGasForRequest,
8
+ estimateReceiveExecution,
12
9
  getDataBytes,
13
10
  networkInfo,
14
- sourceToDestTokenAmounts,
15
11
  } from '@chainlink/ccip-sdk/src/index.ts'
16
12
  import { type BytesLike, formatUnits, toUtf8Bytes } from 'ethers'
17
13
  import type { Argv } from 'yargs'
@@ -22,8 +18,8 @@ import type { Ctx } from './types.ts'
22
18
  import { getCtx, logParsedError, parseTokenAmounts } from './utils.ts'
23
19
  import { fetchChainsFromRpcs, loadChainWallet } from '../providers/index.ts'
24
20
 
25
- export const command = 'send <source> <router> <dest>'
26
- export const describe = 'Send a CCIP message from router on source to dest'
21
+ export const command = 'send'
22
+ export const describe = 'Send a CCIP message from source to destination chain'
27
23
 
28
24
  /**
29
25
  * Yargs builder for the send command.
@@ -32,108 +28,107 @@ export const describe = 'Send a CCIP message from router on source to dest'
32
28
  */
33
29
  export const builder = (yargs: Argv) =>
34
30
  yargs
35
- .positional('source', {
31
+ .option('source', {
32
+ alias: 's',
36
33
  type: 'string',
37
34
  demandOption: true,
38
- describe: 'source network, chainId or name',
39
- example: 'ethereum-testnet-sepolia',
35
+ describe: 'Source chain: chainId, selector, or name',
40
36
  })
41
- .positional('router', {
37
+ .option('dest', {
38
+ alias: 'd',
42
39
  type: 'string',
43
40
  demandOption: true,
44
- describe: 'router contract address on source',
41
+ describe: 'Destination chain: chainId, selector, or name',
45
42
  })
46
- .positional('dest', {
43
+ .option('router', {
44
+ alias: 'r',
47
45
  type: 'string',
48
46
  demandOption: true,
49
- describe: 'destination network, chainId or name',
50
- example: 'ethereum-testnet-sepolia-arbitrum-1',
47
+ describe: 'Router contract address on source',
51
48
  })
52
49
  .options({
53
50
  receiver: {
54
- alias: 'R',
51
+ alias: 'to',
55
52
  type: 'string',
56
- describe:
57
- 'Receiver of the message; defaults to the sender wallet address if same network family',
53
+ describe: 'Receiver address on destination; defaults to sender if same chain family',
58
54
  },
59
55
  data: {
60
- alias: 'd',
61
56
  type: 'string',
62
- describe: 'Data to send in the message (non-hex will be utf-8 encoded)',
63
- example: '0x1234',
57
+ describe: 'Data payload to send (non-hex will be UTF-8 encoded)',
64
58
  },
65
59
  'gas-limit': {
66
60
  alias: ['L', 'compute-units'],
67
61
  type: 'number',
68
- describe:
69
- 'Gas limit for receiver callback execution; defaults to default configured on ramps',
62
+ describe: 'Gas limit for receiver callback; defaults to ramp config',
70
63
  },
71
64
  'estimate-gas-limit': {
72
65
  type: 'number',
73
- describe:
74
- 'Estimate gas limit for receiver callback execution; argument is a % margin to add to the estimate',
75
- example: '10',
66
+ describe: 'Estimate gas limit with % margin (e.g., 10 for +10%)',
76
67
  conflicts: 'gas-limit',
77
68
  },
78
69
  'allow-out-of-order-exec': {
79
70
  alias: 'ooo',
80
71
  type: 'boolean',
81
- describe:
82
- 'Allow execution of messages out of order (i.e. sender nonce not enforced, only v1.5+ lanes, mandatory for some dests)',
72
+ describe: 'Allow out-of-order execution (v1.5+ lanes)',
83
73
  },
84
74
  'fee-token': {
85
75
  type: 'string',
86
- describe:
87
- 'Address or symbol of the fee token (e.g. LINK address on source); if not provided, will pay in native',
76
+ describe: 'Fee token address or symbol (default: native)',
88
77
  },
89
78
  'transfer-tokens': {
90
79
  alias: 't',
91
80
  type: 'array',
92
81
  string: true,
93
- describe: 'List of token amounts (on source) to transfer to the receiver',
94
- example: '0xtoken=0.1',
82
+ describe: 'Token amounts to transfer: token=amount',
95
83
  },
96
84
  wallet: {
97
85
  alias: 'w',
98
86
  type: 'string',
99
- describe:
100
- 'Wallet to send transactions with; pass `ledger[:index_or_derivation]` to use Ledger USB hardware wallet, or private key in `USER_KEY` environment variable',
87
+ describe: 'Wallet: ledger[:index] or private key',
101
88
  },
102
89
  'token-receiver': {
103
90
  type: 'string',
104
- describe: "Address of the Solana tokenReceiver (if different than program's receiver)",
91
+ describe: 'Solana token receiver (if different from program receiver)',
105
92
  },
106
93
  account: {
107
94
  alias: 'receiver-object-id',
108
95
  type: 'array',
109
96
  string: true,
110
- describe:
111
- 'List of accounts needed by Solana receiver program, or receiverObjectIds needed by Sui; On Solana, append `=rw` to specify account as writable; can be specified multiple times',
112
- example: 'requiredPdaAddress=rw',
97
+ describe: 'Solana accounts (append =rw for writable) or Sui object IDs',
113
98
  },
114
99
  'only-get-fee': {
115
100
  type: 'boolean',
116
- describe: 'Fetch and print the fee for the transaction, then exit',
101
+ describe: 'Print fee and exit',
117
102
  },
118
103
  'only-estimate': {
119
104
  type: 'boolean',
120
- describe: 'Only estimate dest exec gasLimit',
105
+ describe: 'Print gas estimate and exit',
106
+ implies: 'estimate-gas-limit',
121
107
  },
122
108
  'approve-max': {
123
109
  type: 'boolean',
124
- describe:
125
- "Approve the maximum amount of tokens to transfer; default=false approves only what's needed",
110
+ describe: 'Approve max token amount instead of exact',
126
111
  },
127
112
  wait: {
128
113
  type: 'boolean',
129
114
  default: false,
130
- describe: 'Wait for execution',
115
+ describe: 'Wait for execution on destination',
131
116
  },
132
117
  })
133
118
  .check(
134
119
  ({ 'transfer-tokens': transferTokens }) =>
135
120
  !transferTokens || transferTokens.every((t) => /^[^=]+=\d+(\.\d+)?$/.test(t)),
136
121
  )
122
+ .example([
123
+ [
124
+ 'ccip-cli send -s ethereum-testnet-sepolia -d arbitrum-sepolia -r 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 --only-get-fee',
125
+ 'Get fee estimate',
126
+ ],
127
+ [
128
+ 'ccip-cli send -s ethereum-testnet-sepolia -d arbitrum-sepolia -r 0x0BF3... --to 0xABC... --data "Hello"',
129
+ 'Send message with data',
130
+ ],
131
+ ])
137
132
 
138
133
  /**
139
134
  * Handler for the send command.
@@ -198,37 +193,34 @@ async function sendMessage(
198
193
 
199
194
  if (argv.estimateGasLimit != null || argv.onlyEstimate) {
200
195
  // TODO: implement for all chain families
201
- if (destNetwork.family !== ChainFamily.EVM)
202
- throw new CCIPChainFamilyUnsupportedError(destNetwork.family, {
203
- context: { feature: 'gas estimation' },
204
- })
205
- const dest = (await getChain(destNetwork.chainSelector)) as unknown as EVMChain
206
- const onRamp = await source.getOnRampForRouter(argv.router, destNetwork.chainSelector)
207
- const lane = {
208
- sourceChainSelector: source.network.chainSelector,
209
- destChainSelector: destNetwork.chainSelector,
210
- onRamp,
211
- version: (await source.typeAndVersion(onRamp))[1] as CCIPVersion,
196
+ const dest = await getChain(destNetwork.chainSelector)
197
+
198
+ if (!walletAddress) {
199
+ try {
200
+ ;[walletAddress, wallet] = await loadChainWallet(source, argv)
201
+ } catch {
202
+ // pass undefined sender for default
203
+ }
212
204
  }
213
- const destTokenAmounts = await sourceToDestTokenAmounts(
205
+ const estimated = await estimateReceiveExecution({
214
206
  source,
215
- destNetwork.chainSelector,
216
- onRamp,
217
- tokenAmounts,
218
- )
219
-
220
- if (!walletAddress) [walletAddress, wallet] = await loadChainWallet(source, argv)
221
- const estimated = await estimateExecGasForRequest(source, dest, {
222
- lane,
207
+ dest,
208
+ routerOrRamp: argv.router,
223
209
  message: {
224
210
  sender: walletAddress,
225
211
  receiver,
226
- data: data || '0x',
227
- tokenAmounts: destTokenAmounts,
212
+ data,
213
+ tokenAmounts,
228
214
  },
229
215
  })
230
- logger.log('Estimated gasLimit:', estimated)
231
216
  argv.gasLimit = Math.ceil(estimated * (1 + (argv.estimateGasLimit ?? 0) / 100))
217
+ logger.log(
218
+ 'Estimated gasLimit for sender =',
219
+ walletAddress,
220
+ ':',
221
+ estimated,
222
+ ...(argv.estimateGasLimit ? ['+', argv.estimateGasLimit, '% =', argv.gasLimit] : []),
223
+ )
232
224
  if (argv.onlyEstimate) return
233
225
  }
234
226
 
@@ -2,7 +2,6 @@ import {
2
2
  type CCIPRequest,
3
3
  type ChainTransaction,
4
4
  CCIPExecTxRevertedError,
5
- CCIPNotImplementedError,
6
5
  ExecutionState,
7
6
  MessageStatus,
8
7
  bigIntReplacer,
@@ -88,9 +87,7 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
88
87
  } else idFromSource = argv.idFromSource
89
88
  const sourceNetwork = networkInfo(idFromSource)
90
89
  source = await getChain(sourceNetwork.chainId)
91
- if (!source.getMessageById)
92
- throw new CCIPNotImplementedError(`getMessageById for ${source.constructor.name}`)
93
- request = await source.getMessageById(argv.txHash, onRamp, argv)
90
+ request = await source.getMessageById(argv.txHash, { ...argv, onRamp })
94
91
  } else {
95
92
  const [getChain_, tx$] = fetchChainsFromRpcs(ctx, argv, argv.txHash)
96
93
  getChain = getChain_
@@ -37,7 +37,7 @@ import { formatDuration, getCtx, logParsedError, prettyTable } from './utils.ts'
37
37
  import type { GlobalOpts } from '../index.ts'
38
38
  import { fetchChainsFromRpcs } from '../providers/index.ts'
39
39
 
40
- export const command = ['getSupportedTokens <source> <address> [token]']
40
+ export const command = 'getSupportedTokens'
41
41
  export const describe =
42
42
  'List supported tokens in a given Router/OnRamp/TokenAdminRegistry, and/or show info about token/pool'
43
43
 
@@ -48,23 +48,34 @@ export const describe =
48
48
  */
49
49
  export const builder = (yargs: Argv) =>
50
50
  yargs
51
- .positional('source', {
51
+ .option('network', {
52
+ alias: 'n',
52
53
  type: 'string',
53
54
  demandOption: true,
54
- describe: 'source network, chainId or name',
55
- example: 'ethereum-testnet-sepolia',
55
+ describe: 'Source network: chainId or name (e.g., ethereum-mainnet)',
56
56
  })
57
- .positional('address', {
57
+ .option('address', {
58
+ alias: 'a',
58
59
  type: 'string',
59
60
  demandOption: true,
60
- describe: 'router/onramp/tokenAdminRegistry/tokenPool contract address on source',
61
+ describe: 'Router/OnRamp/TokenAdminRegistry/TokenPool contract address',
61
62
  })
62
- .positional('token', {
63
+ .option('token', {
64
+ alias: 't',
63
65
  type: 'string',
64
66
  demandOption: false,
65
- describe:
66
- 'If address is router/onramp/tokenAdminRegistry, token may be used to pre-select a token from the supported list',
67
+ describe: 'Token address to query (pre-selects from list if address is a registry)',
67
68
  })
69
+ .example([
70
+ [
71
+ 'ccip-cli getSupportedTokens -n ethereum-mainnet -a 0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D',
72
+ 'List all supported tokens from router',
73
+ ],
74
+ [
75
+ 'ccip-cli getSupportedTokens -n ethereum-mainnet -a 0x80226fc... -t 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
76
+ 'Get details for specific token',
77
+ ],
78
+ ])
68
79
 
69
80
  /**
70
81
  * Handler for the supported-tokens command.
@@ -82,7 +93,7 @@ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']>
82
93
 
83
94
  async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0]) {
84
95
  const { logger } = ctx
85
- const sourceNetwork = networkInfo(argv.source)
96
+ const sourceNetwork = networkInfo(argv.network)
86
97
  const getChain = fetchChainsFromRpcs(ctx, argv)
87
98
  const source = await getChain(sourceNetwork.name)
88
99
  let registry
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Token balance query command.
3
+ * Queries native or token balance for an address.
4
+ */
5
+
6
+ import { type ChainStatic, networkInfo } from '@chainlink/ccip-sdk/src/index.ts'
7
+ import { formatUnits } from 'ethers'
8
+ import type { Argv } from 'yargs'
9
+
10
+ import { type Ctx, Format } from './types.ts'
11
+ import { getCtx, logParsedError, prettyTable } from './utils.ts'
12
+ import type { GlobalOpts } from '../index.ts'
13
+ import { fetchChainsFromRpcs } from '../providers/index.ts'
14
+
15
+ export const command = 'token'
16
+ export const describe = 'Query token balance for an address'
17
+
18
+ /**
19
+ * Yargs builder for the token command.
20
+ * @param yargs - Yargs instance.
21
+ * @returns Configured yargs instance with command options.
22
+ */
23
+ export const builder = (yargs: Argv) =>
24
+ yargs
25
+ .option('network', {
26
+ alias: 'n',
27
+ type: 'string',
28
+ demandOption: true,
29
+ describe: 'Network: chainId or name (e.g., ethereum-mainnet, solana-devnet)',
30
+ })
31
+ .option('holder', {
32
+ alias: 'H',
33
+ type: 'string',
34
+ demandOption: true,
35
+ describe: 'Wallet address to query balance for',
36
+ })
37
+ .option('token', {
38
+ alias: 't',
39
+ type: 'string',
40
+ demandOption: false,
41
+ describe: 'Token address (omit for native token balance)',
42
+ })
43
+ .example([
44
+ ['ccip-cli token -n ethereum-mainnet -H 0x1234...abcd', 'Query native ETH balance'],
45
+ [
46
+ 'ccip-cli token -n ethereum-mainnet -H 0x1234... -t 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
47
+ 'Query USDC token balance',
48
+ ],
49
+ [
50
+ 'ccip-cli token -n solana-devnet -H EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB',
51
+ 'Query native SOL balance',
52
+ ],
53
+ ])
54
+
55
+ /**
56
+ * Handler for the token command.
57
+ * @param argv - Command line arguments.
58
+ */
59
+ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']> & GlobalOpts) {
60
+ const [ctx, destroy] = getCtx(argv)
61
+ return queryTokenBalance(ctx, argv)
62
+ .catch((err) => {
63
+ process.exitCode = 1
64
+ if (!logParsedError.call(ctx, err)) ctx.logger.error(err)
65
+ })
66
+ .finally(destroy)
67
+ }
68
+
69
+ async function queryTokenBalance(ctx: Ctx, argv: Parameters<typeof handler>[0]) {
70
+ const { logger } = ctx
71
+ const networkName = networkInfo(argv.network).name
72
+ const getChain = fetchChainsFromRpcs(ctx, argv)
73
+ const chain = await getChain(networkName)
74
+
75
+ const balance = await chain.getBalance({
76
+ holder: argv.holder,
77
+ token: argv.token,
78
+ })
79
+
80
+ // Get token info for formatting (only for tokens, not native)
81
+ let tokenInfo
82
+ if (argv.token) {
83
+ argv.token = (chain.constructor as ChainStatic).getAddress(argv.token)
84
+ tokenInfo = await chain.getTokenInfo(argv.token)
85
+ }
86
+
87
+ const tokenLabel = tokenInfo?.symbol ?? 'native'
88
+ const formatted = formatUnits(
89
+ balance,
90
+ tokenInfo ? tokenInfo.decimals : (chain.constructor as ChainStatic).decimals,
91
+ )
92
+
93
+ switch (argv.format) {
94
+ case Format.json:
95
+ logger.log(
96
+ JSON.stringify(
97
+ {
98
+ network: networkName,
99
+ holder: argv.holder,
100
+ token: tokenLabel,
101
+ balance: balance.toString(),
102
+ formatted,
103
+ ...tokenInfo,
104
+ },
105
+ null,
106
+ 2,
107
+ ),
108
+ )
109
+ return
110
+ case Format.log:
111
+ logger.log(
112
+ `Balance of`,
113
+ tokenInfo ? argv.token : tokenLabel,
114
+ ':',
115
+ balance,
116
+ `=`,
117
+ tokenInfo ? `${formatted} ${tokenLabel}` : formatted,
118
+ )
119
+ return
120
+ case Format.pretty:
121
+ default:
122
+ prettyTable.call(ctx, {
123
+ network: networkName,
124
+ holder: argv.holder,
125
+ token: argv.token ?? tokenLabel,
126
+ balance,
127
+ formatted,
128
+ ...tokenInfo,
129
+ })
130
+ return
131
+ }
132
+ }
@@ -12,6 +12,7 @@ import {
12
12
  CCIPErrorCode,
13
13
  ExecutionState,
14
14
  getCCIPExplorerUrl,
15
+ getDataBytes,
15
16
  networkInfo,
16
17
  supportedChains,
17
18
  } from '@chainlink/ccip-sdk/src/index.ts'
@@ -19,11 +20,11 @@ import { select } from '@inquirer/prompts'
19
20
  import {
20
21
  dataLength,
21
22
  formatUnits,
22
- getBytes,
23
23
  hexlify,
24
24
  isBytesLike,
25
25
  isHexString,
26
26
  parseUnits,
27
+ toBigInt,
27
28
  toUtf8String,
28
29
  } from 'ethers'
29
30
  import type { PickDeep } from 'type-fest'
@@ -215,6 +216,26 @@ function omit<T extends Record<string, unknown>, K extends string>(
215
216
  return result
216
217
  }
217
218
 
219
+ // while formatData just breaks 0x bytes into 32B chunks for readability, this function first
220
+ // tests if the data looks like a UTF-8 string (with length prefix) and decode that before
221
+ function formatDataString(data: string): Record<string, string> {
222
+ const bytes = getDataBytes(data)
223
+ const isPrintableChars = (bytes_: Uint8Array) => bytes_.every((b) => 32 <= b && b <= 126)
224
+ if (bytes.length > 64 && toBigInt(bytes.subarray(0, 32)) === 32n) {
225
+ const len = toBigInt(bytes.subarray(32, 64))
226
+ if (
227
+ len < 512 &&
228
+ bytes.length - 64 === Math.ceil(Number(len) / 32) * 32 &&
229
+ isPrintableChars(bytes.subarray(64, 64 + Number(len))) &&
230
+ bytes.subarray(64 + Number(len)).every((b) => b === 0)
231
+ ) {
232
+ return { data: toUtf8String(bytes.subarray(64, 64 + Number(len))) }
233
+ }
234
+ }
235
+ if (bytes.length > 0 && isPrintableChars(bytes)) return { data: toUtf8String(bytes) }
236
+ return formatData('data', data)
237
+ }
238
+
218
239
  /**
219
240
  * Prints a CCIP request in a human-readable format.
220
241
  * @param source - Source chain instance.
@@ -289,11 +310,7 @@ export async function prettyRequest(this: Ctx, source: Chain, request: CCIPReque
289
310
  'tokens',
290
311
  await Promise.all(request.message.tokenAmounts.map(formatToken.bind(null, source))),
291
312
  ),
292
- ...(isBytesLike(request.message.data) &&
293
- dataLength(request.message.data) > 0 &&
294
- getBytes(request.message.data).every((b) => 32 <= b && b <= 126) // printable characters
295
- ? { data: toUtf8String(request.message.data) }
296
- : formatData('data', request.message.data)),
313
+ ...formatDataString(request.message.data),
297
314
  ...('accounts' in request.message ? formatArray('accounts', request.message.accounts) : {}),
298
315
  ...rest,
299
316
  })
package/src/index.ts CHANGED
@@ -11,13 +11,13 @@ import { Format } from './commands/index.ts'
11
11
  util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests
12
12
  // generate:nofail
13
13
  // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
14
- const VERSION = '0.94.0-aac4ae6'
14
+ const VERSION = '0.95.0-5f1a7cb'
15
15
  // generate:end
16
16
 
17
17
  const globalOpts = {
18
18
  rpcs: {
19
19
  type: 'array',
20
- alias: ['r', 'rpc'],
20
+ alias: 'rpc',
21
21
  describe: 'List of RPC endpoint URLs, ws[s] or http[s]',
22
22
  string: true,
23
23
  },
@@ -8,10 +8,10 @@ import {
8
8
  type EVMChain,
9
9
  type TONChain,
10
10
  CCIPChainFamilyUnsupportedError,
11
- CCIPNetworkFamilyUnsupportedError,
12
11
  CCIPRpcNotFoundError,
13
12
  CCIPTransactionNotFoundError,
14
13
  ChainFamily,
14
+ NetworkType,
15
15
  networkInfo,
16
16
  supportedChains,
17
17
  } from '@chainlink/ccip-sdk/src/index.ts'
@@ -94,7 +94,7 @@ export function fetchChainsFromRpcs(
94
94
  const loadChainFamily = (F: ChainFamily, txHash?: string) =>
95
95
  (initFamily$[F] ??= (endpoints$ ??= collectEndpoints.call(ctx, argv)).then((endpoints) => {
96
96
  const C = supportedChains[F]
97
- if (!C) throw new CCIPNetworkFamilyUnsupportedError(F)
97
+ if (!C) throw new CCIPChainFamilyUnsupportedError(F)
98
98
  ctx.logger.debug('Racing', endpoints.size, 'RPC endpoints for', F)
99
99
 
100
100
  const chains$: Promise<Chain>[] = []
@@ -214,7 +214,11 @@ export async function loadChainWallet(chain: Chain, argv: { wallet?: unknown; rp
214
214
  wallet = loadSuiWallet(argv)
215
215
  return [wallet.toSuiAddress(), wallet] as const
216
216
  case ChainFamily.TON:
217
- wallet = await loadTonWallet((chain as TONChain).provider, argv, chain.network.isTestnet)
217
+ wallet = await loadTonWallet(
218
+ (chain as TONChain).provider,
219
+ argv,
220
+ chain.network.networkType === NetworkType.Testnet,
221
+ )
218
222
  return [wallet.getAddress(), wallet] as const
219
223
  default:
220
224
  // TypeScript exhaustiveness check - this should never be reached