@chainlink/ccip-cli 0.91.1 → 0.92.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.
Files changed (65) hide show
  1. package/README.md +60 -55
  2. package/dist/commands/manual-exec.d.ts +7 -1
  3. package/dist/commands/manual-exec.d.ts.map +1 -1
  4. package/dist/commands/manual-exec.js +16 -7
  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 +6 -5
  8. package/dist/commands/parse.js.map +1 -1
  9. package/dist/commands/send.d.ts +6 -1
  10. package/dist/commands/send.d.ts.map +1 -1
  11. package/dist/commands/send.js +27 -22
  12. package/dist/commands/send.js.map +1 -1
  13. package/dist/commands/show.d.ts +10 -1
  14. package/dist/commands/show.d.ts.map +1 -1
  15. package/dist/commands/show.js +85 -27
  16. package/dist/commands/show.js.map +1 -1
  17. package/dist/commands/supported-tokens.d.ts.map +1 -1
  18. package/dist/commands/supported-tokens.js +6 -6
  19. package/dist/commands/supported-tokens.js.map +1 -1
  20. package/dist/commands/types.d.ts +4 -3
  21. package/dist/commands/types.d.ts.map +1 -1
  22. package/dist/commands/utils.d.ts +27 -7
  23. package/dist/commands/utils.d.ts.map +1 -1
  24. package/dist/commands/utils.js +97 -35
  25. package/dist/commands/utils.js.map +1 -1
  26. package/dist/index.d.ts +3 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/providers/aptos.d.ts.map +1 -1
  31. package/dist/providers/aptos.js +3 -5
  32. package/dist/providers/aptos.js.map +1 -1
  33. package/dist/providers/evm.d.ts.map +1 -1
  34. package/dist/providers/evm.js +2 -4
  35. package/dist/providers/evm.js.map +1 -1
  36. package/dist/providers/index.d.ts +9 -5
  37. package/dist/providers/index.d.ts.map +1 -1
  38. package/dist/providers/index.js +104 -71
  39. package/dist/providers/index.js.map +1 -1
  40. package/dist/providers/solana.d.ts.map +1 -1
  41. package/dist/providers/solana.js +5 -4
  42. package/dist/providers/solana.js.map +1 -1
  43. package/dist/providers/sui.d.ts +10 -0
  44. package/dist/providers/sui.d.ts.map +1 -0
  45. package/dist/providers/sui.js +14 -0
  46. package/dist/providers/sui.js.map +1 -0
  47. package/dist/providers/ton.d.ts +13 -0
  48. package/dist/providers/ton.d.ts.map +1 -0
  49. package/dist/providers/ton.js +55 -0
  50. package/dist/providers/ton.js.map +1 -0
  51. package/package.json +13 -13
  52. package/src/commands/manual-exec.ts +18 -6
  53. package/src/commands/parse.ts +9 -5
  54. package/src/commands/send.ts +36 -28
  55. package/src/commands/show.ts +94 -31
  56. package/src/commands/supported-tokens.ts +6 -9
  57. package/src/commands/types.ts +4 -3
  58. package/src/commands/utils.ts +118 -43
  59. package/src/index.ts +4 -4
  60. package/src/providers/aptos.ts +3 -5
  61. package/src/providers/evm.ts +2 -4
  62. package/src/providers/index.ts +120 -94
  63. package/src/providers/solana.ts +5 -6
  64. package/src/providers/sui.ts +14 -0
  65. package/src/providers/ton.ts +62 -0
