@chainlink/ccip-cli 0.91.1 → 0.92.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 (65) hide show
  1. package/README.md +60 -55
  2. package/dist/commands/manual-exec.d.ts +7 -1
  3. package/dist/commands/manual-exec.d.ts.map +1 -1
  4. package/dist/commands/manual-exec.js +16 -7
  5. package/dist/commands/manual-exec.js.map +1 -1
  6. package/dist/commands/parse.d.ts.map +1 -1
  7. package/dist/commands/parse.js +6 -5
  8. package/dist/commands/parse.js.map +1 -1
  9. package/dist/commands/send.d.ts +6 -1
  10. package/dist/commands/send.d.ts.map +1 -1
  11. package/dist/commands/send.js +27 -22
  12. package/dist/commands/send.js.map +1 -1
  13. package/dist/commands/show.d.ts +10 -1
  14. package/dist/commands/show.d.ts.map +1 -1
  15. package/dist/commands/show.js +85 -27
  16. package/dist/commands/show.js.map +1 -1
  17. package/dist/commands/supported-tokens.d.ts.map +1 -1
  18. package/dist/commands/supported-tokens.js +6 -6
  19. package/dist/commands/supported-tokens.js.map +1 -1
  20. package/dist/commands/types.d.ts +3 -2
  21. package/dist/commands/types.d.ts.map +1 -1
  22. package/dist/commands/utils.d.ts +27 -7
  23. package/dist/commands/utils.d.ts.map +1 -1
  24. package/dist/commands/utils.js +97 -35
  25. package/dist/commands/utils.js.map +1 -1
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/providers/aptos.d.ts.map +1 -1
  31. package/dist/providers/aptos.js +3 -5
  32. package/dist/providers/aptos.js.map +1 -1
  33. package/dist/providers/evm.d.ts.map +1 -1
  34. package/dist/providers/evm.js +2 -4
  35. package/dist/providers/evm.js.map +1 -1
  36. package/dist/providers/index.d.ts +6 -5
  37. package/dist/providers/index.d.ts.map +1 -1
  38. package/dist/providers/index.js +104 -71
  39. package/dist/providers/index.js.map +1 -1
  40. package/dist/providers/solana.d.ts.map +1 -1
  41. package/dist/providers/solana.js +5 -4
  42. package/dist/providers/solana.js.map +1 -1
  43. package/dist/providers/sui.d.ts +10 -0
  44. package/dist/providers/sui.d.ts.map +1 -0
  45. package/dist/providers/sui.js +15 -0
  46. package/dist/providers/sui.js.map +1 -0
  47. package/dist/providers/ton.d.ts +10 -0
  48. package/dist/providers/ton.d.ts.map +1 -0
  49. package/dist/providers/ton.js +55 -0
  50. package/dist/providers/ton.js.map +1 -0
  51. package/package.json +13 -13
  52. package/src/commands/manual-exec.ts +18 -6
  53. package/src/commands/parse.ts +9 -5
  54. package/src/commands/send.ts +36 -28
  55. package/src/commands/show.ts +94 -31
  56. package/src/commands/supported-tokens.ts +6 -9
  57. package/src/commands/types.ts +3 -2
  58. package/src/commands/utils.ts +118 -43
  59. package/src/index.ts +4 -4
  60. package/src/providers/aptos.ts +3 -5
  61. package/src/providers/evm.ts +2 -4
  62. package/src/providers/index.ts +120 -94
  63. package/src/providers/solana.ts +5 -6
  64. package/src/providers/sui.ts +15 -0
  65. package/src/providers/ton.ts +68 -0
@@ -1,3 +1,4 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
1
2
  import { readFile } from 'node:fs/promises'
2
3
 
