@jellylegsai/aether-cli 1.9.2 → 2.0.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/IMPLEMENTATION_REPORT.md +319 -0
- package/commands/blockheight.js +230 -0
- package/commands/call.js +981 -0
- package/commands/claim.js +98 -72
- package/commands/deploy.js +959 -0
- package/commands/index.js +2 -0
- package/commands/init.js +33 -49
- package/commands/network-diagnostics.js +706 -0
- package/commands/network.js +412 -429
- package/commands/rewards.js +311 -266
- package/commands/sdk.js +791 -656
- package/commands/slot.js +3 -11
- package/commands/stake.js +581 -516
- package/commands/supply.js +483 -391
- package/commands/token-accounts.js +275 -0
- package/commands/transfer.js +3 -11
- package/commands/unstake.js +3 -11
- package/commands/validator-start.js +681 -323
- package/commands/validator.js +959 -0
- package/commands/validators.js +623 -626
- package/commands/version.js +240 -0
- package/commands/wallet.js +17 -24
- package/cycle-report-issue-116.txt +165 -0
- package/index.js +501 -602
- package/lib/ui.js +623 -0
- package/package.json +10 -3
- package/sdk/index.d.ts +546 -0
- package/sdk/index.js +130 -0
- package/sdk/package.json +2 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli network-diagnostics
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive network diagnostics with automatic RPC failover,
|
|
6
|
+
* latency testing, and intelligent error recovery.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Multi-RPC endpoint health checking with automatic failover
|
|
10
|
+
* - Latency benchmarking across endpoints
|
|
11
|
+
* - Connection quality scoring
|
|
12
|
+
* - Automatic RPC selection with --auto flag
|
|
13
|
+
* - Detailed error classification and recovery suggestions
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* aether network-diagnostics # Test default RPC
|
|
17
|
+
* aether network-diagnostics --auto # Auto-select best RPC
|
|
18
|
+
* aether network-diagnostics --rpc <url> # Test specific RPC
|
|
19
|
+
* aether network-diagnostics --benchmark # Latency benchmark mode
|
|
20
|
+
* aether network-diagnostics --all # Test all known endpoints
|
|
21
|
+
*
|
|
22
|
+
* SDK wired to:
|
|
23
|
+
* - client.getHealth() → GET /v1/health
|
|
24
|
+
* - client.getSlot() → GET /v1/slot
|
|
25
|
+
* - client.getVersion() → GET /v1/version
|
|
26
|
+
* - client.getEpochInfo() → GET /v1/epoch
|
|
27
|
+
* - client.ping() → Latency test
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const readline = require('readline');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const os = require('os');
|
|
34
|
+
|
|
35
|
+
// Import SDK
|
|
36
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
37
|
+
const aether = require(sdkPath);
|
|
38
|
+
|
|
39
|
+
// ANSI colours
|
|
40
|
+
const C = {
|
|
41
|
+
reset: '\x1b[0m',
|
|
42
|
+
bright: '\x1b[1m',
|
|
43
|
+
dim: '\x1b[2m',
|
|
44
|
+
red: '\x1b[31m',
|
|
45
|
+
green: '\x1b[32m',
|
|
46
|
+
yellow: '\x1b[33m',
|
|
47
|
+
cyan: '\x1b[36m',
|
|
48
|
+
magenta: '\x1b[35m',
|
|
49
|
+
blue: '\x1b[34m',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const CLI_VERSION = '1.0.0';
|
|
53
|
+
|
|
54
|
+
// Known RPC endpoints for failover
|
|
55
|
+
const KNOWN_RPC_ENDPOINTS = [
|
|
56
|
+
{ url: 'http://127.0.0.1:8899', name: 'Local Node', priority: 1 },
|
|
57
|
+
{ url: 'http://localhost:8899', name: 'Local Node (alt)', priority: 2 },
|
|
58
|
+
{ url: process.env.AETHER_RPC, name: 'Env AETHER_RPC', priority: 0, conditional: true },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Filter out null/undefined endpoints
|
|
62
|
+
function getEffectiveEndpoints() {
|
|
63
|
+
const endpoints = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
|
|
66
|
+
for (const ep of KNOWN_RPC_ENDPOINTS) {
|
|
67
|
+
if (ep.conditional && !ep.url) continue;
|
|
68
|
+
if (!ep.url) continue;
|
|
69
|
+
if (seen.has(ep.url)) continue;
|
|
70
|
+
seen.add(ep.url);
|
|
71
|
+
endpoints.push(ep);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return endpoints.sort((a, b) => a.priority - b.priority);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Config & Paths
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
function getAetherDir() {
|
|
82
|
+
return path.join(os.homedir(), '.aether');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getConfigPath() {
|
|
86
|
+
return path.join(getAetherDir(), 'config.json');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function loadConfig() {
|
|
90
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
91
|
+
return { preferredRpc: null, rpcHistory: [] };
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
|
95
|
+
} catch {
|
|
96
|
+
return { preferredRpc: null, rpcHistory: [] };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function saveConfig(cfg) {
|
|
101
|
+
if (!fs.existsSync(getAetherDir())) {
|
|
102
|
+
fs.mkdirSync(getAetherDir(), { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function updateRpcHistory(rpcUrl, latency, success) {
|
|
108
|
+
const cfg = loadConfig();
|
|
109
|
+
cfg.rpcHistory = cfg.rpcHistory || [];
|
|
110
|
+
|
|
111
|
+
// Add entry
|
|
112
|
+
cfg.rpcHistory.unshift({
|
|
113
|
+
url: rpcUrl,
|
|
114
|
+
latency,
|
|
115
|
+
success,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Keep last 50 entries
|
|
120
|
+
cfg.rpcHistory = cfg.rpcHistory.slice(0, 50);
|
|
121
|
+
|
|
122
|
+
// Update preferred RPC if this one is good
|
|
123
|
+
if (success && latency < 100) {
|
|
124
|
+
cfg.preferredRpc = rpcUrl;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
saveConfig(cfg);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Argument Parsing
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
function parseArgs() {
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
const opts = {
|
|
137
|
+
rpc: process.env.AETHER_RPC || 'http://127.0.0.1:8899',
|
|
138
|
+
auto: false,
|
|
139
|
+
benchmark: false,
|
|
140
|
+
all: false,
|
|
141
|
+
json: false,
|
|
142
|
+
timeout: 10000,
|
|
143
|
+
help: false,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < args.length; i++) {
|
|
147
|
+
switch (args[i]) {
|
|
148
|
+
case '--rpc':
|
|
149
|
+
case '-r':
|
|
150
|
+
opts.rpc = args[++i];
|
|
151
|
+
break;
|
|
152
|
+
case '--auto':
|
|
153
|
+
case '-a':
|
|
154
|
+
opts.auto = true;
|
|
155
|
+
break;
|
|
156
|
+
case '--benchmark':
|
|
157
|
+
case '-b':
|
|
158
|
+
opts.benchmark = true;
|
|
159
|
+
break;
|
|
160
|
+
case '--all':
|
|
161
|
+
opts.all = true;
|
|
162
|
+
break;
|
|
163
|
+
case '--json':
|
|
164
|
+
case '-j':
|
|
165
|
+
opts.json = true;
|
|
166
|
+
break;
|
|
167
|
+
case '--timeout':
|
|
168
|
+
case '-t':
|
|
169
|
+
opts.timeout = parseInt(args[++i], 10) || 10000;
|
|
170
|
+
break;
|
|
171
|
+
case '--help':
|
|
172
|
+
case '-h':
|
|
173
|
+
opts.help = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return opts;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function showHelp() {
|
|
182
|
+
console.log(`
|
|
183
|
+
${C.bright}${C.cyan}aether-cli network-diagnostics${C.reset} - Network health & RPC diagnostics
|
|
184
|
+
|
|
185
|
+
${C.bright}USAGE${C.reset}
|
|
186
|
+
aether network-diagnostics [options]
|
|
187
|
+
|
|
188
|
+
${C.bright}OPTIONS${C.reset}
|
|
189
|
+
--rpc <url> Test specific RPC endpoint
|
|
190
|
+
--auto, -a Auto-select best performing RPC
|
|
191
|
+
--benchmark, -b Run latency benchmark
|
|
192
|
+
--all Test all known RPC endpoints
|
|
193
|
+
--json, -j Output JSON
|
|
194
|
+
--timeout <ms> Request timeout (default: 10000)
|
|
195
|
+
--help, -h Show this help
|
|
196
|
+
|
|
197
|
+
${C.bright}SDK METHODS USED${C.reset}
|
|
198
|
+
client.getHealth() → GET /v1/health
|
|
199
|
+
client.getSlot() → GET /v1/slot
|
|
200
|
+
client.getVersion() → GET /v1/version
|
|
201
|
+
client.getEpochInfo() → GET /v1/epoch
|
|
202
|
+
client.ping() → Latency measurement
|
|
203
|
+
|
|
204
|
+
${C.bright}EXAMPLES${C.reset}
|
|
205
|
+
aether network-diagnostics # Test default RPC
|
|
206
|
+
aether network-diagnostics --auto # Find best RPC
|
|
207
|
+
aether network-diagnostics --benchmark # Latency test
|
|
208
|
+
aether network-diagnostics --all --json # Test all, JSON output
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Diagnostics Logic
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Test a single RPC endpoint comprehensively
|
|
218
|
+
*/
|
|
219
|
+
async function testEndpoint(url, timeout = 10000) {
|
|
220
|
+
const results = {
|
|
221
|
+
url,
|
|
222
|
+
timestamp: new Date().toISOString(),
|
|
223
|
+
tests: {},
|
|
224
|
+
overall: {
|
|
225
|
+
success: false,
|
|
226
|
+
latency: null,
|
|
227
|
+
score: 0,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const client = new aether.AetherClient({ rpcUrl: url, timeoutMs: timeout });
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// Test 1: Basic connectivity (ping)
|
|
236
|
+
const pingStart = Date.now();
|
|
237
|
+
const pingResult = await aether.ping(url);
|
|
238
|
+
results.tests.ping = {
|
|
239
|
+
success: pingResult.ok,
|
|
240
|
+
latency: pingResult.latency,
|
|
241
|
+
error: pingResult.ok ? null : pingResult.error,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (!pingResult.ok) {
|
|
245
|
+
throw new Error(`Ping failed: ${pingResult.error}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Test 2: Health check
|
|
249
|
+
const healthStart = Date.now();
|
|
250
|
+
try {
|
|
251
|
+
const health = await client.getHealth();
|
|
252
|
+
results.tests.health = {
|
|
253
|
+
success: true,
|
|
254
|
+
latency: Date.now() - healthStart,
|
|
255
|
+
status: health,
|
|
256
|
+
};
|
|
257
|
+
} catch (err) {
|
|
258
|
+
results.tests.health = {
|
|
259
|
+
success: false,
|
|
260
|
+
latency: Date.now() - healthStart,
|
|
261
|
+
error: err.message,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Test 3: Slot query
|
|
266
|
+
const slotStart = Date.now();
|
|
267
|
+
try {
|
|
268
|
+
const slot = await client.getSlot();
|
|
269
|
+
results.tests.slot = {
|
|
270
|
+
success: true,
|
|
271
|
+
latency: Date.now() - slotStart,
|
|
272
|
+
slot,
|
|
273
|
+
};
|
|
274
|
+
} catch (err) {
|
|
275
|
+
results.tests.slot = {
|
|
276
|
+
success: false,
|
|
277
|
+
latency: Date.now() - slotStart,
|
|
278
|
+
error: err.message,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Test 4: Version info
|
|
283
|
+
const versionStart = Date.now();
|
|
284
|
+
try {
|
|
285
|
+
const version = await client.getVersion();
|
|
286
|
+
results.tests.version = {
|
|
287
|
+
success: true,
|
|
288
|
+
latency: Date.now() - versionStart,
|
|
289
|
+
version,
|
|
290
|
+
};
|
|
291
|
+
} catch (err) {
|
|
292
|
+
results.tests.version = {
|
|
293
|
+
success: false,
|
|
294
|
+
latency: Date.now() - versionStart,
|
|
295
|
+
error: err.message,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Test 5: Epoch info
|
|
300
|
+
const epochStart = Date.now();
|
|
301
|
+
try {
|
|
302
|
+
const epoch = await client.getEpochInfo();
|
|
303
|
+
results.tests.epoch = {
|
|
304
|
+
success: true,
|
|
305
|
+
latency: Date.now() - epochStart,
|
|
306
|
+
epoch,
|
|
307
|
+
};
|
|
308
|
+
} catch (err) {
|
|
309
|
+
results.tests.epoch = {
|
|
310
|
+
success: false,
|
|
311
|
+
latency: Date.now() - epochStart,
|
|
312
|
+
error: err.message,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Calculate overall stats
|
|
317
|
+
const totalTime = Date.now() - startTime;
|
|
318
|
+
const successfulTests = Object.values(results.tests).filter(t => t.success).length;
|
|
319
|
+
const totalTests = Object.keys(results.tests).length;
|
|
320
|
+
|
|
321
|
+
results.overall = {
|
|
322
|
+
success: successfulTests === totalTests,
|
|
323
|
+
latency: totalTime,
|
|
324
|
+
score: Math.round((successfulTests / totalTests) * 100),
|
|
325
|
+
successfulTests,
|
|
326
|
+
totalTests,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Update history
|
|
330
|
+
updateRpcHistory(url, totalTime, results.overall.success);
|
|
331
|
+
|
|
332
|
+
return results;
|
|
333
|
+
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const totalTime = Date.now() - startTime;
|
|
336
|
+
results.overall = {
|
|
337
|
+
success: false,
|
|
338
|
+
latency: totalTime,
|
|
339
|
+
score: 0,
|
|
340
|
+
error: err.message,
|
|
341
|
+
};
|
|
342
|
+
updateRpcHistory(url, totalTime, false);
|
|
343
|
+
return results;
|
|
344
|
+
} finally {
|
|
345
|
+
client.destroy();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Run benchmark on an endpoint
|
|
351
|
+
*/
|
|
352
|
+
async function benchmarkEndpoint(url, iterations = 5) {
|
|
353
|
+
const latencies = [];
|
|
354
|
+
const errors = [];
|
|
355
|
+
|
|
356
|
+
for (let i = 0; i < iterations; i++) {
|
|
357
|
+
const start = Date.now();
|
|
358
|
+
try {
|
|
359
|
+
const result = await aether.ping(url);
|
|
360
|
+
if (result.ok) {
|
|
361
|
+
latencies.push(result.latency);
|
|
362
|
+
} else {
|
|
363
|
+
errors.push(result.error);
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
errors.push(err.message);
|
|
367
|
+
}
|
|
368
|
+
// Small delay between pings
|
|
369
|
+
if (i < iterations - 1) {
|
|
370
|
+
await new Promise(r => setTimeout(r, 100));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (latencies.length === 0) {
|
|
375
|
+
return {
|
|
376
|
+
url,
|
|
377
|
+
success: false,
|
|
378
|
+
errors,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const sorted = [...latencies].sort((a, b) => a - b);
|
|
383
|
+
const sum = latencies.reduce((a, b) => a + b, 0);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
url,
|
|
387
|
+
success: true,
|
|
388
|
+
iterations,
|
|
389
|
+
min: sorted[0],
|
|
390
|
+
max: sorted[sorted.length - 1],
|
|
391
|
+
avg: Math.round(sum / latencies.length),
|
|
392
|
+
median: sorted[Math.floor(sorted.length / 2)],
|
|
393
|
+
p95: sorted[Math.floor(sorted.length * 0.95)] || sorted[sorted.length - 1],
|
|
394
|
+
latencies,
|
|
395
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Find best RPC endpoint automatically
|
|
401
|
+
*/
|
|
402
|
+
async function findBestEndpoint(endpoints, timeout = 10000) {
|
|
403
|
+
const results = [];
|
|
404
|
+
|
|
405
|
+
// Test all endpoints in parallel
|
|
406
|
+
const tests = endpoints.map(ep =>
|
|
407
|
+
testEndpoint(ep.url, timeout).then(result => ({
|
|
408
|
+
...result,
|
|
409
|
+
name: ep.name,
|
|
410
|
+
}))
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const settled = await Promise.allSettled(tests);
|
|
414
|
+
|
|
415
|
+
for (const result of settled) {
|
|
416
|
+
if (result.status === 'fulfilled') {
|
|
417
|
+
results.push(result.value);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Sort by score (desc), then by latency (asc)
|
|
422
|
+
results.sort((a, b) => {
|
|
423
|
+
if (b.overall.score !== a.overall.score) {
|
|
424
|
+
return b.overall.score - a.overall.score;
|
|
425
|
+
}
|
|
426
|
+
return a.overall.latency - b.overall.latency;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return results;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ============================================================================
|
|
433
|
+
// Output Formatters
|
|
434
|
+
// ============================================================================
|
|
435
|
+
|
|
436
|
+
function getScoreColor(score) {
|
|
437
|
+
if (score >= 80) return C.green;
|
|
438
|
+
if (score >= 50) return C.yellow;
|
|
439
|
+
return C.red;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function getLatencyColor(latency) {
|
|
443
|
+
if (latency < 50) return C.green;
|
|
444
|
+
if (latency < 200) return C.yellow;
|
|
445
|
+
return C.red;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function formatLatency(ms) {
|
|
449
|
+
if (ms === null || ms === undefined) return 'N/A';
|
|
450
|
+
if (ms < 1) return '<1ms';
|
|
451
|
+
return `${Math.round(ms)}ms`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function printResult(result, verbose = false) {
|
|
455
|
+
const scoreColor = getScoreColor(result.overall.score);
|
|
456
|
+
const status = result.overall.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
|
|
457
|
+
|
|
458
|
+
console.log(`\n ${status} ${C.bright}${result.url}${C.reset}`);
|
|
459
|
+
|
|
460
|
+
if (result.name) {
|
|
461
|
+
console.log(` ${C.dim}Name: ${result.name}${C.reset}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(` ${C.dim}Score:${C.reset} ${scoreColor}${result.overall.score}%${C.reset}`);
|
|
465
|
+
console.log(` ${C.dim}Total Time:${C.reset} ${getLatencyColor(result.overall.latency)}${formatLatency(result.overall.latency)}${C.reset}`);
|
|
466
|
+
|
|
467
|
+
if (result.overall.successfulTests !== undefined) {
|
|
468
|
+
console.log(` ${C.dim}Tests Passed:${C.reset} ${result.overall.successfulTests}/${result.overall.totalTests}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (verbose && result.tests) {
|
|
472
|
+
console.log(`\n ${C.dim}── Test Details ──${C.reset}`);
|
|
473
|
+
for (const [name, test] of Object.entries(result.tests)) {
|
|
474
|
+
const testStatus = test.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
|
|
475
|
+
const latency = test.latency !== undefined ? ` (${formatLatency(test.latency)})` : '';
|
|
476
|
+
console.log(` ${testStatus} ${name}${latency}`);
|
|
477
|
+
if (test.error) {
|
|
478
|
+
console.log(` ${C.red}${test.error}${C.reset}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (result.overall.error) {
|
|
484
|
+
console.log(` ${C.red}Error: ${result.overall.error}${C.reset}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function printBenchmarkResult(result) {
|
|
489
|
+
const status = result.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
|
|
490
|
+
console.log(`\n ${status} ${C.bright}${result.url}${C.reset}`);
|
|
491
|
+
|
|
492
|
+
if (!result.success) {
|
|
493
|
+
console.log(` ${C.red}Failed: ${result.errors?.join(', ') || 'Unknown error'}${C.reset}`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const avgColor = getLatencyColor(result.avg);
|
|
498
|
+
console.log(` ${C.dim}Iterations:${C.reset} ${result.iterations}`);
|
|
499
|
+
console.log(` ${C.dim}Min:${C.reset} ${getLatencyColor(result.min)}${formatLatency(result.min)}${C.reset}`);
|
|
500
|
+
console.log(` ${C.dim}Max:${C.reset} ${getLatencyColor(result.max)}${formatLatency(result.max)}${C.reset}`);
|
|
501
|
+
console.log(` ${C.dim}Avg:${C.reset} ${avgColor}${formatLatency(result.avg)}${C.reset}`);
|
|
502
|
+
console.log(` ${C.dim}Median:${C.reset} ${getLatencyColor(result.median)}${formatLatency(result.median)}${C.reset}`);
|
|
503
|
+
console.log(` ${C.dim}P95:${C.reset} ${getLatencyColor(result.p95)}${formatLatency(result.p95)}${C.reset}`);
|
|
504
|
+
|
|
505
|
+
if (result.errors && result.errors.length > 0) {
|
|
506
|
+
console.log(` ${C.yellow}Errors: ${result.errors.length}${C.reset}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function printRecommendations(results, bestResult) {
|
|
511
|
+
console.log(`\n${C.cyan}── Recommendations ──${C.reset}\n`);
|
|
512
|
+
|
|
513
|
+
if (!bestResult || !bestResult.overall.success) {
|
|
514
|
+
console.log(` ${C.red}⚠ No healthy RPC endpoints found${C.reset}\n`);
|
|
515
|
+
console.log(` ${C.dim}Suggestions:${C.reset}`);
|
|
516
|
+
console.log(` 1. Check if your local validator is running: ${C.cyan}aether validator start${C.reset}`);
|
|
517
|
+
console.log(` 2. Verify RPC URL is correct`);
|
|
518
|
+
console.log(` 3. Check firewall settings for port 8899`);
|
|
519
|
+
console.log(` 4. Set custom RPC: ${C.cyan}export AETHER_RPC=http://your-rpc:8899${C.reset}`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
console.log(` ${C.green}✓ Best RPC:${C.reset} ${C.bright}${bestResult.url}${C.reset}`);
|
|
524
|
+
console.log(` ${C.dim}Latency: ${formatLatency(bestResult.overall.latency)} | Score: ${bestResult.overall.score}%${C.reset}\n`);
|
|
525
|
+
|
|
526
|
+
// Check for issues
|
|
527
|
+
const failedEndpoints = results.filter(r => !r.overall.success);
|
|
528
|
+
if (failedEndpoints.length > 0) {
|
|
529
|
+
console.log(` ${C.yellow}⚠ ${failedEndpoints.length} endpoint(s) unavailable${C.reset}`);
|
|
530
|
+
console.log(` ${C.dim}Run with --verbose for details${C.reset}\n`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Network health
|
|
534
|
+
const avgScore = results.reduce((sum, r) => sum + (r.overall.score || 0), 0) / results.length;
|
|
535
|
+
if (avgScore < 50) {
|
|
536
|
+
console.log(` ${C.yellow}⚠ Network health is degraded${C.reset}`);
|
|
537
|
+
console.log(` ${C.dim}Average score: ${avgScore.toFixed(0)}%${C.reset}\n`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ============================================================================
|
|
542
|
+
// Main Command
|
|
543
|
+
// ============================================================================
|
|
544
|
+
|
|
545
|
+
async function networkDiagnosticsCommand() {
|
|
546
|
+
const opts = parseArgs();
|
|
547
|
+
|
|
548
|
+
if (opts.help) {
|
|
549
|
+
showHelp();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!opts.json) {
|
|
554
|
+
console.log(`\n${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════╗`);
|
|
555
|
+
console.log(`${C.bright}${C.cyan}║ AETHER NETWORK DIAGNOSTICS ║`);
|
|
556
|
+
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════╝${C.reset}\n`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let results = [];
|
|
560
|
+
let bestResult = null;
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
if (opts.benchmark) {
|
|
564
|
+
// Benchmark mode
|
|
565
|
+
if (!opts.json) {
|
|
566
|
+
console.log(`${C.dim}Running latency benchmark...${C.reset}\n`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const endpoints = opts.all ? getEffectiveEndpoints() : [{ url: opts.rpc, name: 'Custom' }];
|
|
570
|
+
const benchmarks = [];
|
|
571
|
+
|
|
572
|
+
for (const ep of endpoints) {
|
|
573
|
+
if (!opts.json) {
|
|
574
|
+
process.stdout.write(` Testing ${ep.url}... `);
|
|
575
|
+
}
|
|
576
|
+
const result = await benchmarkEndpoint(ep.url);
|
|
577
|
+
benchmarks.push(result);
|
|
578
|
+
if (!opts.json) {
|
|
579
|
+
console.log(result.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (opts.json) {
|
|
584
|
+
console.log(JSON.stringify({ benchmarks, timestamp: new Date().toISOString() }, null, 2));
|
|
585
|
+
} else {
|
|
586
|
+
console.log(`\n${C.cyan}── Benchmark Results ──${C.reset}`);
|
|
587
|
+
for (const result of benchmarks) {
|
|
588
|
+
printBenchmarkResult(result);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Summary
|
|
592
|
+
const successful = benchmarks.filter(b => b.success);
|
|
593
|
+
if (successful.length > 0) {
|
|
594
|
+
const fastest = successful.sort((a, b) => a.avg - b.avg)[0];
|
|
595
|
+
console.log(`\n${C.green}✓ Fastest RPC:${C.reset} ${C.bright}${fastest.url}${C.reset} (${formatLatency(fastest.avg)} avg)`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (opts.auto) {
|
|
603
|
+
// Auto-select mode
|
|
604
|
+
const endpoints = getEffectiveEndpoints();
|
|
605
|
+
|
|
606
|
+
if (!opts.json) {
|
|
607
|
+
console.log(`${C.dim}Testing ${endpoints.length} RPC endpoint(s)...${C.reset}\n`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
results = await findBestEndpoint(endpoints, opts.timeout);
|
|
611
|
+
|
|
612
|
+
if (results.length > 0) {
|
|
613
|
+
bestResult = results[0];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (opts.json) {
|
|
617
|
+
console.log(JSON.stringify({
|
|
618
|
+
results,
|
|
619
|
+
recommended: bestResult?.url || null,
|
|
620
|
+
timestamp: new Date().toISOString(),
|
|
621
|
+
}, null, 2));
|
|
622
|
+
} else {
|
|
623
|
+
console.log(`${C.cyan}── Test Results ──${C.reset}`);
|
|
624
|
+
for (const result of results) {
|
|
625
|
+
printResult(result);
|
|
626
|
+
}
|
|
627
|
+
printRecommendations(results, bestResult);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Update config with best RPC
|
|
631
|
+
if (bestResult?.overall?.success) {
|
|
632
|
+
const cfg = loadConfig();
|
|
633
|
+
cfg.preferredRpc = bestResult.url;
|
|
634
|
+
saveConfig(cfg);
|
|
635
|
+
|
|
636
|
+
if (!opts.json) {
|
|
637
|
+
console.log(`\n ${C.green}✓ Saved preferred RPC to config${C.reset}`);
|
|
638
|
+
console.log(` ${C.dim}Set AETHER_RPC=${bestResult.url} to use this endpoint${C.reset}\n`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Single RPC test mode
|
|
646
|
+
if (!opts.json) {
|
|
647
|
+
console.log(`${C.dim}Testing RPC endpoint...${C.reset}`);
|
|
648
|
+
console.log(` ${C.dim}URL: ${opts.rpc}${C.reset}`);
|
|
649
|
+
console.log(` ${C.dim}Timeout: ${opts.timeout}ms${C.reset}\n`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const result = await testEndpoint(opts.rpc, opts.timeout);
|
|
653
|
+
results = [result];
|
|
654
|
+
bestResult = result.overall.success ? result : null;
|
|
655
|
+
|
|
656
|
+
if (opts.json) {
|
|
657
|
+
console.log(JSON.stringify(result, null, 2));
|
|
658
|
+
} else {
|
|
659
|
+
printResult(result, true);
|
|
660
|
+
|
|
661
|
+
// Health summary
|
|
662
|
+
console.log(`\n${C.cyan}── Health Summary ──${C.reset}\n`);
|
|
663
|
+
if (result.overall.success) {
|
|
664
|
+
console.log(` ${C.green}✓ RPC is healthy${C.reset}`);
|
|
665
|
+
console.log(` ${C.dim} Score: ${result.overall.score}%${C.reset}`);
|
|
666
|
+
console.log(` ${C.dim} Latency: ${formatLatency(result.overall.latency)}${C.reset}`);
|
|
667
|
+
|
|
668
|
+
if (result.tests?.slot?.slot !== undefined) {
|
|
669
|
+
console.log(` ${C.dim} Current slot: ${result.tests.slot.slot}${C.reset}`);
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
console.log(` ${C.red}✗ RPC is unhealthy${C.reset}`);
|
|
673
|
+
console.log(` ${C.dim} Error: ${result.overall.error || 'Unknown error'}${C.reset}`);
|
|
674
|
+
console.log(`\n ${C.dim}Troubleshooting:${C.reset}`);
|
|
675
|
+
console.log(` • Check if validator is running: ${C.cyan}aether validator status${C.reset}`);
|
|
676
|
+
console.log(` • Verify RPC URL: ${opts.rpc}`);
|
|
677
|
+
console.log(` • Check network connectivity`);
|
|
678
|
+
console.log(` • Try: ${C.cyan}aether network-diagnostics --auto${C.reset}`);
|
|
679
|
+
}
|
|
680
|
+
console.log();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
} catch (err) {
|
|
684
|
+
if (opts.json) {
|
|
685
|
+
console.log(JSON.stringify({
|
|
686
|
+
error: err.message,
|
|
687
|
+
stack: err.stack,
|
|
688
|
+
timestamp: new Date().toISOString(),
|
|
689
|
+
}, null, 2));
|
|
690
|
+
} else {
|
|
691
|
+
console.log(`\n ${C.red}✗ Diagnostics failed:${C.reset} ${err.message}\n`);
|
|
692
|
+
}
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Export for module use
|
|
698
|
+
module.exports = { networkDiagnosticsCommand };
|
|
699
|
+
|
|
700
|
+
// Run if called directly
|
|
701
|
+
if (require.main === module) {
|
|
702
|
+
networkDiagnosticsCommand().catch(err => {
|
|
703
|
+
console.error(`${C.red}✗ Unexpected error:${C.reset}`, err.message);
|
|
704
|
+
process.exit(1);
|
|
705
|
+
});
|
|
706
|
+
}
|