@chainlink/ccip-cli 0.94.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -42
- package/dist/commands/manual-exec.d.ts +1 -1
- package/dist/commands/manual-exec.d.ts.map +1 -1
- package/dist/commands/manual-exec.js +12 -12
- package/dist/commands/manual-exec.js.map +1 -1
- package/dist/commands/parse.d.ts.map +1 -1
- package/dist/commands/parse.js +8 -2
- package/dist/commands/parse.js.map +1 -1
- package/dist/commands/send.d.ts +5 -9
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +72 -52
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +2 -4
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/supported-tokens.d.ts +3 -1
- package/dist/commands/supported-tokens.d.ts.map +1 -1
- package/dist/commands/supported-tokens.js +36 -15
- package/dist/commands/supported-tokens.js.map +1 -1
- package/dist/commands/token.d.ts +26 -0
- package/dist/commands/token.d.ts.map +1 -0
- package/dist/commands/token.js +105 -0
- package/dist/commands/token.js.map +1 -0
- package/dist/commands/utils.d.ts.map +1 -1
- package/dist/commands/utils.js +21 -7
- package/dist/commands/utils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -3
- package/dist/providers/index.js.map +1 -1
- package/package.json +10 -13
- package/src/commands/manual-exec.ts +14 -24
- package/src/commands/parse.ts +8 -2
- package/src/commands/send.ts +80 -66
- package/src/commands/show.ts +1 -4
- package/src/commands/supported-tokens.ts +35 -15
- package/src/commands/token.ts +132 -0
- package/src/commands/utils.ts +23 -6
- package/src/index.ts +2 -2
- package/src/providers/index.ts +7 -3
package/src/commands/send.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type CCIPVersion,
|
|
3
2
|
type ChainStatic,
|
|
4
|
-
type EVMChain,
|
|
5
3
|
type ExtraArgs,
|
|
6
4
|
type MessageInput,
|
|
7
5
|
CCIPArgumentInvalidError,
|
|
8
|
-
|
|
6
|
+
CCIPInsufficientBalanceError,
|
|
9
7
|
CCIPTokenNotFoundError,
|
|
10
8
|
ChainFamily,
|
|
11
|
-
|
|
9
|
+
estimateReceiveExecution,
|
|
12
10
|
getDataBytes,
|
|
13
11
|
networkInfo,
|
|
14
|
-
sourceToDestTokenAmounts,
|
|
15
12
|
} from '@chainlink/ccip-sdk/src/index.ts'
|
|
16
13
|
import { type BytesLike, formatUnits, toUtf8Bytes } from 'ethers'
|
|
17
14
|
import type { Argv } from 'yargs'
|
|
@@ -22,8 +19,8 @@ import type { Ctx } from './types.ts'
|
|
|
22
19
|
import { getCtx, logParsedError, parseTokenAmounts } from './utils.ts'
|
|
23
20
|
import { fetchChainsFromRpcs, loadChainWallet } from '../providers/index.ts'
|
|
24
21
|
|
|
25
|
-
export const command = 'send
|
|
26
|
-
export const describe = 'Send a CCIP message from
|
|
22
|
+
export const command = 'send'
|
|
23
|
+
export const describe = 'Send a CCIP message from source to destination chain'
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
26
|
* Yargs builder for the send command.
|
|
@@ -32,108 +29,107 @@ export const describe = 'Send a CCIP message from router on source to dest'
|
|
|
32
29
|
*/
|
|
33
30
|
export const builder = (yargs: Argv) =>
|
|
34
31
|
yargs
|
|
35
|
-
.
|
|
32
|
+
.option('source', {
|
|
33
|
+
alias: 's',
|
|
36
34
|
type: 'string',
|
|
37
35
|
demandOption: true,
|
|
38
|
-
describe: '
|
|
39
|
-
example: 'ethereum-testnet-sepolia',
|
|
36
|
+
describe: 'Source chain: chainId, selector, or name',
|
|
40
37
|
})
|
|
41
|
-
.
|
|
38
|
+
.option('dest', {
|
|
39
|
+
alias: 'd',
|
|
42
40
|
type: 'string',
|
|
43
41
|
demandOption: true,
|
|
44
|
-
describe: '
|
|
42
|
+
describe: 'Destination chain: chainId, selector, or name',
|
|
45
43
|
})
|
|
46
|
-
.
|
|
44
|
+
.option('router', {
|
|
45
|
+
alias: 'r',
|
|
47
46
|
type: 'string',
|
|
48
47
|
demandOption: true,
|
|
49
|
-
describe: '
|
|
50
|
-
example: 'ethereum-testnet-sepolia-arbitrum-1',
|
|
48
|
+
describe: 'Router contract address on source',
|
|
51
49
|
})
|
|
52
50
|
.options({
|
|
53
51
|
receiver: {
|
|
54
|
-
alias: '
|
|
52
|
+
alias: 'to',
|
|
55
53
|
type: 'string',
|
|
56
|
-
describe:
|
|
57
|
-
'Receiver of the message; defaults to the sender wallet address if same network family',
|
|
54
|
+
describe: 'Receiver address on destination; defaults to sender if same chain family',
|
|
58
55
|
},
|
|
59
56
|
data: {
|
|
60
|
-
alias: 'd',
|
|
61
57
|
type: 'string',
|
|
62
|
-
describe: 'Data to send
|
|
63
|
-
example: '0x1234',
|
|
58
|
+
describe: 'Data payload to send (non-hex will be UTF-8 encoded)',
|
|
64
59
|
},
|
|
65
60
|
'gas-limit': {
|
|
66
61
|
alias: ['L', 'compute-units'],
|
|
67
62
|
type: 'number',
|
|
68
|
-
describe:
|
|
69
|
-
'Gas limit for receiver callback execution; defaults to default configured on ramps',
|
|
63
|
+
describe: 'Gas limit for receiver callback; defaults to ramp config',
|
|
70
64
|
},
|
|
71
65
|
'estimate-gas-limit': {
|
|
72
66
|
type: 'number',
|
|
73
|
-
describe:
|
|
74
|
-
'Estimate gas limit for receiver callback execution; argument is a % margin to add to the estimate',
|
|
75
|
-
example: '10',
|
|
67
|
+
describe: 'Estimate gas limit with % margin (e.g., 10 for +10%)',
|
|
76
68
|
conflicts: 'gas-limit',
|
|
77
69
|
},
|
|
78
70
|
'allow-out-of-order-exec': {
|
|
79
71
|
alias: 'ooo',
|
|
80
72
|
type: 'boolean',
|
|
81
|
-
describe:
|
|
82
|
-
'Allow execution of messages out of order (i.e. sender nonce not enforced, only v1.5+ lanes, mandatory for some dests)',
|
|
73
|
+
describe: 'Allow out-of-order execution (v1.5+ lanes)',
|
|
83
74
|
},
|
|
84
75
|
'fee-token': {
|
|
85
76
|
type: 'string',
|
|
86
|
-
describe:
|
|
87
|
-
'Address or symbol of the fee token (e.g. LINK address on source); if not provided, will pay in native',
|
|
77
|
+
describe: 'Fee token address or symbol (default: native)',
|
|
88
78
|
},
|
|
89
79
|
'transfer-tokens': {
|
|
90
80
|
alias: 't',
|
|
91
81
|
type: 'array',
|
|
92
82
|
string: true,
|
|
93
|
-
describe: '
|
|
94
|
-
example: '0xtoken=0.1',
|
|
83
|
+
describe: 'Token amounts to transfer: token=amount',
|
|
95
84
|
},
|
|
96
85
|
wallet: {
|
|
97
86
|
alias: 'w',
|
|
98
87
|
type: 'string',
|
|
99
|
-
describe:
|
|
100
|
-
'Wallet to send transactions with; pass `ledger[:index_or_derivation]` to use Ledger USB hardware wallet, or private key in `USER_KEY` environment variable',
|
|
88
|
+
describe: 'Wallet: ledger[:index] or private key',
|
|
101
89
|
},
|
|
102
90
|
'token-receiver': {
|
|
103
91
|
type: 'string',
|
|
104
|
-
describe:
|
|
92
|
+
describe: 'Solana token receiver (if different from program receiver)',
|
|
105
93
|
},
|
|
106
94
|
account: {
|
|
107
95
|
alias: 'receiver-object-id',
|
|
108
96
|
type: 'array',
|
|
109
97
|
string: true,
|
|
110
|
-
describe:
|
|
111
|
-
'List of accounts needed by Solana receiver program, or receiverObjectIds needed by Sui; On Solana, append `=rw` to specify account as writable; can be specified multiple times',
|
|
112
|
-
example: 'requiredPdaAddress=rw',
|
|
98
|
+
describe: 'Solana accounts (append =rw for writable) or Sui object IDs',
|
|
113
99
|
},
|
|
114
100
|
'only-get-fee': {
|
|
115
101
|
type: 'boolean',
|
|
116
|
-
describe: '
|
|
102
|
+
describe: 'Print fee and exit',
|
|
117
103
|
},
|
|
118
104
|
'only-estimate': {
|
|
119
105
|
type: 'boolean',
|
|
120
|
-
describe: '
|
|
106
|
+
describe: 'Print gas estimate and exit',
|
|
107
|
+
implies: 'estimate-gas-limit',
|
|
121
108
|
},
|
|
122
109
|
'approve-max': {
|
|
123
110
|
type: 'boolean',
|
|
124
|
-
describe:
|
|
125
|
-
"Approve the maximum amount of tokens to transfer; default=false approves only what's needed",
|
|
111
|
+
describe: 'Approve max token amount instead of exact',
|
|
126
112
|
},
|
|
127
113
|
wait: {
|
|
128
114
|
type: 'boolean',
|
|
129
115
|
default: false,
|
|
130
|
-
describe: 'Wait for execution',
|
|
116
|
+
describe: 'Wait for execution on destination',
|
|
131
117
|
},
|
|
132
118
|
})
|
|
133
119
|
.check(
|
|
134
120
|
({ 'transfer-tokens': transferTokens }) =>
|
|
135
121
|
!transferTokens || transferTokens.every((t) => /^[^=]+=\d+(\.\d+)?$/.test(t)),
|
|
136
122
|
)
|
|
123
|
+
.example([
|
|
124
|
+
[
|
|
125
|
+
'ccip-cli send -s ethereum-testnet-sepolia -d arbitrum-sepolia -r 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 --only-get-fee',
|
|
126
|
+
'Get fee estimate',
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
'ccip-cli send -s ethereum-testnet-sepolia -d arbitrum-sepolia -r 0x0BF3... --to 0xABC... --data "Hello"',
|
|
130
|
+
'Send message with data',
|
|
131
|
+
],
|
|
132
|
+
])
|
|
137
133
|
|
|
138
134
|
/**
|
|
139
135
|
* Handler for the send command.
|
|
@@ -198,37 +194,34 @@ async function sendMessage(
|
|
|
198
194
|
|
|
199
195
|
if (argv.estimateGasLimit != null || argv.onlyEstimate) {
|
|
200
196
|
// TODO: implement for all chain families
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
destChainSelector: destNetwork.chainSelector,
|
|
210
|
-
onRamp,
|
|
211
|
-
version: (await source.typeAndVersion(onRamp))[1] as CCIPVersion,
|
|
197
|
+
const dest = await getChain(destNetwork.chainSelector)
|
|
198
|
+
|
|
199
|
+
if (!walletAddress) {
|
|
200
|
+
try {
|
|
201
|
+
;[walletAddress, wallet] = await loadChainWallet(source, argv)
|
|
202
|
+
} catch {
|
|
203
|
+
// pass undefined sender for default
|
|
204
|
+
}
|
|
212
205
|
}
|
|
213
|
-
const
|
|
206
|
+
const estimated = await estimateReceiveExecution({
|
|
214
207
|
source,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
tokenAmounts,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
if (!walletAddress) [walletAddress, wallet] = await loadChainWallet(source, argv)
|
|
221
|
-
const estimated = await estimateExecGasForRequest(source, dest, {
|
|
222
|
-
lane,
|
|
208
|
+
dest,
|
|
209
|
+
routerOrRamp: argv.router,
|
|
223
210
|
message: {
|
|
224
211
|
sender: walletAddress,
|
|
225
212
|
receiver,
|
|
226
|
-
data
|
|
227
|
-
tokenAmounts
|
|
213
|
+
data,
|
|
214
|
+
tokenAmounts,
|
|
228
215
|
},
|
|
229
216
|
})
|
|
230
|
-
logger.log('Estimated gasLimit:', estimated)
|
|
231
217
|
argv.gasLimit = Math.ceil(estimated * (1 + (argv.estimateGasLimit ?? 0) / 100))
|
|
218
|
+
logger.log(
|
|
219
|
+
'Estimated gasLimit for sender =',
|
|
220
|
+
walletAddress,
|
|
221
|
+
':',
|
|
222
|
+
estimated,
|
|
223
|
+
...(argv.estimateGasLimit ? ['+', argv.estimateGasLimit, '% =', argv.gasLimit] : []),
|
|
224
|
+
)
|
|
232
225
|
if (argv.onlyEstimate) return
|
|
233
226
|
}
|
|
234
227
|
|
|
@@ -295,6 +288,27 @@ async function sendMessage(
|
|
|
295
288
|
if (argv.onlyGetFee) return
|
|
296
289
|
|
|
297
290
|
if (!walletAddress) [walletAddress, wallet] = await loadChainWallet(source, argv)
|
|
291
|
+
|
|
292
|
+
// Check sender has sufficient balance for fee
|
|
293
|
+
try {
|
|
294
|
+
const balance = await source.getBalance({ holder: walletAddress, token: feeToken })
|
|
295
|
+
if (balance < fee) {
|
|
296
|
+
const symbol =
|
|
297
|
+
feeTokenInfo.symbol.startsWith('W') && !feeToken
|
|
298
|
+
? feeTokenInfo.symbol.substring(1)
|
|
299
|
+
: feeTokenInfo.symbol
|
|
300
|
+
throw new CCIPInsufficientBalanceError(
|
|
301
|
+
formatUnits(balance, feeTokenInfo.decimals),
|
|
302
|
+
formatUnits(fee, feeTokenInfo.decimals),
|
|
303
|
+
symbol,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
} catch (e) {
|
|
307
|
+
// may fail for chains that don't implement getBalance yet; log and continue
|
|
308
|
+
if (e instanceof CCIPInsufficientBalanceError) throw e
|
|
309
|
+
logger.debug('Balance check skipped:', e)
|
|
310
|
+
}
|
|
311
|
+
|
|
298
312
|
const request = await source.sendMessage({
|
|
299
313
|
...argv,
|
|
300
314
|
destChainSelector: destNetwork.chainSelector,
|
package/src/commands/show.ts
CHANGED
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
type CCIPRequest,
|
|
3
3
|
type ChainTransaction,
|
|
4
4
|
CCIPExecTxRevertedError,
|
|
5
|
-
CCIPNotImplementedError,
|
|
6
5
|
ExecutionState,
|
|
7
6
|
MessageStatus,
|
|
8
7
|
bigIntReplacer,
|
|
@@ -88,9 +87,7 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
|
|
|
88
87
|
} else idFromSource = argv.idFromSource
|
|
89
88
|
const sourceNetwork = networkInfo(idFromSource)
|
|
90
89
|
source = await getChain(sourceNetwork.chainId)
|
|
91
|
-
|
|
92
|
-
throw new CCIPNotImplementedError(`getMessageById for ${source.constructor.name}`)
|
|
93
|
-
request = await source.getMessageById(argv.txHash, onRamp, argv)
|
|
90
|
+
request = await source.getMessageById(argv.txHash, { ...argv, onRamp })
|
|
94
91
|
} else {
|
|
95
92
|
const [getChain_, tx$] = fetchChainsFromRpcs(ctx, argv, argv.txHash)
|
|
96
93
|
getChain = getChain_
|
|
@@ -37,7 +37,7 @@ import { formatDuration, getCtx, logParsedError, prettyTable } from './utils.ts'
|
|
|
37
37
|
import type { GlobalOpts } from '../index.ts'
|
|
38
38
|
import { fetchChainsFromRpcs } from '../providers/index.ts'
|
|
39
39
|
|
|
40
|
-
export const command = ['getSupportedTokens
|
|
40
|
+
export const command = ['getSupportedTokens', 'get-supported-tokens']
|
|
41
41
|
export const describe =
|
|
42
42
|
'List supported tokens in a given Router/OnRamp/TokenAdminRegistry, and/or show info about token/pool'
|
|
43
43
|
|
|
@@ -48,23 +48,39 @@ export const describe =
|
|
|
48
48
|
*/
|
|
49
49
|
export const builder = (yargs: Argv) =>
|
|
50
50
|
yargs
|
|
51
|
-
.
|
|
51
|
+
.option('network', {
|
|
52
|
+
alias: 'n',
|
|
52
53
|
type: 'string',
|
|
53
54
|
demandOption: true,
|
|
54
|
-
describe: '
|
|
55
|
-
example: 'ethereum-testnet-sepolia',
|
|
55
|
+
describe: 'Source network: chainId or name (e.g., ethereum-mainnet)',
|
|
56
56
|
})
|
|
57
|
-
.
|
|
57
|
+
.option('address', {
|
|
58
|
+
alias: 'a',
|
|
58
59
|
type: 'string',
|
|
59
60
|
demandOption: true,
|
|
60
|
-
describe: '
|
|
61
|
+
describe: 'Router/OnRamp/TokenAdminRegistry/TokenPool contract address',
|
|
61
62
|
})
|
|
62
|
-
.
|
|
63
|
+
.option('token', {
|
|
64
|
+
alias: 't',
|
|
63
65
|
type: 'string',
|
|
64
66
|
demandOption: false,
|
|
65
|
-
describe:
|
|
66
|
-
'If address is router/onramp/tokenAdminRegistry, token may be used to pre-select a token from the supported list',
|
|
67
|
+
describe: 'Token address to query (pre-selects from list if address is a registry)',
|
|
67
68
|
})
|
|
69
|
+
.option('fee-tokens', {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
default: false,
|
|
72
|
+
describe: 'List fee tokens instead of transferable tokens',
|
|
73
|
+
})
|
|
74
|
+
.example([
|
|
75
|
+
[
|
|
76
|
+
'ccip-cli getSupportedTokens -n ethereum-mainnet -a 0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D',
|
|
77
|
+
'List all supported tokens from router',
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
'ccip-cli getSupportedTokens -n ethereum-mainnet -a 0x80226fc... -t 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
81
|
+
'Get details for specific token',
|
|
82
|
+
],
|
|
83
|
+
])
|
|
68
84
|
|
|
69
85
|
/**
|
|
70
86
|
* Handler for the supported-tokens command.
|
|
@@ -82,7 +98,7 @@ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']>
|
|
|
82
98
|
|
|
83
99
|
async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0]) {
|
|
84
100
|
const { logger } = ctx
|
|
85
|
-
const sourceNetwork = networkInfo(argv.
|
|
101
|
+
const sourceNetwork = networkInfo(argv.network)
|
|
86
102
|
const getChain = fetchChainsFromRpcs(ctx, argv)
|
|
87
103
|
const source = await getChain(sourceNetwork.name)
|
|
88
104
|
let registry
|
|
@@ -92,8 +108,8 @@ async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0])
|
|
|
92
108
|
// ignore
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
if (
|
|
111
|
+
// Handle --fee-tokens flag
|
|
112
|
+
if (argv.feeTokens) {
|
|
97
113
|
const feeTokens = await source.getFeeTokens(argv.address)
|
|
98
114
|
switch (argv.format) {
|
|
99
115
|
case Format.pretty:
|
|
@@ -106,19 +122,23 @@ async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0])
|
|
|
106
122
|
default:
|
|
107
123
|
logger.log('feeTokens:', feeTokens)
|
|
108
124
|
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
109
127
|
|
|
128
|
+
let info, tokenPool, poolConfigs, registryConfig
|
|
129
|
+
if (registry && !argv.token) {
|
|
110
130
|
// router + interactive list
|
|
111
131
|
info = await listTokens(ctx, source, registry, argv)
|
|
112
132
|
if (!info) return // format != pretty
|
|
113
133
|
registryConfig = await source.getRegistryTokenConfig(registry, info.token)
|
|
114
134
|
tokenPool = registryConfig.tokenPool
|
|
115
135
|
if (!tokenPool) throw new CCIPTokenNotConfiguredError(info.token, registry)
|
|
116
|
-
poolConfigs = await source.
|
|
136
|
+
poolConfigs = await source.getTokenPoolConfig(tokenPool)
|
|
117
137
|
} else {
|
|
118
138
|
if (!argv.token) {
|
|
119
139
|
// tokenPool
|
|
120
140
|
tokenPool = argv.address
|
|
121
|
-
poolConfigs = await source.
|
|
141
|
+
poolConfigs = await source.getTokenPoolConfig(tokenPool)
|
|
122
142
|
registry ??= await source.getTokenAdminRegistryFor(poolConfigs.router)
|
|
123
143
|
;[info, registryConfig] = await Promise.all([
|
|
124
144
|
source.getTokenInfo(poolConfigs.token),
|
|
@@ -132,7 +152,7 @@ async function getSupportedTokens(ctx: Ctx, argv: Parameters<typeof handler>[0])
|
|
|
132
152
|
registryConfig = await source.getRegistryTokenConfig(registry, argv.token)
|
|
133
153
|
tokenPool = registryConfig.tokenPool
|
|
134
154
|
if (!tokenPool) throw new CCIPTokenNotConfiguredError(argv.token, registry)
|
|
135
|
-
poolConfigs = await source.
|
|
155
|
+
poolConfigs = await source.getTokenPoolConfig(tokenPool)
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
if (argv.format === Format.json) {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token balance query command.
|
|
3
|
+
* Queries native or token balance for an address.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type ChainStatic, networkInfo } from '@chainlink/ccip-sdk/src/index.ts'
|
|
7
|
+
import { formatUnits } from 'ethers'
|
|
8
|
+
import type { Argv } from 'yargs'
|
|
9
|
+
|
|
10
|
+
import { type Ctx, Format } from './types.ts'
|
|
11
|
+
import { getCtx, logParsedError, prettyTable } from './utils.ts'
|
|
12
|
+
import type { GlobalOpts } from '../index.ts'
|
|
13
|
+
import { fetchChainsFromRpcs } from '../providers/index.ts'
|
|
14
|
+
|
|
15
|
+
export const command = 'token'
|
|
16
|
+
export const describe = 'Query token balance for an address'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Yargs builder for the token command.
|
|
20
|
+
* @param yargs - Yargs instance.
|
|
21
|
+
* @returns Configured yargs instance with command options.
|
|
22
|
+
*/
|
|
23
|
+
export const builder = (yargs: Argv) =>
|
|
24
|
+
yargs
|
|
25
|
+
.option('network', {
|
|
26
|
+
alias: 'n',
|
|
27
|
+
type: 'string',
|
|
28
|
+
demandOption: true,
|
|
29
|
+
describe: 'Network: chainId or name (e.g., ethereum-mainnet, solana-devnet)',
|
|
30
|
+
})
|
|
31
|
+
.option('holder', {
|
|
32
|
+
alias: 'H',
|
|
33
|
+
type: 'string',
|
|
34
|
+
demandOption: true,
|
|
35
|
+
describe: 'Wallet address to query balance for',
|
|
36
|
+
})
|
|
37
|
+
.option('token', {
|
|
38
|
+
alias: 't',
|
|
39
|
+
type: 'string',
|
|
40
|
+
demandOption: false,
|
|
41
|
+
describe: 'Token address (omit for native token balance)',
|
|
42
|
+
})
|
|
43
|
+
.example([
|
|
44
|
+
['ccip-cli token -n ethereum-mainnet -H 0x1234...abcd', 'Query native ETH balance'],
|
|
45
|
+
[
|
|
46
|
+
'ccip-cli token -n ethereum-mainnet -H 0x1234... -t 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
47
|
+
'Query USDC token balance',
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
'ccip-cli token -n solana-devnet -H EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB',
|
|
51
|
+
'Query native SOL balance',
|
|
52
|
+
],
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handler for the token command.
|
|
57
|
+
* @param argv - Command line arguments.
|
|
58
|
+
*/
|
|
59
|
+
export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']> & GlobalOpts) {
|
|
60
|
+
const [ctx, destroy] = getCtx(argv)
|
|
61
|
+
return queryTokenBalance(ctx, argv)
|
|
62
|
+
.catch((err) => {
|
|
63
|
+
process.exitCode = 1
|
|
64
|
+
if (!logParsedError.call(ctx, err)) ctx.logger.error(err)
|
|
65
|
+
})
|
|
66
|
+
.finally(destroy)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function queryTokenBalance(ctx: Ctx, argv: Parameters<typeof handler>[0]) {
|
|
70
|
+
const { logger } = ctx
|
|
71
|
+
const networkName = networkInfo(argv.network).name
|
|
72
|
+
const getChain = fetchChainsFromRpcs(ctx, argv)
|
|
73
|
+
const chain = await getChain(networkName)
|
|
74
|
+
|
|
75
|
+
const balance = await chain.getBalance({
|
|
76
|
+
holder: argv.holder,
|
|
77
|
+
token: argv.token,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Get token info for formatting (only for tokens, not native)
|
|
81
|
+
let tokenInfo
|
|
82
|
+
if (argv.token) {
|
|
83
|
+
argv.token = (chain.constructor as ChainStatic).getAddress(argv.token)
|
|
84
|
+
tokenInfo = await chain.getTokenInfo(argv.token)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tokenLabel = tokenInfo?.symbol ?? 'native'
|
|
88
|
+
const formatted = formatUnits(
|
|
89
|
+
balance,
|
|
90
|
+
tokenInfo ? tokenInfo.decimals : (chain.constructor as ChainStatic).decimals,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
switch (argv.format) {
|
|
94
|
+
case Format.json:
|
|
95
|
+
logger.log(
|
|
96
|
+
JSON.stringify(
|
|
97
|
+
{
|
|
98
|
+
network: networkName,
|
|
99
|
+
holder: argv.holder,
|
|
100
|
+
token: tokenLabel,
|
|
101
|
+
balance: balance.toString(),
|
|
102
|
+
formatted,
|
|
103
|
+
...tokenInfo,
|
|
104
|
+
},
|
|
105
|
+
null,
|
|
106
|
+
2,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
case Format.log:
|
|
111
|
+
logger.log(
|
|
112
|
+
`Balance of`,
|
|
113
|
+
tokenInfo ? argv.token : tokenLabel,
|
|
114
|
+
':',
|
|
115
|
+
balance,
|
|
116
|
+
`=`,
|
|
117
|
+
tokenInfo ? `${formatted} ${tokenLabel}` : formatted,
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
case Format.pretty:
|
|
121
|
+
default:
|
|
122
|
+
prettyTable.call(ctx, {
|
|
123
|
+
network: networkName,
|
|
124
|
+
holder: argv.holder,
|
|
125
|
+
token: argv.token ?? tokenLabel,
|
|
126
|
+
balance,
|
|
127
|
+
formatted,
|
|
128
|
+
...tokenInfo,
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/commands/utils.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CCIPErrorCode,
|
|
13
13
|
ExecutionState,
|
|
14
14
|
getCCIPExplorerUrl,
|
|
15
|
+
getDataBytes,
|
|
15
16
|
networkInfo,
|
|
16
17
|
supportedChains,
|
|
17
18
|
} from '@chainlink/ccip-sdk/src/index.ts'
|
|
@@ -19,11 +20,11 @@ import { select } from '@inquirer/prompts'
|
|
|
19
20
|
import {
|
|
20
21
|
dataLength,
|
|
21
22
|
formatUnits,
|
|
22
|
-
getBytes,
|
|
23
23
|
hexlify,
|
|
24
24
|
isBytesLike,
|
|
25
25
|
isHexString,
|
|
26
26
|
parseUnits,
|
|
27
|
+
toBigInt,
|
|
27
28
|
toUtf8String,
|
|
28
29
|
} from 'ethers'
|
|
29
30
|
import type { PickDeep } from 'type-fest'
|
|
@@ -215,6 +216,26 @@ function omit<T extends Record<string, unknown>, K extends string>(
|
|
|
215
216
|
return result
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
// while formatData just breaks 0x bytes into 32B chunks for readability, this function first
|
|
220
|
+
// tests if the data looks like a UTF-8 string (with length prefix) and decode that before
|
|
221
|
+
function formatDataString(data: string): Record<string, string> {
|
|
222
|
+
const bytes = getDataBytes(data)
|
|
223
|
+
const isPrintableChars = (bytes_: Uint8Array) => bytes_.every((b) => 32 <= b && b <= 126)
|
|
224
|
+
if (bytes.length > 64 && toBigInt(bytes.subarray(0, 32)) === 32n) {
|
|
225
|
+
const len = toBigInt(bytes.subarray(32, 64))
|
|
226
|
+
if (
|
|
227
|
+
len < 512 &&
|
|
228
|
+
bytes.length - 64 === Math.ceil(Number(len) / 32) * 32 &&
|
|
229
|
+
isPrintableChars(bytes.subarray(64, 64 + Number(len))) &&
|
|
230
|
+
bytes.subarray(64 + Number(len)).every((b) => b === 0)
|
|
231
|
+
) {
|
|
232
|
+
return { data: toUtf8String(bytes.subarray(64, 64 + Number(len))) }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (bytes.length > 0 && isPrintableChars(bytes)) return { data: toUtf8String(bytes) }
|
|
236
|
+
return formatData('data', data)
|
|
237
|
+
}
|
|
238
|
+
|
|
218
239
|
/**
|
|
219
240
|
* Prints a CCIP request in a human-readable format.
|
|
220
241
|
* @param source - Source chain instance.
|
|
@@ -289,11 +310,7 @@ export async function prettyRequest(this: Ctx, source: Chain, request: CCIPReque
|
|
|
289
310
|
'tokens',
|
|
290
311
|
await Promise.all(request.message.tokenAmounts.map(formatToken.bind(null, source))),
|
|
291
312
|
),
|
|
292
|
-
...(
|
|
293
|
-
dataLength(request.message.data) > 0 &&
|
|
294
|
-
getBytes(request.message.data).every((b) => 32 <= b && b <= 126) // printable characters
|
|
295
|
-
? { data: toUtf8String(request.message.data) }
|
|
296
|
-
: formatData('data', request.message.data)),
|
|
313
|
+
...formatDataString(request.message.data),
|
|
297
314
|
...('accounts' in request.message ? formatArray('accounts', request.message.accounts) : {}),
|
|
298
315
|
...rest,
|
|
299
316
|
})
|
package/src/index.ts
CHANGED
|
@@ -11,13 +11,13 @@ import { Format } from './commands/index.ts'
|
|
|
11
11
|
util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests
|
|
12
12
|
// generate:nofail
|
|
13
13
|
// `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
|
|
14
|
-
const VERSION = '0.
|
|
14
|
+
const VERSION = '0.96.0-983178a'
|
|
15
15
|
// generate:end
|
|
16
16
|
|
|
17
17
|
const globalOpts = {
|
|
18
18
|
rpcs: {
|
|
19
19
|
type: 'array',
|
|
20
|
-
alias:
|
|
20
|
+
alias: 'rpc',
|
|
21
21
|
describe: 'List of RPC endpoint URLs, ws[s] or http[s]',
|
|
22
22
|
string: true,
|
|
23
23
|
},
|
package/src/providers/index.ts
CHANGED
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
type EVMChain,
|
|
9
9
|
type TONChain,
|
|
10
10
|
CCIPChainFamilyUnsupportedError,
|
|
11
|
-
CCIPNetworkFamilyUnsupportedError,
|
|
12
11
|
CCIPRpcNotFoundError,
|
|
13
12
|
CCIPTransactionNotFoundError,
|
|
14
13
|
ChainFamily,
|
|
14
|
+
NetworkType,
|
|
15
15
|
networkInfo,
|
|
16
16
|
supportedChains,
|
|
17
17
|
} from '@chainlink/ccip-sdk/src/index.ts'
|
|
@@ -94,7 +94,7 @@ export function fetchChainsFromRpcs(
|
|
|
94
94
|
const loadChainFamily = (F: ChainFamily, txHash?: string) =>
|
|
95
95
|
(initFamily$[F] ??= (endpoints$ ??= collectEndpoints.call(ctx, argv)).then((endpoints) => {
|
|
96
96
|
const C = supportedChains[F]
|
|
97
|
-
if (!C) throw new
|
|
97
|
+
if (!C) throw new CCIPChainFamilyUnsupportedError(F)
|
|
98
98
|
ctx.logger.debug('Racing', endpoints.size, 'RPC endpoints for', F)
|
|
99
99
|
|
|
100
100
|
const chains$: Promise<Chain>[] = []
|
|
@@ -214,7 +214,11 @@ export async function loadChainWallet(chain: Chain, argv: { wallet?: unknown; rp
|
|
|
214
214
|
wallet = loadSuiWallet(argv)
|
|
215
215
|
return [wallet.toSuiAddress(), wallet] as const
|
|
216
216
|
case ChainFamily.TON:
|
|
217
|
-
wallet = await loadTonWallet(
|
|
217
|
+
wallet = await loadTonWallet(
|
|
218
|
+
(chain as TONChain).provider,
|
|
219
|
+
argv,
|
|
220
|
+
chain.network.networkType === NetworkType.Testnet,
|
|
221
|
+
)
|
|
218
222
|
return [wallet.getAddress(), wallet] as const
|
|
219
223
|
default:
|
|
220
224
|
// TypeScript exhaustiveness check - this should never be reached
|