@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.
- package/README.md +40 -21
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/manual-exec.d.ts +9 -0
- package/dist/commands/manual-exec.d.ts.map +1 -1
- package/dist/commands/manual-exec.js +40 -35
- package/dist/commands/manual-exec.js.map +1 -1
- package/dist/commands/parse.d.ts +9 -0
- package/dist/commands/parse.d.ts.map +1 -1
- package/dist/commands/parse.js +18 -7
- package/dist/commands/parse.js.map +1 -1
- package/dist/commands/send.d.ts +9 -0
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +39 -26
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/show.d.ts +9 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +46 -39
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/supported-tokens.d.ts +9 -2
- package/dist/commands/supported-tokens.d.ts.map +1 -1
- package/dist/commands/supported-tokens.js +35 -30
- package/dist/commands/supported-tokens.js.map +1 -1
- package/dist/commands/types.d.ts +13 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js +1 -0
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/utils.d.ts +75 -10
- package/dist/commands/utils.d.ts.map +1 -1
- package/dist/commands/utils.js +95 -19
- package/dist/commands/utils.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/dist/providers/aptos.d.ts +35 -0
- package/dist/providers/aptos.d.ts.map +1 -1
- package/dist/providers/aptos.js +34 -9
- package/dist/providers/aptos.js.map +1 -1
- package/dist/providers/evm.d.ts +10 -1
- package/dist/providers/evm.d.ts.map +1 -1
- package/dist/providers/evm.js +11 -5
- package/dist/providers/evm.js.map +1 -1
- package/dist/providers/index.d.ts +15 -8
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +47 -11
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/solana.d.ts +30 -0
- package/dist/providers/solana.d.ts.map +1 -1
- package/dist/providers/solana.js +30 -5
- package/dist/providers/solana.js.map +1 -1
- package/package.json +11 -8
- package/src/commands/index.ts +2 -1
- package/src/commands/manual-exec.ts +40 -33
- package/src/commands/parse.ts +19 -8
- package/src/commands/send.ts +47 -30
- package/src/commands/show.ts +47 -48
- package/src/commands/supported-tokens.ts +35 -30
- package/src/commands/types.ts +15 -0
- package/src/commands/utils.ts +112 -26
- package/src/index.ts +21 -7
- package/src/providers/aptos.ts +33 -8
- package/src/providers/evm.ts +22 -5
- package/src/providers/index.ts +67 -15
- package/src/providers/solana.ts +31 -6
- package/tsconfig.json +2 -1
package/src/commands/utils.ts
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
285
|
+
request: PickDeep<CCIPRequest, 'tx.timestamp'>,
|
|
237
286
|
) {
|
|
238
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/providers/aptos.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)}`)
|
package/src/providers/evm.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
27
|
+
* @param wallet - wallet options (as passed to yargs argv)
|
|
22
28
|
* @returns Promise to Signer instance
|
|
23
29
|
*/
|
|
24
|
-
|
|
25
|
-
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')) {
|
package/src/providers/index.ts
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
108
|
+
const tx$ = chain$.then((chain) =>
|
|
109
|
+
chain.getTransaction(txHash).then<[Chain, ChainTransaction]>((tx) => [chain, tx]),
|
|
110
|
+
)
|
|
82
111
|
void tx$.then(
|
|
83
|
-
(
|
|
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
|
-
|
|
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
|
+
}
|