@chainlink/ccip-cli 0.90.2 → 0.91.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 (68) hide show
  1. package/README.md +40 -21
  2. package/dist/commands/index.d.ts +2 -1
  3. package/dist/commands/index.d.ts.map +1 -1
  4. package/dist/commands/index.js +1 -0
  5. package/dist/commands/index.js.map +1 -1
  6. package/dist/commands/manual-exec.d.ts +9 -0
  7. package/dist/commands/manual-exec.d.ts.map +1 -1
  8. package/dist/commands/manual-exec.js +40 -35
  9. package/dist/commands/manual-exec.js.map +1 -1
  10. package/dist/commands/parse.d.ts +9 -0
  11. package/dist/commands/parse.d.ts.map +1 -1
  12. package/dist/commands/parse.js +18 -7
  13. package/dist/commands/parse.js.map +1 -1
  14. package/dist/commands/send.d.ts +9 -0
  15. package/dist/commands/send.d.ts.map +1 -1
  16. package/dist/commands/send.js +39 -26
  17. package/dist/commands/send.js.map +1 -1
  18. package/dist/commands/show.d.ts +9 -0
  19. package/dist/commands/show.d.ts.map +1 -1
  20. package/dist/commands/show.js +46 -39
  21. package/dist/commands/show.js.map +1 -1
  22. package/dist/commands/supported-tokens.d.ts +9 -2
  23. package/dist/commands/supported-tokens.d.ts.map +1 -1
  24. package/dist/commands/supported-tokens.js +35 -30
  25. package/dist/commands/supported-tokens.js.map +1 -1
  26. package/dist/commands/types.d.ts +13 -0
  27. package/dist/commands/types.d.ts.map +1 -1
  28. package/dist/commands/types.js +1 -0
  29. package/dist/commands/types.js.map +1 -1
  30. package/dist/commands/utils.d.ts +75 -10
  31. package/dist/commands/utils.d.ts.map +1 -1
  32. package/dist/commands/utils.js +95 -19
  33. package/dist/commands/utils.js.map +1 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +17 -7
  37. package/dist/index.js.map +1 -1
  38. package/dist/providers/aptos.d.ts +35 -0
  39. package/dist/providers/aptos.d.ts.map +1 -1
  40. package/dist/providers/aptos.js +34 -9
  41. package/dist/providers/aptos.js.map +1 -1
  42. package/dist/providers/evm.d.ts +10 -1
  43. package/dist/providers/evm.d.ts.map +1 -1
  44. package/dist/providers/evm.js +11 -5
  45. package/dist/providers/evm.js.map +1 -1
  46. package/dist/providers/index.d.ts +15 -8
  47. package/dist/providers/index.d.ts.map +1 -1
  48. package/dist/providers/index.js +47 -11
  49. package/dist/providers/index.js.map +1 -1
  50. package/dist/providers/solana.d.ts +30 -0
  51. package/dist/providers/solana.d.ts.map +1 -1
  52. package/dist/providers/solana.js +30 -5
  53. package/dist/providers/solana.js.map +1 -1
  54. package/package.json +11 -8
  55. package/src/commands/index.ts +2 -1
  56. package/src/commands/manual-exec.ts +40 -33
  57. package/src/commands/parse.ts +19 -8
  58. package/src/commands/send.ts +47 -30
  59. package/src/commands/show.ts +47 -48
  60. package/src/commands/supported-tokens.ts +35 -30
  61. package/src/commands/types.ts +15 -0
  62. package/src/commands/utils.ts +112 -26
  63. package/src/index.ts +21 -7
  64. package/src/providers/aptos.ts +33 -8
  65. package/src/providers/evm.ts +22 -5
  66. package/src/providers/index.ts +67 -15
  67. package/src/providers/solana.ts +31 -6
  68. package/tsconfig.json +2 -1
