@chainlink/ccip-cli 0.0.0 → 0.90.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -0
  3. package/dist/commands/index.d.ts +2 -0
  4. package/dist/commands/index.d.ts.map +1 -0
  5. package/dist/commands/index.js +2 -0
  6. package/dist/commands/index.js.map +1 -0
  7. package/dist/commands/manual-exec.d.ts +56 -0
  8. package/dist/commands/manual-exec.d.ts.map +1 -0
  9. package/dist/commands/manual-exec.js +405 -0
  10. package/dist/commands/manual-exec.js.map +1 -0
  11. package/dist/commands/parse.d.ts +9 -0
  12. package/dist/commands/parse.d.ts.map +1 -0
  13. package/dist/commands/parse.js +47 -0
  14. package/dist/commands/parse.js.map +1 -0
  15. package/dist/commands/send.d.ts +80 -0
  16. package/dist/commands/send.d.ts.map +1 -0
  17. package/dist/commands/send.js +258 -0
  18. package/dist/commands/send.js.map +1 -0
  19. package/dist/commands/show.d.ts +18 -0
  20. package/dist/commands/show.d.ts.map +1 -0
  21. package/dist/commands/show.js +112 -0
  22. package/dist/commands/show.js.map +1 -0
  23. package/dist/commands/supported-tokens.d.ts +37 -0
  24. package/dist/commands/supported-tokens.d.ts.map +1 -0
  25. package/dist/commands/supported-tokens.js +214 -0
  26. package/dist/commands/supported-tokens.js.map +1 -0
  27. package/dist/commands/types.d.ts +7 -0
  28. package/dist/commands/types.d.ts.map +1 -0
  29. package/dist/commands/types.js +6 -0
  30. package/dist/commands/types.js.map +1 -0
  31. package/dist/commands/utils.d.ts +40 -0
  32. package/dist/commands/utils.d.ts.map +1 -0
  33. package/dist/commands/utils.js +330 -0
  34. package/dist/commands/utils.js.map +1 -0
  35. package/dist/index.d.ts +34 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +63 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/providers/aptos.d.ts +15 -0
  40. package/dist/providers/aptos.d.ts.map +1 -0
  41. package/dist/providers/aptos.js +74 -0
  42. package/dist/providers/aptos.js.map +1 -0
  43. package/dist/providers/evm.d.ts +2 -0
  44. package/dist/providers/evm.d.ts.map +1 -0
  45. package/dist/providers/evm.js +42 -0
  46. package/dist/providers/evm.js.map +1 -0
  47. package/dist/providers/index.d.ts +13 -0
  48. package/dist/providers/index.d.ts.map +1 -0
  49. package/dist/providers/index.js +104 -0
  50. package/dist/providers/index.js.map +1 -0
  51. package/dist/providers/solana.d.ts +13 -0
  52. package/dist/providers/solana.d.ts.map +1 -0
  53. package/dist/providers/solana.js +79 -0
  54. package/dist/providers/solana.js.map +1 -0
  55. package/package.json +57 -8
  56. package/src/commands/index.ts +1 -0
  57. package/src/commands/manual-exec.ts +468 -0
  58. package/src/commands/parse.ts +52 -0
  59. package/src/commands/send.ts +316 -0
  60. package/src/commands/show.ts +151 -0
  61. package/src/commands/supported-tokens.ts +245 -0
  62. package/src/commands/types.ts +6 -0
  63. package/src/commands/utils.ts +404 -0
  64. package/src/index.ts +70 -0
  65. package/src/providers/aptos.ts +100 -0
  66. package/src/providers/evm.ts +48 -0
  67. package/src/providers/index.ts +141 -0
  68. package/src/providers/solana.ts +93 -0
  69. package/tsconfig.json +18 -0
