@spectratools/etherscan-cli 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1987 -0
- package/package.json +2 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,1987 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync, realpathSync } from "fs";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { Cli as Cli8 } from "incur";
|
|
8
|
+
|
|
9
|
+
// src/commands/account.ts
|
|
10
|
+
import {
|
|
11
|
+
checksumAddress,
|
|
12
|
+
createRateLimiter as createRateLimiter2,
|
|
13
|
+
formatTimestamp,
|
|
14
|
+
isAddress,
|
|
15
|
+
weiToEth,
|
|
16
|
+
withRateLimit as withRateLimit2
|
|
17
|
+
} from "@spectratools/cli-shared";
|
|
18
|
+
import { Cli, z as z3 } from "incur";
|
|
19
|
+
|
|
20
|
+
// src/api.ts
|
|
21
|
+
import {
|
|
22
|
+
createHttpClient,
|
|
23
|
+
createRateLimiter,
|
|
24
|
+
withRateLimit,
|
|
25
|
+
withRetry
|
|
26
|
+
} from "@spectratools/cli-shared";
|
|
27
|
+
import { z } from "incur";
|
|
28
|
+
var DEFAULT_BASE_URL = "https://api.etherscan.io/v2/api";
|
|
29
|
+
var RETRY_OPTIONS = { maxRetries: 3, baseMs: 500, maxMs: 1e4 };
|
|
30
|
+
var etherscanResponseSchema = z.object({
|
|
31
|
+
status: z.string(),
|
|
32
|
+
message: z.string(),
|
|
33
|
+
result: z.unknown()
|
|
34
|
+
});
|
|
35
|
+
var proxyResponseSchema = z.object({
|
|
36
|
+
jsonrpc: z.string(),
|
|
37
|
+
id: z.union([z.number(), z.string()]),
|
|
38
|
+
result: z.unknown()
|
|
39
|
+
});
|
|
40
|
+
var EtherscanError = class extends Error {
|
|
41
|
+
constructor(message, details) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.details = details;
|
|
44
|
+
this.name = "EtherscanError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var EtherscanValidationError = class extends EtherscanError {
|
|
48
|
+
constructor(debug) {
|
|
49
|
+
super("Etherscan response validation failed", {
|
|
50
|
+
code: "INVALID_API_RESPONSE",
|
|
51
|
+
mode: debug.mode,
|
|
52
|
+
params: debug.params,
|
|
53
|
+
issues: debug.issues,
|
|
54
|
+
response: debug.response
|
|
55
|
+
});
|
|
56
|
+
this.debug = debug;
|
|
57
|
+
this.name = "EtherscanValidationError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function parseWithSchema(schema, data, mode, params) {
|
|
61
|
+
const parsed = schema.safeParse(data);
|
|
62
|
+
if (parsed.success) return parsed.data;
|
|
63
|
+
throw new EtherscanValidationError({
|
|
64
|
+
mode,
|
|
65
|
+
params,
|
|
66
|
+
issues: parsed.error.issues,
|
|
67
|
+
response: data
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function createEtherscanClient(apiKey, baseUrl = DEFAULT_BASE_URL) {
|
|
71
|
+
const http = createHttpClient({ baseUrl });
|
|
72
|
+
const acquire = createRateLimiter({ requestsPerSecond: 5 });
|
|
73
|
+
function request(params) {
|
|
74
|
+
return withRetry(
|
|
75
|
+
() => withRateLimit(
|
|
76
|
+
() => http.request("", {
|
|
77
|
+
query: { ...params, apikey: apiKey }
|
|
78
|
+
}),
|
|
79
|
+
acquire
|
|
80
|
+
),
|
|
81
|
+
RETRY_OPTIONS
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
async function call(params, resultSchema) {
|
|
85
|
+
const rawResponse = await request(params);
|
|
86
|
+
const response = parseWithSchema(etherscanResponseSchema, rawResponse, "rest", params);
|
|
87
|
+
if (response.status === "0") {
|
|
88
|
+
const msg = typeof response.result === "string" ? response.result : response.message;
|
|
89
|
+
throw new EtherscanError(msg, {
|
|
90
|
+
code: "ETHERSCAN_API_ERROR",
|
|
91
|
+
params,
|
|
92
|
+
response
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return parseWithSchema(resultSchema, response.result, "rest", params);
|
|
96
|
+
}
|
|
97
|
+
async function callProxy(params, resultSchema) {
|
|
98
|
+
const rawResponse = await request(params);
|
|
99
|
+
const response = parseWithSchema(proxyResponseSchema, rawResponse, "proxy", params);
|
|
100
|
+
return parseWithSchema(resultSchema, response.result, "proxy", params);
|
|
101
|
+
}
|
|
102
|
+
return { call, callProxy };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/auth.ts
|
|
106
|
+
import { z as z2 } from "incur";
|
|
107
|
+
var etherscanEnv = z2.object({
|
|
108
|
+
ETHERSCAN_API_KEY: z2.string().describe("Etherscan V2 API key")
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// src/chains.ts
|
|
112
|
+
var CHAIN_IDS = {
|
|
113
|
+
abstract: 2741,
|
|
114
|
+
ethereum: 1,
|
|
115
|
+
mainnet: 1,
|
|
116
|
+
base: 8453,
|
|
117
|
+
arbitrum: 42161,
|
|
118
|
+
optimism: 10,
|
|
119
|
+
polygon: 137,
|
|
120
|
+
avalanche: 43114,
|
|
121
|
+
bsc: 56,
|
|
122
|
+
linea: 59144,
|
|
123
|
+
scroll: 534352,
|
|
124
|
+
zksync: 324,
|
|
125
|
+
mantle: 5e3,
|
|
126
|
+
blast: 81457,
|
|
127
|
+
mode: 34443,
|
|
128
|
+
sepolia: 11155111,
|
|
129
|
+
goerli: 5
|
|
130
|
+
};
|
|
131
|
+
var DEFAULT_CHAIN = "abstract";
|
|
132
|
+
function resolveChainId(chain) {
|
|
133
|
+
const id = CHAIN_IDS[chain.toLowerCase()];
|
|
134
|
+
if (id === void 0) {
|
|
135
|
+
const known = Object.keys(CHAIN_IDS).join(", ");
|
|
136
|
+
throw new Error(`Unknown chain "${chain}". Supported chains: ${known}`);
|
|
137
|
+
}
|
|
138
|
+
return id;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/commands/account.ts
|
|
142
|
+
var rateLimiter = createRateLimiter2({ requestsPerSecond: 5 });
|
|
143
|
+
var chainOption = z3.string().default(DEFAULT_CHAIN).describe(
|
|
144
|
+
"Chain name (default: abstract). Options: ethereum, base, arbitrum, optimism, polygon, ..."
|
|
145
|
+
);
|
|
146
|
+
var txListItemSchema = z3.object({
|
|
147
|
+
hash: z3.string(),
|
|
148
|
+
from: z3.string(),
|
|
149
|
+
to: z3.string(),
|
|
150
|
+
value: z3.string(),
|
|
151
|
+
timeStamp: z3.string(),
|
|
152
|
+
blockNumber: z3.string(),
|
|
153
|
+
isError: z3.string(),
|
|
154
|
+
gasUsed: z3.string()
|
|
155
|
+
});
|
|
156
|
+
var internalTxItemSchema = z3.object({
|
|
157
|
+
hash: z3.string(),
|
|
158
|
+
from: z3.string(),
|
|
159
|
+
to: z3.string().nullable().optional(),
|
|
160
|
+
value: z3.string(),
|
|
161
|
+
timeStamp: z3.string(),
|
|
162
|
+
blockNumber: z3.string(),
|
|
163
|
+
type: z3.string().optional(),
|
|
164
|
+
traceId: z3.string().optional(),
|
|
165
|
+
isError: z3.string().optional(),
|
|
166
|
+
gasUsed: z3.string().optional()
|
|
167
|
+
});
|
|
168
|
+
var tokenTxItemSchema = z3.object({
|
|
169
|
+
hash: z3.string(),
|
|
170
|
+
from: z3.string(),
|
|
171
|
+
to: z3.string(),
|
|
172
|
+
value: z3.string(),
|
|
173
|
+
tokenName: z3.string(),
|
|
174
|
+
tokenSymbol: z3.string(),
|
|
175
|
+
tokenDecimal: z3.string(),
|
|
176
|
+
timeStamp: z3.string(),
|
|
177
|
+
contractAddress: z3.string()
|
|
178
|
+
});
|
|
179
|
+
var nftTxItemSchema = z3.object({
|
|
180
|
+
hash: z3.string(),
|
|
181
|
+
from: z3.string(),
|
|
182
|
+
to: z3.string(),
|
|
183
|
+
tokenID: z3.string(),
|
|
184
|
+
tokenValue: z3.string().optional(),
|
|
185
|
+
tokenName: z3.string(),
|
|
186
|
+
tokenSymbol: z3.string(),
|
|
187
|
+
timeStamp: z3.string(),
|
|
188
|
+
contractAddress: z3.string()
|
|
189
|
+
});
|
|
190
|
+
function normalizeAddress(address) {
|
|
191
|
+
try {
|
|
192
|
+
return checksumAddress(address);
|
|
193
|
+
} catch {
|
|
194
|
+
return address;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
var accountCli = Cli.create("account", {
|
|
198
|
+
description: "Query account balances, transactions, and token transfers."
|
|
199
|
+
});
|
|
200
|
+
accountCli.command("balance", {
|
|
201
|
+
description: "Get the native-token balance of an address.",
|
|
202
|
+
args: z3.object({
|
|
203
|
+
address: z3.string().describe("Wallet address")
|
|
204
|
+
}),
|
|
205
|
+
options: z3.object({
|
|
206
|
+
chain: chainOption
|
|
207
|
+
}),
|
|
208
|
+
env: etherscanEnv,
|
|
209
|
+
output: z3.object({
|
|
210
|
+
address: z3.string(),
|
|
211
|
+
wei: z3.string(),
|
|
212
|
+
eth: z3.string(),
|
|
213
|
+
chain: z3.string()
|
|
214
|
+
}),
|
|
215
|
+
examples: [
|
|
216
|
+
{
|
|
217
|
+
args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
|
|
218
|
+
options: { chain: "abstract" },
|
|
219
|
+
description: "Get ETH balance on Abstract"
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
async run(c) {
|
|
223
|
+
if (!isAddress(c.args.address)) {
|
|
224
|
+
return c.error({
|
|
225
|
+
code: "INVALID_ADDRESS",
|
|
226
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
230
|
+
const chainId = resolveChainId(c.options.chain);
|
|
231
|
+
const address = normalizeAddress(c.args.address);
|
|
232
|
+
const client = createEtherscanClient(apiKey);
|
|
233
|
+
const wei = await withRateLimit2(
|
|
234
|
+
() => client.call(
|
|
235
|
+
{
|
|
236
|
+
chainid: chainId,
|
|
237
|
+
module: "account",
|
|
238
|
+
action: "balance",
|
|
239
|
+
address,
|
|
240
|
+
tag: "latest"
|
|
241
|
+
},
|
|
242
|
+
z3.string()
|
|
243
|
+
),
|
|
244
|
+
rateLimiter
|
|
245
|
+
);
|
|
246
|
+
return c.ok(
|
|
247
|
+
{ address, wei, eth: weiToEth(wei), chain: c.options.chain },
|
|
248
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
249
|
+
cta: {
|
|
250
|
+
commands: [
|
|
251
|
+
{
|
|
252
|
+
command: "account txlist",
|
|
253
|
+
args: { address },
|
|
254
|
+
description: "List transactions"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
command: "account tokentx",
|
|
258
|
+
args: { address },
|
|
259
|
+
description: "List token transfers"
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
accountCli.command("txlist", {
|
|
268
|
+
description: "List normal transactions for an address.",
|
|
269
|
+
args: z3.object({
|
|
270
|
+
address: z3.string().describe("Wallet address")
|
|
271
|
+
}),
|
|
272
|
+
options: z3.object({
|
|
273
|
+
startblock: z3.number().optional().default(0).describe("Start block number"),
|
|
274
|
+
endblock: z3.string().optional().default("latest").describe("End block number"),
|
|
275
|
+
page: z3.number().optional().default(1).describe("Page number"),
|
|
276
|
+
offset: z3.number().optional().default(10).describe("Number of results per page"),
|
|
277
|
+
sort: z3.string().optional().default("asc").describe("Sort order (asc or desc)"),
|
|
278
|
+
chain: chainOption
|
|
279
|
+
}),
|
|
280
|
+
env: etherscanEnv,
|
|
281
|
+
output: z3.object({
|
|
282
|
+
address: z3.string(),
|
|
283
|
+
chain: z3.string(),
|
|
284
|
+
count: z3.number(),
|
|
285
|
+
transactions: z3.array(
|
|
286
|
+
z3.object({
|
|
287
|
+
hash: z3.string(),
|
|
288
|
+
from: z3.string(),
|
|
289
|
+
to: z3.string(),
|
|
290
|
+
value: z3.string(),
|
|
291
|
+
eth: z3.string(),
|
|
292
|
+
timestamp: z3.string(),
|
|
293
|
+
block: z3.string(),
|
|
294
|
+
status: z3.string(),
|
|
295
|
+
gasUsed: z3.string()
|
|
296
|
+
})
|
|
297
|
+
)
|
|
298
|
+
}),
|
|
299
|
+
examples: [
|
|
300
|
+
{
|
|
301
|
+
args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
|
|
302
|
+
options: { chain: "ethereum", sort: "desc", offset: 5 },
|
|
303
|
+
description: "List most recent transactions for an address"
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
async run(c) {
|
|
307
|
+
if (!isAddress(c.args.address)) {
|
|
308
|
+
return c.error({
|
|
309
|
+
code: "INVALID_ADDRESS",
|
|
310
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
314
|
+
const chainId = resolveChainId(c.options.chain);
|
|
315
|
+
const address = normalizeAddress(c.args.address);
|
|
316
|
+
const client = createEtherscanClient(apiKey);
|
|
317
|
+
const txs = await withRateLimit2(
|
|
318
|
+
() => client.call(
|
|
319
|
+
{
|
|
320
|
+
chainid: chainId,
|
|
321
|
+
module: "account",
|
|
322
|
+
action: "txlist",
|
|
323
|
+
address,
|
|
324
|
+
startblock: c.options.startblock,
|
|
325
|
+
endblock: c.options.endblock,
|
|
326
|
+
page: c.options.page,
|
|
327
|
+
offset: c.options.offset,
|
|
328
|
+
sort: c.options.sort
|
|
329
|
+
},
|
|
330
|
+
z3.array(txListItemSchema)
|
|
331
|
+
),
|
|
332
|
+
rateLimiter
|
|
333
|
+
);
|
|
334
|
+
const formatted = txs.map((tx) => ({
|
|
335
|
+
hash: tx.hash,
|
|
336
|
+
from: normalizeAddress(tx.from),
|
|
337
|
+
to: tx.to ? normalizeAddress(tx.to) : "",
|
|
338
|
+
value: tx.value,
|
|
339
|
+
eth: weiToEth(tx.value),
|
|
340
|
+
timestamp: formatTimestamp(Number(tx.timeStamp)),
|
|
341
|
+
block: tx.blockNumber,
|
|
342
|
+
status: tx.isError === "0" ? "success" : "failed",
|
|
343
|
+
gasUsed: tx.gasUsed
|
|
344
|
+
}));
|
|
345
|
+
const firstHash = formatted[0]?.hash;
|
|
346
|
+
return c.ok(
|
|
347
|
+
{ address, chain: c.options.chain, count: formatted.length, transactions: formatted },
|
|
348
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
349
|
+
cta: {
|
|
350
|
+
commands: firstHash ? [
|
|
351
|
+
{
|
|
352
|
+
command: "tx info",
|
|
353
|
+
args: { txhash: firstHash },
|
|
354
|
+
description: "Get details for the first transaction"
|
|
355
|
+
}
|
|
356
|
+
] : []
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
accountCli.command("internaltx", {
|
|
363
|
+
description: "List internal transactions for an address.",
|
|
364
|
+
args: z3.object({
|
|
365
|
+
address: z3.string().describe("Wallet address")
|
|
366
|
+
}),
|
|
367
|
+
options: z3.object({
|
|
368
|
+
startblock: z3.number().optional().default(0).describe("Start block number"),
|
|
369
|
+
endblock: z3.string().optional().default("latest").describe("End block number"),
|
|
370
|
+
page: z3.number().optional().default(1).describe("Page number"),
|
|
371
|
+
offset: z3.number().optional().default(10).describe("Number of results per page"),
|
|
372
|
+
sort: z3.string().optional().default("asc").describe("Sort order (asc or desc)"),
|
|
373
|
+
chain: chainOption
|
|
374
|
+
}),
|
|
375
|
+
env: etherscanEnv,
|
|
376
|
+
output: z3.object({
|
|
377
|
+
address: z3.string(),
|
|
378
|
+
chain: z3.string(),
|
|
379
|
+
count: z3.number(),
|
|
380
|
+
transactions: z3.array(
|
|
381
|
+
z3.object({
|
|
382
|
+
hash: z3.string(),
|
|
383
|
+
from: z3.string(),
|
|
384
|
+
to: z3.string(),
|
|
385
|
+
value: z3.string(),
|
|
386
|
+
eth: z3.string(),
|
|
387
|
+
timestamp: z3.string(),
|
|
388
|
+
block: z3.string(),
|
|
389
|
+
type: z3.string().optional(),
|
|
390
|
+
traceId: z3.string().optional(),
|
|
391
|
+
status: z3.string(),
|
|
392
|
+
gasUsed: z3.string()
|
|
393
|
+
})
|
|
394
|
+
)
|
|
395
|
+
}),
|
|
396
|
+
examples: [
|
|
397
|
+
{
|
|
398
|
+
args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
|
|
399
|
+
options: { chain: "ethereum", sort: "desc", offset: 5 },
|
|
400
|
+
description: "List recent internal transactions for an address"
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
async run(c) {
|
|
404
|
+
if (!isAddress(c.args.address)) {
|
|
405
|
+
return c.error({
|
|
406
|
+
code: "INVALID_ADDRESS",
|
|
407
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
411
|
+
const chainId = resolveChainId(c.options.chain);
|
|
412
|
+
const address = normalizeAddress(c.args.address);
|
|
413
|
+
const client = createEtherscanClient(apiKey);
|
|
414
|
+
const txs = await withRateLimit2(
|
|
415
|
+
() => client.call(
|
|
416
|
+
{
|
|
417
|
+
chainid: chainId,
|
|
418
|
+
module: "account",
|
|
419
|
+
action: "txlistinternal",
|
|
420
|
+
address,
|
|
421
|
+
startblock: c.options.startblock,
|
|
422
|
+
endblock: c.options.endblock,
|
|
423
|
+
page: c.options.page,
|
|
424
|
+
offset: c.options.offset,
|
|
425
|
+
sort: c.options.sort
|
|
426
|
+
},
|
|
427
|
+
z3.array(internalTxItemSchema)
|
|
428
|
+
),
|
|
429
|
+
rateLimiter
|
|
430
|
+
);
|
|
431
|
+
const formatted = txs.map((tx) => ({
|
|
432
|
+
hash: tx.hash,
|
|
433
|
+
from: normalizeAddress(tx.from),
|
|
434
|
+
to: tx.to ? normalizeAddress(tx.to) : "",
|
|
435
|
+
value: tx.value,
|
|
436
|
+
eth: weiToEth(tx.value),
|
|
437
|
+
timestamp: formatTimestamp(Number(tx.timeStamp)),
|
|
438
|
+
block: tx.blockNumber,
|
|
439
|
+
type: tx.type,
|
|
440
|
+
traceId: tx.traceId,
|
|
441
|
+
status: tx.isError === "0" || tx.isError === void 0 ? "success" : "failed",
|
|
442
|
+
gasUsed: tx.gasUsed ?? "0"
|
|
443
|
+
}));
|
|
444
|
+
return c.ok({
|
|
445
|
+
address,
|
|
446
|
+
chain: c.options.chain,
|
|
447
|
+
count: formatted.length,
|
|
448
|
+
transactions: formatted
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
accountCli.command("tokentx", {
|
|
453
|
+
description: "List ERC-20 token transfers for an address.",
|
|
454
|
+
args: z3.object({
|
|
455
|
+
address: z3.string().describe("Wallet address")
|
|
456
|
+
}),
|
|
457
|
+
options: z3.object({
|
|
458
|
+
contractaddress: z3.string().optional().describe("Filter by token contract address"),
|
|
459
|
+
page: z3.number().optional().default(1).describe("Page number"),
|
|
460
|
+
offset: z3.number().optional().default(20).describe("Results per page"),
|
|
461
|
+
chain: chainOption
|
|
462
|
+
}),
|
|
463
|
+
env: etherscanEnv,
|
|
464
|
+
output: z3.object({
|
|
465
|
+
address: z3.string(),
|
|
466
|
+
chain: z3.string(),
|
|
467
|
+
count: z3.number(),
|
|
468
|
+
transfers: z3.array(
|
|
469
|
+
z3.object({
|
|
470
|
+
hash: z3.string(),
|
|
471
|
+
from: z3.string(),
|
|
472
|
+
to: z3.string(),
|
|
473
|
+
value: z3.string(),
|
|
474
|
+
token: z3.string(),
|
|
475
|
+
tokenName: z3.string(),
|
|
476
|
+
decimals: z3.string(),
|
|
477
|
+
timestamp: z3.string(),
|
|
478
|
+
contract: z3.string()
|
|
479
|
+
})
|
|
480
|
+
)
|
|
481
|
+
}),
|
|
482
|
+
examples: [
|
|
483
|
+
{
|
|
484
|
+
args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
|
|
485
|
+
options: { chain: "base", offset: 10 },
|
|
486
|
+
description: "List recent ERC-20 transfers for an address"
|
|
487
|
+
}
|
|
488
|
+
],
|
|
489
|
+
async run(c) {
|
|
490
|
+
if (!isAddress(c.args.address)) {
|
|
491
|
+
return c.error({
|
|
492
|
+
code: "INVALID_ADDRESS",
|
|
493
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (c.options.contractaddress && !isAddress(c.options.contractaddress)) {
|
|
497
|
+
return c.error({
|
|
498
|
+
code: "INVALID_ADDRESS",
|
|
499
|
+
message: `Invalid contract address: "${c.options.contractaddress}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
503
|
+
const chainId = resolveChainId(c.options.chain);
|
|
504
|
+
const address = normalizeAddress(c.args.address);
|
|
505
|
+
const contract = c.options.contractaddress ? normalizeAddress(c.options.contractaddress) : void 0;
|
|
506
|
+
const client = createEtherscanClient(apiKey);
|
|
507
|
+
const transfers = await withRateLimit2(
|
|
508
|
+
() => client.call(
|
|
509
|
+
{
|
|
510
|
+
chainid: chainId,
|
|
511
|
+
module: "account",
|
|
512
|
+
action: "tokentx",
|
|
513
|
+
address,
|
|
514
|
+
contractaddress: contract,
|
|
515
|
+
page: c.options.page,
|
|
516
|
+
offset: c.options.offset
|
|
517
|
+
},
|
|
518
|
+
z3.array(tokenTxItemSchema)
|
|
519
|
+
),
|
|
520
|
+
rateLimiter
|
|
521
|
+
);
|
|
522
|
+
const formatted = transfers.map((tx) => ({
|
|
523
|
+
hash: tx.hash,
|
|
524
|
+
from: normalizeAddress(tx.from),
|
|
525
|
+
to: normalizeAddress(tx.to),
|
|
526
|
+
value: tx.value,
|
|
527
|
+
token: tx.tokenSymbol,
|
|
528
|
+
tokenName: tx.tokenName,
|
|
529
|
+
decimals: tx.tokenDecimal,
|
|
530
|
+
timestamp: formatTimestamp(Number(tx.timeStamp)),
|
|
531
|
+
contract: normalizeAddress(tx.contractAddress)
|
|
532
|
+
}));
|
|
533
|
+
return c.ok(
|
|
534
|
+
{ address, chain: c.options.chain, count: formatted.length, transfers: formatted },
|
|
535
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
536
|
+
cta: {
|
|
537
|
+
commands: [
|
|
538
|
+
{
|
|
539
|
+
command: "account balance",
|
|
540
|
+
args: { address },
|
|
541
|
+
description: "Check ETH balance"
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
accountCli.command("nfttx", {
|
|
550
|
+
description: "List ERC-721 NFT transfers for an address.",
|
|
551
|
+
args: z3.object({
|
|
552
|
+
address: z3.string().describe("Wallet address")
|
|
553
|
+
}),
|
|
554
|
+
options: z3.object({
|
|
555
|
+
contractaddress: z3.string().optional().describe("Filter by NFT contract address"),
|
|
556
|
+
startblock: z3.number().optional().default(0).describe("Start block number"),
|
|
557
|
+
endblock: z3.string().optional().default("latest").describe("End block number"),
|
|
558
|
+
page: z3.number().optional().default(1).describe("Page number"),
|
|
559
|
+
offset: z3.number().optional().default(20).describe("Results per page"),
|
|
560
|
+
sort: z3.string().optional().default("asc").describe("Sort order (asc or desc)"),
|
|
561
|
+
chain: chainOption
|
|
562
|
+
}),
|
|
563
|
+
env: etherscanEnv,
|
|
564
|
+
output: z3.object({
|
|
565
|
+
address: z3.string(),
|
|
566
|
+
chain: z3.string(),
|
|
567
|
+
count: z3.number(),
|
|
568
|
+
transfers: z3.array(
|
|
569
|
+
z3.object({
|
|
570
|
+
hash: z3.string(),
|
|
571
|
+
from: z3.string(),
|
|
572
|
+
to: z3.string(),
|
|
573
|
+
tokenId: z3.string(),
|
|
574
|
+
tokenName: z3.string(),
|
|
575
|
+
tokenSymbol: z3.string(),
|
|
576
|
+
timestamp: z3.string(),
|
|
577
|
+
contract: z3.string()
|
|
578
|
+
})
|
|
579
|
+
)
|
|
580
|
+
}),
|
|
581
|
+
examples: [
|
|
582
|
+
{
|
|
583
|
+
args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
|
|
584
|
+
options: { chain: "ethereum", offset: 10, sort: "desc" },
|
|
585
|
+
description: "List recent ERC-721 transfers for an address"
|
|
586
|
+
}
|
|
587
|
+
],
|
|
588
|
+
async run(c) {
|
|
589
|
+
if (!isAddress(c.args.address)) {
|
|
590
|
+
return c.error({
|
|
591
|
+
code: "INVALID_ADDRESS",
|
|
592
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
if (c.options.contractaddress && !isAddress(c.options.contractaddress)) {
|
|
596
|
+
return c.error({
|
|
597
|
+
code: "INVALID_ADDRESS",
|
|
598
|
+
message: `Invalid contract address: "${c.options.contractaddress}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
602
|
+
const chainId = resolveChainId(c.options.chain);
|
|
603
|
+
const address = normalizeAddress(c.args.address);
|
|
604
|
+
const contract = c.options.contractaddress ? normalizeAddress(c.options.contractaddress) : void 0;
|
|
605
|
+
const client = createEtherscanClient(apiKey);
|
|
606
|
+
const transfers = await withRateLimit2(
|
|
607
|
+
() => client.call(
|
|
608
|
+
{
|
|
609
|
+
chainid: chainId,
|
|
610
|
+
module: "account",
|
|
611
|
+
action: "tokennfttx",
|
|
612
|
+
address,
|
|
613
|
+
contractaddress: contract,
|
|
614
|
+
startblock: c.options.startblock,
|
|
615
|
+
endblock: c.options.endblock,
|
|
616
|
+
page: c.options.page,
|
|
617
|
+
offset: c.options.offset,
|
|
618
|
+
sort: c.options.sort
|
|
619
|
+
},
|
|
620
|
+
z3.array(nftTxItemSchema)
|
|
621
|
+
),
|
|
622
|
+
rateLimiter
|
|
623
|
+
);
|
|
624
|
+
const formatted = transfers.map((tx) => ({
|
|
625
|
+
hash: tx.hash,
|
|
626
|
+
from: normalizeAddress(tx.from),
|
|
627
|
+
to: normalizeAddress(tx.to),
|
|
628
|
+
tokenId: tx.tokenID,
|
|
629
|
+
tokenName: tx.tokenName,
|
|
630
|
+
tokenSymbol: tx.tokenSymbol,
|
|
631
|
+
timestamp: formatTimestamp(Number(tx.timeStamp)),
|
|
632
|
+
contract: normalizeAddress(tx.contractAddress)
|
|
633
|
+
}));
|
|
634
|
+
return c.ok({
|
|
635
|
+
address,
|
|
636
|
+
chain: c.options.chain,
|
|
637
|
+
count: formatted.length,
|
|
638
|
+
transfers: formatted
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
accountCli.command("erc1155tx", {
|
|
643
|
+
description: "List ERC-1155 token transfers for an address.",
|
|
644
|
+
args: z3.object({
|
|
645
|
+
address: z3.string().describe("Wallet address")
|
|
646
|
+
}),
|
|
647
|
+
options: z3.object({
|
|
648
|
+
contractaddress: z3.string().optional().describe("Filter by ERC-1155 contract address"),
|
|
649
|
+
startblock: z3.number().optional().default(0).describe("Start block number"),
|
|
650
|
+
endblock: z3.string().optional().default("latest").describe("End block number"),
|
|
651
|
+
page: z3.number().optional().default(1).describe("Page number"),
|
|
652
|
+
offset: z3.number().optional().default(20).describe("Results per page"),
|
|
653
|
+
sort: z3.string().optional().default("asc").describe("Sort order (asc or desc)"),
|
|
654
|
+
chain: chainOption
|
|
655
|
+
}),
|
|
656
|
+
env: etherscanEnv,
|
|
657
|
+
output: z3.object({
|
|
658
|
+
address: z3.string(),
|
|
659
|
+
chain: z3.string(),
|
|
660
|
+
count: z3.number(),
|
|
661
|
+
transfers: z3.array(
|
|
662
|
+
z3.object({
|
|
663
|
+
hash: z3.string(),
|
|
664
|
+
from: z3.string(),
|
|
665
|
+
to: z3.string(),
|
|
666
|
+
tokenId: z3.string(),
|
|
667
|
+
amount: z3.string(),
|
|
668
|
+
tokenName: z3.string(),
|
|
669
|
+
tokenSymbol: z3.string(),
|
|
670
|
+
timestamp: z3.string(),
|
|
671
|
+
contract: z3.string()
|
|
672
|
+
})
|
|
673
|
+
)
|
|
674
|
+
}),
|
|
675
|
+
examples: [
|
|
676
|
+
{
|
|
677
|
+
args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
|
|
678
|
+
options: { chain: "ethereum", offset: 10, sort: "desc" },
|
|
679
|
+
description: "List recent ERC-1155 transfers for an address"
|
|
680
|
+
}
|
|
681
|
+
],
|
|
682
|
+
async run(c) {
|
|
683
|
+
if (!isAddress(c.args.address)) {
|
|
684
|
+
return c.error({
|
|
685
|
+
code: "INVALID_ADDRESS",
|
|
686
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (c.options.contractaddress && !isAddress(c.options.contractaddress)) {
|
|
690
|
+
return c.error({
|
|
691
|
+
code: "INVALID_ADDRESS",
|
|
692
|
+
message: `Invalid contract address: "${c.options.contractaddress}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
696
|
+
const chainId = resolveChainId(c.options.chain);
|
|
697
|
+
const address = normalizeAddress(c.args.address);
|
|
698
|
+
const contract = c.options.contractaddress ? normalizeAddress(c.options.contractaddress) : void 0;
|
|
699
|
+
const client = createEtherscanClient(apiKey);
|
|
700
|
+
const transfers = await withRateLimit2(
|
|
701
|
+
() => client.call(
|
|
702
|
+
{
|
|
703
|
+
chainid: chainId,
|
|
704
|
+
module: "account",
|
|
705
|
+
action: "token1155tx",
|
|
706
|
+
address,
|
|
707
|
+
contractaddress: contract,
|
|
708
|
+
startblock: c.options.startblock,
|
|
709
|
+
endblock: c.options.endblock,
|
|
710
|
+
page: c.options.page,
|
|
711
|
+
offset: c.options.offset,
|
|
712
|
+
sort: c.options.sort
|
|
713
|
+
},
|
|
714
|
+
z3.array(nftTxItemSchema)
|
|
715
|
+
),
|
|
716
|
+
rateLimiter
|
|
717
|
+
);
|
|
718
|
+
const formatted = transfers.map((tx) => ({
|
|
719
|
+
hash: tx.hash,
|
|
720
|
+
from: normalizeAddress(tx.from),
|
|
721
|
+
to: normalizeAddress(tx.to),
|
|
722
|
+
tokenId: tx.tokenID,
|
|
723
|
+
amount: tx.tokenValue ?? "0",
|
|
724
|
+
tokenName: tx.tokenName,
|
|
725
|
+
tokenSymbol: tx.tokenSymbol,
|
|
726
|
+
timestamp: formatTimestamp(Number(tx.timeStamp)),
|
|
727
|
+
contract: normalizeAddress(tx.contractAddress)
|
|
728
|
+
}));
|
|
729
|
+
return c.ok({
|
|
730
|
+
address,
|
|
731
|
+
chain: c.options.chain,
|
|
732
|
+
count: formatted.length,
|
|
733
|
+
transfers: formatted
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
accountCli.command("tokenbalance", {
|
|
738
|
+
description: "Get ERC-20 token balance for an address.",
|
|
739
|
+
args: z3.object({
|
|
740
|
+
address: z3.string().describe("Wallet address")
|
|
741
|
+
}),
|
|
742
|
+
options: z3.object({
|
|
743
|
+
contractaddress: z3.string().describe("Token contract address"),
|
|
744
|
+
chain: chainOption
|
|
745
|
+
}),
|
|
746
|
+
env: etherscanEnv,
|
|
747
|
+
output: z3.object({
|
|
748
|
+
address: z3.string(),
|
|
749
|
+
contract: z3.string(),
|
|
750
|
+
balance: z3.string(),
|
|
751
|
+
chain: z3.string()
|
|
752
|
+
}),
|
|
753
|
+
examples: [
|
|
754
|
+
{
|
|
755
|
+
args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
|
|
756
|
+
options: { contractaddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", chain: "ethereum" },
|
|
757
|
+
description: "Get token balance for a wallet + token pair"
|
|
758
|
+
}
|
|
759
|
+
],
|
|
760
|
+
async run(c) {
|
|
761
|
+
if (!isAddress(c.args.address)) {
|
|
762
|
+
return c.error({
|
|
763
|
+
code: "INVALID_ADDRESS",
|
|
764
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (!isAddress(c.options.contractaddress)) {
|
|
768
|
+
return c.error({
|
|
769
|
+
code: "INVALID_ADDRESS",
|
|
770
|
+
message: `Invalid contract address: "${c.options.contractaddress}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
774
|
+
const chainId = resolveChainId(c.options.chain);
|
|
775
|
+
const address = normalizeAddress(c.args.address);
|
|
776
|
+
const contract = normalizeAddress(c.options.contractaddress);
|
|
777
|
+
const client = createEtherscanClient(apiKey);
|
|
778
|
+
const balance = await withRateLimit2(
|
|
779
|
+
() => client.call(
|
|
780
|
+
{
|
|
781
|
+
chainid: chainId,
|
|
782
|
+
module: "account",
|
|
783
|
+
action: "tokenbalance",
|
|
784
|
+
address,
|
|
785
|
+
contractaddress: contract,
|
|
786
|
+
tag: "latest"
|
|
787
|
+
},
|
|
788
|
+
z3.string()
|
|
789
|
+
),
|
|
790
|
+
rateLimiter
|
|
791
|
+
);
|
|
792
|
+
return c.ok(
|
|
793
|
+
{ address, contract, balance, chain: c.options.chain },
|
|
794
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
795
|
+
cta: {
|
|
796
|
+
commands: [
|
|
797
|
+
{
|
|
798
|
+
command: "token info",
|
|
799
|
+
args: { contractaddress: contract },
|
|
800
|
+
description: "Get token info"
|
|
801
|
+
}
|
|
802
|
+
]
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// src/commands/contract.ts
|
|
810
|
+
import {
|
|
811
|
+
checksumAddress as checksumAddress2,
|
|
812
|
+
createRateLimiter as createRateLimiter3,
|
|
813
|
+
isAddress as isAddress2,
|
|
814
|
+
withRateLimit as withRateLimit3
|
|
815
|
+
} from "@spectratools/cli-shared";
|
|
816
|
+
import { Cli as Cli2, z as z4 } from "incur";
|
|
817
|
+
var rateLimiter2 = createRateLimiter3({ requestsPerSecond: 5 });
|
|
818
|
+
var chainOption2 = z4.string().default(DEFAULT_CHAIN).describe(
|
|
819
|
+
"Chain name (default: abstract). Options: ethereum, base, arbitrum, optimism, polygon, ..."
|
|
820
|
+
);
|
|
821
|
+
var contractCli = Cli2.create("contract", {
|
|
822
|
+
description: "Query contract ABI, source code, and deployment metadata."
|
|
823
|
+
});
|
|
824
|
+
var sourceResultSchema = z4.object({
|
|
825
|
+
SourceCode: z4.string(),
|
|
826
|
+
ABI: z4.string(),
|
|
827
|
+
ContractName: z4.string(),
|
|
828
|
+
CompilerVersion: z4.string(),
|
|
829
|
+
OptimizationUsed: z4.string(),
|
|
830
|
+
Runs: z4.string(),
|
|
831
|
+
ConstructorArguments: z4.string(),
|
|
832
|
+
LicenseType: z4.string(),
|
|
833
|
+
Proxy: z4.string(),
|
|
834
|
+
Implementation: z4.string()
|
|
835
|
+
});
|
|
836
|
+
var creationResultSchema = z4.object({
|
|
837
|
+
contractAddress: z4.string(),
|
|
838
|
+
contractCreator: z4.string(),
|
|
839
|
+
txHash: z4.string()
|
|
840
|
+
});
|
|
841
|
+
contractCli.command("abi", {
|
|
842
|
+
description: "Get the ABI for a verified contract.",
|
|
843
|
+
args: z4.object({
|
|
844
|
+
address: z4.string().describe("Contract address")
|
|
845
|
+
}),
|
|
846
|
+
options: z4.object({
|
|
847
|
+
chain: chainOption2
|
|
848
|
+
}),
|
|
849
|
+
env: etherscanEnv,
|
|
850
|
+
output: z4.object({
|
|
851
|
+
address: z4.string(),
|
|
852
|
+
chain: z4.string(),
|
|
853
|
+
abi: z4.array(z4.unknown())
|
|
854
|
+
}),
|
|
855
|
+
examples: [
|
|
856
|
+
{
|
|
857
|
+
args: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
|
|
858
|
+
options: { chain: "ethereum" },
|
|
859
|
+
description: "Fetch ABI for a verified ERC-20 contract"
|
|
860
|
+
}
|
|
861
|
+
],
|
|
862
|
+
async run(c) {
|
|
863
|
+
if (!isAddress2(c.args.address)) {
|
|
864
|
+
return c.error({
|
|
865
|
+
code: "INVALID_ADDRESS",
|
|
866
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
870
|
+
const chainId = resolveChainId(c.options.chain);
|
|
871
|
+
const address = checksumAddress2(c.args.address);
|
|
872
|
+
const client = createEtherscanClient(apiKey);
|
|
873
|
+
const abi = await withRateLimit3(
|
|
874
|
+
() => client.call(
|
|
875
|
+
{
|
|
876
|
+
chainid: chainId,
|
|
877
|
+
module: "contract",
|
|
878
|
+
action: "getabi",
|
|
879
|
+
address
|
|
880
|
+
},
|
|
881
|
+
z4.string()
|
|
882
|
+
),
|
|
883
|
+
rateLimiter2
|
|
884
|
+
);
|
|
885
|
+
return c.ok(
|
|
886
|
+
{ address, chain: c.options.chain, abi: JSON.parse(abi) },
|
|
887
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
888
|
+
cta: {
|
|
889
|
+
commands: [
|
|
890
|
+
{
|
|
891
|
+
command: "contract source",
|
|
892
|
+
args: { address },
|
|
893
|
+
description: "Get verified source code"
|
|
894
|
+
}
|
|
895
|
+
]
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
contractCli.command("source", {
|
|
902
|
+
description: "Get verified source code for a contract.",
|
|
903
|
+
args: z4.object({
|
|
904
|
+
address: z4.string().describe("Contract address")
|
|
905
|
+
}),
|
|
906
|
+
options: z4.object({
|
|
907
|
+
chain: chainOption2
|
|
908
|
+
}),
|
|
909
|
+
env: etherscanEnv,
|
|
910
|
+
output: z4.object({
|
|
911
|
+
address: z4.string(),
|
|
912
|
+
chain: z4.string(),
|
|
913
|
+
name: z4.string(),
|
|
914
|
+
compiler: z4.string(),
|
|
915
|
+
optimized: z4.boolean(),
|
|
916
|
+
runs: z4.string(),
|
|
917
|
+
license: z4.string(),
|
|
918
|
+
proxy: z4.boolean(),
|
|
919
|
+
implementation: z4.string().optional(),
|
|
920
|
+
sourceCode: z4.string(),
|
|
921
|
+
constructorArguments: z4.string()
|
|
922
|
+
}),
|
|
923
|
+
examples: [
|
|
924
|
+
{
|
|
925
|
+
args: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
|
|
926
|
+
options: { chain: "ethereum" },
|
|
927
|
+
description: "Fetch verified source code metadata"
|
|
928
|
+
}
|
|
929
|
+
],
|
|
930
|
+
async run(c) {
|
|
931
|
+
if (!isAddress2(c.args.address)) {
|
|
932
|
+
return c.error({
|
|
933
|
+
code: "INVALID_ADDRESS",
|
|
934
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
938
|
+
const chainId = resolveChainId(c.options.chain);
|
|
939
|
+
const address = checksumAddress2(c.args.address);
|
|
940
|
+
const client = createEtherscanClient(apiKey);
|
|
941
|
+
const results = await withRateLimit3(
|
|
942
|
+
() => client.call(
|
|
943
|
+
{
|
|
944
|
+
chainid: chainId,
|
|
945
|
+
module: "contract",
|
|
946
|
+
action: "getsourcecode",
|
|
947
|
+
address
|
|
948
|
+
},
|
|
949
|
+
z4.array(sourceResultSchema)
|
|
950
|
+
),
|
|
951
|
+
rateLimiter2
|
|
952
|
+
);
|
|
953
|
+
const result = results[0];
|
|
954
|
+
if (!result) {
|
|
955
|
+
return c.error({ code: "NOT_FOUND", message: "No source code found for this contract" });
|
|
956
|
+
}
|
|
957
|
+
return c.ok(
|
|
958
|
+
{
|
|
959
|
+
address,
|
|
960
|
+
chain: c.options.chain,
|
|
961
|
+
name: result.ContractName,
|
|
962
|
+
compiler: result.CompilerVersion,
|
|
963
|
+
optimized: result.OptimizationUsed === "1",
|
|
964
|
+
runs: result.Runs,
|
|
965
|
+
license: result.LicenseType,
|
|
966
|
+
proxy: result.Proxy !== "0",
|
|
967
|
+
implementation: result.Implementation || void 0,
|
|
968
|
+
sourceCode: result.SourceCode,
|
|
969
|
+
constructorArguments: result.ConstructorArguments
|
|
970
|
+
},
|
|
971
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
972
|
+
cta: {
|
|
973
|
+
commands: [
|
|
974
|
+
{
|
|
975
|
+
command: "contract abi",
|
|
976
|
+
args: { address },
|
|
977
|
+
description: "Get the ABI"
|
|
978
|
+
}
|
|
979
|
+
]
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
contractCli.command("creation", {
|
|
986
|
+
description: "Get the deployment transaction and creator for a contract.",
|
|
987
|
+
args: z4.object({
|
|
988
|
+
address: z4.string().describe("Contract address")
|
|
989
|
+
}),
|
|
990
|
+
options: z4.object({
|
|
991
|
+
chain: chainOption2
|
|
992
|
+
}),
|
|
993
|
+
env: etherscanEnv,
|
|
994
|
+
output: z4.object({
|
|
995
|
+
address: z4.string(),
|
|
996
|
+
creator: z4.string(),
|
|
997
|
+
txHash: z4.string(),
|
|
998
|
+
chain: z4.string()
|
|
999
|
+
}),
|
|
1000
|
+
examples: [
|
|
1001
|
+
{
|
|
1002
|
+
args: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
|
|
1003
|
+
options: { chain: "ethereum" },
|
|
1004
|
+
description: "Find deployment tx for a contract"
|
|
1005
|
+
}
|
|
1006
|
+
],
|
|
1007
|
+
async run(c) {
|
|
1008
|
+
if (!isAddress2(c.args.address)) {
|
|
1009
|
+
return c.error({
|
|
1010
|
+
code: "INVALID_ADDRESS",
|
|
1011
|
+
message: `Invalid address: "${c.args.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1015
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1016
|
+
const address = checksumAddress2(c.args.address);
|
|
1017
|
+
const client = createEtherscanClient(apiKey);
|
|
1018
|
+
const results = await withRateLimit3(
|
|
1019
|
+
() => client.call(
|
|
1020
|
+
{
|
|
1021
|
+
chainid: chainId,
|
|
1022
|
+
module: "contract",
|
|
1023
|
+
action: "getcontractcreation",
|
|
1024
|
+
contractaddresses: address
|
|
1025
|
+
},
|
|
1026
|
+
z4.array(creationResultSchema)
|
|
1027
|
+
),
|
|
1028
|
+
rateLimiter2
|
|
1029
|
+
);
|
|
1030
|
+
const result = results[0];
|
|
1031
|
+
if (!result) {
|
|
1032
|
+
return c.error({ code: "NOT_FOUND", message: "Contract creation info not found" });
|
|
1033
|
+
}
|
|
1034
|
+
return c.ok(
|
|
1035
|
+
{
|
|
1036
|
+
address: checksumAddress2(result.contractAddress),
|
|
1037
|
+
creator: checksumAddress2(result.contractCreator),
|
|
1038
|
+
txHash: result.txHash,
|
|
1039
|
+
chain: c.options.chain
|
|
1040
|
+
},
|
|
1041
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1042
|
+
cta: {
|
|
1043
|
+
commands: [
|
|
1044
|
+
{
|
|
1045
|
+
command: "tx info",
|
|
1046
|
+
args: { txhash: result.txHash },
|
|
1047
|
+
description: "Get the creation transaction details"
|
|
1048
|
+
}
|
|
1049
|
+
]
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// src/commands/gas.ts
|
|
1057
|
+
import { createRateLimiter as createRateLimiter4, withRateLimit as withRateLimit4 } from "@spectratools/cli-shared";
|
|
1058
|
+
import { Cli as Cli3, z as z5 } from "incur";
|
|
1059
|
+
var rateLimiter3 = createRateLimiter4({ requestsPerSecond: 5 });
|
|
1060
|
+
var chainOption3 = z5.string().default(DEFAULT_CHAIN).describe(
|
|
1061
|
+
"Chain name (default: abstract). Options: ethereum, base, arbitrum, optimism, polygon, ..."
|
|
1062
|
+
);
|
|
1063
|
+
var gasCli = Cli3.create("gas", {
|
|
1064
|
+
description: "Query gas oracle data and estimate confirmation latency."
|
|
1065
|
+
});
|
|
1066
|
+
var gasOracleSchema = z5.object({
|
|
1067
|
+
LastBlock: z5.string(),
|
|
1068
|
+
SafeGasPrice: z5.string(),
|
|
1069
|
+
ProposeGasPrice: z5.string(),
|
|
1070
|
+
FastGasPrice: z5.string(),
|
|
1071
|
+
suggestBaseFee: z5.string(),
|
|
1072
|
+
gasUsedRatio: z5.string()
|
|
1073
|
+
});
|
|
1074
|
+
gasCli.command("oracle", {
|
|
1075
|
+
description: "Get current gas price recommendations.",
|
|
1076
|
+
options: z5.object({
|
|
1077
|
+
chain: chainOption3
|
|
1078
|
+
}),
|
|
1079
|
+
env: etherscanEnv,
|
|
1080
|
+
output: z5.object({
|
|
1081
|
+
chain: z5.string(),
|
|
1082
|
+
lastBlock: z5.string(),
|
|
1083
|
+
slow: z5.string(),
|
|
1084
|
+
standard: z5.string(),
|
|
1085
|
+
fast: z5.string(),
|
|
1086
|
+
baseFee: z5.string(),
|
|
1087
|
+
gasUsedRatio: z5.string()
|
|
1088
|
+
}),
|
|
1089
|
+
examples: [{ options: { chain: "abstract" }, description: "Get gas oracle on Abstract" }],
|
|
1090
|
+
async run(c) {
|
|
1091
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1092
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1093
|
+
const client = createEtherscanClient(apiKey);
|
|
1094
|
+
const oracle = await withRateLimit4(
|
|
1095
|
+
() => client.call(
|
|
1096
|
+
{
|
|
1097
|
+
chainid: chainId,
|
|
1098
|
+
module: "gastracker",
|
|
1099
|
+
action: "gasoracle"
|
|
1100
|
+
},
|
|
1101
|
+
gasOracleSchema
|
|
1102
|
+
),
|
|
1103
|
+
rateLimiter3
|
|
1104
|
+
);
|
|
1105
|
+
return c.ok(
|
|
1106
|
+
{
|
|
1107
|
+
chain: c.options.chain,
|
|
1108
|
+
lastBlock: oracle.LastBlock,
|
|
1109
|
+
slow: `${oracle.SafeGasPrice} Gwei`,
|
|
1110
|
+
standard: `${oracle.ProposeGasPrice} Gwei`,
|
|
1111
|
+
fast: `${oracle.FastGasPrice} Gwei`,
|
|
1112
|
+
baseFee: `${oracle.suggestBaseFee} Gwei`,
|
|
1113
|
+
gasUsedRatio: oracle.gasUsedRatio
|
|
1114
|
+
},
|
|
1115
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1116
|
+
cta: {
|
|
1117
|
+
commands: [
|
|
1118
|
+
{
|
|
1119
|
+
command: "gas estimate",
|
|
1120
|
+
options: { gasprice: oracle.ProposeGasPrice },
|
|
1121
|
+
description: "Estimate cost at standard gas price"
|
|
1122
|
+
}
|
|
1123
|
+
]
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
gasCli.command("estimate", {
|
|
1130
|
+
description: "Estimate confirmation time in seconds for a gas price (wei).",
|
|
1131
|
+
options: z5.object({
|
|
1132
|
+
gasprice: z5.string().describe("Gas price in wei"),
|
|
1133
|
+
chain: chainOption3
|
|
1134
|
+
}),
|
|
1135
|
+
env: etherscanEnv,
|
|
1136
|
+
output: z5.object({
|
|
1137
|
+
chain: z5.string(),
|
|
1138
|
+
gasprice: z5.string(),
|
|
1139
|
+
estimatedSeconds: z5.string()
|
|
1140
|
+
}),
|
|
1141
|
+
examples: [
|
|
1142
|
+
{
|
|
1143
|
+
options: { gasprice: "1000000000", chain: "ethereum" },
|
|
1144
|
+
description: "Estimate confirmation time at 1 gwei"
|
|
1145
|
+
}
|
|
1146
|
+
],
|
|
1147
|
+
async run(c) {
|
|
1148
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1149
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1150
|
+
const client = createEtherscanClient(apiKey);
|
|
1151
|
+
const estimate = await withRateLimit4(
|
|
1152
|
+
() => client.call(
|
|
1153
|
+
{
|
|
1154
|
+
chainid: chainId,
|
|
1155
|
+
module: "gastracker",
|
|
1156
|
+
action: "gasestimate",
|
|
1157
|
+
gasprice: c.options.gasprice
|
|
1158
|
+
},
|
|
1159
|
+
z5.string()
|
|
1160
|
+
),
|
|
1161
|
+
rateLimiter3
|
|
1162
|
+
);
|
|
1163
|
+
return c.ok(
|
|
1164
|
+
{
|
|
1165
|
+
chain: c.options.chain,
|
|
1166
|
+
gasprice: c.options.gasprice,
|
|
1167
|
+
estimatedSeconds: estimate
|
|
1168
|
+
},
|
|
1169
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1170
|
+
cta: {
|
|
1171
|
+
commands: [
|
|
1172
|
+
{
|
|
1173
|
+
command: "gas oracle",
|
|
1174
|
+
description: "See current gas price recommendations"
|
|
1175
|
+
}
|
|
1176
|
+
]
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// src/commands/logs.ts
|
|
1184
|
+
import {
|
|
1185
|
+
checksumAddress as checksumAddress3,
|
|
1186
|
+
createRateLimiter as createRateLimiter5,
|
|
1187
|
+
formatTimestamp as formatTimestamp2,
|
|
1188
|
+
isAddress as isAddress3,
|
|
1189
|
+
withRateLimit as withRateLimit5
|
|
1190
|
+
} from "@spectratools/cli-shared";
|
|
1191
|
+
import { Cli as Cli4, z as z6 } from "incur";
|
|
1192
|
+
var rateLimiter4 = createRateLimiter5({ requestsPerSecond: 5 });
|
|
1193
|
+
var chainOption4 = z6.string().default(DEFAULT_CHAIN).describe(
|
|
1194
|
+
"Chain name (default: abstract). Options: ethereum, base, arbitrum, optimism, polygon, ..."
|
|
1195
|
+
);
|
|
1196
|
+
var topicOperatorOption = z6.enum(["and", "or"]);
|
|
1197
|
+
var logsItemSchema = z6.object({
|
|
1198
|
+
address: z6.string(),
|
|
1199
|
+
topics: z6.array(z6.string()),
|
|
1200
|
+
data: z6.string(),
|
|
1201
|
+
blockNumber: z6.string(),
|
|
1202
|
+
timeStamp: z6.string(),
|
|
1203
|
+
gasPrice: z6.string().optional(),
|
|
1204
|
+
gasUsed: z6.string().optional(),
|
|
1205
|
+
logIndex: z6.string(),
|
|
1206
|
+
transactionHash: z6.string(),
|
|
1207
|
+
transactionIndex: z6.string().optional()
|
|
1208
|
+
});
|
|
1209
|
+
function normalizeAddress2(address) {
|
|
1210
|
+
try {
|
|
1211
|
+
return checksumAddress3(address);
|
|
1212
|
+
} catch {
|
|
1213
|
+
return address;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
var logsCli = Cli4.create("logs", {
|
|
1217
|
+
description: "Query event logs with topic, address, and block-range filters."
|
|
1218
|
+
});
|
|
1219
|
+
logsCli.command("get", {
|
|
1220
|
+
description: "Get event logs from Etherscan logs.getLogs.",
|
|
1221
|
+
args: z6.object({}),
|
|
1222
|
+
options: z6.object({
|
|
1223
|
+
fromblock: z6.string().optional().default("0").describe("Start block number"),
|
|
1224
|
+
toblock: z6.string().optional().default("latest").describe("End block number"),
|
|
1225
|
+
address: z6.string().optional().describe("Filter by contract address"),
|
|
1226
|
+
topic0: z6.string().optional().describe("First indexed topic"),
|
|
1227
|
+
topic1: z6.string().optional().describe("Second indexed topic"),
|
|
1228
|
+
topic2: z6.string().optional().describe("Third indexed topic"),
|
|
1229
|
+
topic3: z6.string().optional().describe("Fourth indexed topic"),
|
|
1230
|
+
topic0_1_opr: topicOperatorOption.optional().describe("Operator between topic0 and topic1"),
|
|
1231
|
+
topic1_2_opr: topicOperatorOption.optional().describe("Operator between topic1 and topic2"),
|
|
1232
|
+
topic2_3_opr: topicOperatorOption.optional().describe("Operator between topic2 and topic3"),
|
|
1233
|
+
page: z6.number().optional().default(1).describe("Page number"),
|
|
1234
|
+
offset: z6.number().optional().default(100).describe("Results per page"),
|
|
1235
|
+
chain: chainOption4
|
|
1236
|
+
}),
|
|
1237
|
+
env: etherscanEnv,
|
|
1238
|
+
output: z6.object({
|
|
1239
|
+
chain: z6.string(),
|
|
1240
|
+
fromBlock: z6.string(),
|
|
1241
|
+
toBlock: z6.string(),
|
|
1242
|
+
address: z6.string().optional(),
|
|
1243
|
+
count: z6.number(),
|
|
1244
|
+
logs: z6.array(
|
|
1245
|
+
z6.object({
|
|
1246
|
+
address: z6.string(),
|
|
1247
|
+
topics: z6.array(z6.string()),
|
|
1248
|
+
data: z6.string(),
|
|
1249
|
+
block: z6.string(),
|
|
1250
|
+
timestamp: z6.string(),
|
|
1251
|
+
transactionHash: z6.string(),
|
|
1252
|
+
logIndex: z6.string(),
|
|
1253
|
+
transactionIndex: z6.string().optional(),
|
|
1254
|
+
gasPrice: z6.string().optional(),
|
|
1255
|
+
gasUsed: z6.string().optional()
|
|
1256
|
+
})
|
|
1257
|
+
)
|
|
1258
|
+
}),
|
|
1259
|
+
examples: [
|
|
1260
|
+
{
|
|
1261
|
+
args: {},
|
|
1262
|
+
options: {
|
|
1263
|
+
chain: "ethereum",
|
|
1264
|
+
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
1265
|
+
topic0: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55aebec6f6f3c",
|
|
1266
|
+
fromblock: "20000000",
|
|
1267
|
+
toblock: "latest",
|
|
1268
|
+
offset: 25
|
|
1269
|
+
},
|
|
1270
|
+
description: "Query ERC-20 Transfer logs for USDC"
|
|
1271
|
+
}
|
|
1272
|
+
],
|
|
1273
|
+
async run(c) {
|
|
1274
|
+
if (c.options.address && !isAddress3(c.options.address)) {
|
|
1275
|
+
return c.error({
|
|
1276
|
+
code: "INVALID_ADDRESS",
|
|
1277
|
+
message: `Invalid contract address: "${c.options.address}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1281
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1282
|
+
const client = createEtherscanClient(apiKey);
|
|
1283
|
+
const logs = await withRateLimit5(
|
|
1284
|
+
() => client.call(
|
|
1285
|
+
{
|
|
1286
|
+
chainid: chainId,
|
|
1287
|
+
module: "logs",
|
|
1288
|
+
action: "getLogs",
|
|
1289
|
+
fromBlock: c.options.fromblock,
|
|
1290
|
+
toBlock: c.options.toblock,
|
|
1291
|
+
address: c.options.address ? normalizeAddress2(c.options.address) : void 0,
|
|
1292
|
+
topic0: c.options.topic0,
|
|
1293
|
+
topic1: c.options.topic1,
|
|
1294
|
+
topic2: c.options.topic2,
|
|
1295
|
+
topic3: c.options.topic3,
|
|
1296
|
+
topic0_1_opr: c.options.topic0_1_opr,
|
|
1297
|
+
topic1_2_opr: c.options.topic1_2_opr,
|
|
1298
|
+
topic2_3_opr: c.options.topic2_3_opr,
|
|
1299
|
+
page: c.options.page,
|
|
1300
|
+
offset: c.options.offset
|
|
1301
|
+
},
|
|
1302
|
+
z6.array(logsItemSchema)
|
|
1303
|
+
),
|
|
1304
|
+
rateLimiter4
|
|
1305
|
+
);
|
|
1306
|
+
const formatted = logs.map((log) => ({
|
|
1307
|
+
address: normalizeAddress2(log.address),
|
|
1308
|
+
topics: log.topics,
|
|
1309
|
+
data: log.data,
|
|
1310
|
+
block: log.blockNumber,
|
|
1311
|
+
timestamp: formatTimestamp2(Number(log.timeStamp)),
|
|
1312
|
+
transactionHash: log.transactionHash,
|
|
1313
|
+
logIndex: log.logIndex,
|
|
1314
|
+
transactionIndex: log.transactionIndex,
|
|
1315
|
+
gasPrice: log.gasPrice,
|
|
1316
|
+
gasUsed: log.gasUsed
|
|
1317
|
+
}));
|
|
1318
|
+
return c.ok({
|
|
1319
|
+
chain: c.options.chain,
|
|
1320
|
+
fromBlock: c.options.fromblock,
|
|
1321
|
+
toBlock: c.options.toblock,
|
|
1322
|
+
address: c.options.address ? normalizeAddress2(c.options.address) : void 0,
|
|
1323
|
+
count: formatted.length,
|
|
1324
|
+
logs: formatted
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
// src/commands/stats.ts
|
|
1330
|
+
import { createRateLimiter as createRateLimiter6, withRateLimit as withRateLimit6 } from "@spectratools/cli-shared";
|
|
1331
|
+
import { Cli as Cli5, z as z7 } from "incur";
|
|
1332
|
+
var rateLimiter5 = createRateLimiter6({ requestsPerSecond: 5 });
|
|
1333
|
+
var chainOption5 = z7.string().default(DEFAULT_CHAIN).describe(
|
|
1334
|
+
"Chain name (default: abstract). Options: ethereum, base, arbitrum, optimism, polygon, ..."
|
|
1335
|
+
);
|
|
1336
|
+
var statsCli = Cli5.create("stats", {
|
|
1337
|
+
description: "Query ETH price and total supply statistics."
|
|
1338
|
+
});
|
|
1339
|
+
var ethPriceSchema = z7.object({
|
|
1340
|
+
ethbtc: z7.string(),
|
|
1341
|
+
ethbtc_timestamp: z7.string(),
|
|
1342
|
+
ethusd: z7.string(),
|
|
1343
|
+
ethusd_timestamp: z7.string()
|
|
1344
|
+
});
|
|
1345
|
+
statsCli.command("ethprice", {
|
|
1346
|
+
description: "Get latest ETH price in USD and BTC.",
|
|
1347
|
+
options: z7.object({
|
|
1348
|
+
chain: chainOption5
|
|
1349
|
+
}),
|
|
1350
|
+
env: etherscanEnv,
|
|
1351
|
+
output: z7.object({
|
|
1352
|
+
chain: z7.string(),
|
|
1353
|
+
usd: z7.string(),
|
|
1354
|
+
btc: z7.string(),
|
|
1355
|
+
usdTimestamp: z7.string(),
|
|
1356
|
+
btcTimestamp: z7.string()
|
|
1357
|
+
}),
|
|
1358
|
+
examples: [{ options: { chain: "ethereum" }, description: "Get ETH spot price on Ethereum" }],
|
|
1359
|
+
async run(c) {
|
|
1360
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1361
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1362
|
+
const client = createEtherscanClient(apiKey);
|
|
1363
|
+
const price = await withRateLimit6(
|
|
1364
|
+
() => client.call(
|
|
1365
|
+
{
|
|
1366
|
+
chainid: chainId,
|
|
1367
|
+
module: "stats",
|
|
1368
|
+
action: "ethprice"
|
|
1369
|
+
},
|
|
1370
|
+
ethPriceSchema
|
|
1371
|
+
),
|
|
1372
|
+
rateLimiter5
|
|
1373
|
+
);
|
|
1374
|
+
return c.ok(
|
|
1375
|
+
{
|
|
1376
|
+
chain: c.options.chain,
|
|
1377
|
+
usd: price.ethusd,
|
|
1378
|
+
btc: price.ethbtc,
|
|
1379
|
+
usdTimestamp: new Date(Number(price.ethusd_timestamp) * 1e3).toISOString(),
|
|
1380
|
+
btcTimestamp: new Date(Number(price.ethbtc_timestamp) * 1e3).toISOString()
|
|
1381
|
+
},
|
|
1382
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1383
|
+
cta: {
|
|
1384
|
+
commands: [
|
|
1385
|
+
{
|
|
1386
|
+
command: "stats ethsupply",
|
|
1387
|
+
description: "Get total ETH supply"
|
|
1388
|
+
}
|
|
1389
|
+
]
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
statsCli.command("ethsupply", {
|
|
1396
|
+
description: "Get total ETH supply in wei.",
|
|
1397
|
+
options: z7.object({
|
|
1398
|
+
chain: chainOption5
|
|
1399
|
+
}),
|
|
1400
|
+
env: etherscanEnv,
|
|
1401
|
+
output: z7.object({
|
|
1402
|
+
chain: z7.string(),
|
|
1403
|
+
totalSupplyWei: z7.string()
|
|
1404
|
+
}),
|
|
1405
|
+
examples: [{ options: { chain: "ethereum" }, description: "Get total ETH supply" }],
|
|
1406
|
+
async run(c) {
|
|
1407
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1408
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1409
|
+
const client = createEtherscanClient(apiKey);
|
|
1410
|
+
const supply = await withRateLimit6(
|
|
1411
|
+
() => client.call(
|
|
1412
|
+
{
|
|
1413
|
+
chainid: chainId,
|
|
1414
|
+
module: "stats",
|
|
1415
|
+
action: "ethsupply"
|
|
1416
|
+
},
|
|
1417
|
+
z7.string()
|
|
1418
|
+
),
|
|
1419
|
+
rateLimiter5
|
|
1420
|
+
);
|
|
1421
|
+
return c.ok(
|
|
1422
|
+
{
|
|
1423
|
+
chain: c.options.chain,
|
|
1424
|
+
totalSupplyWei: supply
|
|
1425
|
+
},
|
|
1426
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1427
|
+
cta: {
|
|
1428
|
+
commands: [
|
|
1429
|
+
{
|
|
1430
|
+
command: "stats ethprice",
|
|
1431
|
+
description: "Get current ETH price"
|
|
1432
|
+
}
|
|
1433
|
+
]
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
// src/commands/token.ts
|
|
1441
|
+
import {
|
|
1442
|
+
checksumAddress as checksumAddress4,
|
|
1443
|
+
createRateLimiter as createRateLimiter7,
|
|
1444
|
+
isAddress as isAddress4,
|
|
1445
|
+
withRateLimit as withRateLimit7
|
|
1446
|
+
} from "@spectratools/cli-shared";
|
|
1447
|
+
import { Cli as Cli6, z as z8 } from "incur";
|
|
1448
|
+
var rateLimiter6 = createRateLimiter7({ requestsPerSecond: 5 });
|
|
1449
|
+
var chainOption6 = z8.string().default(DEFAULT_CHAIN).describe(
|
|
1450
|
+
"Chain name (default: abstract). Options: ethereum, base, arbitrum, optimism, polygon, ..."
|
|
1451
|
+
);
|
|
1452
|
+
var tokenCli = Cli6.create("token", {
|
|
1453
|
+
description: "Query token metadata, holders, and supply."
|
|
1454
|
+
});
|
|
1455
|
+
var tokenInfoSchema = z8.object({
|
|
1456
|
+
contractAddress: z8.string(),
|
|
1457
|
+
tokenName: z8.string(),
|
|
1458
|
+
symbol: z8.string(),
|
|
1459
|
+
divisor: z8.string(),
|
|
1460
|
+
tokenType: z8.string(),
|
|
1461
|
+
totalSupply: z8.string(),
|
|
1462
|
+
blueCheckmark: z8.string(),
|
|
1463
|
+
description: z8.string(),
|
|
1464
|
+
website: z8.string(),
|
|
1465
|
+
email: z8.string(),
|
|
1466
|
+
blog: z8.string(),
|
|
1467
|
+
reddit: z8.string(),
|
|
1468
|
+
slack: z8.string(),
|
|
1469
|
+
facebook: z8.string(),
|
|
1470
|
+
twitter: z8.string(),
|
|
1471
|
+
bitcointalk: z8.string(),
|
|
1472
|
+
github: z8.string(),
|
|
1473
|
+
telegram: z8.string(),
|
|
1474
|
+
wechat: z8.string(),
|
|
1475
|
+
linkedin: z8.string(),
|
|
1476
|
+
discord: z8.string(),
|
|
1477
|
+
whitepaper: z8.string(),
|
|
1478
|
+
tokenPriceUSD: z8.string()
|
|
1479
|
+
});
|
|
1480
|
+
var holderEntrySchema = z8.object({
|
|
1481
|
+
TokenHolderAddress: z8.string(),
|
|
1482
|
+
TokenHolderQuantity: z8.string()
|
|
1483
|
+
});
|
|
1484
|
+
tokenCli.command("info", {
|
|
1485
|
+
description: "Get metadata for a token contract.",
|
|
1486
|
+
args: z8.object({
|
|
1487
|
+
contractaddress: z8.string().describe("Token contract address")
|
|
1488
|
+
}),
|
|
1489
|
+
options: z8.object({
|
|
1490
|
+
chain: chainOption6
|
|
1491
|
+
}),
|
|
1492
|
+
env: etherscanEnv,
|
|
1493
|
+
output: z8.object({
|
|
1494
|
+
address: z8.string(),
|
|
1495
|
+
chain: z8.string(),
|
|
1496
|
+
name: z8.string(),
|
|
1497
|
+
symbol: z8.string(),
|
|
1498
|
+
type: z8.string(),
|
|
1499
|
+
totalSupply: z8.string(),
|
|
1500
|
+
decimals: z8.string(),
|
|
1501
|
+
priceUsd: z8.string().optional(),
|
|
1502
|
+
website: z8.string().optional(),
|
|
1503
|
+
description: z8.string().optional()
|
|
1504
|
+
}),
|
|
1505
|
+
examples: [
|
|
1506
|
+
{
|
|
1507
|
+
args: { contractaddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
|
|
1508
|
+
options: { chain: "ethereum" },
|
|
1509
|
+
description: "Get token metadata for USDC"
|
|
1510
|
+
}
|
|
1511
|
+
],
|
|
1512
|
+
async run(c) {
|
|
1513
|
+
if (!isAddress4(c.args.contractaddress)) {
|
|
1514
|
+
return c.error({
|
|
1515
|
+
code: "INVALID_ADDRESS",
|
|
1516
|
+
message: `Invalid contract address: "${c.args.contractaddress}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1520
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1521
|
+
const address = checksumAddress4(c.args.contractaddress);
|
|
1522
|
+
const client = createEtherscanClient(apiKey);
|
|
1523
|
+
const results = await withRateLimit7(
|
|
1524
|
+
() => client.call(
|
|
1525
|
+
{
|
|
1526
|
+
chainid: chainId,
|
|
1527
|
+
module: "token",
|
|
1528
|
+
action: "tokeninfo",
|
|
1529
|
+
contractaddress: address
|
|
1530
|
+
},
|
|
1531
|
+
z8.array(tokenInfoSchema)
|
|
1532
|
+
),
|
|
1533
|
+
rateLimiter6
|
|
1534
|
+
);
|
|
1535
|
+
const info = results[0];
|
|
1536
|
+
if (!info) {
|
|
1537
|
+
return c.error({ code: "NOT_FOUND", message: "Token info not found" });
|
|
1538
|
+
}
|
|
1539
|
+
return c.ok(
|
|
1540
|
+
{
|
|
1541
|
+
address,
|
|
1542
|
+
chain: c.options.chain,
|
|
1543
|
+
name: info.tokenName,
|
|
1544
|
+
symbol: info.symbol,
|
|
1545
|
+
type: info.tokenType,
|
|
1546
|
+
totalSupply: info.totalSupply,
|
|
1547
|
+
decimals: info.divisor,
|
|
1548
|
+
priceUsd: info.tokenPriceUSD || void 0,
|
|
1549
|
+
website: info.website || void 0,
|
|
1550
|
+
description: info.description || void 0
|
|
1551
|
+
},
|
|
1552
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1553
|
+
cta: {
|
|
1554
|
+
commands: [
|
|
1555
|
+
{
|
|
1556
|
+
command: "token supply",
|
|
1557
|
+
args: { contractaddress: address },
|
|
1558
|
+
description: "Get circulating supply"
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
command: "token holders",
|
|
1562
|
+
args: { contractaddress: address },
|
|
1563
|
+
description: "List top holders"
|
|
1564
|
+
}
|
|
1565
|
+
]
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
tokenCli.command("holders", {
|
|
1572
|
+
description: "List top token holders.",
|
|
1573
|
+
args: z8.object({
|
|
1574
|
+
contractaddress: z8.string().describe("Token contract address")
|
|
1575
|
+
}),
|
|
1576
|
+
options: z8.object({
|
|
1577
|
+
page: z8.number().optional().default(1).describe("Page number"),
|
|
1578
|
+
offset: z8.number().optional().default(10).describe("Results per page"),
|
|
1579
|
+
chain: chainOption6
|
|
1580
|
+
}),
|
|
1581
|
+
env: etherscanEnv,
|
|
1582
|
+
output: z8.object({
|
|
1583
|
+
contractAddress: z8.string(),
|
|
1584
|
+
chain: z8.string(),
|
|
1585
|
+
count: z8.number(),
|
|
1586
|
+
holders: z8.array(
|
|
1587
|
+
z8.object({
|
|
1588
|
+
rank: z8.number(),
|
|
1589
|
+
address: z8.string(),
|
|
1590
|
+
quantity: z8.string()
|
|
1591
|
+
})
|
|
1592
|
+
)
|
|
1593
|
+
}),
|
|
1594
|
+
examples: [
|
|
1595
|
+
{
|
|
1596
|
+
args: { contractaddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
|
|
1597
|
+
options: { page: 1, offset: 20, chain: "ethereum" },
|
|
1598
|
+
description: "List top 20 holders for a token"
|
|
1599
|
+
}
|
|
1600
|
+
],
|
|
1601
|
+
async run(c) {
|
|
1602
|
+
if (!isAddress4(c.args.contractaddress)) {
|
|
1603
|
+
return c.error({
|
|
1604
|
+
code: "INVALID_ADDRESS",
|
|
1605
|
+
message: `Invalid contract address: "${c.args.contractaddress}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1609
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1610
|
+
const address = checksumAddress4(c.args.contractaddress);
|
|
1611
|
+
const client = createEtherscanClient(apiKey);
|
|
1612
|
+
const holders = await withRateLimit7(
|
|
1613
|
+
() => client.call(
|
|
1614
|
+
{
|
|
1615
|
+
chainid: chainId,
|
|
1616
|
+
module: "token",
|
|
1617
|
+
action: "tokenholderlist",
|
|
1618
|
+
contractaddress: address,
|
|
1619
|
+
page: c.options.page,
|
|
1620
|
+
offset: c.options.offset
|
|
1621
|
+
},
|
|
1622
|
+
z8.array(holderEntrySchema)
|
|
1623
|
+
),
|
|
1624
|
+
rateLimiter6
|
|
1625
|
+
);
|
|
1626
|
+
const formatted = holders.map((h, i) => ({
|
|
1627
|
+
rank: (c.options.page - 1) * c.options.offset + i + 1,
|
|
1628
|
+
address: checksumAddress4(h.TokenHolderAddress),
|
|
1629
|
+
quantity: h.TokenHolderQuantity
|
|
1630
|
+
}));
|
|
1631
|
+
return c.ok(
|
|
1632
|
+
{
|
|
1633
|
+
contractAddress: address,
|
|
1634
|
+
chain: c.options.chain,
|
|
1635
|
+
count: formatted.length,
|
|
1636
|
+
holders: formatted
|
|
1637
|
+
},
|
|
1638
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1639
|
+
cta: {
|
|
1640
|
+
commands: [
|
|
1641
|
+
{
|
|
1642
|
+
command: "token info",
|
|
1643
|
+
args: { contractaddress: address },
|
|
1644
|
+
description: "Get token details"
|
|
1645
|
+
}
|
|
1646
|
+
]
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
tokenCli.command("supply", {
|
|
1653
|
+
description: "Get total token supply.",
|
|
1654
|
+
args: z8.object({
|
|
1655
|
+
contractaddress: z8.string().describe("Token contract address")
|
|
1656
|
+
}),
|
|
1657
|
+
options: z8.object({
|
|
1658
|
+
chain: chainOption6
|
|
1659
|
+
}),
|
|
1660
|
+
env: etherscanEnv,
|
|
1661
|
+
output: z8.object({
|
|
1662
|
+
contractAddress: z8.string(),
|
|
1663
|
+
chain: z8.string(),
|
|
1664
|
+
totalSupply: z8.string()
|
|
1665
|
+
}),
|
|
1666
|
+
examples: [
|
|
1667
|
+
{
|
|
1668
|
+
args: { contractaddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
|
|
1669
|
+
options: { chain: "ethereum" },
|
|
1670
|
+
description: "Get total supply for a token"
|
|
1671
|
+
}
|
|
1672
|
+
],
|
|
1673
|
+
async run(c) {
|
|
1674
|
+
if (!isAddress4(c.args.contractaddress)) {
|
|
1675
|
+
return c.error({
|
|
1676
|
+
code: "INVALID_ADDRESS",
|
|
1677
|
+
message: `Invalid contract address: "${c.args.contractaddress}". Use a valid 0x-prefixed 20-byte hex address.`
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1681
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1682
|
+
const address = checksumAddress4(c.args.contractaddress);
|
|
1683
|
+
const client = createEtherscanClient(apiKey);
|
|
1684
|
+
const supply = await withRateLimit7(
|
|
1685
|
+
() => client.call(
|
|
1686
|
+
{
|
|
1687
|
+
chainid: chainId,
|
|
1688
|
+
module: "stats",
|
|
1689
|
+
action: "tokensupply",
|
|
1690
|
+
contractaddress: address
|
|
1691
|
+
},
|
|
1692
|
+
z8.string()
|
|
1693
|
+
),
|
|
1694
|
+
rateLimiter6
|
|
1695
|
+
);
|
|
1696
|
+
return c.ok(
|
|
1697
|
+
{ contractAddress: address, chain: c.options.chain, totalSupply: supply },
|
|
1698
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1699
|
+
cta: {
|
|
1700
|
+
commands: [
|
|
1701
|
+
{
|
|
1702
|
+
command: "token info",
|
|
1703
|
+
args: { contractaddress: address },
|
|
1704
|
+
description: "Get full token info"
|
|
1705
|
+
}
|
|
1706
|
+
]
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
// src/commands/tx.ts
|
|
1714
|
+
import { createRateLimiter as createRateLimiter8, withRateLimit as withRateLimit8 } from "@spectratools/cli-shared";
|
|
1715
|
+
import { Cli as Cli7, z as z9 } from "incur";
|
|
1716
|
+
var rateLimiter7 = createRateLimiter8({ requestsPerSecond: 5 });
|
|
1717
|
+
function hexToDecimal(hex) {
|
|
1718
|
+
return BigInt(hex).toString();
|
|
1719
|
+
}
|
|
1720
|
+
var chainOption7 = z9.string().default(DEFAULT_CHAIN).describe(
|
|
1721
|
+
"Chain name (default: abstract). Options: ethereum, base, arbitrum, optimism, polygon, ..."
|
|
1722
|
+
);
|
|
1723
|
+
var txCli = Cli7.create("tx", {
|
|
1724
|
+
description: "Query transaction details, receipts, and execution status."
|
|
1725
|
+
});
|
|
1726
|
+
var transactionInfoSchema = z9.object({
|
|
1727
|
+
hash: z9.string(),
|
|
1728
|
+
from: z9.string(),
|
|
1729
|
+
to: z9.string().nullable(),
|
|
1730
|
+
value: z9.string(),
|
|
1731
|
+
gas: z9.string(),
|
|
1732
|
+
gasPrice: z9.string(),
|
|
1733
|
+
nonce: z9.string(),
|
|
1734
|
+
blockNumber: z9.string(),
|
|
1735
|
+
blockHash: z9.string(),
|
|
1736
|
+
input: z9.string()
|
|
1737
|
+
});
|
|
1738
|
+
var transactionReceiptSchema = z9.object({
|
|
1739
|
+
transactionHash: z9.string(),
|
|
1740
|
+
blockNumber: z9.string(),
|
|
1741
|
+
blockHash: z9.string(),
|
|
1742
|
+
from: z9.string(),
|
|
1743
|
+
to: z9.string().nullable(),
|
|
1744
|
+
status: z9.string(),
|
|
1745
|
+
gasUsed: z9.string(),
|
|
1746
|
+
cumulativeGasUsed: z9.string(),
|
|
1747
|
+
contractAddress: z9.string().nullable(),
|
|
1748
|
+
logs: z9.array(z9.unknown())
|
|
1749
|
+
});
|
|
1750
|
+
var txStatusSchema = z9.object({
|
|
1751
|
+
isError: z9.string(),
|
|
1752
|
+
errDescription: z9.string()
|
|
1753
|
+
});
|
|
1754
|
+
txCli.command("info", {
|
|
1755
|
+
description: "Get transaction details by hash.",
|
|
1756
|
+
args: z9.object({
|
|
1757
|
+
txhash: z9.string().describe("Transaction hash")
|
|
1758
|
+
}),
|
|
1759
|
+
options: z9.object({
|
|
1760
|
+
chain: chainOption7
|
|
1761
|
+
}),
|
|
1762
|
+
env: etherscanEnv,
|
|
1763
|
+
output: z9.object({
|
|
1764
|
+
hash: z9.string(),
|
|
1765
|
+
from: z9.string(),
|
|
1766
|
+
to: z9.string().nullable(),
|
|
1767
|
+
value: z9.string(),
|
|
1768
|
+
gas: z9.string(),
|
|
1769
|
+
gasPrice: z9.string(),
|
|
1770
|
+
nonce: z9.string(),
|
|
1771
|
+
block: z9.string(),
|
|
1772
|
+
chain: z9.string()
|
|
1773
|
+
}),
|
|
1774
|
+
examples: [
|
|
1775
|
+
{
|
|
1776
|
+
args: { txhash: "0x1234...abcd" },
|
|
1777
|
+
options: { chain: "abstract" },
|
|
1778
|
+
description: "Inspect one transaction on Abstract"
|
|
1779
|
+
}
|
|
1780
|
+
],
|
|
1781
|
+
async run(c) {
|
|
1782
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1783
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1784
|
+
const client = createEtherscanClient(apiKey);
|
|
1785
|
+
const tx = await withRateLimit8(
|
|
1786
|
+
() => client.callProxy(
|
|
1787
|
+
{
|
|
1788
|
+
chainid: chainId,
|
|
1789
|
+
module: "proxy",
|
|
1790
|
+
action: "eth_getTransactionByHash",
|
|
1791
|
+
txhash: c.args.txhash
|
|
1792
|
+
},
|
|
1793
|
+
transactionInfoSchema
|
|
1794
|
+
),
|
|
1795
|
+
rateLimiter7
|
|
1796
|
+
);
|
|
1797
|
+
return c.ok(
|
|
1798
|
+
{
|
|
1799
|
+
hash: tx.hash,
|
|
1800
|
+
from: tx.from,
|
|
1801
|
+
to: tx.to,
|
|
1802
|
+
value: hexToDecimal(tx.value),
|
|
1803
|
+
gas: hexToDecimal(tx.gas),
|
|
1804
|
+
gasPrice: hexToDecimal(tx.gasPrice),
|
|
1805
|
+
nonce: hexToDecimal(tx.nonce),
|
|
1806
|
+
block: hexToDecimal(tx.blockNumber),
|
|
1807
|
+
chain: c.options.chain
|
|
1808
|
+
},
|
|
1809
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1810
|
+
cta: {
|
|
1811
|
+
commands: [
|
|
1812
|
+
{
|
|
1813
|
+
command: "tx receipt",
|
|
1814
|
+
args: { txhash: c.args.txhash },
|
|
1815
|
+
description: "Get the transaction receipt"
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
command: "tx status",
|
|
1819
|
+
args: { txhash: c.args.txhash },
|
|
1820
|
+
description: "Check execution status"
|
|
1821
|
+
}
|
|
1822
|
+
]
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
txCli.command("receipt", {
|
|
1829
|
+
description: "Get the receipt for a transaction.",
|
|
1830
|
+
args: z9.object({
|
|
1831
|
+
txhash: z9.string().describe("Transaction hash")
|
|
1832
|
+
}),
|
|
1833
|
+
options: z9.object({
|
|
1834
|
+
chain: chainOption7
|
|
1835
|
+
}),
|
|
1836
|
+
env: etherscanEnv,
|
|
1837
|
+
output: z9.object({
|
|
1838
|
+
hash: z9.string(),
|
|
1839
|
+
block: z9.string(),
|
|
1840
|
+
from: z9.string(),
|
|
1841
|
+
to: z9.string().nullable(),
|
|
1842
|
+
status: z9.string(),
|
|
1843
|
+
gasUsed: z9.string(),
|
|
1844
|
+
contractAddress: z9.string().nullable(),
|
|
1845
|
+
logCount: z9.number(),
|
|
1846
|
+
chain: z9.string()
|
|
1847
|
+
}),
|
|
1848
|
+
examples: [
|
|
1849
|
+
{
|
|
1850
|
+
args: { txhash: "0x1234...abcd" },
|
|
1851
|
+
options: { chain: "ethereum" },
|
|
1852
|
+
description: "Get receipt details including status and logs"
|
|
1853
|
+
}
|
|
1854
|
+
],
|
|
1855
|
+
async run(c) {
|
|
1856
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1857
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1858
|
+
const client = createEtherscanClient(apiKey);
|
|
1859
|
+
const receipt = await withRateLimit8(
|
|
1860
|
+
() => client.callProxy(
|
|
1861
|
+
{
|
|
1862
|
+
chainid: chainId,
|
|
1863
|
+
module: "proxy",
|
|
1864
|
+
action: "eth_getTransactionReceipt",
|
|
1865
|
+
txhash: c.args.txhash
|
|
1866
|
+
},
|
|
1867
|
+
transactionReceiptSchema
|
|
1868
|
+
),
|
|
1869
|
+
rateLimiter7
|
|
1870
|
+
);
|
|
1871
|
+
return c.ok(
|
|
1872
|
+
{
|
|
1873
|
+
hash: receipt.transactionHash,
|
|
1874
|
+
block: hexToDecimal(receipt.blockNumber),
|
|
1875
|
+
from: receipt.from,
|
|
1876
|
+
to: receipt.to,
|
|
1877
|
+
status: receipt.status === "0x1" ? "success" : "failed",
|
|
1878
|
+
gasUsed: hexToDecimal(receipt.gasUsed),
|
|
1879
|
+
contractAddress: receipt.contractAddress,
|
|
1880
|
+
logCount: receipt.logs.length,
|
|
1881
|
+
chain: c.options.chain
|
|
1882
|
+
},
|
|
1883
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1884
|
+
cta: {
|
|
1885
|
+
commands: [
|
|
1886
|
+
{
|
|
1887
|
+
command: "tx info",
|
|
1888
|
+
args: { txhash: c.args.txhash },
|
|
1889
|
+
description: "Get full transaction details"
|
|
1890
|
+
}
|
|
1891
|
+
]
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
txCli.command("status", {
|
|
1898
|
+
description: "Check whether a transaction succeeded or failed.",
|
|
1899
|
+
args: z9.object({
|
|
1900
|
+
txhash: z9.string().describe("Transaction hash")
|
|
1901
|
+
}),
|
|
1902
|
+
options: z9.object({
|
|
1903
|
+
chain: chainOption7
|
|
1904
|
+
}),
|
|
1905
|
+
env: etherscanEnv,
|
|
1906
|
+
output: z9.object({
|
|
1907
|
+
hash: z9.string(),
|
|
1908
|
+
status: z9.string(),
|
|
1909
|
+
error: z9.string().optional(),
|
|
1910
|
+
chain: z9.string()
|
|
1911
|
+
}),
|
|
1912
|
+
examples: [
|
|
1913
|
+
{
|
|
1914
|
+
args: { txhash: "0x1234...abcd" },
|
|
1915
|
+
options: { chain: "base" },
|
|
1916
|
+
description: "Get pass/fail status for a transaction"
|
|
1917
|
+
}
|
|
1918
|
+
],
|
|
1919
|
+
async run(c) {
|
|
1920
|
+
const apiKey = c.env.ETHERSCAN_API_KEY;
|
|
1921
|
+
const chainId = resolveChainId(c.options.chain);
|
|
1922
|
+
const client = createEtherscanClient(apiKey);
|
|
1923
|
+
const result = await withRateLimit8(
|
|
1924
|
+
() => client.call(
|
|
1925
|
+
{
|
|
1926
|
+
chainid: chainId,
|
|
1927
|
+
module: "transaction",
|
|
1928
|
+
action: "getstatus",
|
|
1929
|
+
txhash: c.args.txhash
|
|
1930
|
+
},
|
|
1931
|
+
txStatusSchema
|
|
1932
|
+
),
|
|
1933
|
+
rateLimiter7
|
|
1934
|
+
);
|
|
1935
|
+
return c.ok(
|
|
1936
|
+
{
|
|
1937
|
+
hash: c.args.txhash,
|
|
1938
|
+
status: result.isError === "0" ? "success" : "failed",
|
|
1939
|
+
error: result.errDescription || void 0,
|
|
1940
|
+
chain: c.options.chain
|
|
1941
|
+
},
|
|
1942
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1943
|
+
cta: {
|
|
1944
|
+
commands: [
|
|
1945
|
+
{
|
|
1946
|
+
command: "tx receipt",
|
|
1947
|
+
args: { txhash: c.args.txhash },
|
|
1948
|
+
description: "Get the full receipt"
|
|
1949
|
+
}
|
|
1950
|
+
]
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
// src/cli.ts
|
|
1958
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1959
|
+
var pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"));
|
|
1960
|
+
var cli = Cli8.create("etherscan", {
|
|
1961
|
+
version: pkg.version,
|
|
1962
|
+
description: "Query Etherscan API data from the command line."
|
|
1963
|
+
});
|
|
1964
|
+
cli.command(accountCli);
|
|
1965
|
+
cli.command(contractCli);
|
|
1966
|
+
cli.command(txCli);
|
|
1967
|
+
cli.command(tokenCli);
|
|
1968
|
+
cli.command(gasCli);
|
|
1969
|
+
cli.command(statsCli);
|
|
1970
|
+
cli.command(logsCli);
|
|
1971
|
+
var isMain = (() => {
|
|
1972
|
+
const entrypoint = process.argv[1];
|
|
1973
|
+
if (!entrypoint) {
|
|
1974
|
+
return false;
|
|
1975
|
+
}
|
|
1976
|
+
try {
|
|
1977
|
+
return realpathSync(entrypoint) === realpathSync(fileURLToPath(import.meta.url));
|
|
1978
|
+
} catch {
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
})();
|
|
1982
|
+
if (isMain) {
|
|
1983
|
+
cli.serve();
|
|
1984
|
+
}
|
|
1985
|
+
export {
|
|
1986
|
+
cli
|
|
1987
|
+
};
|