@@ -1,3 +1,5 @@
1
+ import { Console } from 'node:console'
2
+
1
3
  import {
2
4
  type CCIPCommit,
3
5
  type CCIPExecution,
@@ -21,7 +23,17 @@ import {
21
23
  parseUnits,
22
24
  toUtf8String,
23
25
  } from 'ethers'
26
+ import type { PickDeep } from 'type-fest'
27
+
28
+ import type { Ctx } from './types.ts'
24
29
 
30
+ /**
31
+ * Prompts user to select a CCIP request from a list.
32
+ * @param requests - List of CCIP requests to choose from.
33
+ * @param promptSuffix - Optional suffix for the prompt message.
34
+ * @param hints - Optional hints for pre-filtering requests.
35
+ * @returns Selected CCIP request.
36
+ */
25
37
  export async function selectRequest(
26
38
  requests: readonly CCIPRequest[],
27
39
  promptSuffix?: string,
@@ -55,17 +67,29 @@ tokenTransfers =\t[${req.message.tokenAmounts.map((ta) => ('token' in ta ? ta.to
55
67
  return requests[answer]
56
68
  }
57
69
 
58
- export function withDateTimestamp<T extends { readonly timestamp: number }>(
59
- obj: T,
60
- ): Omit<T, 'timestamp'> & { timestamp: Date } {
61
- return { ...obj, timestamp: new Date(obj.timestamp * 1e3) }
70
+ /**
71
+ * Converts a Unix timestamp to a Date object.
72
+ * @param obj - Object with timestamp property.
73
+ * @returns Object with Date timestamp.
74
+ */
75
+ export function withDateTimestamp<
76
+ T extends { readonly timestamp: number } | { readonly tx: { readonly timestamp: number } },
77
+ >(obj: T): Omit<T, 'timestamp'> & { timestamp: Date } {
78
+ return {
79
+ ...obj,
80
+ timestamp: new Date(('timestamp' in obj ? obj.timestamp : obj.tx.timestamp) * 1e3),
81
+ }
62
82
  }
63
83
 
64
- export function prettyLane(lane: Lane) {
65
- console.info('Lane:')
84
+ /**
85
+ * Prints lane information in a human-readable format.
86
+ * @param lane - Lane configuration.
87
+ */
88
+ export function prettyLane(this: Ctx, lane: Lane) {
89
+ this.logger.info('Lane:')
66
90
  const source = networkInfo(lane.sourceChainSelector),
67
91
  dest = networkInfo(lane.destChainSelector)
68
- console.table({
92
+ this.logger.table({
69
93
  name: { source: source.name, dest: dest.name },
70
94
  chainId: { source: source.chainId, dest: dest.chainId },
71
95
  chainSelector: { source: source.chainSelector, dest: dest.chainSelector },
@@ -86,6 +110,12 @@ async function formatToken(
86
110
  return `${formatUnits(ta.amount, decimals)} ${symbol}`
87
111
  }
88
112
 
113
+ /**
114
+ * Formats an array into a record with indexed keys.
115
+ * @param name - Base name for the keys.
116
+ * @param values - Array values to format.
117
+ * @returns Record with indexed keys.
118
+ */
89
119
  export function formatArray<T>(name: string, values: readonly T[]): Record<string, T> {
90
120
  if (values.length <= 1) return { [name]: values[0] }
91
121
  return Object.fromEntries(values.map((v, i) => [`${name}[${i}]`, v] as const))
@@ -126,6 +156,11 @@ function formatDate(timestamp: number) {
126
156
  return new Date(timestamp * 1e3).toISOString().substring(0, 19).replace('T', ' ')
127
157
  }
128
158
 
159
+ /**
160
+ * Formats duration in seconds to human-readable string.
161
+ * @param secs - Duration in seconds.
162
+ * @returns Formatted duration string (e.g., "1h 30m").
163
+ */
129
164
  export function formatDuration(secs: number) {
130
165
  if (secs < 0) secs = -secs
131
166
  if (secs >= 3540 && Math.floor(secs) % 60 >= 50)
@@ -154,13 +189,20 @@ function omit<T extends Record<string, unknown>, K extends string>(
154
189
  return result
155
190
  }
156
191
 
192
+ /**
193
+ * Prints a CCIP request in a human-readable format.
194
+ * @param source - Source chain instance.
195
+ * @param request - CCIP request to print.
196
+ * @param offchainTokenData - Optional offchain token data.
197
+ */
157
198
  export async function prettyRequest(
199
+ this: Ctx,
158
200
  source: Chain,
159
201
  request: CCIPRequest,
160
202
  offchainTokenData?: OffchainTokenData[],
161
203
  ) {
162
- prettyLane(request.lane)
163
- console.info('Request (source):')
204
+ prettyLane.call(this, request.lane)
205
+ this.logger.info('Request (source):')
164
206
 
165
207
  let finalized
166
208
  try {
@@ -184,7 +226,7 @@ export async function prettyRequest(
184
226
  'extraArgs',
185
227
  'accounts',
186
228
  )
187
- prettyTable({
229
+ prettyTable.call(this, {
188
230
  messageId: request.message.header.messageId,
189
231
  ...(request.tx.from ? { origin: request.tx.from } : {}),
190
232
  sender: request.message.sender,
@@ -199,11 +241,11 @@ export async function prettyRequest(
199
241
  transactionHash: request.log.transactionHash,
200
242
  logIndex: request.log.index,
201
243
  blockNumber: request.log.blockNumber,
202
- timestamp: `${formatDate(request.timestamp)} (${formatDuration(Date.now() / 1e3 - request.timestamp)} ago)`,
244
+ timestamp: `${formatDate(request.tx.timestamp)} (${formatDuration(Date.now() / 1e3 - request.tx.timestamp)} ago)`,
203
245
  finalized:
204
246
  finalized &&
205
- (finalized < request.timestamp
206
- ? formatDuration(request.timestamp - finalized) + ' left'
247
+ (finalized < request.tx.timestamp
248
+ ? formatDuration(request.tx.timestamp - finalized) + ' left'
207
249
  : true),
208
250
  fee: await formatToken(source, {
209
251
  token: request.message.feeToken,
@@ -223,21 +265,28 @@ export async function prettyRequest(
223
265
  })
224
266
 
225
267
  if (!offchainTokenData?.length || offchainTokenData.every((d) => !d)) return
226
- console.info('Attestations:')
268
+ this.logger.info('Attestations:')
227
269
  for (const attestation of offchainTokenData) {
228
270
  const { _tag: type, ...rest } = attestation!
229
- prettyTable({ type, ...rest })
271
+ prettyTable.call(this, { type, ...rest })
230
272
  }
231
273
  }
232
274
 
275
+ /**
276
+ * Prints a CCIP commit in a human-readable format.
277
+ * @param dest - Destination chain instance.
278
+ * @param commit - CCIP commit to print.
279
+ * @param request - CCIP request for timestamp comparison.
280
+ */
233
281
  export async function prettyCommit(
282
+ this: Ctx,
234
283
  dest: Chain,
235
284
  commit: CCIPCommit,
236
- request: { timestamp: number },
285
+ request: PickDeep<CCIPRequest, 'tx.timestamp'>,
237
286
  ) {
238
- console.info('Commit (dest):')
287
+ this.logger.info('Commit (dest):')
239
288
  const timestamp = await dest.getBlockTimestamp(commit.log.blockNumber)
240
- prettyTable({
289
+ prettyTable.call(this, {
241
290
  merkleRoot: commit.report.merkleRoot,
242
291
  min: Number(commit.report.minSeqNr),
243
292
  max: Number(commit.report.maxSeqNr),
@@ -245,7 +294,7 @@ export async function prettyCommit(
245
294
  contract: commit.log.address,
246
295
  transactionHash: commit.log.transactionHash,
247
296
  blockNumber: commit.log.blockNumber,
248
- timestamp: `${formatDate(timestamp)} (${formatDuration(timestamp - request.timestamp)} after request)`,
297
+ timestamp: `${formatDate(timestamp)} (${formatDuration(timestamp - request.tx.timestamp)} after request)`,
249
298
  })
250
299
  }
251
300
 
@@ -305,7 +354,13 @@ function wrapText(text: string, maxWidth: number, threshold: number = 0.1): stri
305
354
  return lines
306
355
  }
307
356
 
357
+ /**
358
+ * Prints a formatted table of key-value pairs.
359
+ * @param args - Key-value pairs to print.
360
+ * @param opts - Formatting options.
361
+ */
308
362
  export function prettyTable(
363
+ this: Ctx,
309
364
  args: Record<string, unknown>,
310
365
  opts = { parseErrorKeys: ['returnData'], spcount: 0 },
311
366
  ) {
@@ -329,15 +384,22 @@ export function prettyTable(
329
384
  out.push(...Object.entries(value).map(([k, v]) => [`${key}.${k}`, v] as const))
330
385
  } else out.push([key, value])
331
386
  }
332
- return console.table(Object.fromEntries(out))
387
+ return this.logger.table(Object.fromEntries(out))
333
388
  }
334
389
 
390
+ /**
391
+ * Prints a CCIP execution receipt in a human-readable format.
392
+ * @param receipt - CCIP execution receipt to print.
393
+ * @param request - CCIP request for timestamp comparison.
394
+ * @param origin - Optional transaction origin address.
395
+ */
335
396
  export function prettyReceipt(
397
+ this: Ctx,
336
398
  receipt: CCIPExecution,
337
- request: { timestamp: number },
399
+ request: PickDeep<CCIPRequest, 'tx.timestamp'>,
338
400
  origin?: string,
339
401
  ) {
340
- prettyTable({
402
+ prettyTable.call(this, {
341
403
  state: receipt.receipt.state === ExecutionState.Success ? '✅ success' : '❌ failed',
342
404
  ...(receipt.receipt.state !== ExecutionState.Success ||
343
405
  (receipt.receipt.returnData && receipt.receipt.returnData !== '0x')
@@ -349,24 +411,29 @@ export function prettyReceipt(
349
411
  transactionHash: receipt.log.transactionHash,
350
412
  logIndex: receipt.log.index,
351
413
  blockNumber: receipt.log.blockNumber,
352
- timestamp: `${formatDate(receipt.timestamp)} (${formatDuration(receipt.timestamp - request.timestamp)} after request)`,
414
+ timestamp: `${formatDate(receipt.timestamp)} (${formatDuration(receipt.timestamp - request.tx.timestamp)} after request)`,
353
415
  })
354
416
  }
355
417
 
356
- export function logParsedError(err: unknown): boolean {
418
+ /**
419
+ * Logs a parsed error message if the error can be decoded.
420
+ * @param err - Error to parse and log.
421
+ * @returns True if error was successfully parsed and logged.
422
+ */
423
+ export function logParsedError(this: Ctx, err: unknown): boolean {
357
424
  for (const chain of Object.values<ChainStatic>(supportedChains)) {
358
425
  const parsed = chain.parse?.(err)
359
426
  if (!parsed) continue
360
427
  const { method, Instruction: instruction, ...rest } = parsed
361
428
  if (method || instruction) {
362
- console.error(
429
+ this.logger.error(
363
430
  `🛑 Failed to call "${(method || instruction) as string}"`,
364
431
  ...Object.entries(rest)
365
432
  .map(([k, e]) => [`\n${k.substring(0, 1).toUpperCase()}${k.substring(1)} =`, e])
366
433
  .flat(1),
367
434
  )
368
435
  } else {
369
- console.error('🛑 Error:', parsed)
436
+ this.logger.error('🛑 Error:', parsed)
370
437
  }
371
438
  return true
372
439
  }
@@ -402,3 +469,22 @@ export async function* yieldResolved<T>(promises: readonly Promise<T>[]): AsyncG
402
469
  yield res
403
470
  }
404
471
  }
472
+
473
+ /**
474
+ * Create context for command execution
475
+ * @param argv - yargs argv containing verbose flag
476
+ * @returns AbortController and context object with destroy$ signal and logger
477
+ */
478
+ export function getCtx(argv: { verbose?: boolean }): [controller: AbortController, ctx: Ctx] {
479
+ const controller = new AbortController()
480
+ const destroy$ = controller.signal
481
+
482
+ const logger = new Console(process.stdout, process.stderr, true)
483
+ if (argv.verbose) {
484
+ logger.debug('Verbose mode enabled')
485
+ } else {
486
+ logger.debug = () => {}
487
+ }
488
+
489
+ return [controller, { destroy$, logger }]
490
+ }
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ import { Format } from './commands/index.ts'
9
9
  util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests
10
10
  // generate:nofail
11
11
  // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
12
- const VERSION = '0.90.2-e2dea99'
12
+ const VERSION = '0.91.0-b8bd948'
13
13
  // generate:end
14
14
 
15
15
  const globalOpts = {
@@ -43,6 +43,7 @@ const globalOpts = {
43
43
  },
44
44
  } as const
45
45
 
46
+ /** Type for global CLI options. */
46
47
  export type GlobalOpts = InferredOptionTypes<typeof globalOpts>
47
48
 
48
49
  async function main() {
@@ -50,11 +51,6 @@ async function main() {
50
51
  .scriptName(process.env.CLI_NAME || 'ccip-cli')
51
52
  .env('CCIP')
52
53
  .options(globalOpts)
53
- .middleware((argv) => {
54
- if (!argv.verbose) {
55
- console.debug = () => {}
56
- }
57
- })
58
54
  .commandDir('commands', {
59
55
  extensions: [new URL(import.meta.url).pathname.split('.').pop()!],
60
56
  exclude: /\.test\.[tj]s$/,
@@ -67,4 +63,22 @@ async function main() {
67
63
  .parse()
68
64
  }
69
65
 
70
- await main()
66
+ if (import.meta.url === `file://${process.argv[1]}`) {
67
+ const later = setTimeout(() => {}, 2 ** 31 - 1) // keep event-loop alive
68
+ await main()
69
+ .catch((err) => {
70
+ console.error(err)
71
+ throw err
72
+ })
73
+ .finally(() => {
74
+ clearTimeout(later)
75
+ setTimeout(() => {
76
+ util.inspect.defaultOptions.depth = 2
77
+ console.debug(
78
+ 'Pending handles after main completion:',
79
+ (process as any)._getActiveHandles().length, // eslint-disable-line
80
+ )
81
+ process.exit()
82
+ }, 5e3).unref()
83
+ })
84
+ }
@@ -12,20 +12,25 @@ import {
12
12
  Ed25519Signature,
13
13
  generateSigningMessageForTransaction,
14
14
  } from '@aptos-labs/ts-sdk'
15
- import { AptosChain } from '@chainlink/ccip-sdk/src/index.ts'
16
15
  import AptosLedger from '@ledgerhq/hw-app-aptos'
17
16
  import HIDTransport from '@ledgerhq/hw-transport-node-hid'
18
17
  import { type BytesLike, getBytes, hexlify } from 'ethers'
19
18
 
20
- // A LedgerSigner object represents a signer for a private key on a Ledger hardware wallet.
21
- // This object is initialized alongside a LedgerClient connection, and can be used to sign
22
- // transactions via a ledger hardware wallet.
19
+ /**
20
+ * A LedgerSigner object represents a signer for a private key on a Ledger hardware wallet.
21
+ * This object is initialized alongside a LedgerClient connection, and can be used to sign
22
+ * transactions via a ledger hardware wallet.
23
+ */
23
24
  export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
24
25
  derivationPath: string
25
26
  readonly client: AptosLedger.default
26
27
  readonly publicKey: Ed25519PublicKey
27
28
  readonly accountAddress: AccountAddress
28
29
 
30
+ /**
31
+ * Private constructor - use static `create` method instead.
32
+ * @internal
33
+ */
29
34
  private constructor(
30
35
  ledgerClient: AptosLedger.default,
31
36
  derivationPath: string,
@@ -40,6 +45,11 @@ export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
40
45
  this.accountAddress = authKey.derivedAddress()
41
46
  }
42
47
 
48
+ /**
49
+ * Creates a new AptosLedgerSigner instance.
50
+ * @param derivationPath - BIP44 derivation path.
51
+ * @returns A new AptosLedgerSigner instance.
52
+ */
43
53
  static async create(derivationPath: string) {
44
54
  const transport = await HIDTransport.default.create()
45
55
  const client = new AptosLedger.default(transport)
@@ -47,7 +57,11 @@ export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
47
57
  return new AptosLedgerSigner(client, derivationPath, publicKey)
48
58
  }
49
59
 
50
- // Prompts user to sign associated transaction on their Ledger hardware wallet.
60
+ /**
61
+ * Prompts user to sign associated transaction on their Ledger hardware wallet.
62
+ * @param txn - Raw transaction to sign.
63
+ * @returns Account authenticator with the signature.
64
+ */
51
65
  async signTransactionWithAuthenticator(txn: AnyRawTransaction) {
52
66
  const signingMessage = generateSigningMessageForTransaction(txn)
53
67
 
@@ -55,7 +69,11 @@ export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
55
69
  return new AccountAuthenticatorEd25519(this.publicKey, signature)
56
70
  }
57
71
 
58
- // Sign a message - returns just the signature
72
+ /**
73
+ * Signs a message - returns just the signature.
74
+ * @param message - Message bytes to sign.
75
+ * @returns Ed25519 signature.
76
+ */
59
77
  async sign(message: BytesLike): Promise<Ed25519Signature> {
60
78
  const messageBytes = getBytes(message)
61
79
  // This line prompts the user to sign the transaction on their Ledger hardware wallet
@@ -66,13 +84,20 @@ export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
66
84
  return new Ed25519Signature(signature)
67
85
  }
68
86
 
69
- // Terminates the LedgerClient connection.
87
+ /**
88
+ * Terminates the LedgerClient connection.
89
+ */
70
90
  async close() {
71
91
  await this.client.transport.close()
72
92
  }
73
93
  }
74
94
 
75
- AptosChain.getWallet = async function loadAptosWallet({ wallet: walletOpt }: { wallet?: unknown }) {
95
+ /**
96
+ * Loads an Aptos wallet from the provided options.
97
+ * @param wallet - wallet options (as passed from yargs argv)
98
+ * @returns Promise to AptosAsyncAccount instance
99
+ */
100
+ export async function loadAptosWallet({ wallet: walletOpt }: { wallet?: unknown }) {
76
101
  if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY']
77
102
  if (typeof walletOpt !== 'string')
78
103
  throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)
@@ -2,11 +2,17 @@ import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import util from 'util'
4
4
 
5
- import { EVMChain } from '@chainlink/ccip-sdk/src/index.ts'
6
5
  import { LedgerSigner } from '@ethers-ext/signer-ledger'
7
6
  import { password } from '@inquirer/prompts'
8
7
  import HIDTransport from '@ledgerhq/hw-transport-node-hid'
9
- import { type Provider, type Signer, BaseWallet, SigningKey, Wallet } from 'ethers'
8
+ import {
9
+ type JsonRpcApiProvider,
10
+ type Provider,
11
+ type Signer,
12
+ BaseWallet,
13
+ SigningKey,
14
+ Wallet,
15
+ } from 'ethers'
10
16
 
11
17
  // monkey-patch @ethers-ext/signer-ledger to preserve path when `.connect`ing provider
12
18
  Object.assign(LedgerSigner.prototype, {
@@ -18,14 +24,25 @@ Object.assign(LedgerSigner.prototype, {
18
24
  /**
19
25
  * Overwrite EVMChain.getWallet to support reading private key from file, env var or Ledger
20
26
  * @param provider - provider instance to be connected to signers
21
- * @param opts - wallet options (as passed to yargs argv)
27
+ * @param wallet - wallet options (as passed to yargs argv)
22
28
  * @returns Promise to Signer instance
23
29
  */
24
- EVMChain.getWallet = async function loadEvmWallet(
25
- provider: Provider,
30
+ export async function loadEvmWallet(
31
+ provider: JsonRpcApiProvider,
26
32
  { wallet: walletOpt }: { wallet?: unknown },
27
33
  ): Promise<Signer> {
28
34
  if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY']
35
+ if (
36
+ typeof walletOpt === 'number' ||
37
+ (typeof walletOpt === 'string' && walletOpt.match(/^(\d+|0x[a-fA-F0-9]{40})$/))
38
+ ) {
39
+ // if given a number, numeric string or address, use ethers `provider.getSigner` (e.g. geth or MM)
40
+ return provider.getSigner(
41
+ typeof walletOpt === 'string' && walletOpt.match(/^0x[a-fA-F0-9]{40}$/)
42
+ ? walletOpt
43
+ : Number(walletOpt),
44
+ )
45
+ }
29
46
  if (typeof walletOpt !== 'string')
30
47
  throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)
31
48
  if ((walletOpt ?? '').startsWith('ledger')) {
@@ -4,16 +4,43 @@ import {
4
4
  type Chain,
5
5
  type ChainGetter,
6
6
  type ChainTransaction,
7
+ type EVMChain,
8
+ ChainFamily,
7
9
  networkInfo,
8
10
  supportedChains,
9
11
  } from '@chainlink/ccip-sdk/src/index.ts'
10
12
 
11
- import './aptos.ts'
12
- import './evm.ts'
13
- import './solana.ts'
13
+ import { loadAptosWallet } from './aptos.ts'
14
+ import { loadEvmWallet } from './evm.ts'
15
+ import { loadSolanaWallet } from './solana.ts'
16
+ import type { Ctx } from '../commands/index.ts'
14
17
 
15
18
  const RPCS_RE = /\b(?:http|ws)s?:\/\/[\w/\\@&?%~#.,;:=+-]+/
16
19
 
20
+ const signalToPromiseMap = new WeakMap<AbortSignal, Promise<void>>()
21
+ function signalToPromise(signal: AbortSignal) {
22
+ let promise = signalToPromiseMap.get(signal)
23
+ if (!promise) {
24
+ signalToPromiseMap.set(
25
+ signal,
26
+ (promise = new Promise((_, reject) => {
27
+ signal.throwIfAborted()
28
+ signal.addEventListener(
29
+ 'abort',
30
+ () =>
31
+ reject(
32
+ signal.reason instanceof Error
33
+ ? signal.reason
34
+ : new Error(`Aborted: ${signal.reason as string}`),
35
+ ),
36
+ { once: true },
37
+ )
38
+ })),
39
+ )
40
+ }
41
+ return promise
42
+ }
43
+
17
44
  async function collectEndpoints({
18
45
  rpcs,
19
46
  'rpcs-file': rpcsFile,
@@ -40,47 +67,49 @@ async function collectEndpoints({
40
67
  }
41
68
 
42
69
  export function fetchChainsFromRpcs(
70
+ ctx: Ctx,
43
71
  argv: { rpcs?: string[]; 'rpcs-file'?: string },
44
- txHash?: undefined,
45
- destroy?: Promise<unknown>,
46
72
  ): ChainGetter
47
73
  export function fetchChainsFromRpcs(
74
+ ctx: Ctx,
48
75
  argv: { rpcs?: string[]; 'rpcs-file'?: string },
49
76
  txHash: string,
50
- destroy?: Promise<unknown>,
51
- ): [ChainGetter, Promise<ChainTransaction>]
77
+ ): [ChainGetter, Promise<[Chain, ChainTransaction]>]
52
78
 
53
79
  /**
54
80
  * Receives a list of rpcs and/or rpcs file, and loads them all concurrently
55
81
  * Returns a ChainGetter function and optinoally a ChainTransaction promise
82
+ * @param ctx - Context object containing destroy$ promise and logger properties
56
83
  * @param argv - Options containing rpcs (list) and/or rpcs file
57
84
  * @param txHash - Optional txHash to fetch concurrently; causes the function to return a [ChainGetter, Promise<ChainTransaction>]
58
- * @param destroy - A promise to signal when to stop fetching chains
59
85
  * @returns a ChainGetter (alone if no txHash was provided), or a tuple of [ChainGetter, Promise<ChainTransaction>]
60
86
  */
61
87
  export function fetchChainsFromRpcs(
88
+ ctx: Ctx,
62
89
  argv: { rpcs?: string[]; 'rpcs-file'?: string },
63
90
  txHash?: string,
64
- destroy?: Promise<unknown>,
65
91
  ) {
92
+ const { destroy$ } = ctx
66
93
  const chains: Record<string, Promise<Chain>> = {}
67
94
  const chainsCbs: Record<
68
95
  string,
69
96
  readonly [resolve: (value: Chain) => void, reject: (reason?: unknown) => void]
70
97
  > = {}
71
98
  let finished = false
72
- const txs: Promise<ChainTransaction>[] = []
99
+ const txs: Promise<[Chain, ChainTransaction]>[] = []
73
100
 
74
101
  const init$ = collectEndpoints(argv).then((endpoints) => {
75
102
  const pendingPromises: Promise<unknown>[] = []
76
103
  let txFound = false
77
104
  for (const C of Object.values(supportedChains)) {
78
105
  for (const url of endpoints) {
79
- const chain$ = C.fromUrl(url)
106
+ const chain$ = C.fromUrl(url, ctx)
80
107
  if (txHash) {
81
- const tx$ = chain$.then((chain) => chain.getTransaction(txHash))
108
+ const tx$ = chain$.then((chain) =>
109
+ chain.getTransaction(txHash).then<[Chain, ChainTransaction]>((tx) => [chain, tx]),
110
+ )
82
111
  void tx$.then(
83
- ({ chain }) => {
112
+ ([chain]) => {
84
113
  if (txFound) return
85
114
  txFound = true
86
115
  // in case tx is found, prefer it over any previously found chain
@@ -96,7 +125,7 @@ export function fetchChainsFromRpcs(
96
125
  chain$.then((chain) => {
97
126
  if (chain.network.name in chains && !(chain.network.name in chainsCbs))
98
127
  return chain.destroy?.() // lost race
99
- void destroy?.finally(() => {
128
+ destroy$.addEventListener('abort', () => {
100
129
  void chain.destroy?.() // cleanup
101
130
  })
102
131
  if (!(chain.network.name in chains)) {
@@ -110,7 +139,7 @@ export function fetchChainsFromRpcs(
110
139
  }
111
140
  }
112
141
  const res = Promise.allSettled(pendingPromises)
113
- void (destroy ? Promise.race([res, destroy]) : res).finally(() => {
142
+ void (destroy$ ? Promise.race([res, signalToPromise(destroy$)]) : res).finally(() => {
114
143
  finished = true
115
144
  Object.entries(chainsCbs).forEach(([name, [_, reject]]) =>
116
145
  reject(new Error(`No provider/chain found for network=${name}`)),
@@ -139,3 +168,26 @@ export function fetchChainsFromRpcs(
139
168
  return chainGetter
140
169
  }
141
170
  }
171
+
172
+ /**
173
+ * Load chain-specific wallet for given chain
174
+ * @param chain - Chain instance to load wallet for
175
+ * @param opts - Wallet options (as passed from yargs argv)
176
+ * @returns Promise to chain-specific wallet instance
177
+ */
178
+ export async function loadChainWallet(chain: Chain, opts: { wallet?: unknown }) {
179
+ let wallet
180
+ switch (chain.network.family) {
181
+ case ChainFamily.EVM:
182
+ wallet = await loadEvmWallet((chain as EVMChain).provider, opts)
183
+ return [await wallet.getAddress(), wallet] as const
184
+ case ChainFamily.Solana:
185
+ wallet = await loadSolanaWallet(opts)
186
+ return [wallet.publicKey.toBase58(), wallet] as const
187
+ case ChainFamily.Aptos:
188
+ wallet = await loadAptosWallet(opts)
189
+ return [wallet.accountAddress.toString(), wallet] as const
190
+ default:
191
+ throw new Error(`Unsupported chain family: ${chain.network.family}`)
192
+ }
193
+ }