@chainlink/ccip-cli 0.0.0 → 0.90.2

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,316 @@
1
+ import {
2
+ type AnyMessage,
3
+ type CCIPVersion,
4
+ type ChainStatic,
5
+ type EVMChain,
6
+ type ExtraArgs,
7
+ ChainFamily,
8
+ bigIntReplacer,
9
+ estimateExecGasForRequest,
10
+ fetchCCIPRequestsInTx,
11
+ getDataBytes,
12
+ networkInfo,
13
+ sourceToDestTokenAmounts,
14
+ } from '@chainlink/ccip-sdk/src/index.ts'
15
+ import { type BytesLike, dataLength, formatUnits, toUtf8Bytes } from 'ethers'
16
+ import type { Argv } from 'yargs'
17
+
18
+ import type { GlobalOpts } from '../index.ts'
19
+ import { Format } from './types.ts'
20
+ import { logParsedError, parseTokenAmounts, prettyRequest, withDateTimestamp } from './utils.ts'
21
+ import { fetchChainsFromRpcs } from '../providers/index.ts'
22
+
23
+ export const command = 'send <source> <router> <dest>'
24
+ export const describe = 'Send a CCIP message from router on source to dest'
25
+
26
+ export const builder = (yargs: Argv) =>
27
+ yargs
28
+ .positional('source', {
29
+ type: 'string',
30
+ demandOption: true,
31
+ describe: 'source network, chainId or name',
32
+ example: 'ethereum-testnet-sepolia',
33
+ })
34
+ .positional('router', {
35
+ type: 'string',
36
+ demandOption: true,
37
+ describe: 'router contract address on source',
38
+ })
39
+ .positional('dest', {
40
+ type: 'string',
41
+ demandOption: true,
42
+ describe: 'destination network, chainId or name',
43
+ example: 'ethereum-testnet-sepolia-arbitrum-1',
44
+ })
45
+ .options({
46
+ receiver: {
47
+ alias: 'R',
48
+ type: 'string',
49
+ describe:
50
+ 'Receiver of the message; defaults to the sender wallet address if same network family',
51
+ },
52
+ data: {
53
+ alias: 'd',
54
+ type: 'string',
55
+ describe: 'Data to send in the message (non-hex will be utf-8 encoded)',
56
+ example: '0x1234',
57
+ },
58
+ 'gas-limit': {
59
+ alias: ['L', 'compute-units'],
60
+ type: 'number',
61
+ describe:
62
+ 'Gas limit for receiver callback execution; defaults to default configured on ramps',
63
+ default: 0,
64
+ },
65
+ 'estimate-gas-limit': {
66
+ type: 'number',
67
+ describe:
68
+ 'Estimate gas limit for receiver callback execution; argument is a % margin to add to the estimate',
69
+ example: '10',
70
+ conflicts: 'gas-limit',
71
+ },
72
+ 'allow-out-of-order-exec': {
73
+ alias: 'ooo',
74
+ type: 'boolean',
75
+ describe:
76
+ 'Allow execution of messages out of order (i.e. sender nonce not enforced, only v1.5+ lanes, mandatory for some dests)',
77
+ },
78
+ 'fee-token': {
79
+ type: 'string',
80
+ describe:
81
+ 'Address or symbol of the fee token (e.g. LINK address on source); if not provided, will pay in native',
82
+ },
83
+ 'transfer-tokens': {
84
+ alias: 't',
85
+ type: 'array',
86
+ string: true,
87
+ describe: 'List of token amounts (on source) to transfer to the receiver',
88
+ example: '0xtoken=0.1',
89
+ },
90
+ wallet: {
91
+ alias: 'w',
92
+ type: 'string',
93
+ describe:
94
+ 'Wallet to send transactions with; pass `ledger[:index_or_derivation]` to use Ledger USB hardware wallet, or private key in `USER_KEY` environment variable',
95
+ },
96
+ 'token-receiver': {
97
+ type: 'string',
98
+ describe: "Address of the Solana tokenReceiver (if different than program's receiver)",
99
+ },
100
+ account: {
101
+ type: 'array',
102
+ string: true,
103
+ describe:
104
+ 'List of accounts needed by Solana receiver program; append `=rw` to specify account as writable; can be specified multiple times',
105
+ example: 'requiredPdaAddress=rw',
106
+ },
107
+ 'only-get-fee': {
108
+ type: 'boolean',
109
+ describe: 'Fetch and print the fee for the transaction, then exit',
110
+ },
111
+ 'only-estimate': {
112
+ type: 'boolean',
113
+ describe: 'Only estimate dest exec gasLimit',
114
+ },
115
+ 'approve-max': {
116
+ type: 'boolean',
117
+ describe:
118
+ "Approve the maximum amount of tokens to transfer; default=false approves only what's needed",
119
+ },
120
+ })
121
+ .check(
122
+ ({ 'transfer-tokens': transferTokens }) =>
123
+ !transferTokens || transferTokens.every((t) => /^[^=]+=\d+(\.\d+)?$/.test(t)),
124
+ )
125
+
126
+ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']> & GlobalOpts) {
127
+ let destroy
128
+ const destroy$ = new Promise((resolve) => {
129
+ destroy = resolve
130
+ })
131
+ return sendMessage(argv, destroy$)
132
+ .catch((err) => {
133
+ process.exitCode = 1
134
+ if (!logParsedError(err)) console.error(err)
135
+ })
136
+ .finally(destroy)
137
+ }
138
+
139
+ async function sendMessage(
140
+ argv: Awaited<ReturnType<typeof builder>['argv']> & GlobalOpts,
141
+ destroy: Promise<unknown>,
142
+ ) {
143
+ const sourceNetwork = networkInfo(argv.source)
144
+ const destNetwork = networkInfo(argv.dest)
145
+ const getChain = fetchChainsFromRpcs(argv, undefined, destroy)
146
+ const source = await getChain(sourceNetwork.name)
147
+
148
+ let data: BytesLike
149
+ if (argv.data) {
150
+ try {
151
+ data = getDataBytes(argv.data)
152
+ } catch (_) {
153
+ data = toUtf8Bytes(argv.data)
154
+ }
155
+ } else {
156
+ data = '0x'
157
+ }
158
+
159
+ const tokenAmounts: { token: string; amount: bigint }[] = argv.transferTokens?.length
160
+ ? await parseTokenAmounts(source, argv.transferTokens)
161
+ : []
162
+
163
+ let receiver = argv.receiver
164
+ let tokenReceiver
165
+ let accounts,
166
+ accountIsWritableBitmap = 0n
167
+ if (destNetwork.family === ChainFamily.Solana) {
168
+ if (argv.tokenReceiver) tokenReceiver = argv.tokenReceiver
169
+ else if (!tokenAmounts.length) {
170
+ tokenReceiver = '11111111111111111111111111111111'
171
+ } else if (!dataLength(data)) {
172
+ // sending tokens without data, i.e. not for a receiver contract
173
+ tokenReceiver = receiver
174
+ receiver = '11111111111111111111111111111111'
175
+ } else {
176
+ throw new Error('--token-receiver is required when sending tokens with data')
177
+ }
178
+
179
+ if (argv.account) {
180
+ accounts = argv.account.map((account, i) => {
181
+ if (account.endsWith('=rw')) {
182
+ accountIsWritableBitmap |= 1n << BigInt(i)
183
+ account = account.substring(0, account.length - 3)
184
+ }
185
+ return account
186
+ })
187
+ } else accounts = [] as string[]
188
+ } else if (argv.tokenReceiver || argv.account?.length) {
189
+ throw new Error('--token-receiver and --account intended only for Solana dest')
190
+ }
191
+
192
+ if (!receiver) {
193
+ if (sourceNetwork.family !== destNetwork.family)
194
+ throw new Error('--receiver is required when sending to a different chain family')
195
+ receiver = await source.getWalletAddress(argv) // send to self if same family
196
+ }
197
+
198
+ if (argv.estimateGasLimit != null || argv.onlyEstimate) {
199
+ // TODO: implement for all chain families
200
+ if (destNetwork.family !== ChainFamily.EVM)
201
+ throw new Error(`Estimating gasLimit supported only on EVM, got=${destNetwork.family}`)
202
+ const dest = (await getChain(destNetwork.chainSelector)) as unknown as EVMChain
203
+ const onRamp = await source.getOnRampForRouter(argv.router, destNetwork.chainSelector)
204
+ const lane = {
205
+ sourceChainSelector: source.network.chainSelector,
206
+ destChainSelector: destNetwork.chainSelector,
207
+ onRamp,
208
+ version: (await source.typeAndVersion(onRamp))[1] as CCIPVersion,
209
+ }
210
+ const destTokenAmounts = await sourceToDestTokenAmounts(
211
+ source,
212
+ destNetwork.chainSelector,
213
+ onRamp,
214
+ tokenAmounts,
215
+ )
216
+
217
+ const estimated = await estimateExecGasForRequest(source, dest, {
218
+ lane,
219
+ message: {
220
+ sender: await source.getWalletAddress(argv),
221
+ receiver,
222
+ data,
223
+ tokenAmounts: destTokenAmounts,
224
+ },
225
+ })
226
+ console.log('Estimated gasLimit:', estimated)
227
+ argv.gasLimit = Math.ceil(estimated * (1 + (argv.estimateGasLimit ?? 0) / 100))
228
+ if (argv.onlyEstimate) return
229
+ }
230
+
231
+ // `--allow-out-of-order-exec` forces EVMExtraArgsV2, which shouldn't work on v1.2 lanes;
232
+ // otherwise, fallsback to EVMExtraArgsV1 (compatible with v1.2 & v1.5)
233
+ const extraArgs = {
234
+ ...(argv.allowOutOfOrderExec != null || destNetwork.family !== ChainFamily.EVM
235
+ ? { allowOutOfOrderExecution: !!argv.allowOutOfOrderExec }
236
+ : {}),
237
+ ...(destNetwork.family === ChainFamily.Solana
238
+ ? { computeUnits: BigInt(argv.gasLimit) }
239
+ : { gasLimit: BigInt(argv.gasLimit) }),
240
+ ...(tokenReceiver ? { tokenReceiver } : {}),
241
+ ...(accounts ? { accounts, accountIsWritableBitmap } : {}),
242
+ }
243
+
244
+ let feeToken, feeTokenInfo
245
+ if (argv.feeToken) {
246
+ try {
247
+ feeToken = (source.constructor as ChainStatic).getAddress(argv.feeToken)
248
+ feeTokenInfo = await source.getTokenInfo(feeToken)
249
+ } catch (_) {
250
+ const feeTokens = await source.listFeeTokens(argv.router)
251
+ console.debug('supported feeTokens:', feeTokens)
252
+ for (const [token, info] of Object.entries(feeTokens)) {
253
+ if (info.symbol === 'UNKNOWN' || info.symbol !== argv.feeToken) continue
254
+ feeToken = token
255
+ feeTokenInfo = info
256
+ break
257
+ }
258
+ if (!feeTokenInfo) throw new Error(`Fee token "${argv.feeToken}" not found`)
259
+ }
260
+ } else {
261
+ const nativeToken = await source.getNativeTokenForRouter(argv.router)
262
+ feeTokenInfo = await source.getTokenInfo(nativeToken)
263
+ }
264
+
265
+ const message: AnyMessage = {
266
+ receiver,
267
+ data,
268
+ extraArgs: extraArgs as ExtraArgs,
269
+ feeToken, // feeToken==ZeroAddress means native
270
+ tokenAmounts,
271
+ }
272
+
273
+ // calculate fee
274
+ const fee = await source.getFee(argv.router, destNetwork.chainSelector, message)
275
+
276
+ console.info(
277
+ 'Fee:',
278
+ fee,
279
+ '=',
280
+ formatUnits(fee, feeTokenInfo.decimals),
281
+ !argv.feeToken && feeTokenInfo.symbol.startsWith('W')
282
+ ? feeTokenInfo.symbol.substring(1)
283
+ : feeTokenInfo.symbol,
284
+ )
285
+ if (argv.onlyGetFee) return
286
+
287
+ const tx = await source.sendMessage(
288
+ argv.router,
289
+ destNetwork.chainSelector,
290
+ { ...message, fee },
291
+ argv,
292
+ )
293
+ console.log(
294
+ '🚀 Sending message to',
295
+ tokenReceiver || receiver,
296
+ '@',
297
+ destNetwork.name,
298
+ ', tx =>',
299
+ tx.hash,
300
+ )
301
+
302
+ // print CCIPRequest from tx receipt
303
+ const request = (await fetchCCIPRequestsInTx(tx))[0]
304
+
305
+ switch (argv.format) {
306
+ case Format.log:
307
+ console.log(`message ${request.log.index} =`, withDateTimestamp(request))
308
+ break
309
+ case Format.pretty:
310
+ await prettyRequest(source, request)
311
+ break
312
+ case Format.json:
313
+ console.info(JSON.stringify(request, bigIntReplacer, 2))
314
+ break
315
+ }
316
+ }
@@ -0,0 +1,151 @@
1
+ import util from 'util'
2
+
3
+ import {
4
+ type CCIPRequest,
5
+ ChainFamily,
6
+ bigIntReplacer,
7
+ discoverOffRamp,
8
+ fetchCCIPMessageById,
9
+ fetchCCIPRequestsInTx,
10
+ networkInfo,
11
+ } from '@chainlink/ccip-sdk/src/index.ts'
12
+ import type { Argv } from 'yargs'
13
+
14
+ import type { GlobalOpts } from '../index.ts'
15
+ import { Format } from './types.ts'
16
+ import {
17
+ logParsedError,
18
+ prettyCommit,
19
+ prettyReceipt,
20
+ prettyRequest,
21
+ selectRequest,
22
+ withDateTimestamp,
23
+ } from './utils.ts'
24
+ import { fetchChainsFromRpcs } from '../providers/index.ts'
25
+
26
+ export const command = ['show <tx-hash>', '* <tx-hash>']
27
+ export const describe = 'Show details of a CCIP request'
28
+
29
+ export const builder = (yargs: Argv) =>
30
+ yargs
31
+ .positional('tx-hash', {
32
+ type: 'string',
33
+ demandOption: true,
34
+ describe: 'transaction hash of the request (source) message',
35
+ })
36
+ .options({
37
+ 'log-index': {
38
+ type: 'number',
39
+ describe:
40
+ 'Pre-select a message request by logIndex, if more than one in tx; by default, a selection menu is shown',
41
+ },
42
+ 'id-from-source': {
43
+ type: 'string',
44
+ describe: 'Search by messageId instead of txHash; requires specifying source network',
45
+ },
46
+ })
47
+
48
+ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']> & GlobalOpts) {
49
+ let destroy
50
+ const destroy$ = new Promise((resolve) => {
51
+ destroy = resolve
52
+ })
53
+ return showRequests(argv, destroy$)
54
+ .catch((err) => {
55
+ process.exitCode = 1
56
+ if (!logParsedError(err)) console.error(err)
57
+ })
58
+ .finally(destroy)
59
+ }
60
+
61
+ async function showRequests(argv: Parameters<typeof handler>[0], destroy: Promise<unknown>) {
62
+ let source, getChain, tx, request: CCIPRequest
63
+ // messageId not yet implemented for Solana
64
+ if (argv.idFromSource) {
65
+ getChain = fetchChainsFromRpcs(argv, undefined, destroy)
66
+ const sourceNetwork = networkInfo(argv.idFromSource)
67
+ if (sourceNetwork.family === ChainFamily.Solana) {
68
+ throw new Error(
69
+ `Message ID search is not yet supported for Solana networks.\n` +
70
+ `Please use show with Solana transaction signature instead`,
71
+ )
72
+ }
73
+ source = await getChain(sourceNetwork.chainId)
74
+ request = await fetchCCIPMessageById(source, argv.txHash, argv)
75
+ } else {
76
+ const [getChain_, tx$] = fetchChainsFromRpcs(argv, argv.txHash, destroy)
77
+ getChain = getChain_
78
+ tx = await tx$
79
+ source = tx.chain
80
+ request = await selectRequest(await fetchCCIPRequestsInTx(tx), 'to know more', argv)
81
+ }
82
+
83
+ const offchainTokenData = await source.fetchOffchainTokenData(request)
84
+
85
+ switch (argv.format) {
86
+ case Format.log: {
87
+ console.log(
88
+ `message ${request.log.index} =`,
89
+ withDateTimestamp(request),
90
+ '\nattestations =',
91
+ offchainTokenData,
92
+ )
93
+ break
94
+ }
95
+ case Format.pretty:
96
+ await prettyRequest(source, request, offchainTokenData)
97
+ break
98
+ case Format.json:
99
+ console.info(JSON.stringify({ ...request, offchainTokenData }, bigIntReplacer, 2))
100
+ break
101
+ }
102
+ if (request.tx.error) throw new Error(`Request tx reverted: ${util.inspect(request.tx.error)}`)
103
+
104
+ const dest = await getChain(request.lane.destChainSelector)
105
+ const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)
106
+ const commitStore = await dest.getCommitStoreForOffRamp(offRamp)
107
+
108
+ const commit = await dest.fetchCommitReport(commitStore, request, argv)
109
+ switch (argv.format) {
110
+ case Format.log:
111
+ console.log('commit =', commit)
112
+ break
113
+ case Format.pretty:
114
+ await prettyCommit(dest, commit, request)
115
+ break
116
+ case Format.json:
117
+ console.info(JSON.stringify(commit, bigIntReplacer, 2))
118
+ break
119
+ }
120
+
121
+ let found = false
122
+ for await (const receipt of dest.fetchExecutionReceipts(
123
+ offRamp,
124
+ new Set([request.message.header.messageId]),
125
+ {
126
+ startBlock: commit.log.blockNumber,
127
+ page: argv.page,
128
+ commit: commit.report,
129
+ },
130
+ )) {
131
+ switch (argv.format) {
132
+ case Format.log:
133
+ console.log('receipt =', withDateTimestamp(receipt))
134
+ break
135
+ case Format.pretty:
136
+ if (!found) console.info('Receipts (dest):')
137
+ prettyReceipt(
138
+ receipt,
139
+ request,
140
+ receipt.log.tx?.from ??
141
+ (await dest.getTransaction(receipt.log.transactionHash).catch(() => null))?.from,
142
+ )
143
+ break
144
+ case Format.json:
145
+ console.info(JSON.stringify(receipt, bigIntReplacer, 2))
146
+ break
147
+ }
148
+ found = true
149
+ }
150
+ if (!found) console.warn(`No execution receipt found for request`)
151
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * CCIP Token Discovery Service
3
+ *
4
+ * Discovers and validates tokens that can be transferred between chains using Chainlink's CCIP.
5
+ * The service handles pagination, parallel processing, and comprehensive error collection.
6
+ *
7
+ * Architecture:
8
+ * 1. Chain & Contract Setup: Validates cross-chain paths and initializes core contracts
9
+ * 2. Token Discovery: Fetches all registered tokens with pagination
10
+ * 3. Support Validation: Checks token support for destination chain
11
+ * 4. Detail Collection: Gathers token and pool information in parallel
12
+ *
13
+ * Performance Considerations:
14
+ * - Uses batching to prevent RPC timeouts (configurable batch sizes)
15
+ * - Implements parallel processing with rate limiting
16
+ * - Memory-efficient token processing through pagination
17
+ *
18
+ * Error Handling:
19
+ * - Individual token failures don't halt the process
20
+ * - Errors are collected and reported comprehensively
21
+ * - Detailed error reporting for debugging
22
+ *
23
+ * @module supported-tokens
24
+ */
25
+
26
+ import {
27
+ type Chain,
28
+ type RateLimiterState,
29
+ bigIntReplacer,
30
+ networkInfo,
31
+ } from '@chainlink/ccip-sdk/src/index.ts'
32
+ import { search } from '@inquirer/prompts'
33
+ import { formatUnits } from 'ethers'
34
+ import type { Argv } from 'yargs'
35
+
36
+ import { Format } from './types.ts'
37
+ import { formatDuration, logParsedError, prettyTable } from './utils.ts'
38
+ import type { GlobalOpts } from '../index.ts'
39
+ import { fetchChainsFromRpcs } from '../providers/index.ts'
40
+
41
+ export const command = ['getSupportedTokens <source> <address> [token]']
42
+ export const describe =
43
+ 'List supported tokens in a given Router/OnRamp/TokenAdminRegistry, and/or show info about token/pool'
44
+
45
+ export const builder = (yargs: Argv) =>
46
+ yargs
47
+ .positional('source', {
48
+ type: 'string',
49
+ demandOption: true,
50
+ describe: 'source network, chainId or name',
51
+ example: 'ethereum-testnet-sepolia',
52
+ })
53
+ .positional('address', {
54
+ type: 'string',
55
+ demandOption: true,
56
+ describe: 'router/onramp/tokenAdminRegistry/tokenPool contract address on source',
57
+ })
58
+ .positional('token', {
59
+ type: 'string',
60
+ demandOption: false,
61
+ describe:
62
+ 'If address is router/onramp/tokenAdminRegistry, token may be used to pre-select a token from the supported list',
63
+ })
64
+
65
+ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']> & GlobalOpts) {
66
+ let destroy
67
+ const destroy$ = new Promise((resolve) => {
68
+ destroy = resolve
69
+ })
70
+ return getSupportedTokens(argv, destroy$)
71
+ .catch((err) => {
72
+ process.exitCode = 1
73
+ if (!logParsedError(err)) console.error(err)
74
+ })
75
+ .finally(destroy)
76
+ }
77
+
78
+ async function getSupportedTokens(argv: Parameters<typeof handler>[0], destroy: Promise<unknown>) {
79
+ const sourceNetwork = networkInfo(argv.source)
80
+ const getChain = fetchChainsFromRpcs(argv, undefined, destroy)
81
+ const source = await getChain(sourceNetwork.name)
82
+ let registry
83
+ try {
84
+ registry = await source.getTokenAdminRegistryFor(argv.address)
85
+ } catch (_) {
86
+ // ignore
87
+ }
88
+
89
+ let info, tokenPool, poolConfigs, registryConfig
90
+ if (registry && !argv.token) {
91
+ const feeTokens = await source.listFeeTokens(argv.address)
92
+ switch (argv.format) {
93
+ case Format.pretty:
94
+ console.info('Fee Tokens:')
95
+ console.table(feeTokens)
96
+ break
97
+ case Format.json:
98
+ console.log(JSON.stringify(feeTokens, null, 2))
99
+ break
100
+ default:
101
+ console.log('feeTokens:', feeTokens)
102
+ }
103
+
104
+ // router + interactive list
105
+ info = await listTokens(source, registry, argv)
106
+ if (!info) return // format != pretty
107
+ registryConfig = await source.getRegistryTokenConfig(registry, info.token)
108
+ tokenPool = registryConfig.tokenPool
109
+ if (!tokenPool)
110
+ throw new Error(`TokenPool not set in tokenAdminRegistry=${registry} for token=${info.token}`)
111
+ poolConfigs = await source.getTokenPoolConfigs(tokenPool)
112
+ } else {
113
+ if (!argv.token) {
114
+ // tokenPool
115
+ tokenPool = argv.address
116
+ poolConfigs = await source.getTokenPoolConfigs(tokenPool)
117
+ registry ??= await source.getTokenAdminRegistryFor(poolConfigs.router)
118
+ ;[info, registryConfig] = await Promise.all([
119
+ source.getTokenInfo(poolConfigs.token),
120
+ source.getRegistryTokenConfig(registry, poolConfigs.token),
121
+ ])
122
+ } else {
123
+ registry ??= await source.getTokenAdminRegistryFor(argv.address)
124
+ // router|ramp|registry + token
125
+ info = await source.getTokenInfo(argv.token)
126
+
127
+ registryConfig = await source.getRegistryTokenConfig(registry, argv.token)
128
+ tokenPool = registryConfig.tokenPool
129
+ if (!tokenPool)
130
+ throw new Error(
131
+ `TokenPool not set in tokenAdminRegistry=${registry} for token=${argv.token}`,
132
+ )
133
+ poolConfigs = await source.getTokenPoolConfigs(tokenPool)
134
+ }
135
+
136
+ if (argv.format === Format.json) {
137
+ console.log(JSON.stringify({ ...info, tokenPool, ...poolConfigs }, bigIntReplacer, 2))
138
+ return
139
+ } else if (argv.format === Format.log) {
140
+ console.log('Token:', poolConfigs.token, info)
141
+ console.log('Token Pool:', tokenPool)
142
+ console.log('Pool Configs:', poolConfigs)
143
+ return
144
+ }
145
+ }
146
+ const remotes = await source.getTokenPoolRemotes(tokenPool)
147
+
148
+ prettyTable({
149
+ network: `${source.network.name} [${source.network.chainSelector}]`,
150
+ token: poolConfigs.token,
151
+ symbol: info.symbol,
152
+ name: info.name,
153
+ decimals: info.decimals,
154
+ tokenPool,
155
+ typeAndVersion: poolConfigs.typeAndVersion,
156
+ router: poolConfigs.router,
157
+ tokenAdminRegistry: registry,
158
+ administrator: registryConfig.administrator,
159
+ ...(registryConfig.pendingAdministrator && {
160
+ pendingAdministrator: registryConfig.pendingAdministrator,
161
+ }),
162
+ })
163
+ const remotesLen = Object.keys(remotes).length
164
+ if (remotesLen > 0) console.info('Remotes [', remotesLen, ']:')
165
+ for (const [network, remote] of Object.entries(remotes))
166
+ prettyTable({
167
+ remoteNetwork: `${network} [${networkInfo(network).chainSelector}]`,
168
+ remoteToken: remote.remoteToken,
169
+ remotePool: remote.remotePools,
170
+ inbound: prettyRateLimiter(remote.inboundRateLimiterState, info),
171
+ outbound: prettyRateLimiter(remote.outboundRateLimiterState, info),
172
+ })
173
+ }
174
+
175
+ async function listTokens(source: Chain, registry: string, argv: GlobalOpts) {
176
+ const tokens = await source.getSupportedTokens(registry)
177
+ const infos: { token: string; symbol: string; decimals: number; name?: string }[] = []
178
+ const batch = 500
179
+ for (let i = 0; i < tokens.length; i += batch) {
180
+ const infos_ = (
181
+ await Promise.all(
182
+ tokens.slice(i, i + batch).map((token) =>
183
+ source.getTokenInfo(token).then(
184
+ (info) => {
185
+ const res = { token, ...info }
186
+ if (argv.format === Format.log) {
187
+ // Format.log prints out-of-order, as it fetches data, concurrently
188
+ console.info(token, '=', info)
189
+ }
190
+ return res
191
+ },
192
+ (err) => {
193
+ console.debug(`getTokenInfo errored`, token, err)
194
+ },
195
+ ),
196
+ ),
197
+ )
198
+ ).filter((e) => e !== undefined)
199
+ if (argv.format === Format.json) {
200
+ // Format.json keeps order, prints newline-separated objects
201
+ for (const info of infos_) {
202
+ console.log(JSON.stringify(info))
203
+ }
204
+ }
205
+ infos.push(...infos_)
206
+ }
207
+ if (argv.format !== Format.pretty) return // Format.pretty interactive search and details
208
+
209
+ return search({
210
+ message: 'Select a token to know more:',
211
+ pageSize: 20,
212
+ source: (term) => {
213
+ const filtered = infos.filter(
214
+ (info) =>
215
+ !term ||
216
+ `${info.token} ${info.symbol} ${info.name ?? ''} ${info.decimals}`
217
+ .toLowerCase()
218
+ .includes(term.toLowerCase()),
219
+ )
220
+ const symbolPad = Math.min(Math.max(...filtered.map(({ symbol }) => symbol.length)), 10)
221
+ const decimalsPad = Math.max(...filtered.map(({ decimals }) => decimals.toString().length))
222
+ return filtered.map((info, i) => ({
223
+ name: `${info.token}\t[${info.decimals.toString().padStart(decimalsPad)}] ${info.symbol.padEnd(symbolPad)}\t${info.name ?? ''}`,
224
+ value: info,
225
+ short: `${info.token} [${info.symbol}]`,
226
+ description: `${i + 1} / ${filtered.length} / ${tokens.length}`,
227
+ }))
228
+ },
229
+ })
230
+ }
231
+
232
+ function prettyRateLimiter(
233
+ state: RateLimiterState,
234
+ { decimals, symbol }: { decimals: number; symbol: string },
235
+ ) {
236
+ if (!state) return null
237
+ return {
238
+ capacity: formatUnits(state.capacity, decimals) + ' ' + symbol,
239
+ tokens: `${formatUnits(state.tokens, decimals)} (${Math.round((Number(state.tokens) / Number(state.capacity)) * 100)}%)`,
240
+ rate: `${formatUnits(state.rate, decimals)}/s (0-to-full in ${formatDuration(Number(state.capacity / state.rate))})`,
241
+ ...(state.tokens < state.capacity && {
242
+ timeToFull: formatDuration(Number(state.capacity - state.tokens) / Number(state.rate)),
243
+ }),
244
+ }
245
+ }