@@ -5,9 +5,11 @@ import {
5
5
  type CCIPExecution,
6
6
  type CCIPRequest,
7
7
  type Chain,
8
+ type ChainFamily,
8
9
  type ChainStatic,
9
10
  type Lane,
10
- type OffchainTokenData,
11
+ CCIPError,
12
+ CCIPErrorCode,
11
13
  ExecutionState,
12
14
  networkInfo,
13
15
  supportedChains,
@@ -46,7 +48,7 @@ export async function selectRequest(
46
48
  choices: [
47
49
  ...requests.map((req, i) => ({
48
50
  value: i,
49
- name: `${req.log.index} => ${req.message.header.messageId}`,
51
+ name: `${req.log.index} => ${req.message.messageId}`,
50
52
  description:
51
53
  `sender =\t\t${req.message.sender}
52
54
  receiver =\t\t${req.message.receiver}
@@ -63,7 +65,7 @@ tokenTransfers =\t[${req.message.tokenAmounts.map((ta) => ('token' in ta ? ta.to
63
65
  },
64
66
  ],
65
67
  })
66
- if (answer < 0) throw new Error('User requested exit')
68
+ if (answer < 0) throw new CCIPError(CCIPErrorCode.UNKNOWN, 'User requested exit')
67
69
  return requests[answer]
68
70
  }
69
71
 
@@ -93,10 +95,33 @@ export function prettyLane(this: Ctx, lane: Lane) {
93
95
  name: { source: source.name, dest: dest.name },
94
96
  chainId: { source: source.chainId, dest: dest.chainId },
95
97
  chainSelector: { source: source.chainSelector, dest: dest.chainSelector },
96
- 'onRamp/version': { source: lane.onRamp, dest: lane.version },
98
+ 'onRamp/version': {
99
+ source: formatDisplayAddress(lane.onRamp, source.family),
100
+ dest: lane.version,
101
+ },
97
102
  })
98
103
  }
99
104
 
105
+ /**
106
+ * Format an address for display using chain-specific formatting.
107
+ * @param address - Address string
108
+ * @param family - Chain family for formatting
109
+ * @returns Formatted address for display
110
+ */
111
+ export function formatDisplayAddress(address: string, family: ChainFamily): string {
112
+ return supportedChains[family]?.formatAddress?.(address) ?? address
113
+ }
114
+
115
+ /**
116
+ * Format a transaction hash for display using chain-specific formatting.
117
+ * @param hash - Transaction hash string
118
+ * @param family - Chain family for formatting
119
+ * @returns Formatted hash for display
120
+ */
121
+ export function formatDisplayTxHash(hash: string, family: ChainFamily): string {
122
+ return supportedChains[family]?.formatTxHash?.(hash) ?? hash
123
+ }
124
+
100
125
  async function formatToken(
101
126
  source: Chain,
102
127
  ta: { amount: bigint } & ({ token: string } | { sourcePoolAddress: string }),
@@ -159,7 +184,7 @@ function formatDate(timestamp: number) {
159
184
  /**
160
185
  * Formats duration in seconds to human-readable string.
161
186
  * @param secs - Duration in seconds.
162
- * @returns Formatted duration string (e.g., "1h 30m").
187
+ * @returns Formatted duration string (e.g., "2d 1h30m").
163
188
  */
164
189
  export function formatDuration(secs: number) {
165
190
  if (secs < 0) secs = -secs
@@ -174,7 +199,7 @@ export function formatDuration(secs: number) {
174
199
  }
175
200
  return Object.entries(time)
176
201
  .filter((val) => val[1] !== 0)
177
- .map(([key, val]) => `${val}${key}${key === 'd' ? ' ' : ''}`)
202
+ .map(([key, val], i, arr) => `${val}${key}${key === 'd' && i < arr.length - 1 ? ' ' : ''}`)
178
203
  .join('')
179
204
  }
180
205
 
@@ -193,14 +218,8 @@ function omit<T extends Record<string, unknown>, K extends string>(
193
218
  * Prints a CCIP request in a human-readable format.
194
219
  * @param source - Source chain instance.
195
220
  * @param request - CCIP request to print.
196
- * @param offchainTokenData - Optional offchain token data.
197
221
  */
198
- export async function prettyRequest(
199
- this: Ctx,
200
- source: Chain,
201
- request: CCIPRequest,
202
- offchainTokenData?: OffchainTokenData[],
203
- ) {
222
+ export async function prettyRequest(this: Ctx, source: Chain, request: CCIPRequest) {
204
223
  prettyLane.call(this, request.lane)
205
224
  this.logger.info('Request (source):')
206
225
 
@@ -210,11 +229,24 @@ export async function prettyRequest(
210
229
  } catch (_) {
211
230
  // no finalized tag support
212
231
  }
213
- const nonce = Number(request.message.header.nonce)
232
+ const nonce = Number(request.message.nonce)
233
+
234
+ const sourceFamily = networkInfo(request.lane.sourceChainSelector).family
235
+ const destFamily = networkInfo(request.lane.destChainSelector).family
236
+
237
+ // Normalize receiver to destination chain format for display
238
+ const displaySender = formatDisplayAddress(request.message.sender, sourceFamily)
239
+ const displayReceiver = formatDisplayAddress(request.message.receiver, destFamily)
240
+ const displayOrigin = request.tx.from
241
+ ? formatDisplayAddress(request.tx.from, sourceFamily)
242
+ : undefined
243
+ const displayTxHash = formatDisplayTxHash(request.log.transactionHash, sourceFamily)
214
244
 
215
245
  const rest = omit(
216
246
  request.message,
217
- 'header',
247
+ 'messageId',
248
+ 'sequenceNumber',
249
+ 'nonce',
218
250
  'sender',
219
251
  'receiver',
220
252
  'tokenAmounts',
@@ -223,22 +255,23 @@ export async function prettyRequest(
223
255
  'feeTokenAmount',
224
256
  'sourceTokenData',
225
257
  'sourceChainSelector',
258
+ 'destChainSelector',
226
259
  'extraArgs',
227
260
  'accounts',
228
261
  )
229
262
  prettyTable.call(this, {
230
- messageId: request.message.header.messageId,
231
- ...(request.tx.from ? { origin: request.tx.from } : {}),
232
- sender: request.message.sender,
233
- receiver: request.message.receiver,
234
- sequenceNumber: Number(request.message.header.sequenceNumber),
263
+ messageId: request.message.messageId,
264
+ ...(displayOrigin ? { origin: displayOrigin } : {}),
265
+ sender: displaySender,
266
+ receiver: displayReceiver,
267
+ sequenceNumber: Number(request.message.sequenceNumber),
235
268
  nonce: nonce === 0 ? '0 => allow out-of-order exec' : nonce,
236
269
  ...('gasLimit' in request.message
237
270
  ? { gasLimit: Number(request.message.gasLimit) }
238
271
  : 'computeUnits' in request.message
239
272
  ? { computeUnits: Number(request.message.computeUnits) }
240
273
  : {}),
241
- transactionHash: request.log.transactionHash,
274
+ transactionHash: displayTxHash,
242
275
  logIndex: request.log.index,
243
276
  blockNumber: request.log.blockNumber,
244
277
  timestamp: `${formatDate(request.tx.timestamp)} (${formatDuration(Date.now() / 1e3 - request.tx.timestamp)} ago)`,
@@ -263,13 +296,6 @@ export async function prettyRequest(
263
296
  ...('accounts' in request.message ? formatArray('accounts', request.message.accounts) : {}),
264
297
  ...rest,
265
298
  })
266
-
267
- if (!offchainTokenData?.length || offchainTokenData.every((d) => !d)) return
268
- this.logger.info('Attestations:')
269
- for (const attestation of offchainTokenData) {
270
- const { _tag: type, ...rest } = attestation!
271
- prettyTable.call(this, { type, ...rest })
272
- }
273
299
  }
274
300
 
275
301
  /**
@@ -282,17 +308,19 @@ export async function prettyCommit(
282
308
  this: Ctx,
283
309
  dest: Chain,
284
310
  commit: CCIPCommit,
285
- request: PickDeep<CCIPRequest, 'tx.timestamp'>,
311
+ request: PickDeep<CCIPRequest, 'tx.timestamp' | 'lane.destChainSelector'>,
286
312
  ) {
287
- this.logger.info('Commit (dest):')
288
313
  const timestamp = await dest.getBlockTimestamp(commit.log.blockNumber)
314
+ const destFamily = networkInfo(request.lane.destChainSelector).family
315
+ const origin = commit.log.tx?.from ?? (await dest.getTransaction(commit.log.transactionHash)).from
316
+
289
317
  prettyTable.call(this, {
290
318
  merkleRoot: commit.report.merkleRoot,
291
319
  min: Number(commit.report.minSeqNr),
292
320
  max: Number(commit.report.maxSeqNr),
293
- origin: commit.log.tx?.from ?? (await dest.getTransaction(commit.log.transactionHash)).from,
294
- contract: commit.log.address,
295
- transactionHash: commit.log.transactionHash,
321
+ origin: formatDisplayAddress(origin, destFamily),
322
+ contract: formatDisplayAddress(commit.log.address, destFamily),
323
+ transactionHash: formatDisplayTxHash(commit.log.transactionHash, destFamily),
296
324
  blockNumber: commit.log.blockNumber,
297
325
  timestamp: `${formatDate(timestamp)} (${formatDuration(timestamp - request.tx.timestamp)} after request)`,
298
326
  })
@@ -396,9 +424,11 @@ export function prettyTable(
396
424
  export function prettyReceipt(
397
425
  this: Ctx,
398
426
  receipt: CCIPExecution,
399
- request: PickDeep<CCIPRequest, 'tx.timestamp'>,
427
+ request: PickDeep<CCIPRequest, 'tx.timestamp' | 'lane.destChainSelector'>,
400
428
  origin?: string,
401
429
  ) {
430
+ const destFamily = networkInfo(request.lane.destChainSelector).family
431
+
402
432
  prettyTable.call(this, {
403
433
  state: receipt.receipt.state === ExecutionState.Success ? '✅ success' : '❌ failed',
404
434
  ...(receipt.receipt.state !== ExecutionState.Success ||
@@ -406,34 +436,79 @@ export function prettyReceipt(
406
436
  ? { returnData: receipt.receipt.returnData }
407
437
  : {}),
408
438
  ...(receipt.receipt.gasUsed ? { gasUsed: Number(receipt.receipt.gasUsed) } : {}),
409
- ...(origin ? { origin } : {}),
410
- contract: receipt.log.address,
411
- transactionHash: receipt.log.transactionHash,
439
+ ...(origin ? { origin: formatDisplayAddress(origin, destFamily) } : {}),
440
+ contract: formatDisplayAddress(receipt.log.address, destFamily),
441
+ transactionHash: formatDisplayTxHash(receipt.log.transactionHash, destFamily),
412
442
  logIndex: receipt.log.index,
413
443
  blockNumber: receipt.log.blockNumber,
414
444
  timestamp: `${formatDate(receipt.timestamp)} (${formatDuration(receipt.timestamp - request.tx.timestamp)} after request)`,
415
445
  })
416
446
  }
417
447
 
448
+ /**
449
+ * Format a CCIPError with recovery hints for user-friendly display.
450
+ * @param err - Error to format.
451
+ * @param verbose - If true, include stack trace for debugging.
452
+ * @returns Formatted error string if CCIPError, null otherwise.
453
+ */
454
+ export function formatCCIPError(err: unknown, verbose = false): string | null {
455
+ if (!CCIPError.isCCIPError(err)) return null
456
+
457
+ const lines: string[] = []
458
+
459
+ lines.push(`error[${err.code}]: ${err.message}`)
460
+
461
+ if (err.recovery) {
462
+ lines.push(` help: ${err.recovery}`)
463
+ }
464
+
465
+ if (err.isTransient) {
466
+ let note = 'this error may resolve on retry'
467
+ if (err.retryAfterMs) {
468
+ note += ` (wait ${Math.round(err.retryAfterMs / 1000)}s)`
469
+ }
470
+ lines.push(` note: ${note}`)
471
+ }
472
+
473
+ if (verbose && err.stack) {
474
+ lines.push('')
475
+ lines.push(' Stack trace:')
476
+ const stackLines = err.stack.split('\n').slice(1)
477
+ for (const line of stackLines) {
478
+ lines.push(` ${line}`)
479
+ }
480
+ }
481
+
482
+ return lines.join('\n')
483
+ }
484
+
418
485
  /**
419
486
  * Logs a parsed error message if the error can be decoded.
420
487
  * @param err - Error to parse and log.
421
488
  * @returns True if error was successfully parsed and logged.
422
489
  */
423
490
  export function logParsedError(this: Ctx, err: unknown): boolean {
491
+ // First check if it's a CCIPError with recovery hints
492
+ const formatted = formatCCIPError(err, this.verbose)
493
+ if (formatted) {
494
+ this.logger.error(formatted)
495
+ return true
496
+ }
497
+
498
+ // Then try chain-specific parsing for revert data
424
499
  for (const chain of Object.values<ChainStatic>(supportedChains)) {
425
500
  const parsed = chain.parse?.(err)
426
501
  if (!parsed) continue
427
502
  const { method, Instruction: instruction, ...rest } = parsed
428
503
  if (method || instruction) {
429
504
  this.logger.error(
430
- `🛑 Failed to call "${(method || instruction) as string}"`,
505
+ `error: Failed to call "${(method || instruction) as string}"`,
431
506
  ...Object.entries(rest)
432
507
  .map(([k, e]) => [`\n${k.substring(0, 1).toUpperCase()}${k.substring(1)} =`, e])
433
508
  .flat(1),
434
509
  )
435
510
  } else {
436
- this.logger.error('🛑 Error:', parsed)
511
+ this.logger.error('error:', parsed)
437
512
  }
438
513
  return true
439
514
  }
@@ -475,9 +550,9 @@ export async function* yieldResolved<T>(promises: readonly Promise<T>[]): AsyncG
475
550
  * @param argv - yargs argv containing verbose flag
476
551
  * @returns AbortController and context object with destroy$ signal and logger
477
552
  */
478
- export function getCtx(argv: { verbose?: boolean }): [controller: AbortController, ctx: Ctx] {
479
- const controller = new AbortController()
480
- const destroy$ = controller.signal
553
+ export function getCtx(argv: { verbose?: boolean }): [ctx: Ctx, destroy: () => void] {
554
+ let destroy
555
+ const destroy$ = new Promise<void>((resolve) => (destroy = resolve))
481
556
 
482
557
  const logger = new Console(process.stdout, process.stderr, true)
483
558
  if (argv.verbose) {
@@ -486,5 +561,5 @@ export function getCtx(argv: { verbose?: boolean }): [controller: AbortControlle
486
561
  logger.debug = () => {}
487
562
  }
488
563
 
489
- return [controller, { destroy$, logger }]
564
+ return [{ destroy$, logger, verbose: argv.verbose }, destroy!]
490
565
  }
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import { realpathSync } from 'fs'
3
3
  import util from 'node:util'
4
4
  import { pathToFileURL } from 'url'
5
5
 
6
- import yargs, { type InferredOptionTypes } from 'yargs'
6
+ import yargs, { type ArgumentsCamelCase, type InferredOptionTypes } from 'yargs'
7
7
  import { hideBin } from 'yargs/helpers'
8
8
 
9
9
  import { Format } from './commands/index.ts'
@@ -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.91.1-e1fb60a'
14
+ const VERSION = '0.92.1-22759cb'
15
15
  // generate:end
16
16
 
17
17
  const globalOpts = {
18
18
  rpcs: {
19
19
  type: 'array',
20
- alias: 'r',
20
+ alias: ['r', 'rpc'],
21
21
  describe: 'List of RPC endpoint URLs, ws[s] or http[s]',
22
22
  string: true,
23
23
  },
@@ -46,7 +46,7 @@ const globalOpts = {
46
46
  } as const
47
47
 
48
48
  /** Type for global CLI options. */
49
- export type GlobalOpts = InferredOptionTypes<typeof globalOpts>
49
+ export type GlobalOpts = ArgumentsCamelCase<InferredOptionTypes<typeof globalOpts>>
50
50
 
51
51
  async function main() {
52
52
  await yargs(hideBin(process.argv))
@@ -1,5 +1,4 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
- import util from 'node:util'
3
2
 
4
3
  import {
5
4
  type AccountAddress,
@@ -12,6 +11,7 @@ import {
12
11
  Ed25519Signature,
13
12
  generateSigningMessageForTransaction,
14
13
  } from '@aptos-labs/ts-sdk'
14
+ import { CCIPArgumentInvalidError } from '@chainlink/ccip-sdk/src/index.ts'
15
15
  import AptosLedger from '@ledgerhq/hw-app-aptos'
16
16
  import HIDTransport from '@ledgerhq/hw-transport-node-hid'
17
17
  import { type BytesLike, getBytes, hexlify } from 'ethers'
@@ -98,9 +98,7 @@ export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
98
98
  * @returns Promise to AptosAsyncAccount instance
99
99
  */
100
100
  export async function loadAptosWallet({ wallet: walletOpt }: { wallet?: unknown }) {
101
- if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY']
102
- if (typeof walletOpt !== 'string')
103
- throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)
101
+ if (typeof walletOpt !== 'string') throw new CCIPArgumentInvalidError('wallet', String(walletOpt))
104
102
  if ((walletOpt ?? '').startsWith('ledger')) {
105
103
  let derivationPath = walletOpt.split(':')[1]
106
104
  if (!derivationPath) derivationPath = "m/44'/637'/0'/0'/0'"
@@ -121,5 +119,5 @@ export async function loadAptosWallet({ wallet: walletOpt }: { wallet?: unknown
121
119
  privateKey: new Ed25519PrivateKey(walletOpt as string, false),
122
120
  })
123
121
  }
124
- throw new Error('Wallet not specified')
122
+ throw new CCIPArgumentInvalidError('wallet', 'not specified')
125
123
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
- import util from 'util'
4
3
 
4
+ import { CCIPArgumentInvalidError } from '@chainlink/ccip-sdk/src/index.ts'
5
5
  import { LedgerSigner } from '@ethers-ext/signer-ledger'
6
6
  import { password } from '@inquirer/prompts'
7
7
  import HIDTransport from '@ledgerhq/hw-transport-node-hid'
@@ -31,7 +31,6 @@ export async function loadEvmWallet(
31
31
  provider: JsonRpcApiProvider,
32
32
  { wallet: walletOpt }: { wallet?: unknown },
33
33
  ): Promise<Signer> {
34
- if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY']
35
34
  if (
36
35
  typeof walletOpt === 'number' ||
37
36
  (typeof walletOpt === 'string' && walletOpt.match(/^(\d+|0x[a-fA-F0-9]{40})$/))
@@ -43,8 +42,7 @@ export async function loadEvmWallet(
43
42
  : Number(walletOpt),
44
43
  )
45
44
  }
46
- if (typeof walletOpt !== 'string')
47
- throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)
45
+ if (typeof walletOpt !== 'string') throw new CCIPArgumentInvalidError('wallet', String(walletOpt))
48
46
  if ((walletOpt ?? '').startsWith('ledger')) {
49
47
  let derivationPath = walletOpt.split(':')[1]
50
48
  if (derivationPath && !isNaN(Number(derivationPath)))