@chainlink/ccip-cli 0.96.0 → 0.97.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 (38) hide show
  1. package/dist/commands/lane-latency.d.ts +17 -0
  2. package/dist/commands/lane-latency.d.ts.map +1 -1
  3. package/dist/commands/lane-latency.js +18 -1
  4. package/dist/commands/lane-latency.js.map +1 -1
  5. package/dist/commands/manual-exec.d.ts +20 -0
  6. package/dist/commands/manual-exec.d.ts.map +1 -1
  7. package/dist/commands/manual-exec.js +34 -18
  8. package/dist/commands/manual-exec.js.map +1 -1
  9. package/dist/commands/parse.d.ts +17 -0
  10. package/dist/commands/parse.d.ts.map +1 -1
  11. package/dist/commands/parse.js +17 -0
  12. package/dist/commands/parse.js.map +1 -1
  13. package/dist/commands/send.d.ts +20 -0
  14. package/dist/commands/send.d.ts.map +1 -1
  15. package/dist/commands/send.js +22 -4
  16. package/dist/commands/send.js.map +1 -1
  17. package/dist/commands/show.d.ts +22 -6
  18. package/dist/commands/show.d.ts.map +1 -1
  19. package/dist/commands/show.js +77 -43
  20. package/dist/commands/show.js.map +1 -1
  21. package/dist/commands/utils.d.ts +7 -7
  22. package/dist/commands/utils.d.ts.map +1 -1
  23. package/dist/commands/utils.js +89 -43
  24. package/dist/commands/utils.js.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/loadtest.d.ts +2 -0
  27. package/dist/loadtest.d.ts.map +1 -0
  28. package/dist/loadtest.js +132 -0
  29. package/dist/loadtest.js.map +1 -0
  30. package/package.json +8 -8
  31. package/src/commands/lane-latency.ts +19 -1
  32. package/src/commands/manual-exec.ts +35 -27
  33. package/src/commands/parse.ts +18 -0
  34. package/src/commands/send.ts +23 -4
  35. package/src/commands/show.ts +85 -44
  36. package/src/commands/utils.ts +105 -43
  37. package/src/index.ts +1 -1
  38. package/src/loadtest.ts +171 -0
@@ -1,9 +1,9 @@
1
1
  import { Console } from 'node:console'
2
2
 