@@ -0,0 +1,6 @@
1
+ export const Format = {
2
+ log: 'log',
3
+ pretty: 'pretty',
4
+ json: 'json',
5
+ } as const
6
+ export type Format = (typeof Format)[keyof typeof Format]
@@ -0,0 +1,404 @@
1
+ import {
2
+ type CCIPCommit,
3
+ type CCIPExecution,
4
+ type CCIPRequest,
5
+ type Chain,
6
+ type ChainStatic,
7
+ type Lane,
8
+ type OffchainTokenData,
9
+ ExecutionState,
10
+ networkInfo,
11
+ supportedChains,
12
+ } from '@chainlink/ccip-sdk/src/index.ts'
13
+ import { select } from '@inquirer/prompts'
14
+ import {
15
+ dataLength,
16
+ formatUnits,
17
+ getBytes,
18
+ hexlify,
19
+ isBytesLike,
20
+ isHexString,
21
+ parseUnits,
22
+ toUtf8String,
23
+ } from 'ethers'
24
+
25
+ export async function selectRequest(
26
+ requests: readonly CCIPRequest[],
27
+ promptSuffix?: string,
28
+ hints?: { logIndex?: number },
29
+ ): Promise<CCIPRequest> {
30
+ if (hints?.logIndex != null) requests = requests.filter((req) => req.log.index === hints.logIndex)
31
+ if (requests.length === 1) return requests[0]
32
+ const answer = await select({
33
+ message: `${requests.length} messageIds found; select one${promptSuffix ? ' ' + promptSuffix : ''}`,
34
+ choices: [
35
+ ...requests.map((req, i) => ({
36
+ value: i,
37
+ name: `${req.log.index} => ${req.message.header.messageId}`,
38
+ description:
39
+ `sender =\t\t${req.message.sender}
40
+ receiver =\t\t${req.message.receiver}
41
+ gasLimit =\t\t${(req.message as { gasLimit: bigint }).gasLimit}
42
+ tokenTransfers =\t[${req.message.tokenAmounts.map((ta) => ('token' in ta ? ta.token : ta.destTokenAddress)).join(',')}]` +
43
+ ('lane' in req
44
+ ? `\ndestination =\t\t${networkInfo(req.lane.destChainSelector).name} [${networkInfo(req.lane.destChainSelector).chainId}]`
45
+ : ''),
46
+ })),
47
+ {
48
+ value: -1,
49
+ name: 'Exit',
50
+ description: 'Quit the application',
51
+ },
52
+ ],
53
+ })
54
+ if (answer < 0) throw new Error('User requested exit')
55
+ return requests[answer]
56
+ }
57
+
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) }
62
+ }
63
+
64
+ export function prettyLane(lane: Lane) {
65
+ console.info('Lane:')
66
+ const source = networkInfo(lane.sourceChainSelector),
67
+ dest = networkInfo(lane.destChainSelector)
68
+ console.table({
69
+ name: { source: source.name, dest: dest.name },
70
+ chainId: { source: source.chainId, dest: dest.chainId },
71
+ chainSelector: { source: source.chainSelector, dest: dest.chainSelector },
72
+ 'onRamp/version': { source: lane.onRamp, dest: lane.version },
73
+ })
74
+ }
75
+
76
+ async function formatToken(
77
+ source: Chain,
78
+ ta: { amount: bigint } & ({ token: string } | { sourcePoolAddress: string }),
79
+ ): Promise<string> {
80
+ let token
81
+ if ('token' in ta) token = ta.token
82
+ else {
83
+ token = await source.getTokenForTokenPool(ta.sourcePoolAddress)
84
+ }
85
+ const { symbol, decimals } = await source.getTokenInfo(token)
86
+ return `${formatUnits(ta.amount, decimals)} ${symbol}`
87
+ }
88
+
89
+ export function formatArray<T>(name: string, values: readonly T[]): Record<string, T> {
90
+ if (values.length <= 1) return { [name]: values[0] }
91
+ return Object.fromEntries(values.map((v, i) => [`${name}[${i}]`, v] as const))
92
+ }
93
+
94
+ // join truthy property names, separated by a dot
95
+ function j(...args: string[]): string {
96
+ return args.filter(Boolean).join('.')
97
+ }
98
+
99
+ function formatData(name: string, data: string, parseError = false): Record<string, string> {
100
+ if (parseError) {
101
+ let parsed
102
+ for (const chain of Object.values(supportedChains)) {
103
+ parsed = chain.parse?.(data)
104
+ if (parsed) break
105
+ }
106
+ if (parsed) {
107
+ const res: Record<string, string> = {}
108
+ for (const [key, error] of Object.entries(parsed)) {
109
+ if (isHexString(error)) Object.assign(res, formatData(j(name, key), error))
110
+ else res[j(name, key)] = error as string
111
+ }
112
+ return res
113
+ }
114
+ }
115
+ if (!isHexString(data)) return { [name]: data }
116
+ const split = []
117
+ if (data.length <= 66) split.push(data)
118
+ else
119
+ for (let i = data.length; i > 2; i -= 64) {
120
+ split.unshift(data.substring(Math.max(i - 64, 0), i))
121
+ }
122
+ return formatArray(name, split)
123
+ }
124
+
125
+ function formatDate(timestamp: number) {
126
+ return new Date(timestamp * 1e3).toISOString().substring(0, 19).replace('T', ' ')
127
+ }
128
+
129
+ export function formatDuration(secs: number) {
130
+ if (secs < 0) secs = -secs
131
+ if (secs >= 3540 && Math.floor(secs) % 60 >= 50)
132
+ secs += 60 - (secs % 60) // round up 50+s
133
+ else if (secs >= 118 && Math.floor(secs) % 60 >= 58) secs += 60 - (secs % 60) // round up 58+s
134
+ const time = {
135
+ d: Math.floor(secs / 86400),
136
+ h: Math.floor(secs / 3600) % 24,
137
+ m: Math.floor(secs / 60) % 60,
138
+ s: Math.floor(secs) % 60,
139
+ }
140
+ return Object.entries(time)
141
+ .filter((val) => val[1] !== 0)
142
+ .map(([key, val]) => `${val}${key}${key === 'd' ? ' ' : ''}`)
143
+ .join('')
144
+ }
145
+
146
+ function omit<T extends Record<string, unknown>, K extends string>(
147
+ obj: T,
148
+ ...keys: K[]
149
+ ): Omit<T, K> {
150
+ const result = { ...obj }
151
+ for (const key of keys) {
152
+ delete result[key]
153
+ }
154
+ return result
155
+ }
156
+
157
+ export async function prettyRequest(
158
+ source: Chain,
159
+ request: CCIPRequest,
160
+ offchainTokenData?: OffchainTokenData[],
161
+ ) {
162
+ prettyLane(request.lane)
163
+ console.info('Request (source):')
164
+
165
+ let finalized
166
+ try {
167
+ finalized = await source.getBlockTimestamp('finalized')
168
+ } catch (_) {
169
+ // no finalized tag support
170
+ }
171
+ const nonce = Number(request.message.header.nonce)
172
+
173
+ const rest = omit(
174
+ request.message,
175
+ 'header',
176
+ 'sender',
177
+ 'receiver',
178
+ 'tokenAmounts',
179
+ 'data',
180
+ 'feeToken',
181
+ 'feeTokenAmount',
182
+ 'sourceTokenData',
183
+ 'sourceChainSelector',
184
+ 'extraArgs',
185
+ 'accounts',
186
+ )
187
+ prettyTable({
188
+ messageId: request.message.header.messageId,
189
+ ...(request.tx.from ? { origin: request.tx.from } : {}),
190
+ sender: request.message.sender,
191
+ receiver: request.message.receiver,
192
+ sequenceNumber: Number(request.message.header.sequenceNumber),
193
+ nonce: nonce === 0 ? '0 => allow out-of-order exec' : nonce,
194
+ ...('gasLimit' in request.message
195
+ ? { gasLimit: Number(request.message.gasLimit) }
196
+ : 'computeUnits' in request.message
197
+ ? { computeUnits: Number(request.message.computeUnits) }
198
+ : {}),
199
+ transactionHash: request.log.transactionHash,
200
+ logIndex: request.log.index,
201
+ blockNumber: request.log.blockNumber,
202
+ timestamp: `${formatDate(request.timestamp)} (${formatDuration(Date.now() / 1e3 - request.timestamp)} ago)`,
203
+ finalized:
204
+ finalized &&
205
+ (finalized < request.timestamp
206
+ ? formatDuration(request.timestamp - finalized) + ' left'
207
+ : true),
208
+ fee: await formatToken(source, {
209
+ token: request.message.feeToken,
210
+ amount: request.message.feeTokenAmount,
211
+ }),
212
+ ...formatArray(
213
+ 'tokens',
214
+ await Promise.all(request.message.tokenAmounts.map(formatToken.bind(null, source))),
215
+ ),
216
+ ...(isBytesLike(request.message.data) &&
217
+ dataLength(request.message.data) > 0 &&
218
+ getBytes(request.message.data).every((b) => 32 <= b && b <= 126) // printable characters
219
+ ? { data: toUtf8String(request.message.data) }
220
+ : formatData('data', request.message.data)),
221
+ ...('accounts' in request.message ? formatArray('accounts', request.message.accounts) : {}),
222
+ ...rest,
223
+ })
224
+
225
+ if (!offchainTokenData?.length || offchainTokenData.every((d) => !d)) return
226
+ console.info('Attestations:')
227
+ for (const attestation of offchainTokenData) {
228
+ const { _tag: type, ...rest } = attestation!
229
+ prettyTable({ type, ...rest })
230
+ }
231
+ }
232
+
233
+ export async function prettyCommit(
234
+ dest: Chain,
235
+ commit: CCIPCommit,
236
+ request: { timestamp: number },
237
+ ) {
238
+ console.info('Commit (dest):')
239
+ const timestamp = await dest.getBlockTimestamp(commit.log.blockNumber)
240
+ prettyTable({
241
+ merkleRoot: commit.report.merkleRoot,
242
+ min: Number(commit.report.minSeqNr),
243
+ max: Number(commit.report.maxSeqNr),
244
+ origin: commit.log.tx?.from ?? (await dest.getTransaction(commit.log.transactionHash)).from,
245
+ contract: commit.log.address,
246
+ transactionHash: commit.log.transactionHash,
247
+ blockNumber: commit.log.blockNumber,
248
+ timestamp: `${formatDate(timestamp)} (${formatDuration(timestamp - request.timestamp)} after request)`,
249
+ })
250
+ }
251
+
252
+ /**
253
+ * Add line breaks to a string to fit within a specified column width
254
+ * @param text - The input string to wrap
255
+ * @param maxWidth - Maximum column width before wrapping
256
+ * @param threshold - Percentage of maxWidth to look back for spaces (default 0.1 = 10%)
257
+ * @returns The wrapped string with line breaks inserted
258
+ */
259
+ function wrapText(text: string, maxWidth: number, threshold: number = 0.1): string[] {
260
+ const lines: string[] = []
261
+
262
+ // First split by existing line breaks
263
+ const existingLines = text.split('\n')
264
+
265
+ for (const line of existingLines) {
266
+ const words = line.split(' ')
267
+ let currentLine = ''
268
+
269
+ for (const word of words) {
270
+ const testLine = currentLine ? `${currentLine} ${word}` : word
271
+
272
+ if (testLine.length <= maxWidth) {
273
+ currentLine = testLine
274
+ } else {
275
+ if (currentLine) {
276
+ lines.push(currentLine)
277
+ currentLine = word
278
+ } else {
279
+ // Word is longer than maxWidth, break it
280
+ const thresholdDistance = Math.floor(maxWidth * threshold)
281
+ let remaining = word
282
+
283
+ while (remaining.length > maxWidth) {
284
+ let breakPoint = maxWidth
285
+ // Look for a good break point within threshold distance
286
+ for (let i = maxWidth - thresholdDistance; i < maxWidth; i++) {
287
+ if (remaining[i] === '-' || remaining[i] === '_') {
288
+ breakPoint = i + 1
289
+ break
290
+ }
291
+ }
292
+ lines.push(remaining.substring(0, breakPoint))
293
+ remaining = remaining.substring(breakPoint)
294
+ }
295
+ currentLine = remaining
296
+ }
297
+ }
298
+ }
299
+
300
+ if (currentLine) {
301
+ lines.push(currentLine)
302
+ }
303
+ }
304
+
305
+ return lines
306
+ }
307
+
308
+ export function prettyTable(
309
+ args: Record<string, unknown>,
310
+ opts = { parseErrorKeys: ['returnData'], spcount: 0 },
311
+ ) {
312
+ const out: (readonly [string, unknown])[] = []
313
+ for (const [key, value] of Object.entries(args)) {
314
+ if (isBytesLike(value)) {
315
+ let parseError
316
+ if (opts.parseErrorKeys.includes(key)) parseError = true
317
+ if (dataLength(value) <= 32 && !parseError) out.push([key, value])
318
+ else out.push(...Object.entries(formatData(key, hexlify(value), parseError)))
319
+ } else if (typeof value === 'string') {
320
+ out.push(
321
+ ...wrapText(value, Math.max(100, +(process.env.COLUMNS || 80) * 0.9)).map(
322
+ (l, i) => [!i ? key : ' '.repeat(opts.spcount++), l] as const,
323
+ ),
324
+ )
325
+ } else if (Array.isArray(value)) {
326
+ if (value.length <= 1) out.push([key, value[0] as unknown])
327
+ else out.push(...value.map((v, i) => [`${key}[${i}]`, v as unknown] as const))
328
+ } else if (value && typeof value === 'object') {
329
+ out.push(...Object.entries(value).map(([k, v]) => [`${key}.${k}`, v] as const))
330
+ } else out.push([key, value])
331
+ }
332
+ return console.table(Object.fromEntries(out))
333
+ }
334
+
335
+ export function prettyReceipt(
336
+ receipt: CCIPExecution,
337
+ request: { timestamp: number },
338
+ origin?: string,
339
+ ) {
340
+ prettyTable({
341
+ state: receipt.receipt.state === ExecutionState.Success ? '✅ success' : '❌ failed',
342
+ ...(receipt.receipt.state !== ExecutionState.Success ||
343
+ (receipt.receipt.returnData && receipt.receipt.returnData !== '0x')
344
+ ? { returnData: receipt.receipt.returnData }
345
+ : {}),
346
+ ...(receipt.receipt.gasUsed ? { gasUsed: Number(receipt.receipt.gasUsed) } : {}),
347
+ ...(origin ? { origin } : {}),
348
+ contract: receipt.log.address,
349
+ transactionHash: receipt.log.transactionHash,
350
+ logIndex: receipt.log.index,
351
+ blockNumber: receipt.log.blockNumber,
352
+ timestamp: `${formatDate(receipt.timestamp)} (${formatDuration(receipt.timestamp - request.timestamp)} after request)`,
353
+ })
354
+ }
355
+
356
+ export function logParsedError(err: unknown): boolean {
357
+ for (const chain of Object.values<ChainStatic>(supportedChains)) {
358
+ const parsed = chain.parse?.(err)
359
+ if (!parsed) continue
360
+ const { method, Instruction: instruction, ...rest } = parsed
361
+ if (method || instruction) {
362
+ console.error(
363
+ `🛑 Failed to call "${(method || instruction) as string}"`,
364
+ ...Object.entries(rest)
365
+ .map(([k, e]) => [`\n${k.substring(0, 1).toUpperCase()}${k.substring(1)} =`, e])
366
+ .flat(1),
367
+ )
368
+ } else {
369
+ console.error('🛑 Error:', parsed)
370
+ }
371
+ return true
372
+ }
373
+ return false
374
+ }
375
+
376
+ /**
377
+ * Parse `--transfer-tokens token1=amount1 token2=amount2 ...` into `{ token, amount }[]`
378
+ **/
379
+ export async function parseTokenAmounts(source: Chain, transferTokens: readonly string[]) {
380
+ return Promise.all(
381
+ transferTokens.map(async (tokenAmount) => {
382
+ const [token, amount_] = tokenAmount.split('=')
383
+ const { decimals } = await source.getTokenInfo(token)
384
+ const amount = parseUnits(amount_, decimals)
385
+ return { token, amount }
386
+ }),
387
+ )
388
+ }
389
+
390
+ /**
391
+ * Yield resolved promises (like Promise.all), but as they resolve.
392
+ * Throws as soon as any promise rejects.
393
+ *
394
+ * @param promises - Promises to resolve
395
+ * @returns Resolved values as they resolve
396
+ **/
397
+ export async function* yieldResolved<T>(promises: readonly Promise<T>[]): AsyncGenerator<T> {
398
+ const map = new Map(promises.map((p) => [p, p.then((res) => [p, res] as const)] as const))
399
+ while (map.size > 0) {
400
+ const [p, res] = await Promise.race(map.values())
401
+ map.delete(p)
402
+ yield res
403
+ }
404
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import util from 'node:util'
3
+
4
+ import yargs, { type InferredOptionTypes } from 'yargs'
5
+ import { hideBin } from 'yargs/helpers'
6
+
7
+ import { Format } from './commands/index.ts'
8
+
9
+ util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests
10
+ // generate:nofail
11
+ // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
12
+ const VERSION = '0.90.0-0724dff'
13
+ // generate:end
14
+
15
+ const globalOpts = {
16
+ rpcs: {
17
+ type: 'array',
18
+ alias: 'r',
19
+ describe: 'List of RPC endpoint URLs, ws[s] or http[s]',
20
+ string: true,
21
+ },
22
+ 'rpcs-file': {
23
+ type: 'string',
24
+ default: './.env',
25
+ describe: 'File containing a list of RPCs endpoints to use',
26
+ // demandOption: true,
27
+ },
28
+ format: {
29
+ alias: 'f',
30
+ describe: "Output to console format: pretty tables, node's console.log or JSON",
31
+ choices: Object.values(Format),
32
+ default: Format.pretty,
33
+ },
34
+ verbose: {
35
+ alias: 'v',
36
+ describe: 'enable debug logging',
37
+ type: 'boolean',
38
+ },
39
+ page: {
40
+ type: 'number',
41
+ describe: 'getLogs page/range size',
42
+ default: 10_000,
43
+ },
44
+ } as const
45
+
46
+ export type GlobalOpts = InferredOptionTypes<typeof globalOpts>
47
+
48
+ async function main() {
49
+ await yargs(hideBin(process.argv))
50
+ .scriptName(process.env.CLI_NAME || 'ccip-cli')
51
+ .env('CCIP')
52
+ .options(globalOpts)
53
+ .middleware((argv) => {
54
+ if (!argv.verbose) {
55
+ console.debug = () => {}
56
+ }
57
+ })
58
+ .commandDir('commands', {
59
+ extensions: [new URL(import.meta.url).pathname.split('.').pop()!],
60
+ exclude: /\.test\.[tj]s$/,
61
+ })
62
+ .demandCommand()
63
+ .strict()
64
+ .help()
65
+ .version(VERSION)
66
+ .alias({ h: 'help', V: 'version' })
67
+ .parse()
68
+ }
69
+
70
+ await main()
@@ -0,0 +1,100 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import util from 'node:util'
3
+
4
+ import {
5
+ type AccountAddress,
6
+ type AnyRawTransaction,
7
+ Account,
8
+ AccountAuthenticatorEd25519,
9
+ AuthenticationKey,
10
+ Ed25519PrivateKey,
11
+ Ed25519PublicKey,
12
+ Ed25519Signature,
13
+ generateSigningMessageForTransaction,
14
+ } from '@aptos-labs/ts-sdk'
15
+ import { AptosChain } from '@chainlink/ccip-sdk/src/index.ts'
16
+ import AptosLedger from '@ledgerhq/hw-app-aptos'
17
+ import HIDTransport from '@ledgerhq/hw-transport-node-hid'
18
+ import { type BytesLike, getBytes, hexlify } from 'ethers'
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
+ export class AptosLedgerSigner /*implements AptosAsyncAccount*/ {
24
+ derivationPath: string
25
+ readonly client: AptosLedger.default
26
+ readonly publicKey: Ed25519PublicKey
27
+ readonly accountAddress: AccountAddress
28
+
29
+ private constructor(
30
+ ledgerClient: AptosLedger.default,
31
+ derivationPath: string,
32
+ publicKey: BytesLike,
33
+ ) {
34
+ this.client = ledgerClient
35
+ this.derivationPath = derivationPath
36
+ this.publicKey = new Ed25519PublicKey(publicKey)
37
+ const authKey = AuthenticationKey.fromPublicKey({
38
+ publicKey: this.publicKey,
39
+ })
40
+ this.accountAddress = authKey.derivedAddress()
41
+ }
42
+
43
+ static async create(derivationPath: string) {
44
+ const transport = await HIDTransport.default.create()
45
+ const client = new AptosLedger.default(transport)
46
+ const { publicKey } = await client.getAddress(derivationPath)
47
+ return new AptosLedgerSigner(client, derivationPath, publicKey)
48
+ }
49
+
50
+ // Prompts user to sign associated transaction on their Ledger hardware wallet.
51
+ async signTransactionWithAuthenticator(txn: AnyRawTransaction) {
52
+ const signingMessage = generateSigningMessageForTransaction(txn)
53
+
54
+ const signature = await this.sign(signingMessage)
55
+ return new AccountAuthenticatorEd25519(this.publicKey, signature)
56
+ }
57
+
58
+ // Sign a message - returns just the signature
59
+ async sign(message: BytesLike): Promise<Ed25519Signature> {
60
+ const messageBytes = getBytes(message)
61
+ // This line prompts the user to sign the transaction on their Ledger hardware wallet
62
+ const { signature } = await this.client.signTransaction(
63
+ this.derivationPath,
64
+ Buffer.from(messageBytes),
65
+ )
66
+ return new Ed25519Signature(signature)
67
+ }
68
+
69
+ // Terminates the LedgerClient connection.
70
+ async close() {
71
+ await this.client.transport.close()
72
+ }
73
+ }
74
+
75
+ AptosChain.getWallet = async function loadAptosWallet({ wallet: walletOpt }: { wallet?: unknown }) {
76
+ if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY']
77
+ if (typeof walletOpt !== 'string')
78
+ throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)
79
+ if ((walletOpt ?? '').startsWith('ledger')) {
80
+ let derivationPath = walletOpt.split(':')[1]
81
+ if (!derivationPath) derivationPath = "m/44'/637'/0'/0'/0'"
82
+ else if (!isNaN(Number(derivationPath))) derivationPath = `m/44'/637'/${derivationPath}'/0'/0'`
83
+ const signer = await AptosLedgerSigner.create(derivationPath)
84
+ console.info(
85
+ 'Ledger connected:',
86
+ signer.accountAddress.toStringLong(),
87
+ ', derivationPath:',
88
+ signer.derivationPath,
89
+ )
90
+ return signer
91
+ } else if (existsSync(walletOpt)) {
92
+ walletOpt = hexlify(readFileSync(walletOpt, 'utf8').trim())
93
+ }
94
+ if (walletOpt) {
95
+ return Account.fromPrivateKey({
96
+ privateKey: new Ed25519PrivateKey(walletOpt as string, false),
97
+ })
98
+ }
99
+ throw new Error('Wallet not specified')
100
+ }
@@ -0,0 +1,48 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile } from 'node:fs/promises'
3
+ import util from 'util'
4
+
5
+ import { EVMChain } from '@chainlink/ccip-sdk/src/index.ts'
6
+ import { LedgerSigner } from '@ethers-ext/signer-ledger'
7
+ import { password } from '@inquirer/prompts'
8
+ import HIDTransport from '@ledgerhq/hw-transport-node-hid'
9
+ import { type Provider, type Signer, BaseWallet, SigningKey, Wallet } from 'ethers'
10
+
11
+ // monkey-patch @ethers-ext/signer-ledger to preserve path when `.connect`ing provider
12
+ Object.assign(LedgerSigner.prototype, {
13
+ connect: function (this: LedgerSigner, provider?: Provider | null) {
14
+ return new LedgerSigner(HIDTransport, provider, this.path)
15
+ },
16
+ })
17
+
18
+ /**
19
+ * Overwrite EVMChain.getWallet to support reading private key from file, env var or Ledger
20
+ * @param provider - provider instance to be connected to signers
21
+ * @param opts - wallet options (as passed to yargs argv)
22
+ * @returns Promise to Signer instance
23
+ */
24
+ EVMChain.getWallet = async function loadEvmWallet(
25
+ provider: Provider,
26
+ { wallet: walletOpt }: { wallet?: unknown },
27
+ ): Promise<Signer> {
28
+ if (!walletOpt) walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY']
29
+ if (typeof walletOpt !== 'string')
30
+ throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)
31
+ if ((walletOpt ?? '').startsWith('ledger')) {
32
+ let derivationPath = walletOpt.split(':')[1]
33
+ if (derivationPath && !isNaN(Number(derivationPath)))
34
+ derivationPath = `m/44'/60'/${derivationPath}'/0/0`
35
+ const ledger = new LedgerSigner(HIDTransport, provider, derivationPath)
36
+ console.info('Ledger connected:', await ledger.getAddress(), ', derivationPath:', ledger.path)
37
+ return ledger
38
+ }
39
+ if (existsSync(walletOpt)) {
40
+ let pw = process.env['USER_KEY_PASSWORD']
41
+ if (!pw) pw = await password({ message: 'Enter password for json wallet' })
42
+ return (await Wallet.fromEncryptedJson(await readFile(walletOpt, 'utf8'), pw)).connect(provider)
43
+ }
44
+ return new BaseWallet(
45
+ new SigningKey((walletOpt.startsWith('0x') ? '' : '0x') + walletOpt),
46
+ provider,
47
+ )
48
+ }