@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.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/cli.js +953 -0
- 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
|
+
}
|