evm-rpc-checker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +86 -0
  2. package/dist/index.js +519 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # evm-rpc-checker
2
+
3
+ A CLI tool for analyzing EVM-compatible blockchain RPC endpoints. Performs 10 automated tests and outputs a comprehensive quality score.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g evm-rpc-checker
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ evm-rpc-checker <rpc-url>
15
+
16
+ # or without installing
17
+ npx evm-rpc-checker https://ethereum-rpc.publicnode.com
18
+ ```
19
+
20
+ ## Example Output
21
+
22
+ ```
23
+ EVM RPC Checker
24
+ ──────────────────────────────────────────────────
25
+ Target: https://ethereum-rpc.publicnode.com
26
+
27
+ Chain: Ethereum Mainnet (ID: 1)
28
+ Block: #24,696,724
29
+
30
+ ⚡ Connectivity PASS
31
+ Chain ID: 1
32
+ Chain: Ethereum Mainnet
33
+ Latest Block: 24,696,724
34
+ Latency: 332 ms
35
+
36
+ ⏱ Latency PASS
37
+ Avg: 332 ms
38
+ Min: 304 ms
39
+ Max: 409 ms
40
+ Jitter: 105 ms
41
+
42
+ 🌐 CORS PASS
43
+ Allow-Origin: *
44
+ Browser OK: Yes
45
+
46
+ 📦 Batch Request PASS
47
+ Supported: Yes
48
+ Responses: 3/3
49
+
50
+ 🔌 WebSocket PASS
51
+ 📚 Archive Node PASS
52
+ 🔧 Methods PASS (6/7)
53
+ 🚀 Throughput PASS (3.0 req/s)
54
+ 🧩 Consistency PASS
55
+ ⛽ Gas Tracker PASS
56
+
57
+ ──────────────────────────────────────────────────
58
+ Overall Score: 88/100 ██████████████████░░
59
+ ```
60
+
61
+ ## Test Suites
62
+
63
+ | Test | Description |
64
+ |------|-------------|
65
+ | **Connectivity** | `eth_chainId`, `eth_blockNumber`, `net_version` |
66
+ | **Latency** | Avg/min/max latency and jitter over 5 requests |
67
+ | **CORS** | CORS header validation via OPTIONS preflight |
68
+ | **Batch Request** | JSON-RPC batch request support |
69
+ | **WebSocket** | HTTP-to-WS/WSS upgrade support |
70
+ | **Archive Node** | Historical state query at block #1 |
71
+ | **Methods** | 7 RPC methods including trace/debug |
72
+ | **Throughput** | Sequential request throughput (req/s) |
73
+ | **Consistency** | Block height variance across 3 queries |
74
+ | **Gas Tracker** | Gas price, EIP-1559 base fee and priority fees |
75
+
76
+ ## Requirements
77
+
78
+ - Node.js >= 18
79
+
80
+ ## Related
81
+
82
+ Desktop app with GUI available at [github.com/lolieatapple/rpc-checker](https://github.com/lolieatapple/rpc-checker)
83
+
84
+ ## License
85
+
86
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,519 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ var c = {
5
+ reset: "\x1B[0m",
6
+ bold: "\x1B[1m",
7
+ dim: "\x1B[2m",
8
+ green: "\x1B[32m",
9
+ red: "\x1B[31m",
10
+ yellow: "\x1B[33m",
11
+ blue: "\x1B[34m",
12
+ cyan: "\x1B[36m",
13
+ magenta: "\x1B[35m",
14
+ white: "\x1B[37m",
15
+ gray: "\x1B[90m",
16
+ bgGreen: "\x1B[42m",
17
+ bgRed: "\x1B[41m",
18
+ bgYellow: "\x1B[43m",
19
+ bgBlue: "\x1B[44m"
20
+ };
21
+ var KNOWN_CHAINS = {
22
+ 1: "Ethereum Mainnet",
23
+ 5: "Goerli",
24
+ 11155111: "Sepolia",
25
+ 56: "BNB Smart Chain",
26
+ 137: "Polygon",
27
+ 42161: "Arbitrum One",
28
+ 10: "Optimism",
29
+ 43114: "Avalanche C-Chain",
30
+ 250: "Fantom Opera",
31
+ 100: "Gnosis Chain",
32
+ 8453: "Base",
33
+ 324: "zkSync Era",
34
+ 59144: "Linea",
35
+ 534352: "Scroll",
36
+ 1101: "Polygon zkEVM",
37
+ 5000: "Mantle",
38
+ 81457: "Blast",
39
+ 34443: "Mode",
40
+ 169: "Manta Pacific",
41
+ 7777777: "Zora",
42
+ 2222: "Kava EVM",
43
+ 1284: "Moonbeam",
44
+ 1285: "Moonriver",
45
+ 42220: "Celo",
46
+ 25: "Cronos",
47
+ 1088: "Metis",
48
+ 128: "HECO",
49
+ 66: "OKXChain"
50
+ };
51
+ function fmtMs(ms) {
52
+ return ms < 1 ? "<1 ms" : ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(2)} s`;
53
+ }
54
+ async function rpcCall(rpcUrl, method, params = []) {
55
+ const body = JSON.stringify({ jsonrpc: "2.0", method, params, id: Date.now() });
56
+ const start = performance.now();
57
+ try {
58
+ const controller = new AbortController;
59
+ const timeout = setTimeout(() => controller.abort(), 1e4);
60
+ const res = await fetch(rpcUrl, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body,
64
+ signal: controller.signal
65
+ });
66
+ clearTimeout(timeout);
67
+ const latency = performance.now() - start;
68
+ const data = await res.json();
69
+ return { success: true, data, latency, status: res.status };
70
+ } catch (err) {
71
+ return { success: false, error: err.message, latency: performance.now() - start };
72
+ }
73
+ }
74
+ async function corsCheck(rpcUrl) {
75
+ try {
76
+ const controller = new AbortController;
77
+ const timeout = setTimeout(() => controller.abort(), 1e4);
78
+ const res = await fetch(rpcUrl, {
79
+ method: "OPTIONS",
80
+ headers: {
81
+ Origin: "http://localhost:3000",
82
+ "Access-Control-Request-Method": "POST",
83
+ "Access-Control-Request-Headers": "Content-Type"
84
+ },
85
+ signal: controller.signal
86
+ });
87
+ clearTimeout(timeout);
88
+ const headers = {};
89
+ for (const [k, v] of res.headers.entries()) {
90
+ if (k.startsWith("access-control"))
91
+ headers[k] = v;
92
+ }
93
+ return { success: true, status: res.status, headers };
94
+ } catch (err) {
95
+ return { success: false, error: err.message };
96
+ }
97
+ }
98
+ async function batchCall(rpcUrl, calls) {
99
+ const body = JSON.stringify(calls.map((c2, i) => ({ jsonrpc: "2.0", method: c2.method, params: c2.params, id: i + 1 })));
100
+ const start = performance.now();
101
+ try {
102
+ const controller = new AbortController;
103
+ const timeout = setTimeout(() => controller.abort(), 15000);
104
+ const res = await fetch(rpcUrl, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body,
108
+ signal: controller.signal
109
+ });
110
+ clearTimeout(timeout);
111
+ const latency = performance.now() - start;
112
+ const data = await res.json();
113
+ return { success: true, data, latency };
114
+ } catch (err) {
115
+ return { success: false, error: err.message, latency: performance.now() - start };
116
+ }
117
+ }
118
+ async function wsCheck(rpcUrl) {
119
+ let wsUrl = rpcUrl;
120
+ if (wsUrl.startsWith("https://"))
121
+ wsUrl = wsUrl.replace("https://", "wss://");
122
+ else if (wsUrl.startsWith("http://"))
123
+ wsUrl = wsUrl.replace("http://", "ws://");
124
+ return new Promise((resolve) => {
125
+ try {
126
+ const ws = new WebSocket(wsUrl);
127
+ const timer = setTimeout(() => {
128
+ ws.close();
129
+ resolve({ success: false, error: "Connection timeout (5s)" });
130
+ }, 5000);
131
+ ws.onopen = () => {
132
+ ws.send(JSON.stringify({ jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1 }));
133
+ };
134
+ ws.onmessage = (event) => {
135
+ clearTimeout(timer);
136
+ ws.close();
137
+ try {
138
+ const data = JSON.parse(String(event.data));
139
+ resolve({ success: true, data, wsUrl });
140
+ } catch {
141
+ resolve({ success: false, error: "Invalid JSON response" });
142
+ }
143
+ };
144
+ ws.onerror = () => {
145
+ clearTimeout(timer);
146
+ resolve({ success: false, error: "WebSocket connection failed" });
147
+ };
148
+ } catch (err) {
149
+ resolve({ success: false, error: err.message });
150
+ }
151
+ });
152
+ }
153
+ function statusIcon(status) {
154
+ switch (status) {
155
+ case "pass":
156
+ return `${c.green}PASS${c.reset}`;
157
+ case "fail":
158
+ return `${c.red}FAIL${c.reset}`;
159
+ case "warn":
160
+ return `${c.yellow}WARN${c.reset}`;
161
+ }
162
+ }
163
+ function printHeader(rpcUrl) {
164
+ console.log();
165
+ console.log(`${c.bold}${c.cyan} EVM RPC Checker${c.reset}`);
166
+ console.log(`${c.gray} ${"─".repeat(50)}${c.reset}`);
167
+ console.log(`${c.gray} Target: ${c.white}${rpcUrl}${c.reset}`);
168
+ console.log();
169
+ }
170
+ function printChainInfo(chainId, chainName, blockNumber) {
171
+ console.log(`${c.blue} Chain:${c.reset} ${chainName} ${c.dim}(ID: ${chainId})${c.reset}`);
172
+ console.log(`${c.blue} Block:${c.reset} #${blockNumber.toLocaleString()}`);
173
+ console.log();
174
+ }
175
+ function printTestResult(result) {
176
+ console.log(` ${result.icon} ${c.bold}${result.name}${c.reset} ${statusIcon(result.status)}`);
177
+ if (result.message) {
178
+ console.log(` ${c.dim}${result.message}${c.reset}`);
179
+ }
180
+ for (const d of result.details) {
181
+ console.log(` ${c.gray}${d.label}:${c.reset} ${d.value}`);
182
+ }
183
+ console.log();
184
+ }
185
+ function printScore(score) {
186
+ const color = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
187
+ const bar = "█".repeat(Math.round(score / 5)) + "░".repeat(20 - Math.round(score / 5));
188
+ console.log(`${c.gray} ${"─".repeat(50)}${c.reset}`);
189
+ console.log(` ${c.bold}Overall Score:${c.reset} ${color}${c.bold}${score}${c.reset}/100 ${color}${bar}${c.reset}`);
190
+ console.log();
191
+ }
192
+ function printProgress(current, total, name) {
193
+ const pct = Math.round(current / total * 100);
194
+ const filled = Math.round(pct / 5);
195
+ const bar = "█".repeat(filled) + "░".repeat(20 - filled);
196
+ process.stdout.write(`\r ${c.cyan}${bar}${c.reset} ${pct}% ${c.dim}${name}...${c.reset} `);
197
+ }
198
+ function clearProgress() {
199
+ process.stdout.write("\r" + " ".repeat(70) + "\r");
200
+ }
201
+ var TESTS = [
202
+ { id: "connectivity", name: "Connectivity", icon: "⚡" },
203
+ { id: "latency", name: "Latency", icon: "⏱" },
204
+ { id: "cors", name: "CORS", icon: "\uD83C\uDF10" },
205
+ { id: "batch", name: "Batch Request", icon: "\uD83D\uDCE6" },
206
+ { id: "websocket", name: "WebSocket", icon: "\uD83D\uDD0C" },
207
+ { id: "archive", name: "Archive Node", icon: "\uD83D\uDCDA" },
208
+ { id: "methods", name: "Methods", icon: "\uD83D\uDD27" },
209
+ { id: "throughput", name: "Throughput", icon: "\uD83D\uDE80" },
210
+ { id: "consistency", name: "Consistency", icon: "\uD83E\uDDE9" },
211
+ { id: "gastracker", name: "Gas Tracker", icon: "⛽" }
212
+ ];
213
+ async function runAnalysis(rpcUrl) {
214
+ printHeader(rpcUrl);
215
+ const results = [];
216
+ let completed = 0;
217
+ const total = TESTS.length;
218
+ printProgress(completed, total, "Connectivity");
219
+ let chainId = 0;
220
+ let blockNumber = 0;
221
+ try {
222
+ const [chainRes, blockRes, netRes] = await Promise.all([
223
+ rpcCall(rpcUrl, "eth_chainId"),
224
+ rpcCall(rpcUrl, "eth_blockNumber"),
225
+ rpcCall(rpcUrl, "net_version")
226
+ ]);
227
+ if (!chainRes.success || chainRes.data?.error)
228
+ throw new Error(chainRes.data?.error?.message || chainRes.error || "Cannot connect");
229
+ chainId = parseInt(chainRes.data.result, 16);
230
+ blockNumber = parseInt(blockRes.data?.result || "0", 16);
231
+ const netVersion = netRes.data?.result || "N/A";
232
+ const chainName = KNOWN_CHAINS[chainId] || `Unknown Chain (${chainId})`;
233
+ results.push({
234
+ name: "Connectivity",
235
+ icon: "⚡",
236
+ status: "pass",
237
+ score: 10,
238
+ details: [
239
+ { label: "Chain ID", value: String(chainId) },
240
+ { label: "Chain", value: chainName },
241
+ { label: "Latest Block", value: blockNumber.toLocaleString() },
242
+ { label: "Net Version", value: netVersion },
243
+ { label: "Latency", value: fmtMs(chainRes.latency) }
244
+ ]
245
+ });
246
+ completed++;
247
+ clearProgress();
248
+ printChainInfo(chainId, chainName, blockNumber);
249
+ } catch (e) {
250
+ clearProgress();
251
+ results.push({ name: "Connectivity", icon: "⚡", status: "fail", score: 0, details: [], message: `Connection failed: ${e.message}` });
252
+ printTestResult(results[0]);
253
+ console.log(` ${c.red}${c.bold}Connectivity test failed. Cannot continue.${c.reset}`);
254
+ console.log();
255
+ return;
256
+ }
257
+ async function runTest(id, name, icon, fn) {
258
+ printProgress(completed, total, name);
259
+ try {
260
+ const result = await fn();
261
+ results.push({ name, icon, ...result });
262
+ } catch (e) {
263
+ results.push({ name, icon, status: "fail", score: 0, details: [], message: `Exception: ${e.message}` });
264
+ }
265
+ completed++;
266
+ }
267
+ await runTest("latency", "Latency", "⏱", async () => {
268
+ const times = [];
269
+ for (let i = 0;i < 5; i++) {
270
+ const res = await rpcCall(rpcUrl, "eth_blockNumber");
271
+ if (res.success)
272
+ times.push(res.latency);
273
+ }
274
+ if (times.length === 0)
275
+ return { status: "fail", score: 0, details: [], message: "All requests failed" };
276
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
277
+ const min = Math.min(...times);
278
+ const max = Math.max(...times);
279
+ const jitter = max - min;
280
+ const score = avg < 100 ? 10 : avg < 300 ? 8 : avg < 600 ? 6 : avg < 1000 ? 4 : 2;
281
+ return {
282
+ status: avg < 1000 ? "pass" : "warn",
283
+ score,
284
+ details: [
285
+ { label: "Avg", value: fmtMs(avg) },
286
+ { label: "Min", value: fmtMs(min) },
287
+ { label: "Max", value: fmtMs(max) },
288
+ { label: "Jitter", value: fmtMs(jitter) },
289
+ { label: "Samples", value: `${times.length}/5` }
290
+ ]
291
+ };
292
+ });
293
+ await runTest("cors", "CORS", "\uD83C\uDF10", async () => {
294
+ const res = await corsCheck(rpcUrl);
295
+ if (!res.success) {
296
+ return { status: "warn", score: 5, details: [
297
+ { label: "Status", value: "Cannot detect" },
298
+ { label: "Info", value: res.error || "Preflight may not be supported" }
299
+ ] };
300
+ }
301
+ const headers = res.headers || {};
302
+ const hasOrigin = !!headers["access-control-allow-origin"];
303
+ const origin = headers["access-control-allow-origin"] || "Not set";
304
+ const methods = headers["access-control-allow-methods"] || "Not set";
305
+ const isOpen = origin === "*";
306
+ return {
307
+ status: hasOrigin ? isOpen ? "pass" : "warn" : "fail",
308
+ score: isOpen ? 10 : hasOrigin ? 7 : 3,
309
+ details: [
310
+ { label: "Allow-Origin", value: origin },
311
+ { label: "Allow-Methods", value: methods },
312
+ { label: "Browser OK", value: hasOrigin ? "Yes" : "No" }
313
+ ]
314
+ };
315
+ });
316
+ await runTest("batch", "Batch Request", "\uD83D\uDCE6", async () => {
317
+ const res = await batchCall(rpcUrl, [
318
+ { method: "eth_blockNumber", params: [] },
319
+ { method: "eth_chainId", params: [] },
320
+ { method: "eth_gasPrice", params: [] }
321
+ ]);
322
+ if (!res.success)
323
+ return { status: "fail", score: 0, details: [], message: `Batch failed: ${res.error}` };
324
+ const isArray = Array.isArray(res.data);
325
+ const allOk = isArray && res.data.every((r) => r.result !== undefined);
326
+ return {
327
+ status: isArray ? "pass" : "fail",
328
+ score: isArray ? allOk ? 10 : 7 : 0,
329
+ details: [
330
+ { label: "Supported", value: isArray ? "Yes" : "No" },
331
+ { label: "Responses", value: isArray ? `${res.data.length}/3` : "N/A" },
332
+ { label: "All OK", value: allOk ? "Yes" : "No" },
333
+ { label: "Latency", value: fmtMs(res.latency) }
334
+ ]
335
+ };
336
+ });
337
+ await runTest("websocket", "WebSocket", "\uD83D\uDD0C", async () => {
338
+ const res = await wsCheck(rpcUrl);
339
+ if (!res.success) {
340
+ return { status: "warn", score: 4, details: [
341
+ { label: "WebSocket", value: "Not Supported" },
342
+ { label: "Reason", value: res.error || "Unknown" }
343
+ ] };
344
+ }
345
+ const bn = parseInt(res.data?.result || "0", 16);
346
+ return {
347
+ status: "pass",
348
+ score: 10,
349
+ details: [
350
+ { label: "WebSocket", value: "Supported" },
351
+ { label: "WS URL", value: res.wsUrl || "" },
352
+ { label: "Block Height", value: bn.toLocaleString() }
353
+ ]
354
+ };
355
+ });
356
+ await runTest("archive", "Archive Node", "\uD83D\uDCDA", async () => {
357
+ const res = await rpcCall(rpcUrl, "eth_getBalance", ["0x0000000000000000000000000000000000000000", "0x1"]);
358
+ if (!res.success)
359
+ return { status: "fail", score: 0, details: [], message: `Request failed: ${res.error}` };
360
+ const hasError = !!res.data?.error;
361
+ const isArchive = !hasError && res.data?.result !== undefined;
362
+ return {
363
+ status: isArchive ? "pass" : "warn",
364
+ score: isArchive ? 10 : 5,
365
+ details: [
366
+ { label: "Archive", value: isArchive ? "Yes" : "No" },
367
+ { label: "History Query", value: isArchive ? "Supported" : "Not Supported" },
368
+ ...hasError ? [{ label: "Error", value: res.data.error.message || "unknown" }] : [],
369
+ { label: "Latency", value: fmtMs(res.latency) }
370
+ ]
371
+ };
372
+ });
373
+ await runTest("methods", "Methods", "\uD83D\uDD27", async () => {
374
+ const methods = [
375
+ { method: "eth_gasPrice", params: [], label: "eth_gasPrice" },
376
+ { method: "eth_estimateGas", params: [{ to: "0x0000000000000000000000000000000000000000", value: "0x0" }], label: "eth_estimateGas" },
377
+ { method: "eth_getLogs", params: [{ fromBlock: "latest", toBlock: "latest" }], label: "eth_getLogs" },
378
+ { method: "eth_getCode", params: ["0x0000000000000000000000000000000000000001", "latest"], label: "eth_getCode" },
379
+ { method: "trace_block", params: ["latest"], label: "trace_block" },
380
+ { method: "debug_traceBlockByNumber", params: ["latest", {}], label: "debug_traceBlock" },
381
+ { method: "eth_feeHistory", params: ["0x5", "latest", [25, 75]], label: "eth_feeHistory" }
382
+ ];
383
+ const callResults = [];
384
+ for (const m of methods) {
385
+ callResults.push(await rpcCall(rpcUrl, m.method, m.params));
386
+ }
387
+ let supported = 0;
388
+ const details = methods.map((m, i) => {
389
+ const r = callResults[i];
390
+ const ok = r.success && !r.data?.error;
391
+ if (ok)
392
+ supported++;
393
+ return { label: m.label, value: ok ? "✓" : "✗" };
394
+ });
395
+ const score = Math.round(supported / methods.length * 10);
396
+ return {
397
+ status: supported >= 4 ? "pass" : supported >= 2 ? "warn" : "fail",
398
+ score,
399
+ details: [{ label: "Support Rate", value: `${supported}/${methods.length}` }, ...details]
400
+ };
401
+ });
402
+ await runTest("throughput", "Throughput", "\uD83D\uDE80", async () => {
403
+ const count = 10;
404
+ const start = performance.now();
405
+ const callResults = [];
406
+ for (let i = 0;i < count; i++) {
407
+ callResults.push(await rpcCall(rpcUrl, "eth_blockNumber"));
408
+ }
409
+ const elapsed = performance.now() - start;
410
+ const successCount = callResults.filter((r) => r.success && !r.data?.error).length;
411
+ const rps = (successCount / (elapsed / 1000)).toFixed(1);
412
+ const avgLatency = callResults.filter((r) => r.success).reduce((s, r) => s + r.latency, 0) / (successCount || 1);
413
+ const score = parseFloat(rps) >= 20 ? 10 : parseFloat(rps) >= 10 ? 8 : parseFloat(rps) >= 3 ? 5 : 2;
414
+ return {
415
+ status: successCount === count ? "pass" : "warn",
416
+ score,
417
+ details: [
418
+ { label: "Requests", value: String(count) },
419
+ { label: "Success", value: `${successCount}/${count}` },
420
+ { label: "Throughput", value: `${rps} req/s` },
421
+ { label: "Total Time", value: fmtMs(elapsed) },
422
+ { label: "Avg Latency", value: fmtMs(avgLatency) }
423
+ ]
424
+ };
425
+ });
426
+ await runTest("consistency", "Consistency", "\uD83E\uDDE9", async () => {
427
+ const r1 = await rpcCall(rpcUrl, "eth_blockNumber");
428
+ const r2 = await rpcCall(rpcUrl, "eth_blockNumber");
429
+ const r3 = await rpcCall(rpcUrl, "eth_blockNumber");
430
+ const blocks = [r1, r2, r3].filter((r) => r.success && r.data?.result).map((r) => parseInt(r.data.result, 16));
431
+ if (blocks.length < 3)
432
+ return { status: "fail", score: 0, details: [], message: "Some requests failed" };
433
+ const maxDiff = Math.max(...blocks) - Math.min(...blocks);
434
+ const consistent = maxDiff <= 1;
435
+ return {
436
+ status: consistent ? "pass" : "warn",
437
+ score: consistent ? 10 : maxDiff <= 3 ? 6 : 2,
438
+ details: [
439
+ { label: "Block #1", value: blocks[0].toLocaleString() },
440
+ { label: "Block #2", value: blocks[1].toLocaleString() },
441
+ { label: "Block #3", value: blocks[2].toLocaleString() },
442
+ { label: "Max Drift", value: `${maxDiff} blocks` },
443
+ { label: "Consistent", value: consistent ? "Yes" : "No" }
444
+ ]
445
+ };
446
+ });
447
+ await runTest("gastracker", "Gas Tracker", "⛽", async () => {
448
+ const gasPriceRes = await rpcCall(rpcUrl, "eth_gasPrice");
449
+ const feeHistoryRes = await rpcCall(rpcUrl, "eth_feeHistory", ["0xa", "latest", [10, 50, 90]]);
450
+ if (!gasPriceRes.success || gasPriceRes.data?.error) {
451
+ return { status: "fail", score: 0, details: [], message: "Cannot get gas price" };
452
+ }
453
+ const gasPrice = parseInt(gasPriceRes.data.result, 16);
454
+ const gasPriceGwei = (gasPrice / 1e9).toFixed(2);
455
+ const details = [
456
+ { label: "Gas Price", value: `${gasPriceGwei} Gwei` }
457
+ ];
458
+ if (feeHistoryRes.success && !feeHistoryRes.data?.error && feeHistoryRes.data?.result) {
459
+ const fh = feeHistoryRes.data.result;
460
+ const baseFees = (fh.baseFeePerGas || []).map((b) => parseInt(b, 16) / 1e9);
461
+ if (baseFees.length > 0) {
462
+ const avgBase = (baseFees.reduce((a, b) => a + b, 0) / baseFees.length).toFixed(2);
463
+ details.push({ label: "Avg Base Fee", value: `${avgBase} Gwei` });
464
+ }
465
+ const rewards = fh.reward;
466
+ if (rewards?.length > 0) {
467
+ const latest = rewards[rewards.length - 1];
468
+ if (latest) {
469
+ details.push({ label: "Priority P10", value: `${(parseInt(latest[0], 16) / 1e9).toFixed(2)} Gwei` });
470
+ details.push({ label: "Priority P50", value: `${(parseInt(latest[1], 16) / 1e9).toFixed(2)} Gwei` });
471
+ details.push({ label: "Priority P90", value: `${(parseInt(latest[2], 16) / 1e9).toFixed(2)} Gwei` });
472
+ }
473
+ }
474
+ details.push({ label: "EIP-1559", value: "Supported" });
475
+ } else {
476
+ details.push({ label: "EIP-1559", value: "Not Supported" });
477
+ }
478
+ return { status: "pass", score: 8, details };
479
+ });
480
+ clearProgress();
481
+ for (const r of results) {
482
+ printTestResult(r);
483
+ }
484
+ const totalScore = results.reduce((s, r) => s + r.score, 0);
485
+ const maxScore = results.length * 10;
486
+ const pct = Math.round(totalScore / maxScore * 100);
487
+ printScore(pct);
488
+ }
489
+ function printUsage() {
490
+ console.log();
491
+ console.log(`${c.bold}${c.cyan} EVM RPC Checker${c.reset} - Analyze EVM-compatible RPC endpoints`);
492
+ console.log();
493
+ console.log(` ${c.bold}Usage:${c.reset}`);
494
+ console.log(` ${c.green}evm-rpc-checker${c.reset} <rpc-url>`);
495
+ console.log(` ${c.green}bun cli/src/index.ts${c.reset} <rpc-url>`);
496
+ console.log();
497
+ console.log(` ${c.bold}Example:${c.reset}`);
498
+ console.log(` ${c.dim}evm-rpc-checker https://eth.llamarpc.com${c.reset}`);
499
+ console.log(` ${c.dim}evm-rpc-checker https://rpc.ankr.com/eth${c.reset}`);
500
+ console.log();
501
+ }
502
+ var args = process.argv.slice(2);
503
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
504
+ printUsage();
505
+ process.exit(0);
506
+ }
507
+ var rpcUrl = args[0];
508
+ if (!rpcUrl.startsWith("http://") && !rpcUrl.startsWith("https://")) {
509
+ console.error(`
510
+ ${c.red}Error: Invalid URL. Must start with http:// or https://${c.reset}
511
+ `);
512
+ process.exit(1);
513
+ }
514
+ runAnalysis(rpcUrl).catch((err) => {
515
+ console.error(`
516
+ ${c.red}Fatal error: ${err.message}${c.reset}
517
+ `);
518
+ process.exit(1);
519
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "evm-rpc-checker",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for analyzing EVM-compatible RPC endpoints",
5
+ "type": "module",
6
+ "bin": {
7
+ "evm-rpc-checker": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "bun build src/index.ts --outfile dist/index.js --target node",
11
+ "prepublishOnly": "bun run build",
12
+ "dev": "bun src/index.ts"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18.0.0"
19
+ },
20
+ "keywords": [
21
+ "evm",
22
+ "rpc",
23
+ "ethereum",
24
+ "blockchain",
25
+ "checker",
26
+ "analyzer",
27
+ "web3",
28
+ "cli"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/lolieatapple/rpc-checker.git",
33
+ "directory": "cli"
34
+ },
35
+ "homepage": "https://github.com/lolieatapple/rpc-checker",
36
+ "license": "MIT"
37
+ }