3
4
  import {
@@ -5,6 +6,10 @@ import {
5
6
  type ChainGetter,
6
7
  type ChainTransaction,
7
8
  type EVMChain,
9
+ CCIPChainFamilyUnsupportedError,
10
+ CCIPNetworkFamilyUnsupportedError,
11
+ CCIPRpcNotFoundError,
12
+ CCIPTransactionNotFoundError,
8
13
  ChainFamily,
9
14
  networkInfo,
10
15
  supportedChains,
@@ -13,46 +18,26 @@ import {
13
18
  import { loadAptosWallet } from './aptos.ts'
14
19
  import { loadEvmWallet } from './evm.ts'
15
20
  import { loadSolanaWallet } from './solana.ts'
21
+ import { loadSuiWallet } from './sui.ts'
22
+ import { loadTonWallet } from './ton.ts'
16
23
  import type { Ctx } from '../commands/index.ts'
17
24
 
18
25
  const RPCS_RE = /\b(?:http|ws)s?:\/\/[\w/\\@&?%~#.,;:=+-]+/
19
26
 
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
-
44
- async function collectEndpoints({
45
- rpcs,
46
- 'rpcs-file': rpcsFile,
47
- }: {
48
- rpcs?: string[]
49
- 'rpcs-file'?: string
50
- }): Promise<Set<string>> {
51
- const endpoints = new Set<string>(rpcs || [])
27
+ async function collectEndpoints(
28
+ this: Ctx,
29
+ { rpcs, rpcsFile }: { rpcs?: string[]; rpcsFile?: string },
30
+ ): Promise<Set<string>> {
31
+ const endpoints = new Set<string>(
32
+ rpcs
33
+ ?.map((s) => s.split(','))
34
+ .flat()
35
+ .map((s) => s.trim()) || [],
36
+ )
52
37
  for (const [env, val] of Object.entries(process.env)) {
53
38
  if (env.startsWith('RPC_') && val && RPCS_RE.test(val)) endpoints.add(val)
54
39
  }
55
- if (rpcsFile) {
40
+ if (rpcsFile && existsSync(rpcsFile)) {
56
41
  try {
57
42
  const fileContent = await readFile(rpcsFile, 'utf8')
58
43
  for (const line of fileContent.toString().split(/(?:\r\n|\r|\n)/g)) {
@@ -60,7 +45,7 @@ async function collectEndpoints({
60
45
  if (match) endpoints.add(match[0])
61
46
  }
62
47
  } catch (error) {
63
- console.debug('Error reading RPCs file', error)
48
+ this.logger.debug('Error reading RPCs file', error)
64
49
  }
65
50
  }
66
51
  return endpoints
@@ -68,126 +53,167 @@ async function collectEndpoints({
68
53
 
69
54
  export function fetchChainsFromRpcs(
70
55
  ctx: Ctx,
71
- argv: { rpcs?: string[]; 'rpcs-file'?: string },
56
+ argv: { rpcs?: string[]; rpcsFile?: string },
72
57
  ): ChainGetter
73
58
  export function fetchChainsFromRpcs(
74
59
  ctx: Ctx,
75
- argv: { rpcs?: string[]; 'rpcs-file'?: string },
60
+ argv: { rpcs?: string[]; rpcsFile?: string },
76
61
  txHash: string,
77
62
  ): [ChainGetter, Promise<[Chain, ChainTransaction]>]
78
63
 
79
64
  /**
80
- * Receives a list of rpcs and/or rpcs file, and loads them all concurrently
81
- * Returns a ChainGetter function and optinoally a ChainTransaction promise
65
+ * Receives a list of rpcs and/or rpcs file, and loads them concurrently for each chain family
66
+ * If txHash is provided, fetches matching families first and returns [chainGetter, txPromise];
67
+ * Otherwise, spawns racing URLs for each family asked by `getChain` getter
82
68
  * @param ctx - Context object containing destroy$ promise and logger properties
83
69
  * @param argv - Options containing rpcs (list) and/or rpcs file
84
70
  * @param txHash - Optional txHash to fetch concurrently; causes the function to return a [ChainGetter, Promise<ChainTransaction>]
85
- * @returns a ChainGetter (alone if no txHash was provided), or a tuple of [ChainGetter, Promise<ChainTransaction>]
71
+ * @returns a ChainGetter (if txHash was provided), or a tuple of [ChainGetter, Promise<ChainTransaction>]
86
72
  */
87
73
  export function fetchChainsFromRpcs(
88
74
  ctx: Ctx,
89
- argv: { rpcs?: string[]; 'rpcs-file'?: string },
75
+ argv: { rpcs?: string[]; rpcsFile?: string },
90
76
  txHash?: string,
91
77
  ) {
92
- const { destroy$ } = ctx
93
78
  const chains: Record<string, Promise<Chain>> = {}
94
79
  const chainsCbs: Record<
95
80
  string,
96
81
  readonly [resolve: (value: Chain) => void, reject: (reason?: unknown) => void]
97
82
  > = {}
98
- let finished = false
99
- const txs: Promise<[Chain, ChainTransaction]>[] = []
83
+ const finished: Partial<Record<ChainFamily, boolean>> = {}
84
+ const initFamily$: Partial<Record<ChainFamily, Promise<unknown>>> = {}
85
+ let endpoints$: Promise<Set<string>>
86
+
87
+ let txResolve: (value: [Chain, ChainTransaction]) => void, txReject: (reason?: unknown) => void
88
+ const txResult = new Promise<[Chain, ChainTransaction]>((resolve, reject) => {
89
+ txResolve = resolve
90
+ txReject = reject
91
+ })
92
+
93
+ const loadChainFamily = (F: ChainFamily, txHash?: string) =>
94
+ (initFamily$[F] ??= (endpoints$ ??= collectEndpoints.call(ctx, argv)).then((endpoints) => {
95
+ const C = supportedChains[F]
96
+ if (!C) throw new CCIPNetworkFamilyUnsupportedError(F)
97
+ ctx.logger.debug('Racing', endpoints.size, 'RPC endpoints for', F)
100
98
 
101
- const init$ = collectEndpoints(argv).then((endpoints) => {
102
- const pendingPromises: Promise<unknown>[] = []
103
- let txFound = false
104
- for (const C of Object.values(supportedChains)) {
99
+ const chains$: Promise<Chain>[] = []
100
+ const txs$: Promise<unknown>[] = []
101
+ let txFound = false
105
102
  for (const url of endpoints) {
106
103
  const chain$ = C.fromUrl(url, ctx)
107
- if (txHash) {
108
- const tx$ = chain$.then((chain) =>
109
- chain.getTransaction(txHash).then<[Chain, ChainTransaction]>((tx) => [chain, tx]),
110
- )
111
- void tx$.then(
112
- ([chain]) => {
113
- if (txFound) return
114
- txFound = true
115
- // in case tx is found, prefer it over any previously found chain
116
- chains[chain.network.name] = chain$
117
- delete chainsCbs[chain.network.name]
118
- },
119
- () => {},
120
- )
121
- txs.push(tx$)
122
- }
104
+ chains$.push(chain$)
123
105
 
124
- pendingPromises.push(
125
- chain$.then((chain) => {
106
+ void chain$.then(
107
+ (chain) => {
108
+ endpoints.delete(url) // when resolved, remove from set so it isn't tried for future families
109
+ // on chain detected for url
126
110
  if (chain.network.name in chains && !(chain.network.name in chainsCbs))
127
- return chain.destroy?.() // lost race
128
- destroy$.addEventListener('abort', () => {
129
- void chain.destroy?.() // cleanup
130
- })
111
+ return chain.destroy?.() // but lost race, cleanup right away
112
+ // keep and schedule cleanup on shutdown
113
+ if (chain.destroy) void ctx.destroy$.finally(chain.destroy.bind(chain))
131
114
  if (!(chain.network.name in chains)) {
115
+ // chain won for this network, but was not "asked" by getChain (yet?): save
132
116
  chains[chain.network.name] = Promise.resolve(chain)
133
117
  } else if (chain.network.name in chainsCbs) {
118
+ // chain detected, and there's a "pending request" by getChain: resolve
134
119
  const [resolve] = chainsCbs[chain.network.name]
135
120
  resolve(chain)
136
121
  }
137
- }),
122
+ return chain
123
+ },
124
+ () => {},
138
125
  )
126
+
127
+ if (txHash) {
128
+ txs$.push(
129
+ chain$.then(async (chain) => {
130
+ const tx = await chain.getTransaction(txHash)
131
+ if (!txFound) {
132
+ txFound = true
133
+ // in case tx is first found, prefer it over any previously found chain for this network
134
+ chains[chain.network.name] = chain$
135
+ delete chainsCbs[chain.network.name]
136
+ }
137
+ txResolve([chain, tx])
138
+ }),
139
+ )
140
+ }
139
141
  }
140
- }
141
- const res = Promise.allSettled(pendingPromises)
142
- void (destroy$ ? Promise.race([res, signalToPromise(destroy$)]) : res).finally(() => {
143
- finished = true
144
- Object.entries(chainsCbs).forEach(([name, [_, reject]]) =>
145
- reject(new Error(`No provider/chain found for network=${name}`)),
146
- )
147
- })
148
- return Promise.any(txs)
149
- })
142
+
143
+ void Promise.race([Promise.allSettled(chains$), ctx.destroy$]).finally(() => {
144
+ if (finished[F]) return
145
+ finished[F] = true
146
+ Object.entries(chainsCbs)
147
+ .filter(([name]) => networkInfo(name).family === F)
148
+ .forEach(([name, [_, reject]]) => reject(new CCIPRpcNotFoundError(name)))
149
+ })
150
+ return Promise.any(txHash ? txs$ : chains$)
151
+ }))
150
152
 
151
153
  const chainGetter = async (idOrSelectorOrName: number | string | bigint): Promise<Chain> => {
152
154
  const network = networkInfo(idOrSelectorOrName)
153
155
  if (network.name in chains) return chains[network.name]
154
- if (finished) throw new Error(`No provider/chain found for network=${network.name}`)
156
+ if (finished[network.family]) throw new CCIPRpcNotFoundError(network.name)
155
157
  chains[network.name] = new Promise((resolve, reject) => {
156
158
  chainsCbs[network.name] = [resolve, reject]
157
159
  })
158
160
  void chains[network.name].finally(() => {
159
- delete chainsCbs[network.name]
161
+ delete chainsCbs[network.name] // when chain is settled, delete the callbacks
160
162
  })
163
+ void loadChainFamily(network.family)
161
164
  return chains[network.name]
162
165
  }
163
166
 
164
- if (txHash) {
165
- return [chainGetter, init$]
166
- } else {
167
- void init$.catch(() => {})
168
- return chainGetter
169
- }
167
+ if (!txHash) return chainGetter
168
+
169
+ void Promise.allSettled(
170
+ Object.values(supportedChains)
171
+ .filter((C) => C.isTxHash(txHash))
172
+ .map((C) => loadChainFamily(C.family, txHash)),
173
+ ).finally(() => txReject(new CCIPTransactionNotFoundError(txHash))) // noop if txResolved
174
+ return [chainGetter, txResult]
170
175
  }
171
176
 
172
177
  /**
173
178
  * Load chain-specific wallet for given chain
174
179
  * @param chain - Chain instance to load wallet for
175
- * @param opts - Wallet options (as passed from yargs argv)
180
+ * @param argv - Wallet options (as passed from yargs argv)
176
181
  * @returns Promise to chain-specific wallet instance
177
182
  */
178
- export async function loadChainWallet(chain: Chain, opts: { wallet?: unknown }) {
183
+ export async function loadChainWallet(chain: Chain, argv: { wallet?: unknown; rpcsFile?: string }) {
184
+ // Centralized wallet resolution: check env vars first, then rpcsFile
185
+ if (!argv.wallet) {
186
+ argv.wallet = process.env['PRIVATE_KEY'] || process.env['USER_KEY'] || process.env['OWNER_KEY']
187
+ }
188
+ if (!argv.wallet && argv.rpcsFile && existsSync(argv.rpcsFile)) {
189
+ try {
190
+ const file = readFileSync(argv.rpcsFile, 'utf8')
191
+ const match = file.match(/^\s*(PRIVATE_KEY|USER_KEY|OWNER_KEY)=(\S+)/m)
192
+ if (match) argv.wallet = match[2]
193
+ } catch (_) {
194
+ // pass
195
+ }
196
+ }
197
+
179
198
  let wallet
180
199
  switch (chain.network.family) {
181
200
  case ChainFamily.EVM:
182
- wallet = await loadEvmWallet((chain as EVMChain).provider, opts)
201
+ wallet = await loadEvmWallet((chain as EVMChain).provider, argv)
183
202
  return [await wallet.getAddress(), wallet] as const
184
203
  case ChainFamily.Solana:
185
- wallet = await loadSolanaWallet(opts)
204
+ wallet = await loadSolanaWallet(argv)
186
205
  return [wallet.publicKey.toBase58(), wallet] as const
187
206
  case ChainFamily.Aptos:
188
- wallet = await loadAptosWallet(opts)
207
+ wallet = await loadAptosWallet(argv)
189
208
  return [wallet.accountAddress.toString(), wallet] as const
209
+ case ChainFamily.Sui:
210
+ wallet = loadSuiWallet(argv)
211
+ return [wallet.toSuiAddress(), wallet] as const
212
+ case ChainFamily.TON:
213
+ wallet = await loadTonWallet(argv)
214
+ return [wallet.contract.address.toString(), wallet] as const
190
215
  default:
191
- throw new Error(`Unsupported chain family: ${chain.network.family}`)
216
+ // TypeScript exhaustiveness check - this should never be reached
217
+ throw new CCIPChainFamilyUnsupportedError((chain.network as { family: string }).family)
192
218
  }
193
219
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
- import util from 'node:util'
3
2
 
3
+ import { CCIPArgumentInvalidError, CCIPNotImplementedError } from '@chainlink/ccip-sdk/src/index.ts'
4
4
  import { Wallet as AnchorWallet } from '@coral-xyz/anchor'
5
5
  import SolanaLedger from '@ledgerhq/hw-app-solana'
6
6
  import HIDTransport from '@ledgerhq/hw-transport-node-hid'
@@ -86,7 +86,7 @@ export class LedgerSolanaWallet {
86
86
 
87
87
  /** Payer property - not available on Ledger. */
88
88
  get payer(): Keypair {
89
- throw new Error('Payer method not available on Ledger')
89
+ throw new CCIPNotImplementedError('payer for Ledger')
90
90
  }
91
91
  }
92
92
 
@@ -98,11 +98,10 @@ export class LedgerSolanaWallet {
98
98
  export async function loadSolanaWallet({
99
99
  wallet: walletOpt,
100
100
  }: { wallet?: unknown } = {}): Promise<AnchorWallet> {
101
- if (!walletOpt)
102
- walletOpt = process.env['USER_KEY'] || process.env['OWNER_KEY'] || '~/.config/solana/id.json'
101
+ // Default to Solana's standard keypair location if no wallet provided
102
+ if (!walletOpt) walletOpt = '~/.config/solana/id.json'
103
103
  let wallet: string
104
- if (typeof walletOpt !== 'string')
105
- throw new Error(`Invalid wallet option: ${util.inspect(walletOpt)}`)
104
+ if (typeof walletOpt !== 'string') throw new CCIPArgumentInvalidError('wallet', String(walletOpt))
106
105
  wallet = walletOpt
107
106
  if (walletOpt === 'ledger' || walletOpt.startsWith('ledger:')) {
108
107
  let derivationPath = walletOpt.split(':')[1]
@@ -0,0 +1,15 @@
1
+ import { CCIPArgumentInvalidError } from '@chainlink/ccip-sdk/src/index.ts'
2
+ import { bytesToBuffer } from '@chainlink/ccip-sdk/src/utils.ts'
3
+ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
4
+
5
+ /**
6
+ * Loads a Sui wallet from the provided options.
7
+ * @param wallet - wallet options (as passed from yargs argv)
8
+ * @returns Sui Keypair instance
9
+ */
10
+ export function loadSuiWallet({ wallet: walletOpt }: { wallet?: unknown }) {
11
+ if (typeof walletOpt !== 'string') throw new CCIPArgumentInvalidError('wallet', String(walletOpt))
12
+
13
+ const keyBytes = bytesToBuffer(walletOpt)
14
+ return Ed25519Keypair.fromSecretKey(keyBytes)
15
+ }
@@ -0,0 +1,68 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+
3
+ import {
4
+ CCIPArgumentInvalidError,
5
+ CCIPWalletInvalidError,
6
+ } from '@chainlink/ccip-sdk/src/errors/specialized.ts'
7
+ import type { TONWallet } from '@chainlink/ccip-sdk/src/ton/types.ts'
8
+ import { keyPairFromSecretKey, mnemonicToPrivateKey } from '@ton/crypto'
9
+ import { WalletContractV4 } from '@ton/ton'
10
+
11
+ /**
12
+ * Loads a TON wallet from the provided options.
13
+ * @param wallet - wallet options (as passed from yargs argv)
14
+ * @returns Promise to TONWallet instance
15
+ */
16
+ export async function loadTonWallet({
17
+ wallet: walletOpt,
18
+ }: { wallet?: unknown } = {}): Promise<TONWallet> {
19
+ if (typeof walletOpt !== 'string') throw new CCIPWalletInvalidError(walletOpt)
20
+
21
+ // Handle mnemonic phrase
22
+ if (walletOpt.includes(' ')) {
23
+ const mnemonic = walletOpt.trim().split(' ')
24
+ const keyPair = await mnemonicToPrivateKey(mnemonic)
25
+ const contract = WalletContractV4.create({
26
+ workchain: 0,
27
+ publicKey: keyPair.publicKey,
28
+ })
29
+ return { contract, keyPair }
30
+ }
31
+
32
+ // Handle hex private key
33
+ if (walletOpt.startsWith('0x')) {
34
+ const secretKey = Buffer.from(walletOpt.slice(2), 'hex')
35
+ if (secretKey.length === 32) {
36
+ throw new CCIPArgumentInvalidError(
37
+ 'wallet',
38
+ '32-byte seeds not supported. Use 64-byte secret key or mnemonic.',
39
+ )
40
+ }
41
+ if (secretKey.length !== 64) {
42
+ throw new CCIPArgumentInvalidError('wallet', 'must be 64 bytes (or use mnemonic)')
43
+ }
44
+ const keyPair = keyPairFromSecretKey(secretKey)
45
+ const contract = WalletContractV4.create({
46
+ workchain: 0,
47
+ publicKey: keyPair.publicKey,
48
+ })
49
+ return { contract, keyPair }
50
+ }
51
+
52
+ // Handle file path
53
+ if (existsSync(walletOpt)) {
54
+ const content = readFileSync(walletOpt, 'utf8').trim()
55
+ const secretKey = Buffer.from(content.startsWith('0x') ? content.slice(2) : content, 'hex')
56
+ if (secretKey.length !== 64) {
57
+ throw new CCIPArgumentInvalidError('wallet', 'Invalid private key in file: must be 64 bytes')
58
+ }
59
+ const keyPair = keyPairFromSecretKey(secretKey)
60
+ const contract = WalletContractV4.create({
61
+ workchain: 0,
62
+ publicKey: keyPair.publicKey,
63
+ })
64
+ return { contract, keyPair }
65
+ }
66
+
67
+ throw new CCIPArgumentInvalidError('wallet', 'Wallet not specified')
68
+ }