@chainlink/ccip-cli 0.94.0 → 0.96.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 (42) hide show
  1. package/README.md +148 -42
  2. package/dist/commands/manual-exec.d.ts +1 -1
  3. package/dist/commands/manual-exec.d.ts.map +1 -1
  4. package/dist/commands/manual-exec.js +12 -12
  5. package/dist/commands/manual-exec.js.map +1 -1
  6. package/dist/commands/parse.d.ts.map +1 -1
  7. package/dist/commands/parse.js +8 -2
  8. package/dist/commands/parse.js.map +1 -1
  9. package/dist/commands/send.d.ts +5 -9
  10. package/dist/commands/send.d.ts.map +1 -1
  11. package/dist/commands/send.js +72 -52
  12. package/dist/commands/send.js.map +1 -1
  13. package/dist/commands/show.d.ts.map +1 -1
  14. package/dist/commands/show.js +2 -4
  15. package/dist/commands/show.js.map +1 -1
  16. package/dist/commands/supported-tokens.d.ts +3 -1
  17. package/dist/commands/supported-tokens.d.ts.map +1 -1
  18. package/dist/commands/supported-tokens.js +36 -15
  19. package/dist/commands/supported-tokens.js.map +1 -1
  20. package/dist/commands/token.d.ts +26 -0
  21. package/dist/commands/token.d.ts.map +1 -0
  22. package/dist/commands/token.js +105 -0
  23. package/dist/commands/token.js.map +1 -0
  24. package/dist/commands/utils.d.ts.map +1 -1
  25. package/dist/commands/utils.js +21 -7
  26. package/dist/commands/utils.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/providers/index.d.ts.map +1 -1
  31. package/dist/providers/index.js +3 -3
  32. package/dist/providers/index.js.map +1 -1
  33. package/package.json +10 -13
  34. package/src/commands/manual-exec.ts +14 -24
  35. package/src/commands/parse.ts +8 -2
  36. package/src/commands/send.ts +80 -66
  37. package/src/commands/show.ts +1 -4
  38. package/src/commands/supported-tokens.ts +35 -15
  39. package/src/commands/token.ts +132 -0
  40. package/src/commands/utils.ts +23 -6
  41. package/src/index.ts +2 -2
  42. package/src/providers/index.ts +7 -3
@@ -1,17 +1,14 @@
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,
6
+ CCIPInsufficientBalanceError,
9
7
  CCIPTokenNotFoundError,
10
8
  ChainFamily,
11
- estimateExecGasForRequest,
9
+ estimateReceiveExecution,
12
10
  getDataBytes,
13
11
  networkInfo,
14
- sourceToDestTokenAmounts,
15
12
  } from '@chainlink/ccip-sdk/src/index.ts'
16
13
  import { type BytesLike, formatUnits, toUtf8Bytes } from 'ethers'
17
14
  import type { Argv } from 'yargs'
@@ -22,8 +19,8 @@ import type { Ctx } from './types.ts'
22
19
  import { getCtx, logParsedError, parseTokenAmounts } from './utils.ts'
23
20
  import { fetchChainsFromRpcs, loadChainWallet } from '../providers/index.ts'
24
21
 
25
- export const command = 'send <source> <router> <dest>'
26
- export const describe = 'Send a CCIP message from router on source to dest'
22
+ export const command = 'send'
23
+ export const describe = 'Send a CCIP message from source to destination chain'
27
24
 
28
25
  /**
29
26
  * Yargs builder for the send command.
@@ -32,108 +29,107 @@ export const describe = 'Send a CCIP message from router on source to dest'
32
29
  */
33
30
  export const builder = (yargs: Argv) =>
34
31
  yargs
