@spectratools/etherscan-cli 0.1.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/cli.js +953 -0
  4. package/package.json +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 spectra-the-bot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @spectra-the-bot/etherscan-cli
2
+
3
+ Etherscan V2 API CLI built with [incur](https://github.com/wevm/incur). Supports 60+ chains including Abstract (2741).
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ export ETHERSCAN_API_KEY=your_api_key
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### Account
14
+
15
+ ```bash
16
+ # ETH balance
17
+ etherscan-cli account balance <address> [--chain abstract]
18
+
19
+ # Transaction list
20
+ etherscan-cli account txlist <address> [--startblock 0] [--endblock latest] [--page 1] [--offset 10] [--sort asc]
21
+
22
+ # ERC-20 token transfers
23
+ etherscan-cli account tokentx <address> [--contractaddress 0x...] [--chain abstract]
24
+
25
+ # ERC-20 token balance
26
+ etherscan-cli account tokenbalance <address> --contractaddress <0x...> [--chain abstract]
27
+ ```
28
+
29
+ ### Contract
30
+
31
+ ```bash
32
+ # ABI (verified contracts only)
33
+ etherscan-cli contract abi <address> [--chain abstract]
34
+
35
+ # Verified source code
36
+ etherscan-cli contract source <address> [--chain abstract]
37
+
38
+ # Deployment transaction
39
+ etherscan-cli contract creation <address> [--chain abstract]
40
+ ```
41
+
42
+ ### Transaction
43
+
44
+ ```bash
45
+ etherscan-cli tx info <txhash> [--chain abstract]
46
+ etherscan-cli tx receipt <txhash> [--chain abstract]
47
+ etherscan-cli tx status <txhash> [--chain abstract]
48
+ ```
49
+
50
+ ### Token
51
+
52
+ ```bash
53
+ etherscan-cli token info <contractaddress> [--chain abstract]
54
+ etherscan-cli token holders <contractaddress> [--page 1] [--offset 10] [--chain abstract]
55
+ etherscan-cli token supply <contractaddress> [--chain abstract]
56
+ ```
57
+
58
+ ### Gas
59
+
60
+ ```bash
61
+ etherscan-cli gas oracle [--chain abstract]
62
+ etherscan-cli gas estimate --gasprice <wei> [--chain abstract]
63
+ ```
64
+
65
+ ### Stats
66
+
67
+ ```bash
68
+ etherscan-cli stats ethprice [--chain abstract]
69
+ etherscan-cli stats ethsupply [--chain abstract]
70
+ ```
71
+
72
+ ## Supported Chains
73
+
74
+ | Name | Chain ID |
75
+ |------|----------|
76
+ | abstract | 2741 |
77
+ | ethereum / mainnet | 1 |
78
+ | base | 8453 |
79
+ | arbitrum | 42161 |
80
+ | optimism | 10 |
81
+ | polygon | 137 |
82
+ | avalanche | 43114 |
83
+ | bsc | 56 |
84
+ | linea | 59144 |
85
+ | scroll | 534352 |
86
+ | zksync | 324 |
87
+ | mantle | 5000 |
88
+ | blast | 81457 |
89
+ | mode | 34443 |
90
+ | sepolia | 11155111 |
91
+
92
+ ## Rate Limiting
93
+
94
+ The free Etherscan API tier allows 5 requests/second. The CLI enforces this automatically using a token-bucket rate limiter from `@spectra-the-bot/cli-shared`.
95
+
96
+ ## Output Formats
97
+
98
+ ```bash
99
+ # Human-readable (default)
100
+ etherscan-cli account balance 0x...
101
+
102
+ # JSON (for piping/agents)
103
+ etherscan-cli account balance 0x... --json
104
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,953 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { fileURLToPath } from "url";
5
+ import { Cli as Cli7 } from "incur";
6
+
7
+ // src/commands/account.ts
8
+ import {
9
+ apiKeyAuth,
10
+ checksumAddress,
11
+ createRateLimiter as createRateLimiter2,
12
+ formatTimestamp,
13
+ weiToEth,
14
+ withRateLimit as withRateLimit2
15
+ } from "@spectratools/cli-shared";
16
+ import { Cli, z } from "incur";
17
+
18
+ // src/api.ts
19
+ import {
20
+ createHttpClient,
21
+ createRateLimiter,
22
+ withRateLimit,
23
+ withRetry
24
+ } from "@spectratools/cli-shared";
25
+ var DEFAULT_BASE_URL = "https://api.etherscan.io/v2/api";
26
+ var RETRY_OPTIONS = { maxRetries: 3, baseMs: 500, maxMs: 1e4 };
27
+ var EtherscanError = class extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "EtherscanError";
31
+ }
32
+ };
33
+ function createEtherscanClient(apiKey, baseUrl = DEFAULT_BASE_URL) {
34
+ const http = createHttpClient({ baseUrl });
35
+ const acquire = createRateLimiter({ requestsPerSecond: 5 });
36
+ function request(params) {
37
+ return withRetry(
38
+ () => withRateLimit(
39
+ () => http.request("", {
40
+ query: { ...params, apikey: apiKey }
41
+ }),
42
+ acquire
43
+ ),
44
+ RETRY_OPTIONS
45
+ );
46
+ }
47
+ async function call(params) {
48
+ const response = await request(params);
49
+ if (response.status === "0") {
50
+ const msg = typeof response.result === "string" ? response.result : response.message;
51
+ throw new EtherscanError(msg);
52
+ }
53
+ return response.result;
54
+ }
55
+ async function callProxy(params) {
56
+ const response = await request(params);
57
+ return response.result;
58
+ }
59
+ return { call, callProxy };
60
+ }
61
+
62
+ // src/chains.ts
63
+ var CHAIN_IDS = {
64
+ abstract: 2741,
65
+ ethereum: 1,
66
+ mainnet: 1,
67
+ base: 8453,
68
+ arbitrum: 42161,
69
+ optimism: 10,
70
+ polygon: 137,
71
+ avalanche: 43114,
72
+ bsc: 56,
73
+ linea: 59144,
74
+ scroll: 534352,
75
+ zksync: 324,
76
+ mantle: 5e3,
77
+ blast: 81457,
78
+ mode: 34443,
79
+ sepolia: 11155111,
80
+ goerli: 5
81
+ };
82
+ var DEFAULT_CHAIN = "abstract";
83
+ function resolveChainId(chain) {
84
+ const id = CHAIN_IDS[chain.toLowerCase()];
85
+ if (id === void 0) {
86
+ const known = Object.keys(CHAIN_IDS).join(", ");
87
+ throw new Error(`Unknown chain "${chain}". Supported chains: ${known}`);
88
+ }
89
+ return id;
90
+ }
91
+
92
+ // src/commands/account.ts
93
+ var rateLimiter = createRateLimiter2({ requestsPerSecond: 5 });
94
+ var chainOption = z.string().default(DEFAULT_CHAIN).describe("Chain name (abstract, ethereum, base, arbitrum, ...)");
95
+ function normalizeAddress(address) {
96
+ try {
97
+ return checksumAddress(address);
98
+ } catch {
99
+ return address;
100
+ }
101
+ }
102
+ var accountCli = Cli.create("account", {
103
+ description: "Query account balances, transactions, and token transfers"
104
+ });
105
+ accountCli.command("balance", {
106
+ description: "Get the ETH balance of an address",
107
+ args: z.object({
108
+ address: z.string().describe("Ethereum address")
109
+ }),
110
+ options: z.object({
111
+ chain: chainOption
112
+ }),
113
+ async run(c) {
114
+ const { apiKey } = apiKeyAuth("ETHERSCAN_API_KEY");
115
+ const chainId = resolveChainId(c.options.chain);
116
+ const address = normalizeAddress(c.args.address);
117
+ const client = createEtherscanClient(apiKey);
118
+ const wei = await withRateLimit2(
119
+ () => client.call({
120
+ chainid: chainId,
121
+ module: "account",
122
+ action: "balance",
123
+ address,
124
+ tag: "latest"
125
+ }),
126
+ rateLimiter
127
+ );
128
+ return c.ok(
129
+ { address, wei, eth: weiToEth(wei), chain: c.options.chain },
130
+ {
131
+ cta: {
132
+ commands: [
133
+ { command: "account txlist", args: { address }, description: "List transactions" },
134
+ {
135
+ command: "account tokentx",
136
+ args: { address },
137
+ description: "List token transfers"
138
+ }
139
+ ]
140
+ }
141
+ }
142
+ );
143
+ }
144
+ });
145
+ accountCli.command("txlist", {
146
+ description: "List normal transactions for an address",
147
+ args: z.object({
148
+ address: z.string().describe("Ethereum address")
149
+ }),
150
+ options: z.object({
151
+ startblock: z.number().optional().default(0).describe("Start block number"),
152
+ endblock: z.string().optional().default("latest").describe("End block number"),
153
+ page: z.number().optional().default(1).describe("Page number"),
154
+ offset: z.number().optional().default(10).describe("Number of results per page"),
155
+ sort: z.string().optional().default("asc").describe("Sort order (asc or desc)"),
156
+ chain: chainOption
157
+ }),
158
+ async run(c) {
159
+ const { apiKey } = apiKeyAuth("ETHERSCAN_API_KEY");
160
+ const chainId = resolveChainId(c.options.chain);
161
+ const address = normalizeAddress(c.args.address);
162
+ const client = createEtherscanClient(apiKey);
163
+ const txs = await withRateLimit2(
164
+ () => client.call({
165
+ chainid: chainId,
166
+ module: "account",
167
+ action: "txlist",
168
+ address,
169
+ startblock: c.options.startblock,
170
+ endblock: c.options.endblock,
171
+ page: c.options.page,
172
+ offset: c.options.offset,
173
+ sort: c.options.sort
174
+ }),
175
+ rateLimiter
176
+ );
177
+ const formatted = txs.map((tx) => ({
178
+ hash: tx.hash,
179
+ from: normalizeAddress(tx.from),
180
+ to: tx.to ? normalizeAddress(tx.to) : "",
181
+ value: tx.value,
182
+ eth: weiToEth(tx.value),
183
+ timestamp: formatTimestamp(Number(tx.timeStamp)),
184
+ block: tx.blockNumber,
185
+ status: tx.isError === "0" ? "success" : "failed",
186
+ gasUsed: tx.gasUsed
187
+ }));
188
+ const firstHash = formatted[0]?.hash;
189
+ return c.ok(
190
+ { address, chain: c.options.chain, count: formatted.length, transactions: formatted },
191
+ {
192
+ cta: {
193
+ commands: firstHash ? [
194
+ {
195
+ command: "tx info",
196
+ args: { txhash: firstHash },
197
+ description: "Get details for the first transaction"
198
+ }
199
+ ] : []
200
+ }
201
+ }
202
+ );
203
+ }
204
+ });
205
+ accountCli.command("tokentx", {
206
+ description: "List ERC-20 token transfers for an address",
207
+ args: z.object({
208
+ address: z.string().describe("Ethereum address")
209
+ }),
210
+ options: z.object({
211
+ contractaddress: z.string().optional().describe("Filter by token contract address"),
212
+ page: z.number().optional().default(1).describe("Page number"),
213
+ offset: z.number().optional().default(20).describe("Results per page"),
214
+ chain: chainOption
215
+ }),
216
+ async run(c) {
217
+ const { apiKey } = apiKeyAuth("ETHERSCAN_API_KEY");
218
+ const chainId = resolveChainId(c.options.chain);
219
+ const address = normalizeAddress(c.args.address);
220
+ const client = createEtherscanClient(apiKey);
221
+ const transfers = await withRateLimit2(
222
+ () => client.call({
223
+ chainid: chainId,
224
+ module: "account",
225
+ action: "tokentx",
226
+ address,
227
+ contractaddress: c.options.contractaddress,
228
+ page: c.options.page,
229
+ offset: c.options.offset
230
+ }),
231
+ rateLimiter
232
+ );
233
+ const formatted = transfers.map((tx) => ({
234
+ hash: tx.hash,
235
+ from: normalizeAddress(tx.from),
236
+ to: normalizeAddress(tx.to),
237
+ value: tx.value,
238
+ token: tx.tokenSymbol,
239
+ tokenName: tx.tokenName,
240
+ decimals: tx.tokenDecimal,
241
+ timestamp: formatTimestamp(Number(tx.timeStamp)),
242
+ contract: normalizeAddress(tx.contractAddress)
243
+ }));
244
+ return c.ok(
245
+ { address, chain: c.options.chain, count: formatted.length, transfers: formatted },
246
+ {
247
+ cta: {
248
+ commands: [
249
+ {
250
+ command: "account balance",
251
+ args: { address },
252
+ description: "Check ETH balance"
253
+ }
254
+ ]
255
+ }
256
+ }
257
+ );
258
+ }
259
+ });
260
+ accountCli.command("tokenbalance", {
261
+ description: "Get ERC-20 token balance for an address",
262
+ args: z.object({
263
+ address: z.string().describe("Ethereum address")
264
+ }),
265
+ options: z.object({
266
+ contractaddress: z.string().describe("Token contract address"),
267
+ chain: chainOption
268
+ }),
269
+ async run(c) {
270
+ const { apiKey } = apiKeyAuth("ETHERSCAN_API_KEY");
271
+ const chainId = resolveChainId(c.options.chain);
272
+ const address = normalizeAddress(c.args.address);
273
+ const contract = normalizeAddress(c.options.contractaddress);
274
+ const client = createEtherscanClient(apiKey);
275
+ const balance = await withRateLimit2(
276
+ () => client.call({
277
+ chainid: chainId,
278
+ module: "account",
279
+ action: "tokenbalance",
280
+ address,
281
+ contractaddress: contract,
282
+ tag: "latest"
283
+ }),
284
+ rateLimiter
285
+ );
286
+ return c.ok(
287
+ { address, contract, balance, chain: c.options.chain },
288
+ {
289
+ cta: {
290
+ commands: [
291
+ {
292
+ command: "token info",
293
+ args: { contractaddress: contract },
294
+ description: "Get token info"
295
+ }
296
+ ]
297
+ }
298
+ }
299
+ );
300
+ }
301
+ });
302
+
303
+ // src/commands/contract.ts
304
+ import {
305
+ apiKeyAuth as apiKeyAuth2,
306
+ checksumAddress as checksumAddress2,
307
+ createRateLimiter as createRateLimiter3,
308
+ withRateLimit as withRateLimit3
309
+ } from "@spectratools/cli-shared";
310
+ import { Cli as Cli2, z as z2 } from "incur";
311
+ var rateLimiter2 = createRateLimiter3({ requestsPerSecond: 5 });
312
+ var chainOption2 = z2.string().default(DEFAULT_CHAIN).describe("Chain name (abstract, ethereum, base, arbitrum, ...)");
313
+ var contractCli = Cli2.create("contract", {
314
+ description: "Query contract ABI, source code, and deployment info"
315
+ });
316
+ contractCli.command("abi", {
317
+ description: "Get the ABI for a verified contract",
318
+ args: z2.object({
319
+ address: z2.string().describe("Contract address")
320
+ }),
321
+ options: z2.object({
322
+ chain: chainOption2
323
+ }),
324
+ async run(c) {
325
+ const { apiKey } = apiKeyAuth2("ETHERSCAN_API_KEY");
326
+ const chainId = resolveChainId(c.options.chain);
327
+ const address = checksumAddress2(c.args.address);
328
+ const client = createEtherscanClient(apiKey);
329
+ const abi = await withRateLimit3(
330
+ () => client.call({
331
+ chainid: chainId,
332
+ module: "contract",
333
+ action: "getabi",
334
+ address
335
+ }),
336
+ rateLimiter2
337
+ );
338
+ return c.ok(
339
+ { address, chain: c.options.chain, abi: JSON.parse(abi) },
340
+ {
341
+ cta: {
342
+ commands: [
343
+ {
344
+ command: "contract source",
345
+ args: { address },
346
+ description: "Get verified source code"
347
+ }
348
+ ]
349
+ }
350
+ }
351
+ );
352
+ }
353
+ });
354
+ contractCli.command("source", {
355
+ description: "Get verified source code for a contract",
356
+ args: z2.object({
357
+ address: z2.string().describe("Contract address")
358
+ }),
359
+ options: z2.object({
360
+ chain: chainOption2
361
+ }),
362
+ async run(c) {
363
+ const { apiKey } = apiKeyAuth2("ETHERSCAN_API_KEY");
364
+ const chainId = resolveChainId(c.options.chain);
365
+ const address = checksumAddress2(c.args.address);
366
+ const client = createEtherscanClient(apiKey);
367
+ const results = await withRateLimit3(
368
+ () => client.call({
369
+ chainid: chainId,
370
+ module: "contract",
371
+ action: "getsourcecode",
372
+ address
373
+ }),
374
+ rateLimiter2
375
+ );
376
+ const result = results[0];
377
+ if (!result) {
378
+ return c.error({ code: "NOT_FOUND", message: "No source code found for this contract" });
379
+ }
380
+ return c.ok(
381
+ {
382
+ address,
383
+ chain: c.options.chain,
384
+ name: result.ContractName,
385
+ compiler: result.CompilerVersion,
386
+ optimized: result.OptimizationUsed === "1",
387
+ runs: result.Runs,
388
+ license: result.LicenseType,
389
+ proxy: result.Proxy !== "0",
390
+ implementation: result.Implementation || void 0,
391
+ sourceCode: result.SourceCode,
392
+ constructorArguments: result.ConstructorArguments
393
+ },
394
+ {
395
+ cta: {
396
+ commands: [
397
+ {
398
+ command: "contract abi",
399
+ args: { address },
400
+ description: "Get the ABI"
401
+ }
402
+ ]
403
+ }
404
+ }
405
+ );
406
+ }
407
+ });
408
+ contractCli.command("creation", {
409
+ description: "Get the creation transaction for a contract",
410
+ args: z2.object({
411
+ address: z2.string().describe("Contract address")
412
+ }),
413
+ options: z2.object({
414
+ chain: chainOption2
415
+ }),
416
+ async run(c) {
417
+ const { apiKey } = apiKeyAuth2("ETHERSCAN_API_KEY");
418
+ const chainId = resolveChainId(c.options.chain);
419
+ const address = checksumAddress2(c.args.address);
420
+ const client = createEtherscanClient(apiKey);
421
+ const results = await withRateLimit3(
422
+ () => client.call({
423
+ chainid: chainId,
424
+ module: "contract",
425
+ action: "getcontractcreation",
426
+ contractaddresses: address
427
+ }),
428
+ rateLimiter2
429
+ );
430
+ const result = results[0];
431
+ if (!result) {
432
+ return c.error({ code: "NOT_FOUND", message: "Contract creation info not found" });
433
+ }
434
+ return c.ok(
435
+ {
436
+ address: checksumAddress2(result.contractAddress),
437
+ creator: checksumAddress2(result.contractCreator),
438
+ txHash: result.txHash,
439
+ chain: c.options.chain
440
+ },
441
+ {
442
+ cta: {
443
+ commands: [
444
+ {
445
+ command: "tx info",
446
+ args: { txhash: result.txHash },
447
+ description: "Get the creation transaction details"
448
+ }
449
+ ]
450
+ }
451
+ }
452
+ );
453
+ }
454
+ });
455
+
456
+ // src/commands/gas.ts
457
+ import { apiKeyAuth as apiKeyAuth3, createRateLimiter as createRateLimiter4, withRateLimit as withRateLimit4 } from "@spectratools/cli-shared";
458
+ import { Cli as Cli3, z as z3 } from "incur";
459
+ var rateLimiter3 = createRateLimiter4({ requestsPerSecond: 5 });
460
+ var chainOption3 = z3.string().default(DEFAULT_CHAIN).describe("Chain name (abstract, ethereum, base, arbitrum, ...)");
461
+ var gasCli = Cli3.create("gas", {
462
+ description: "Query gas oracle and estimate gas costs"
463
+ });
464
+ gasCli.command("oracle", {
465
+ description: "Get current gas price recommendations",
466
+ options: z3.object({
467
+ chain: chainOption3
468
+ }),
469
+ async run(c) {
470
+ const { apiKey } = apiKeyAuth3("ETHERSCAN_API_KEY");
471
+ const chainId = resolveChainId(c.options.chain);
472
+ const client = createEtherscanClient(apiKey);
473
+ const oracle = await withRateLimit4(
474
+ () => client.call({
475
+ chainid: chainId,
476
+ module: "gastracker",
477
+ action: "gasoracle"
478
+ }),
479
+ rateLimiter3
480
+ );
481
+ return c.ok(
482
+ {
483
+ chain: c.options.chain,
484
+ lastBlock: oracle.LastBlock,
485
+ slow: `${oracle.SafeGasPrice} Gwei`,
486
+ standard: `${oracle.ProposeGasPrice} Gwei`,
487
+ fast: `${oracle.FastGasPrice} Gwei`,
488
+ baseFee: `${oracle.suggestBaseFee} Gwei`,
489
+ gasUsedRatio: oracle.gasUsedRatio
490
+ },
491
+ {
492
+ cta: {
493
+ commands: [
494
+ {
495
+ command: "gas estimate",
496
+ options: { gasprice: oracle.ProposeGasPrice },
497
+ description: "Estimate cost at standard gas price"
498
+ }
499
+ ]
500
+ }
501
+ }
502
+ );
503
+ }
504
+ });
505
+ gasCli.command("estimate", {
506
+ description: "Estimate gas cost in wei for a given gas price",
507
+ options: z3.object({
508
+ gasprice: z3.string().describe("Gas price in wei"),
509
+ chain: chainOption3
510
+ }),
511
+ async run(c) {
512
+ const { apiKey } = apiKeyAuth3("ETHERSCAN_API_KEY");
513
+ const chainId = resolveChainId(c.options.chain);
514
+ const client = createEtherscanClient(apiKey);
515
+ const estimate = await withRateLimit4(
516
+ () => client.call({
517
+ chainid: chainId,
518
+ module: "gastracker",
519
+ action: "gasestimate",
520
+ gasprice: c.options.gasprice
521
+ }),
522
+ rateLimiter3
523
+ );
524
+ return c.ok(
525
+ {
526
+ chain: c.options.chain,
527
+ gasprice: c.options.gasprice,
528
+ estimatedSeconds: estimate
529
+ },
530
+ {
531
+ cta: {
532
+ commands: [
533
+ {
534
+ command: "gas oracle",
535
+ description: "See current gas price recommendations"
536
+ }
537
+ ]
538
+ }
539
+ }
540
+ );
541
+ }
542
+ });
543
+
544
+ // src/commands/stats.ts
545
+ import { apiKeyAuth as apiKeyAuth4, createRateLimiter as createRateLimiter5, withRateLimit as withRateLimit5 } from "@spectratools/cli-shared";
546
+ import { Cli as Cli4, z as z4 } from "incur";
547
+ var rateLimiter4 = createRateLimiter5({ requestsPerSecond: 5 });
548
+ var chainOption4 = z4.string().default(DEFAULT_CHAIN).describe("Chain name (abstract, ethereum, base, arbitrum, ...)");
549
+ var statsCli = Cli4.create("stats", {
550
+ description: "Query ETH price and supply statistics"
551
+ });
552
+ statsCli.command("ethprice", {
553
+ description: "Get the latest ETH price in USD and BTC",
554
+ options: z4.object({
555
+ chain: chainOption4
556
+ }),
557
+ async run(c) {
558
+ const { apiKey } = apiKeyAuth4("ETHERSCAN_API_KEY");
559
+ const chainId = resolveChainId(c.options.chain);
560
+ const client = createEtherscanClient(apiKey);
561
+ const price = await withRateLimit5(
562
+ () => client.call({
563
+ chainid: chainId,
564
+ module: "stats",
565
+ action: "ethprice"
566
+ }),
567
+ rateLimiter4
568
+ );
569
+ return c.ok(
570
+ {
571
+ chain: c.options.chain,
572
+ usd: price.ethusd,
573
+ btc: price.ethbtc,
574
+ usdTimestamp: new Date(Number(price.ethusd_timestamp) * 1e3).toISOString(),
575
+ btcTimestamp: new Date(Number(price.ethbtc_timestamp) * 1e3).toISOString()
576
+ },
577
+ {
578
+ cta: {
579
+ commands: [
580
+ {
581
+ command: "stats ethsupply",
582
+ description: "Get total ETH supply"
583
+ }
584
+ ]
585
+ }
586
+ }
587
+ );
588
+ }
589
+ });
590
+ statsCli.command("ethsupply", {
591
+ description: "Get the total supply of ETH",
592
+ options: z4.object({
593
+ chain: chainOption4
594
+ }),
595
+ async run(c) {
596
+ const { apiKey } = apiKeyAuth4("ETHERSCAN_API_KEY");
597
+ const chainId = resolveChainId(c.options.chain);
598
+ const client = createEtherscanClient(apiKey);
599
+ const supply = await withRateLimit5(
600
+ () => client.call({
601
+ chainid: chainId,
602
+ module: "stats",
603
+ action: "ethsupply"
604
+ }),
605
+ rateLimiter4
606
+ );
607
+ return c.ok(
608
+ {
609
+ chain: c.options.chain,
610
+ totalSupplyWei: supply
611
+ },
612
+ {
613
+ cta: {
614
+ commands: [
615
+ {
616
+ command: "stats ethprice",
617
+ description: "Get current ETH price"
618
+ }
619
+ ]
620
+ }
621
+ }
622
+ );
623
+ }
624
+ });
625
+
626
+ // src/commands/token.ts
627
+ import {
628
+ apiKeyAuth as apiKeyAuth5,
629
+ checksumAddress as checksumAddress3,
630
+ createRateLimiter as createRateLimiter6,
631
+ withRateLimit as withRateLimit6
632
+ } from "@spectratools/cli-shared";
633
+ import { Cli as Cli5, z as z5 } from "incur";
634
+ var rateLimiter5 = createRateLimiter6({ requestsPerSecond: 5 });
635
+ var chainOption5 = z5.string().default(DEFAULT_CHAIN).describe("Chain name (abstract, ethereum, base, arbitrum, ...)");
636
+ var tokenCli = Cli5.create("token", {
637
+ description: "Query token info, holders, and supply"
638
+ });
639
+ tokenCli.command("info", {
640
+ description: "Get information about a token contract",
641
+ args: z5.object({
642
+ contractaddress: z5.string().describe("Token contract address")
643
+ }),
644
+ options: z5.object({
645
+ chain: chainOption5
646
+ }),
647
+ async run(c) {
648
+ const { apiKey } = apiKeyAuth5("ETHERSCAN_API_KEY");
649
+ const chainId = resolveChainId(c.options.chain);
650
+ const address = checksumAddress3(c.args.contractaddress);
651
+ const client = createEtherscanClient(apiKey);
652
+ const results = await withRateLimit6(
653
+ () => client.call({
654
+ chainid: chainId,
655
+ module: "token",
656
+ action: "tokeninfo",
657
+ contractaddress: address
658
+ }),
659
+ rateLimiter5
660
+ );
661
+ const info = results[0];
662
+ if (!info) {
663
+ return c.error({ code: "NOT_FOUND", message: "Token info not found" });
664
+ }
665
+ return c.ok(
666
+ {
667
+ address,
668
+ chain: c.options.chain,
669
+ name: info.tokenName,
670
+ symbol: info.symbol,
671
+ type: info.tokenType,
672
+ totalSupply: info.totalSupply,
673
+ decimals: info.divisor,
674
+ priceUsd: info.tokenPriceUSD || void 0,
675
+ website: info.website || void 0,
676
+ description: info.description || void 0
677
+ },
678
+ {
679
+ cta: {
680
+ commands: [
681
+ {
682
+ command: "token supply",
683
+ args: { contractaddress: address },
684
+ description: "Get circulating supply"
685
+ },
686
+ {
687
+ command: "token holders",
688
+ args: { contractaddress: address },
689
+ description: "List top holders"
690
+ }
691
+ ]
692
+ }
693
+ }
694
+ );
695
+ }
696
+ });
697
+ tokenCli.command("holders", {
698
+ description: "List top token holders",
699
+ args: z5.object({
700
+ contractaddress: z5.string().describe("Token contract address")
701
+ }),
702
+ options: z5.object({
703
+ page: z5.number().optional().default(1).describe("Page number"),
704
+ offset: z5.number().optional().default(10).describe("Results per page"),
705
+ chain: chainOption5
706
+ }),
707
+ async run(c) {
708
+ const { apiKey } = apiKeyAuth5("ETHERSCAN_API_KEY");
709
+ const chainId = resolveChainId(c.options.chain);
710
+ const address = checksumAddress3(c.args.contractaddress);
711
+ const client = createEtherscanClient(apiKey);
712
+ const holders = await withRateLimit6(
713
+ () => client.call({
714
+ chainid: chainId,
715
+ module: "token",
716
+ action: "tokenholderlist",
717
+ contractaddress: address,
718
+ page: c.options.page,
719
+ offset: c.options.offset
720
+ }),
721
+ rateLimiter5
722
+ );
723
+ const formatted = holders.map((h, i) => ({
724
+ rank: (c.options.page - 1) * c.options.offset + i + 1,
725
+ address: checksumAddress3(h.TokenHolderAddress),
726
+ quantity: h.TokenHolderQuantity
727
+ }));
728
+ return c.ok(
729
+ {
730
+ contractAddress: address,
731
+ chain: c.options.chain,
732
+ count: formatted.length,
733
+ holders: formatted
734
+ },
735
+ {
736
+ cta: {
737
+ commands: [
738
+ {
739
+ command: "token info",
740
+ args: { contractaddress: address },
741
+ description: "Get token details"
742
+ }
743
+ ]
744
+ }
745
+ }
746
+ );
747
+ }
748
+ });
749
+ tokenCli.command("supply", {
750
+ description: "Get the total supply of a token",
751
+ args: z5.object({
752
+ contractaddress: z5.string().describe("Token contract address")
753
+ }),
754
+ options: z5.object({
755
+ chain: chainOption5
756
+ }),
757
+ async run(c) {
758
+ const { apiKey } = apiKeyAuth5("ETHERSCAN_API_KEY");
759
+ const chainId = resolveChainId(c.options.chain);
760
+ const address = checksumAddress3(c.args.contractaddress);
761
+ const client = createEtherscanClient(apiKey);
762
+ const supply = await withRateLimit6(
763
+ () => client.call({
764
+ chainid: chainId,
765
+ module: "stats",
766
+ action: "tokensupply",
767
+ contractaddress: address
768
+ }),
769
+ rateLimiter5
770
+ );
771
+ return c.ok(
772
+ { contractAddress: address, chain: c.options.chain, totalSupply: supply },
773
+ {
774
+ cta: {
775
+ commands: [
776
+ {
777
+ command: "token info",
778
+ args: { contractaddress: address },
779
+ description: "Get full token info"
780
+ }
781
+ ]
782
+ }
783
+ }
784
+ );
785
+ }
786
+ });
787
+
788
+ // src/commands/tx.ts
789
+ import { apiKeyAuth as apiKeyAuth6, createRateLimiter as createRateLimiter7, withRateLimit as withRateLimit7 } from "@spectratools/cli-shared";
790
+ import { Cli as Cli6, z as z6 } from "incur";
791
+ var rateLimiter6 = createRateLimiter7({ requestsPerSecond: 5 });
792
+ var chainOption6 = z6.string().default(DEFAULT_CHAIN).describe("Chain name (abstract, ethereum, base, arbitrum, ...)");
793
+ var txCli = Cli6.create("tx", {
794
+ description: "Query transaction details, receipts, and status"
795
+ });
796
+ txCli.command("info", {
797
+ description: "Get transaction details by hash",
798
+ args: z6.object({
799
+ txhash: z6.string().describe("Transaction hash")
800
+ }),
801
+ options: z6.object({
802
+ chain: chainOption6
803
+ }),
804
+ async run(c) {
805
+ const { apiKey } = apiKeyAuth6("ETHERSCAN_API_KEY");
806
+ const chainId = resolveChainId(c.options.chain);
807
+ const client = createEtherscanClient(apiKey);
808
+ const tx = await withRateLimit7(
809
+ () => client.callProxy({
810
+ chainid: chainId,
811
+ module: "proxy",
812
+ action: "eth_getTransactionByHash",
813
+ txhash: c.args.txhash
814
+ }),
815
+ rateLimiter6
816
+ );
817
+ return c.ok(
818
+ {
819
+ hash: tx.hash,
820
+ from: tx.from,
821
+ to: tx.to,
822
+ value: tx.value,
823
+ gas: tx.gas,
824
+ gasPrice: tx.gasPrice,
825
+ nonce: tx.nonce,
826
+ block: tx.blockNumber,
827
+ chain: c.options.chain
828
+ },
829
+ {
830
+ cta: {
831
+ commands: [
832
+ {
833
+ command: "tx receipt",
834
+ args: { txhash: c.args.txhash },
835
+ description: "Get the transaction receipt"
836
+ },
837
+ {
838
+ command: "tx status",
839
+ args: { txhash: c.args.txhash },
840
+ description: "Check execution status"
841
+ }
842
+ ]
843
+ }
844
+ }
845
+ );
846
+ }
847
+ });
848
+ txCli.command("receipt", {
849
+ description: "Get the receipt for a transaction",
850
+ args: z6.object({
851
+ txhash: z6.string().describe("Transaction hash")
852
+ }),
853
+ options: z6.object({
854
+ chain: chainOption6
855
+ }),
856
+ async run(c) {
857
+ const { apiKey } = apiKeyAuth6("ETHERSCAN_API_KEY");
858
+ const chainId = resolveChainId(c.options.chain);
859
+ const client = createEtherscanClient(apiKey);
860
+ const receipt = await withRateLimit7(
861
+ () => client.callProxy({
862
+ chainid: chainId,
863
+ module: "proxy",
864
+ action: "eth_getTransactionReceipt",
865
+ txhash: c.args.txhash
866
+ }),
867
+ rateLimiter6
868
+ );
869
+ return c.ok(
870
+ {
871
+ hash: receipt.transactionHash,
872
+ block: receipt.blockNumber,
873
+ from: receipt.from,
874
+ to: receipt.to,
875
+ status: receipt.status === "0x1" ? "success" : "failed",
876
+ gasUsed: receipt.gasUsed,
877
+ contractAddress: receipt.contractAddress,
878
+ logCount: receipt.logs.length,
879
+ chain: c.options.chain
880
+ },
881
+ {
882
+ cta: {
883
+ commands: [
884
+ {
885
+ command: "tx info",
886
+ args: { txhash: c.args.txhash },
887
+ description: "Get full transaction details"
888
+ }
889
+ ]
890
+ }
891
+ }
892
+ );
893
+ }
894
+ });
895
+ txCli.command("status", {
896
+ description: "Check whether a transaction succeeded or failed",
897
+ args: z6.object({
898
+ txhash: z6.string().describe("Transaction hash")
899
+ }),
900
+ options: z6.object({
901
+ chain: chainOption6
902
+ }),
903
+ async run(c) {
904
+ const { apiKey } = apiKeyAuth6("ETHERSCAN_API_KEY");
905
+ const chainId = resolveChainId(c.options.chain);
906
+ const client = createEtherscanClient(apiKey);
907
+ const result = await withRateLimit7(
908
+ () => client.call({
909
+ chainid: chainId,
910
+ module: "transaction",
911
+ action: "gettxreceiptstatus",
912
+ txhash: c.args.txhash
913
+ }),
914
+ rateLimiter6
915
+ );
916
+ return c.ok(
917
+ {
918
+ hash: c.args.txhash,
919
+ status: result.status === "1" ? "success" : "failed",
920
+ error: result.errDescription || void 0,
921
+ chain: c.options.chain
922
+ },
923
+ {
924
+ cta: {
925
+ commands: [
926
+ {
927
+ command: "tx receipt",
928
+ args: { txhash: c.args.txhash },
929
+ description: "Get the full receipt"
930
+ }
931
+ ]
932
+ }
933
+ }
934
+ );
935
+ }
936
+ });
937
+
938
+ // src/cli.ts
939
+ var cli = Cli7.create("etherscan", {
940
+ description: "Query Etherscan API data from the command line."
941
+ });
942
+ cli.command(accountCli);
943
+ cli.command(contractCli);
944
+ cli.command(txCli);
945
+ cli.command(tokenCli);
946
+ cli.command(gasCli);
947
+ cli.command(statsCli);
948
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
949
+ cli.serve();
950
+ }
951
+ export {
952
+ cli
953
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@spectratools/etherscan-cli",
3
+ "version": "0.1.0",
4
+ "description": "Etherscan API CLI for spectra-the-bot",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "spectra-the-bot",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "bin": {
12
+ "etherscan-cli": "./dist/cli.js"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "dependencies": {
21
+ "incur": "^0.2.2",
22
+ "@spectratools/cli-shared": "0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "5.7.3",
26
+ "vitest": "2.1.8"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "main": "./dist/cli.js",
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "typecheck": "tsc --noEmit -p tsconfig.json",
36
+ "test": "vitest run"
37
+ }
38
+ }