@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.
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/manual-exec.d.ts +56 -0
- package/dist/commands/manual-exec.d.ts.map +1 -0
- package/dist/commands/manual-exec.js +405 -0
- package/dist/commands/manual-exec.js.map +1 -0
- package/dist/commands/parse.d.ts +9 -0
- package/dist/commands/parse.d.ts.map +1 -0
- package/dist/commands/parse.js +47 -0
- package/dist/commands/parse.js.map +1 -0
- package/dist/commands/send.d.ts +80 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +258 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/show.d.ts +18 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +112 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/supported-tokens.d.ts +37 -0
- package/dist/commands/supported-tokens.d.ts.map +1 -0
- package/dist/commands/supported-tokens.js +214 -0
- package/dist/commands/supported-tokens.js.map +1 -0
- package/dist/commands/types.d.ts +7 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +6 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/utils.d.ts +40 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +330 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/aptos.d.ts +15 -0
- package/dist/providers/aptos.d.ts.map +1 -0
- package/dist/providers/aptos.js +74 -0
- package/dist/providers/aptos.js.map +1 -0
- package/dist/providers/evm.d.ts +2 -0
- package/dist/providers/evm.d.ts.map +1 -0
- package/dist/providers/evm.js +42 -0
- package/dist/providers/evm.js.map +1 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +104 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/solana.d.ts +13 -0
- package/dist/providers/solana.d.ts.map +1 -0
- package/dist/providers/solana.js +79 -0
- package/dist/providers/solana.js.map +1 -0
- package/package.json +57 -8
- package/src/commands/index.ts +1 -0
- package/src/commands/manual-exec.ts +468 -0
- package/src/commands/parse.ts +52 -0
- package/src/commands/send.ts +316 -0
- package/src/commands/show.ts +151 -0
- package/src/commands/supported-tokens.ts +245 -0
- package/src/commands/types.ts +6 -0
- package/src/commands/utils.ts +404 -0
- package/src/index.ts +70 -0
- package/src/providers/aptos.ts +100 -0
- package/src/providers/evm.ts +48 -0
- package/src/providers/index.ts +141 -0
- package/src/providers/solana.ts +93 -0
- package/tsconfig.json +18 -0
|
@@ -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
|
+
}
|