35
- .positional('source', {
32
+ .option('source', {
33
+ alias: 's',
36
34
  type: 'string',
37
35
  demandOption: true,
38
- describe: 'source network, chainId or name',
39
- example: 'ethereum-testnet-sepolia',
36
+ describe: 'Source chain: chainId, selector, or name',
40
37
  })
41
- .positional('router', {
38
+ .option('dest', {
39
+ alias: 'd',
42
40
  type: 'string',
43
41
  demandOption: true,
44
- describe: 'router contract address on source',
42
+ describe: 'Destination chain: chainId, selector, or name',
45
43
  })
46
- .positional('dest', {
44
+ .option('router', {
45
+ alias: 'r',
47
46
  type: 'string',
48
47
  demandOption: true,
49
- describe: 'destination network, chainId or name',
50
- example: 'ethereum-testnet-sepolia-arbitrum-1',
48
+ describe: 'Router contract address on source',
51
49
  })
52
50
  .options({
53
51
  receiver: {
54
- alias: 'R',
52
+ alias: 'to',
55
53
  type: 'string',
56
- describe:
57
- 'Receiver of the message; defaults to the sender wallet address if same network family',
54
+ describe: 'Receiver address on destination; defaults to sender if same chain family',
58
55
  },
59
56
  data: {
60
- alias: 'd',
61
57
  type: 'string',
62
- describe: 'Data to send in the message (non-hex will be utf-8 encoded)',
63
- example: '0x1234',
58
+ describe: 'Data payload to send (non-hex will be UTF-8 encoded)',
64
59
  },
65
60
  'gas-limit': {
66
61
  alias: ['L', 'compute-units'],
67
62
  type: 'number',
68
- describe:
69
- 'Gas limit for receiver callback execution; defaults to default configured on ramps',
63
+ describe: 'Gas limit for receiver callback; defaults to ramp config',
70
64
  },
71
65
  'estimate-gas-limit': {
72
66
  type: 'number',
73
- describe:
74
- 'Estimate gas limit for receiver callback execution; argument is a % margin to add to the estimate',
75
- example: '10',
67
+ describe: 'Estimate gas limit with % margin (e.g., 10 for +10%)',
76
68
  conflicts: 'gas-limit',
77
69
  },
78
70
  'allow-out-of-order-exec': {
79
71
  alias: 'ooo',
80
72
  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)',
73
+ describe: 'Allow out-of-order execution (v1.5+ lanes)',
83
74
  },
84
75
  'fee-token': {
85
76
  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',
77
+ describe: 'Fee token address or symbol (default: native)',
88
78
  },
89
79
  'transfer-tokens': {
90
80
  alias: 't',
91
81
  type: 'array',
92
82
  string: true,
93
- describe: 'List of token amounts (on source) to transfer to the receiver',
94
- example: '0xtoken=0.1',
83
+ describe: 'Token amounts to transfer: token=amount',
95
84
  },
96
85
  wallet: {
97
86
  alias: 'w',
98
87
  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',
88
+ describe: 'Wallet: ledger[:index] or private key',
101
89
  },
102
90
  'token-receiver': {
103
91
  type: 'string',
104
- describe: "Address of the Solana tokenReceiver (if different than program's receiver)",
92
+ describe: 'Solana token receiver (if different from program receiver)',
105
93
  },
106
94
  account: {
107
95
  alias: 'receiver-object-id',
108
96
  type: 'array',
109
97
  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',
98
+ describe: 'Solana accounts (append =rw for writable) or Sui object IDs',
113
99
  },
114
100
  'only-get-fee': {
115
101
  type: 'boolean',
116
- describe: 'Fetch and print the fee for the transaction, then exit',
102
+ describe: 'Print fee and exit',
117
103
  },
118
104
  'only-estimate': {
119
105
  type: 'boolean',
120
- describe: 'Only estimate dest exec gasLimit',
106
+ describe: 'Print gas estimate and exit',
107
+ implies: 'estimate-gas-limit',
121
108
  },
122
109
  'approve-max': {
123
110
  type: 'boolean',
124
- describe:
125
- "Approve the maximum amount of tokens to transfer; default=false approves only what's needed",
111
+ describe: 'Approve max token amount instead of exact',
126
112
  },
127
113
  wait: {
128
114
  type: 'boolean',
129
115
  default: false,
130
- describe: 'Wait for execution',
116
+ describe: 'Wait for execution on destination',
131
117
  },
132
118
  })
133
119
  .check(
134
120
  ({ 'transfer-tokens': transferTokens }) =>
135
121
  !transferTokens || transferTokens.every((t) => /^[^=]+=\d+(\.\d+)?$/.test(t)),
136
122
  )
123
+ .example([
124
+ [
125
+ 'ccip-cli send -s ethereum-testnet-sepolia -d arbitrum-sepolia -r 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 --only-get-fee',
126
+ 'Get fee estimate',
127
+ ],
128
+ [
129
+ 'ccip-cli send -s ethereum-testnet-sepolia -d arbitrum-sepolia -r 0x0BF3... --to 0xABC... --data "Hello"',
130
+ 'Send message with data',
131
+ ],
132
+ ])
137
133
 
138
134
  /**
139
135
  * Handler for the send command.
@@ -198,37 +194,34 @@ async function sendMessage(
198
194
 
199
195
  if (argv.estimateGasLimit != null || argv.onlyEstimate) {
200
196
  // 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,
197
+ const dest = await getChain(destNetwork.chainSelector)
198
+
199
+ if (!walletAddress) {
200
+ try {
201
+ ;[walletAddress, wallet] = await loadChainWallet(source, argv)
202
+ } catch {
203
+ // pass undefined sender for default
204
+ }
212
205
  }
213
- const destTokenAmounts = await sourceToDestTokenAmounts(
206
+ const estimated = await estimateReceiveExecution({
214
207
  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,
208
+ dest,
209
+ routerOrRamp: argv.router,
223
210
  message: {
224
211
  sender: walletAddress,
225
212
  receiver,
226
- data: data || '0x',
227
- tokenAmounts: destTokenAmounts,
213
+ data,
214
+ tokenAmounts,
228
215
  },
229
216
  })
230
- logger.log('Estimated gasLimit:', estimated)
231
217
  argv.gasLimit = Math.ceil(estimated * (1 + (argv.estimateGasLimit ?? 0) / 100))
218
+ logger.log(
219
+ 'Estimated gasLimit for sender =',
220
+ walletAddress,
221
+ ':',
222
+ estimated,
223
+ ...(argv.estimateGasLimit ? ['+', argv.estimateGasLimit, '% =', argv.gasLimit] : []),
224
+ )
232
225
  if (argv.onlyEstimate) return
233
226
  }
234
227
 
@@ -295,6 +288,27 @@ async function sendMessage(
295
288
  if (argv.onlyGetFee) return
296
289
 
297
290
  if (!walletAddress) [walletAddress, wallet] = await loadChainWallet(source, argv)
291
+
292
+ // Check sender has sufficient balance for fee
293
+ try {
294
+ const balance = await source.getBalance({ holder: walletAddress, token: feeToken })
295
+ if (balance < fee) {
296
+ const symbol =
297
+ feeTokenInfo.symbol.startsWith('W') && !feeToken
298
+ ? feeTokenInfo.symbol.substring(1)
299
+ : feeTokenInfo.symbol
300
+ throw new CCIPInsufficientBalanceError(
301
+ formatUnits(balance, feeTokenInfo.decimals),
302
+ formatUnits(fee, feeTokenInfo.decimals),
303
+ symbol,
304
+ )
305
+ }
306
+ } catch (e) {
307
+ // may fail for chains that don't implement getBalance yet; log and continue
308
+ if (e instanceof CCIPInsufficientBalanceError) throw e
309
+ logger.debug('Balance check skipped:', e)
310
+ }
311
+
298
312
  const request = await source.sendMessage({
299
313
  ...argv,
300
314
  destChainSelector: destNetwork.chainSelector,
@@ -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', 'get-supported-tokens']
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,39 @@ 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
+ .option('fee-tokens', {
70
+ type: 'boolean',
71
+ default: false,
72
+ describe: 'List fee tokens instead of transferable tokens',
73
+ })
74
+ .example([
75
+ [
76
+ 'ccip-cli getSupportedTokens -n ethereum-mainnet -a 0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D',
77
+ 'List all supported tokens from router',
78
+ ],
79
+ [
80
+ 'ccip-cli getSupportedTokens -n ethereum-mainnet -a 0x80226fc... -t 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
81
+ 'Get details for specific token',
82
+ ],
83
+ ])
68
84
 
69
85
  /**
70
86
  * Handler for the supported-tokens command.
@@ -82,7 +98,7 @@ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']>
82
98
 
83
99
  async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0]) {
84
100
  const { logger } = ctx
85
- const sourceNetwork = networkInfo(argv.source)
101
+ const sourceNetwork = networkInfo(argv.network)
86
102
  const getChain = fetchChainsFromRpcs(ctx, argv)
87
103
  const source = await getChain(sourceNetwork.name)
88
104
  let registry
@@ -92,8 +108,8 @@ async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0])
92
108
  // ignore
93
109
  }
94
110
 
95
- let info, tokenPool, poolConfigs, registryConfig
96
- if (registry && !argv.token) {
111
+ // Handle --fee-tokens flag
112
+ if (argv.feeTokens) {
97
113
  const feeTokens = await source.getFeeTokens(argv.address)
98
114
  switch (argv.format) {
99
115
  case Format.pretty:
@@ -106,19 +122,23 @@ async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0])
106
122
  default:
107
123
  logger.log('feeTokens:', feeTokens)
108
124
  }
125
+ return
126
+ }
109
127
 
128
+ let info, tokenPool, poolConfigs, registryConfig
129
+ if (registry && !argv.token) {
110
130
  // router + interactive list
111
131
  info = await listTokens(ctx, source, registry, argv)
112
132
  if (!info) return // format != pretty
113
133
  registryConfig = await source.getRegistryTokenConfig(registry, info.token)
114
134
  tokenPool = registryConfig.tokenPool
115
135
  if (!tokenPool) throw new CCIPTokenNotConfiguredError(info.token, registry)
116
- poolConfigs = await source.getTokenPoolConfigs(tokenPool)
136
+ poolConfigs = await source.getTokenPoolConfig(tokenPool)
117
137
  } else {
118
138
  if (!argv.token) {
119
139
  // tokenPool
120
140
  tokenPool = argv.address
121
- poolConfigs = await source.getTokenPoolConfigs(tokenPool)
141
+ poolConfigs = await source.getTokenPoolConfig(tokenPool)
122
142
  registry ??= await source.getTokenAdminRegistryFor(poolConfigs.router)
123
143
  ;[info, registryConfig] = await Promise.all([
124
144
  source.getTokenInfo(poolConfigs.token),
@@ -132,7 +152,7 @@ async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0])
132
152
  registryConfig = await source.getRegistryTokenConfig(registry, argv.token)
133
153
  tokenPool = registryConfig.tokenPool
134
154
  if (!tokenPool) throw new CCIPTokenNotConfiguredError(argv.token, registry)
135
- poolConfigs = await source.getTokenPoolConfigs(tokenPool)
155
+ poolConfigs = await source.getTokenPoolConfig(tokenPool)
136
156
  }
137
157
 
138
158
  if (argv.format === Format.json) {
@@ -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.96.0-983178a'
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