3
3
  import {
4
- type CCIPCommit,
5
4
  type CCIPExecution,
6
5
  type CCIPRequest,
6
+ type CCIPVerifications,
7
7
  type Chain,
8
8
  type ChainFamily,
9
9
  type ChainStatic,
@@ -18,7 +18,6 @@ import {
18
18
  } from '@chainlink/ccip-sdk/src/index.ts'
19
19
  import { select } from '@inquirer/prompts'
20
20
  import {
21
- dataLength,
22
21
  formatUnits,
23
22
  hexlify,
24
23
  isBytesLike,
@@ -125,14 +124,17 @@ export function formatDisplayTxHash(hash: string, family: ChainFamily): string {
125
124
  }
126
125
 
127
126
  async function formatToken(
128
- source: Chain,
129
- ta: { amount: bigint } & ({ token: string } | { sourcePoolAddress: string }),
127
+ source: Chain | undefined,
128
+ ta: { amount: bigint } & (
129
+ | { token: string }
130
+ | { sourceTokenAddress?: string; sourcePoolAddress: string }
131
+ ),
130
132
  ): Promise<string> {
133
+ if (!source) return `${ta.amount} ${'sourcePoolAddress' in ta ? ta.sourcePoolAddress : ta.token}`
131
134
  let token
132
135
  if ('token' in ta) token = ta.token
133
- else {
134
- token = await source.getTokenForTokenPool(ta.sourcePoolAddress)
135
- }
136
+ else if (ta.sourceTokenAddress) token = ta.sourceTokenAddress
137
+ else token = await source.getTokenForTokenPool(ta.sourcePoolAddress)
136
138
  const { symbol, decimals } = await source.getTokenInfo(token)
137
139
  return `${formatUnits(ta.amount, decimals)} ${symbol}`
138
140
  }
@@ -143,21 +145,37 @@ async function formatToken(
143
145
  * @param values - Array values to format.
144
146
  * @returns Record with indexed keys.
145
147
  */
146
- export function formatArray<T>(name: string, values: readonly T[]): Record<string, T> {
148
+ export function formatArray<T>(
149
+ name: string,
150
+ values: readonly T[],
151
+ edges = '[]',
152
+ ): Record<string, T> {
147
153
  if (values.length <= 1) return { [name]: values[0]! }
148
- return Object.fromEntries(values.map((v, i) => [`${name}[${i}]`, v] as const))
154
+ return Object.fromEntries(values.map((v, i) => [`${name}${edges[0]}${i}${edges[1]}`, v] as const))
149
155
  }
150
156
 
151
157
  // join truthy property names, separated by a dot
152
- function j(...args: string[]): string {
153
- return args.filter(Boolean).join('.')
158
+ function j(...args: (string | number)[]): string {
159
+ return args.reduce(
160
+ (acc: string, v): string =>
161
+ v === ''
162
+ ? acc
163
+ : acc
164
+ ? acc + (typeof v === 'number' ? `[${v}]` : (v.match(/^\w/) ? '.' : '') + v)
165
+ : v.toString(),
166
+ '',
167
+ )
154
168
  }
155
169
 
156
170
  function formatData(name: string, data: string, parseError = false): Record<string, string> {
157
171
  if (parseError) {
158
172
  let parsed
159
173
  for (const chain of Object.values(supportedChains)) {
160
- parsed = chain.parse?.(data)
174
+ try {
175
+ parsed = chain.parse?.(data)
176
+ } catch {
177
+ // ignore
178
+ }
161
179
  if (parsed) break
162
180
  }
163
181
  if (parsed) {
@@ -176,7 +194,7 @@ function formatData(name: string, data: string, parseError = false): Record<stri
176
194
  for (let i = data.length; i > 2; i -= 64) {
177
195
  split.unshift(data.substring(Math.max(i - 64, 0), i))
178
196
  }
179
- return formatArray(name, split)
197
+ return formatArray(name, split, '{}')
180
198
  }
181
199
 
182
200
  function formatDate(timestamp: number) {
@@ -238,20 +256,21 @@ function formatDataString(data: string): Record<string, string> {
238
256
 
239
257
  /**
240
258
  * Prints a CCIP request in a human-readable format.
241
- * @param source - Source chain instance.
242
259
  * @param request - CCIP request to print.
260
+ * @param source - Source chain instance.
243
261
  */
244
- export async function prettyRequest(this: Ctx, source: Chain, request: CCIPRequest) {
262
+ export async function prettyRequest(this: Ctx, request: CCIPRequest, source?: Chain) {
245
263
  prettyLane.call(this, request.lane)
246
264
  this.logger.info('Request (source):')
247
265
 
248
266
  let finalized
249
267
  try {
250
- finalized = await source.getBlockTimestamp('finalized')
268
+ if (source) finalized = await source.getBlockTimestamp('finalized')
251
269
  } catch (_) {
252
270
  // no finalized tag support
253
271
  }
254
- const nonce = Number(request.message.nonce)
272
+ let nonce
273
+ if ('nonce' in request.message) nonce = Number(request.message.nonce)
255
274
 
256
275
  const sourceFamily = networkInfo(request.lane.sourceChainSelector).family
257
276
  const destFamily = networkInfo(request.lane.destChainSelector).family
@@ -280,6 +299,8 @@ export async function prettyRequest(this: Ctx, source: Chain, request: CCIPReque
280
299
  'destChainSelector',
281
300
  'extraArgs',
282
301
  'accounts',
302
+ 'receipts',
303
+ 'encodedMessage',
283
304
  )
284
305
  prettyTable.call(this, {
285
306
  messageId: request.message.messageId,
@@ -287,7 +308,7 @@ export async function prettyRequest(this: Ctx, source: Chain, request: CCIPReque
287
308
  sender: displaySender,
288
309
  receiver: displayReceiver,
289
310
  sequenceNumber: Number(request.message.sequenceNumber),
290
- nonce: nonce === 0 ? '0 => allow out-of-order exec' : nonce,
311
+ ...(nonce != null && { nonce: nonce === 0 ? '0 => allow out-of-order exec' : nonce }),
291
312
  ...('gasLimit' in request.message
292
313
  ? { gasLimit: Number(request.message.gasLimit) }
293
314
  : 'computeUnits' in request.message
@@ -312,37 +333,53 @@ export async function prettyRequest(this: Ctx, source: Chain, request: CCIPReque
312
333
  ),
313
334
  ...formatDataString(request.message.data),
314
335
  ...('accounts' in request.message ? formatArray('accounts', request.message.accounts) : {}),
336
+ ...('receipts' in request.message ? formatArray('receipts', request.message.receipts) : {}),
315
337
  ...rest,
316
338
  })
317
339
  this.logger.info('CCIP Explorer:', getCCIPExplorerUrl('msg', request.message.messageId))
318
340
  }
319
341
 
320
342
  /**
321
- * Prints a CCIP commit in a human-readable format.
343
+ * Prints CCIP Verifications in a human-readable format.
322
344
  * @param dest - Destination chain instance.
323
- * @param commit - CCIP commit to print.
345
+ * @param verifications - CCIP verifications to print.
324
346
  * @param request - CCIP request for timestamp comparison.
325
347
  */
326
- export async function prettyCommit(
348
+ export async function prettyVerifications(
327
349
  this: Ctx,
328
350
  dest: Chain,
329
- commit: CCIPCommit,
351
+ verifications: CCIPVerifications,
330
352
  request: PickDeep<CCIPRequest, 'tx.timestamp' | 'lane.destChainSelector'>,
331
353
  ) {
332
- const timestamp = await dest.getBlockTimestamp(commit.log.blockNumber)
333
354
  const destFamily = networkInfo(request.lane.destChainSelector).family
334
- const origin = commit.log.tx?.from ?? (await dest.getTransaction(commit.log.transactionHash)).from
335
355
 
336
- prettyTable.call(this, {
337
- merkleRoot: commit.report.merkleRoot,
338
- min: Number(commit.report.minSeqNr),
339
- max: Number(commit.report.maxSeqNr),
340
- origin: formatDisplayAddress(origin, destFamily),
341
- contract: formatDisplayAddress(commit.log.address, destFamily),
342
- transactionHash: formatDisplayTxHash(commit.log.transactionHash, destFamily),
343
- blockNumber: commit.log.blockNumber,
344
- timestamp: `${formatDate(timestamp)} (${formatDuration(timestamp - request.tx.timestamp)} after request)`,
345
- })
356
+ if ('report' in verifications) {
357
+ const timestamp = await dest.getBlockTimestamp(verifications.log.blockNumber)
358
+ const origin =
359
+ verifications.log.tx?.from ??
360
+ (await dest.getTransaction(verifications.log.transactionHash)).from
361
+ prettyTable.call(this, {
362
+ merkleRoot: verifications.report.merkleRoot,
363
+ min: Number(verifications.report.minSeqNr),
364
+ max: Number(verifications.report.maxSeqNr),
365
+ origin: formatDisplayAddress(origin, destFamily),
366
+ contract: formatDisplayAddress(verifications.log.address, destFamily),
367
+ transactionHash: formatDisplayTxHash(verifications.log.transactionHash, destFamily),
368
+ blockNumber: verifications.log.blockNumber,
369
+ timestamp: `${formatDate(timestamp)} (${formatDuration(timestamp - request.tx.timestamp)} after request)`,
370
+ })
371
+ } else {
372
+ let ts = 0
373
+ for (const { timestamp } of verifications.verifications)
374
+ if (timestamp && timestamp > ts) ts = timestamp
375
+
376
+ prettyTable.call(this, {
377
+ ...verifications,
378
+ ...(ts && {
379
+ timestamp: `${formatDate(ts)} (${formatDuration(ts - request.tx.timestamp)} after request)`,
380
+ }),
381
+ })
382
+ }
346
383
  }
347
384
 
348
385
  /**
@@ -401,6 +438,18 @@ function wrapText(text: string, maxWidth: number, threshold: number = 0.1): stri
401
438
  return lines
402
439
  }
403
440
 
441
+ function flatten(val: unknown, path: (string | number)[] = []): [(string | number)[], unknown][] {
442
+ if (Array.isArray(val)) {
443
+ if (val.length) return val.map((v: unknown, i: number) => flatten(v, [...path, i])).flat(1)
444
+ } else if (val && typeof val === 'object') {
445
+ if (Object.keys(val).length === 0) return [[path, val]]
446
+ return Object.entries(val)
447
+ .map(([k, v]) => flatten(v, [...path, k]))
448
+ .flat(1)
449
+ }
450
+ return [[path, val]]
451
+ }
452
+
404
453
  /**
405
454
  * Prints a formatted table of key-value pairs.
406
455
  * @param args - Key-value pairs to print.
@@ -412,23 +461,29 @@ export function prettyTable(
412
461
  opts = { parseErrorKeys: ['returnData'], spcount: 0 },
413
462
  ) {
414
463
  const out: (readonly [string, unknown])[] = []
415
- for (const [key, value] of Object.entries(args)) {
464
+ for (const [path, value] of flatten(args)) {
465
+ const last = path[path.length - 1]
466
+ const key = j(...path)
416
467
  if (isBytesLike(value)) {
417
468
  let parseError
418
- if (opts.parseErrorKeys.includes(key)) parseError = true
419
- if (dataLength(value) <= 32 && !parseError) out.push([key, value])
420
- else out.push(...Object.entries(formatData(key, hexlify(value), parseError)))
469
+ if (opts.parseErrorKeys.includes(path[path.length - 1]!.toString())) parseError = true
470
+ out.push(
471
+ ...Object.entries(
472
+ formatData(key, typeof value !== 'string' ? hexlify(value) : value, parseError),
473
+ ),
474
+ )
421
475
  } else if (typeof value === 'string') {
422
476
  out.push(
423
477
  ...wrapText(value, Math.max(100, +(process.env.COLUMNS || 80) * 0.9)).map(
424
478
  (l, i) => [!i ? key : ' '.repeat(opts.spcount++), l] as const,
425
479
  ),
426
480
  )
427
- } else if (Array.isArray(value)) {
428
- if (value.length <= 1) out.push([key, value[0] as unknown])
429
- else out.push(...value.map((v, i) => [`${key}[${i}]`, v as unknown] as const))
430
- } else if (value && typeof value === 'object') {
431
- out.push(...Object.entries(value).map(([k, v]) => [`${key}.${k}`, v] as const))
481
+ } else if (
482
+ typeof last === 'string' &&
483
+ last.toLowerCase().includes('timestamp') &&
484
+ typeof value === 'number'
485
+ ) {
486
+ out.push([key, formatDate(value)])
432
487
  } else out.push([key, value])
433
488
  }
434
489
  return this.logger.table(Object.fromEntries(out))
@@ -477,6 +532,13 @@ export function formatCCIPError(err: unknown, verbose = false): string | null {
477
532
 
478
533
  lines.push(`error[${err.code}]: ${err.message}`)
479
534
 
535
+ if (Object.keys(err.context).length > 0) {
536
+ lines.push(' context:')
537
+ for (const [key, value] of Object.entries(err.context)) {
538
+ lines.push(` ${key}: ${value as string}`)
539
+ }
540
+ }
541
+
480
542
  if (err.recovery) {
481
543
  lines.push(` help: ${err.recovery}`)
482
544
  }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ 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.96.0-983178a'
14
+ const VERSION = '0.97.0-8811550'
15
15
  // generate:end
16
16
 
17
17
  const globalOpts = {
@@ -0,0 +1,171 @@
1
+ import { ChainFamily, networkInfo } from '@chainlink/ccip-sdk/src/index.ts'
2
+ import { formatUnits, toUtf8Bytes } from 'ethers'
3
+
4
+ import { getCtx } from './commands/utils.ts'
5
+ import { fetchChainsFromRpcs, loadChainWallet } from './providers/index.ts'
6
+
7
+ const DEST = networkInfo('ethereum-testnet-sepolia-base-1')
8
+ const RECEIVER = '0x'
9
+ const RPCS_FILE = '../../.env'
10
+ const RPCS = [
11
+ 'https://ethereum-sepolia-rpc.publicnode.com',
12
+ 'https://avalanche-fuji-c-chain-rpc.publicnode.com',
13
+ ]
14
+ const PRIVATE_KEYS = {
15
+ [ChainFamily.Solana]: process.env['PRIVATE_KEY_SOLANA'],
16
+ [ChainFamily.Aptos]: process.env['PRIVATE_KEY_APTOS'],
17
+ [ChainFamily.EVM]: process.env['PRIVATE_KEY'],
18
+ }
19
+
20
+ // mapping of source_network_name to its router
21
+ const SOURCES: Record<string, string> = {
22
+ 'avalanche-testnet-fuji': '0xF694E193200268f9a4868e4Aa017A0118C9a8177',
23
+ }
24
+ // per source:
25
+ const MAX_COUNT = 100
26
+ const MAX_INFLIGHT = 10 // inflight on RPC (tx to be accepted/included), not on CCIP
27
+ const MAX_PER_SECOND = 1
28
+
29
+ /** like Promise.all, but receives Promise factories and spawn a maximum number of them in parallel */
30
+ function promiseAllMax<T>(
31
+ promises: readonly (() => Promise<T>)[],
32
+ maxParallelJobs: number,
33
+ cancel?: Promise<unknown>,
34
+ ): Promise<T[]> {
35
+ return new Promise((resolve, reject) => {
36
+ const results = new Array(promises.length) as T[]
37
+ let completed = 0
38
+ let started = 0
39
+ let rejected = false
40
+
41
+ if (promises.length === 0) {
42
+ resolve([])
43
+ return
44
+ }
45
+
46
+ const startNext = () => {
47
+ if (rejected || started >= promises.length) return
48
+
49
+ const index = started++
50
+ const promiseFactory = promises[index]!
51
+
52
+ promiseFactory()
53
+ .then((result) => {
54
+ if (rejected) return
55
+ results[index] = result
56
+ completed++
57
+
58
+ if (completed === promises.length) {
59
+ resolve(results)
60
+ } else {
61
+ startNext()
62
+ }
63
+ })
64
+ .catch((err) => {
65
+ rejected = true
66
+ reject(err as Error)
67
+ })
68
+ }
69
+
70
+ // Handle cancellation
71
+ void cancel?.then(() => {
72
+ if (!rejected && completed < promises.length) {
73
+ rejected = true
74
+ // eslint-disable-next-line no-restricted-syntax
75
+ reject(new Error('Cancelled'))
76
+ }
77
+ })
78
+
79
+ // Start up to maxParallelJobs promises
80
+ for (let i = 0; i < maxParallelJobs && i < promises.length; i++) {
81
+ startNext()
82
+ }
83
+ })
84
+ }
85
+
86
+ async function main() {
87
+ const [ctx] = getCtx({ verbose: !!process.env['CCIP_VERBOSE'] })
88
+ const { logger } = ctx
89
+ const getChain = fetchChainsFromRpcs(ctx, {
90
+ noApi: true,
91
+ rpcsFile: RPCS_FILE,
92
+ rpcs: RPCS,
93
+ })
94
+ const allLanes = []
95
+ for (const [name, router] of Object.entries(SOURCES)) {
96
+ const source = await getChain(name)
97
+ const [walletAddr, wallet] = await loadChainWallet(source, {
98
+ wallet: PRIVATE_KEYS[source.network.family as keyof typeof PRIVATE_KEYS],
99
+ })
100
+ const initialBalance = await source.getBalance({ holder: walletAddr })
101
+ const nativeToken = await source.getNativeTokenForRouter(router)
102
+ const nativeInfo = await source.getTokenInfo(nativeToken)
103
+ const symbol = nativeInfo.symbol.startsWith('W')
104
+ ? nativeInfo.symbol.substring(1)
105
+ : nativeInfo.symbol
106
+ logger.info(
107
+ `Initial balance of ${walletAddr} @ ${name}:`,
108
+ initialBalance,
109
+ '=',
110
+ formatUnits(initialBalance, nativeInfo.decimals),
111
+ symbol,
112
+ )
113
+
114
+ const startTime = performance.now()
115
+ let inflight = 0,
116
+ completed = 0
117
+ const tasks = Array.from({ length: MAX_COUNT }, (_, i) => async () => {
118
+ const deltaMs = performance.now() - startTime
119
+ const delay = (1e3 * completed) / MAX_PER_SECOND - deltaMs
120
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay))
121
+
122
+ inflight++
123
+ const req = await source.sendMessage({
124
+ router,
125
+ destChainSelector: DEST.chainSelector,
126
+ message: {
127
+ receiver: RECEIVER,
128
+ data: toUtf8Bytes(`ccip-cli load test: ${i + 1}/${MAX_COUNT}`),
129
+ extraArgs: { gasLimit: 0n },
130
+ },
131
+ wallet,
132
+ })
133
+ inflight--
134
+ completed++
135
+ logger.info(`[${i + 1}] LOAD TEST`, name, '=>', DEST.name, {
136
+ inflight,
137
+ completed,
138
+ total: MAX_COUNT,
139
+ messageId: req.message.messageId,
140
+ tx: req.log.transactionHash,
141
+ })
142
+ })
143
+ allLanes.push(
144
+ promiseAllMax(tasks, MAX_INFLIGHT).then(async () => {
145
+ const finalBalance = await source.getBalance({ holder: walletAddr })
146
+ logger.info(
147
+ `Final balance of ${walletAddr} @ ${name}:`,
148
+ finalBalance,
149
+ '=',
150
+ formatUnits(finalBalance, nativeInfo.decimals),
151
+ symbol,
152
+ ', spent =',
153
+ formatUnits(initialBalance - finalBalance, nativeInfo.decimals),
154
+ )
155
+ const delta = (performance.now() - startTime) / 1e3
156
+ logger.warn(
157
+ `[${name}] Sent`,
158
+ completed,
159
+ `requests in`,
160
+ delta,
161
+ `seconds =~`,
162
+ completed / delta,
163
+ `reqs/s`,
164
+ )
165
+ }),
166
+ )
167
+ }
168
+ await Promise.all(allLanes)
169
+ }
170
+
171
+ await main()