@jellylegsai/aether-cli 1.9.2 → 2.0.1

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.
@@ -1,626 +1,623 @@
1
- #!/usr/bin/env node
2
- /**
3
- * aether-cli validators
4
- *
5
- * List, filter, and inspect validators on the Aether network.
6
- * Shows identity, tier, stake, APY, uptime, and performance metrics.
7
- *
8
- * Usage:
9
- * aether validators list List all active validators
10
- * aether validators list --tier full Filter by tier (full|lite|observer)
11
- * aether validators list --json JSON output for scripting
12
- * aether validators list --rpc <url> Query a specific RPC endpoint
13
- * aether validators list --sort stake Sort by stake (default: score)
14
- *
15
- * Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
16
- *
17
- * SDK wired to: GET /v1/validators, GET /v1/epoch, GET /v1/supply
18
- */
19
-
20
- const path = require('path');
21
-
22
- // Import SDK for real blockchain RPC calls
23
- const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
24
- const aether = require(sdkPath);
25
-
26
- // ANSI colours
27
- const C = {
28
- reset: '\x1b[0m',
29
- bright: '\x1b[1m',
30
- dim: '\x1b[2m',
31
- red: '\x1b[31m',
32
- green: '\x1b[32m',
33
- yellow: '\x1b[33m',
34
- blue: '\x1b[34m',
35
- cyan: '\x1b[36m',
36
- magenta: '\x1b[35m',
37
- };
38
-
39
- function getDefaultRpc() {
40
- return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
41
- }
42
-
43
- function createClient(rpcUrl) {
44
- return new aether.AetherClient({ rpcUrl });
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // Argument parsing
49
- // ---------------------------------------------------------------------------
50
-
51
- function parseArgs() {
52
- const args = process.argv.slice(3); // [node, index.js, validators, list, ...]
53
- const opts = {
54
- rpc: getDefaultRpc(),
55
- subcmd: 'list',
56
- tier: null,
57
- asJson: false,
58
- sortBy: 'score',
59
- limit: 100,
60
- rank: false,
61
- };
62
-
63
- for (let i = 0; i < args.length; i++) {
64
- const arg = args[i];
65
- if (arg === '--tier' || arg === '-t') {
66
- const tier = (args[++i] || '').toLowerCase();
67
- if (['full', 'lite', 'observer'].includes(tier)) {
68
- opts.tier = tier;
69
- } else {
70
- console.log(` ${C.yellow}⚠ Invalid tier "${tier}". Valid: full, lite, observer${C.reset}`);
71
- }
72
- } else if (arg === '--json' || arg === '-j') {
73
- opts.asJson = true;
74
- } else if (arg === '--rpc' || arg === '-r') {
75
- opts.rpc = args[++i];
76
- } else if (arg === '--sort' || arg === '-s') {
77
- const sortVal = (args[++i] || '').toLowerCase();
78
- if (['stake', 'score', 'apy', 'uptime', 'name'].includes(sortVal)) {
79
- opts.sortBy = sortVal;
80
- } else {
81
- console.log(` ${C.yellow}⚠ Invalid sort "${sortVal}". Valid: stake, score, apy, uptime, name${C.reset}`);
82
- }
83
- } else if (arg === '--limit' || arg === '-l') {
84
- const limit = parseInt(args[++i], 10);
85
- if (!isNaN(limit) && limit > 0 && limit <= 500) {
86
- opts.limit = limit;
87
- }
88
- } else if (arg === '--help' || arg === '-h') {
89
- showHelp();
90
- process.exit(0);
91
- } else if (arg === '--rank') {
92
- opts.rank = true;
93
- opts.subcmd = 'rank';
94
- }
95
- }
96
-
97
- return opts;
98
- }
99
-
100
- function showHelp() {
101
- const defaultRpc = getDefaultRpc();
102
- console.log(`
103
- ${C.bright}${C.cyan}aether-cli validators${C.reset} - List and inspect Aether validators
104
-
105
- ${C.bright}Usage:${C.reset}
106
- aether validators list [options]
107
- aether validators rank [options] Ranked leaderboard (sorted by stake)
108
-
109
- ${C.bright}Options (list):${C.reset}
110
- -t, --tier <type> Filter by tier: full, lite, observer
111
- -s, --sort <field> Sort by: stake, score, apy, uptime, name (default: score)
112
- -l, --limit <n> Max validators to show (default: 100, max: 500)
113
- -r, --rpc <url> RPC endpoint (default: ${defaultRpc} or $AETHER_RPC)
114
- -j, --json Output raw JSON (for scripting)
115
- -h, --help Show this help message
116
-
117
- ${C.bright}Options (rank):${C.reset}
118
- -t, --tier <type> Filter by tier: full, lite, observer
119
- -l, --limit <n> Max validators to show (default: 50, max: 200)
120
- -r, --rpc <url> RPC endpoint (default: ${defaultRpc} or $AETHER_RPC)
121
- -j, --json Output raw JSON (for scripting)
122
- -h, --help Show this help message
123
-
124
- ${C.bright}SDK Methods Used:${C.reset}
125
- client.getValidators() → GET /v1/validators
126
- client.getEpochInfo() → GET /v1/epoch
127
- client.getSupply() GET /v1/supply
128
-
129
- ${C.bright}Examples:${C.reset}
130
- aether validators list # All validators, sorted by score
131
- aether validators list --tier full # Full validators only
132
- aether validators list --sort stake # Sort by total stake
133
- aether validators list --sort apy # Sort by estimated APY
134
- aether validators list --json # JSON for scripts
135
- aether validators rank # Top validators by stake (leaderboard)
136
- aether validators rank --tier full # Full validators only
137
- aether validators rank --limit 20 # Top 20 validators
138
- aether validators list --rpc http://custom-rpc:8899
139
- `.trim());
140
- }
141
-
142
- // ---------------------------------------------------------------------------
143
- // Data fetchers using SDK - Real blockchain RPC calls
144
- // ---------------------------------------------------------------------------
145
-
146
- /** Fetch all validators from the network using SDK */
147
- async function fetchValidators(rpc) {
148
- try {
149
- const client = createClient(rpc);
150
- // SDK getValidators() → GET /v1/validators
151
- const result = await client.getValidators();
152
- return Array.isArray(result) ? result : [];
153
- } catch {
154
- return [];
155
- }
156
- }
157
-
158
- /** Fetch epoch info for APY calculations using SDK */
159
- async function fetchEpochInfo(rpc) {
160
- try {
161
- const client = createClient(rpc);
162
- // SDK getEpochInfo() → GET /v1/epoch
163
- const result = await client.getEpochInfo();
164
- return result || null;
165
- } catch {
166
- return null;
167
- }
168
- }
169
-
170
- /** Fetch network-wide stake totals for APY estimation using SDK */
171
- async function fetchSupply(rpc) {
172
- try {
173
- const client = createClient(rpc);
174
- // SDK getSupply() → GET /v1/supply
175
- const result = await client.getSupply();
176
- return result || null;
177
- } catch {
178
- return null;
179
- }
180
- }
181
-
182
- // ---------------------------------------------------------------------------
183
- // Normalise validator record from various RPC response shapes
184
- // ---------------------------------------------------------------------------
185
-
186
- function normaliseValidator(v) {
187
- // Handle different response shapes from different RPC implementations
188
- const pubkey = v.pubkey || v.address || v.identity || v.id || null;
189
- const name = v.name || v.moniker || v.label || v.identity_name || null;
190
- const tier = (v.tier || v.node_type || v.type || 'full').toLowerCase();
191
- const stake = BigInt(v.stake || v.delegatedStake || v.stake_lamports || v.lamports || 0);
192
- const score = v.score !== undefined ? v.score : (v.uptime !== undefined ? Math.round(v.uptime * 100) : null);
193
- const apy = v.apy !== undefined ? v.apy : (v.apy_bps !== undefined ? v.apy_bps / 100 : null);
194
- const commission = v.commission !== undefined ? v.commission : (v.commission_bps !== undefined ? v.commission_bps / 100 : null);
195
- const version = v.version || v.agent || v.app_version || null;
196
- const ip = v.ip || v.remote || null;
197
- const lastVote = v.last_vote || v.lastVote || null;
198
- const epoch = v.epoch || null;
199
- const voteAccount = v.vote_account || v.voteAccount || null;
200
-
201
- return {
202
- pubkey,
203
- name,
204
- tier,
205
- stake: stake.toString(),
206
- stakeFormatted: formatAether(stake),
207
- stakeAeth: Number(stake) / 1e9,
208
- score,
209
- apy,
210
- commission,
211
- version,
212
- ip,
213
- lastVote,
214
- epoch,
215
- voteAccount,
216
- // Raw for JSON export
217
- _raw: v,
218
- };
219
- }
220
-
221
- function formatAether(lamports) {
222
- const aeth = Number(lamports) / 1e9;
223
- if (aeth === 0) return '0 AETH';
224
- return aeth.toFixed(2).replace(/\.?0+$/, '') + ' AETH';
225
- }
226
-
227
- function formatScore(score) {
228
- if (score === null || score === undefined) return `${C.dim}—${C.reset}`;
229
- if (score >= 80) return `${C.green}${score}${C.reset}`;
230
- if (score >= 50) return `${C.yellow}${score}${C.reset}`;
231
- return `${C.red}${score}${C.reset}`;
232
- }
233
-
234
- // ---------------------------------------------------------------------------
235
- // Render outputs
236
- // ---------------------------------------------------------------------------
237
-
238
- function tierColor(tier) {
239
- if (tier === 'full') return `${C.cyan}FULL${C.reset}`;
240
- if (tier === 'lite') return `${C.yellow}LITE${C.reset}`;
241
- if (tier === 'observer') return `${C.green}OBS${C.reset}`;
242
- return `${C.dim}${tier.toUpperCase()}${C.reset}`;
243
- }
244
-
245
- function tierBadge(tier) {
246
- if (tier === 'full') return `${C.cyan}◆ FULL${C.reset}`;
247
- if (tier === 'lite') return `${C.yellow}◇ LITE${C.reset}`;
248
- if (tier === 'observer') return `${C.green}○ OBS${C.reset}`;
249
- return `${C.dim}[${tier}]${C.reset}`;
250
- }
251
-
252
- function renderTable(validators, opts) {
253
- const sortBy = opts.sortBy;
254
- const tier = opts.tier;
255
-
256
- // Sort validators
257
- const sorted = [...validators].sort((a, b) => {
258
- if (sortBy === 'stake') return b.stakeAeth - a.stakeAeth;
259
- if (sortBy === 'score') return (b.score || 0) - (a.score || 0);
260
- if (sortBy === 'apy') return (b.apy || 0) - (a.apy || 0);
261
- if (sortBy === 'name') return (a.name || '').localeCompare(b.name || '');
262
- return 0;
263
- });
264
-
265
- // Filter by tier
266
- const filtered = tier
267
- ? sorted.filter(v => v.tier === tier)
268
- : sorted;
269
-
270
- const shown = filtered.slice(0, opts.limit);
271
- const total = filtered.length;
272
-
273
- // Header
274
- console.log();
275
- console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════════════════╗${C.reset}`);
276
- console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER VALIDATORS${C.reset} ${C.dim}(total: ${total})${C.reset} ${C.bright}║${C.reset}`);
277
- console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════════════════╝${C.reset}`);
278
- if (tier) console.log(` ${C.dim}Tier filter: ${tier.toUpperCase()} Sort: ${sortBy} RPC: ${opts.rpc}${C.reset}`);
279
- else console.log(` ${C.dim}Sort: ${sortBy} RPC: ${opts.rpc}${C.reset}`);
280
- console.log();
281
-
282
- if (shown.length === 0) {
283
- console.log(` ${C.yellow}⚠ No validators found${C.reset}${tier ? ` for tier "${tier}"` : ''}.`);
284
- console.log(` ${C.dim} Check your RPC endpoint: ${opts.rpc}${C.reset}\n`);
285
- return;
286
- }
287
-
288
- // Table header
289
- console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────────────────────┐${C.reset}`);
290
- console.log(
291
- ` ${C.bright}│${C.reset}` +
292
- ` ${C.cyan}#${C.reset}`.padEnd(4) +
293
- `${C.cyan}Validator${C.reset}`.padEnd(36) +
294
- `${C.cyan}Tier${C.reset}`.padEnd(8) +
295
- `${C.cyan}Stake${C.reset}`.padEnd(14) +
296
- `${C.cyan}Score${C.reset}`.padEnd(8) +
297
- `${C.cyan}APY${C.reset}`.padEnd(8) +
298
- `${C.cyan}Version${C.reset}`.padEnd(10) +
299
- `${C.bright}│${C.reset}`
300
- );
301
- console.log(` ${C.bright}├${'─'.repeat(90)}${C.bright}│${C.reset}`);
302
-
303
- for (let i = 0; i < shown.length; i++) {
304
- const v = shown[i];
305
- const num = (i + 1).toString().padStart(3);
306
- const nameOrKey = v.name
307
- ? v.name.substring(0, 20).padEnd(20)
308
- : (v.pubkey ? v.pubkey.substring(0, 20).padEnd(20) : 'unknown'.padEnd(20));
309
- const tierStr = tierBadge(v.tier);
310
- const stakeStr = v.stakeFormatted.padEnd(12);
311
- const scoreStr = v.score !== null && v.score !== undefined
312
- ? `${v.score}%`.padEnd(6)
313
- : '—'.padEnd(6);
314
- const apyStr = v.apy !== null && v.apy !== undefined
315
- ? `${v.apy.toFixed(1)}%`.padEnd(6)
316
- : ''.padEnd(6);
317
- const versionStr = v.version ? v.version.substring(0, 10).padEnd(10) : '—'.padEnd(10);
318
-
319
- const scoreColor = v.score === null || v.score === undefined ? C.dim
320
- : v.score >= 80 ? C.green
321
- : v.score >= 50 ? C.yellow
322
- : C.red;
323
-
324
- console.log(
325
- ` ${C.bright}│${C.reset}` +
326
- ` ${C.dim}${num}${C.reset} `.substring(0, 5) +
327
- `${C.cyan}${nameOrKey}${C.reset} ` +
328
- `${tierStr} `.substring(0, 9) +
329
- `${C.green}${stakeStr}${C.reset} ` +
330
- `${scoreColor}${scoreStr}${C.reset} ` +
331
- `${C.green}${apyStr}${C.reset} ` +
332
- `${C.dim}${versionStr}${C.reset} ` +
333
- `${C.bright}│${C.reset}`
334
- );
335
- }
336
-
337
- console.log(` ${C.bright}└${''.repeat(90)}${C.bright}│${C.reset}`);
338
- console.log();
339
-
340
- // Summary row
341
- const totalStake = shown.reduce((sum, v) => sum + v.stakeAeth, 0);
342
- const avgScore = shown.reduce((sum, v) => sum + (v.score || 0), 0) / shown.filter(v => v.score !== null).length;
343
- const fullCount = shown.filter(v => v.tier === 'full').length;
344
- const liteCount = shown.filter(v => v.tier === 'lite').length;
345
- const obsCount = shown.filter(v => v.tier === 'observer').length;
346
-
347
- console.log(` ${C.dim}Showing ${shown.length} of ${total} validators${total !== shown.length ? ` (limit ${opts.limit})` : ''}${C.reset}`);
348
- console.log(` ${C.dim}Total stake shown: ${C.reset}${C.green}${totalStake.toFixed(2)} AETH${C.reset} ${C.dim}Avg score: ${C.reset}${avgScore ? `${avgScore.toFixed(1)}%` : '—'}${C.reset}`);
349
- if (!tier) {
350
- console.log(` ${C.cyan}◆${C.reset} ${C.cyan}Full${C.reset}: ${fullCount} ${C.yellow}◇${C.reset} ${C.yellow}Lite${C.reset}: ${liteCount} ${C.green}○${C.reset} ${C.green}Observer${C.reset}: ${obsCount}`);
351
- }
352
- console.log();
353
- console.log(` ${C.dim}Tip: --tier full|lite|observer | --sort stake|score|apy|name | --json for data${C.reset}`);
354
- console.log();
355
- }
356
-
357
- function renderJson(validators, opts) {
358
- const tier = opts.tier;
359
- const filtered = tier
360
- ? validators.filter(v => v.tier === tier)
361
- : validators;
362
-
363
- const out = {
364
- rpc: opts.rpc,
365
- total: filtered.length,
366
- sort: opts.sortBy,
367
- tier_filter: tier,
368
- fetched_at: new Date().toISOString(),
369
- validators: filtered.map(v => ({
370
- pubkey: v.pubkey,
371
- name: v.name,
372
- tier: v.tier,
373
- stake: v.stake,
374
- stake_aeth: v.stakeAeth,
375
- stake_formatted: v.stakeFormatted,
376
- score: v.score,
377
- apy: v.apy,
378
- commission: v.commission,
379
- version: v.version,
380
- ip: v.ip,
381
- vote_account: v.voteAccount,
382
- last_vote: v.lastVote,
383
- epoch: v.epoch,
384
- })),
385
- };
386
-
387
- console.log(JSON.stringify(out, null, 2));
388
- }
389
-
390
- // ---------------------------------------------------------------------------
391
- // Main
392
- // ---------------------------------------------------------------------------
393
-
394
- async function validatorsList(opts) {
395
- const rpc = opts.rpc;
396
-
397
- if (!opts.asJson) {
398
- console.log(`${C.dim}Fetching validators from ${rpc}...${C.reset}`);
399
- }
400
-
401
- const [rawValidators, epochInfo, supply] = await Promise.all([
402
- fetchValidators(rpc),
403
- fetchEpochInfo(rpc),
404
- fetchSupply(rpc),
405
- ]);
406
-
407
- if (rawValidators.length === 0) {
408
- if (opts.asJson) {
409
- console.log(JSON.stringify({ rpc, validators: [], total: 0, error: 'No validator data returned from RPC' }, null, 2));
410
- } else {
411
- console.log(`\n ${C.yellow}⚠ No validator data returned from RPC.${C.reset}`);
412
- console.log(` ${C.dim} RPC: ${rpc}${C.reset}`);
413
- console.log(` ${C.dim} Check that your validator is running and the RPC endpoint is accessible.${C.reset}`);
414
- console.log(` ${C.dim} Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
415
- }
416
- return;
417
- }
418
-
419
- // Normalise all validators
420
- let validators = rawValidators.map(normaliseValidator);
421
-
422
- // Estimate APY if not provided by RPC (rough approximation)
423
- if (supply && !supply.error) {
424
- const totalStake = Number(supply.total_staked || supply.total || 0);
425
- const rewardsPerEpoch = Number(epochInfo?.rewards_per_epoch || '2000000000');
426
- if (totalStake > 0 && rewardsPerEpoch > 0) {
427
- const apyEstimate = (rewardsPerEpoch / totalStake) * 73; // ~73 epochs/year
428
- validators = validators.map(v => {
429
- if (v.apy === null || v.apy === undefined) {
430
- return { ...v, apy: apyEstimate };
431
- }
432
- return v;
433
- });
434
- }
435
- }
436
-
437
- if (opts.asJson) {
438
- renderJson(validators, opts);
439
- } else {
440
- renderTable(validators, opts);
441
- }
442
- }
443
-
444
- // ---------------------------------------------------------------------------
445
- // Validators Rank leaderboard sorted by stake
446
- // ---------------------------------------------------------------------------
447
-
448
- function rankBadge(rank) {
449
- if (rank === 1) return `${C.yellow}🥇 1${C.reset}`;
450
- if (rank === 2) return `${C.dim}🥈 2${C.reset}`;
451
- if (rank === 3) return `${C.yellow}🥉 3${C.reset}`;
452
- return `${C.dim}${String(rank).padStart(3)}${C.reset}`;
453
- }
454
-
455
- function renderRankTable(validators, opts) {
456
- const tier = opts.tier;
457
-
458
- // Sort by stake descending, filter by tier
459
- const sorted = [...validators]
460
- .sort((a, b) => b.stakeAeth - a.stakeAeth);
461
-
462
- const filtered = tier
463
- ? sorted.filter(v => v.tier === tier)
464
- : sorted;
465
-
466
- const shown = filtered.slice(0, opts.limit);
467
- const total = filtered.length;
468
-
469
- // Total staked across all shown validators
470
- const totalStaked = shown.reduce((sum, v) => sum + v.stakeAeth, 0);
471
- const maxStake = shown.length > 0 ? shown[0].stakeAeth : 1;
472
-
473
- console.log();
474
- console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════════════════════╗${C.reset}`);
475
- console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER VALIDATOR LEADERBOARD${C.reset} ${C.dim}(${total} validators)${C.reset} ${C.bright}║${C.reset}`);
476
- console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════════════════════╝${C.reset}`);
477
- if (tier) console.log(` ${C.dim}Tier: ${tier.toUpperCase()} RPC: ${opts.rpc}${C.reset}`);
478
- else console.log(` ${C.dim}Sorted by stake RPC: ${opts.rpc}${C.reset}`);
479
- console.log();
480
- console.log(` ${C.bright}┌────────────────────────────────────────────────────────────────────────────────────────────┐${C.reset}`);
481
- console.log(
482
- ` ${C.bright}│${C.reset}` +
483
- ` ${C.cyan}Rank${C.reset}`.padEnd(6) +
484
- `${C.cyan}Validator${C.reset}`.padEnd(34) +
485
- `${C.cyan}Tier${C.reset}`.padEnd(8) +
486
- `${C.cyan}Stake (AETH)${C.reset}`.padEnd(16) +
487
- `${C.cyan}Score${C.reset}`.padEnd(8) +
488
- `${C.cyan}APY${C.reset}`.padEnd(8) +
489
- `${C.bright}│${C.reset}`
490
- );
491
- console.log(` ${C.bright}├${'─'.repeat(94)}${C.bright}│${C.reset}`);
492
-
493
- for (let i = 0; i < shown.length; i++) {
494
- const v = shown[i];
495
- const rank = i + 1;
496
- const rankStr = rankBadge(rank);
497
- const nameOrKey = v.name
498
- ? v.name.substring(0, 20).padEnd(20)
499
- : (v.pubkey ? v.pubkey.substring(0, 20).padEnd(20) : 'unknown'.padEnd(20));
500
- const tierStr = tierBadge(v.tier);
501
- const stakeFormatted = v.stakeFormatted.padEnd(14);
502
- const scoreStr = v.score !== null && v.score !== undefined
503
- ? `${v.score}%`.padEnd(6)
504
- : ''.padEnd(6);
505
- const apyStr = v.apy !== null && v.apy !== undefined
506
- ? `${v.apy.toFixed(1)}%`.padEnd(6)
507
- : '—'.padEnd(6);
508
-
509
- const scoreColor = v.score === null || v.score === undefined ? C.dim
510
- : v.score >= 80 ? C.green
511
- : v.score >= 50 ? C.yellow
512
- : C.red;
513
-
514
- // Mini bar for relative stake
515
- const barLen = 12;
516
- const fillLen = maxStake > 0 ? Math.round((v.stakeAeth / maxStake) * barLen) : 0;
517
- const stakeBar = `${C.green}${''.repeat(fillLen)}${C.dim}${'░'.repeat(barLen - fillLen)}${C.reset}`;
518
-
519
- console.log(
520
- ` ${C.bright}│${C.reset}` +
521
- ` ${rankStr} `.substring(0, 7) +
522
- `${C.cyan}${nameOrKey}${C.reset} ` +
523
- `${tierStr} `.substring(0, 9) +
524
- `${stakeBar}${C.reset} ` +
525
- `${C.green}${stakeFormatted}${C.reset} ` +
526
- `${scoreColor}${scoreStr}${C.reset} ` +
527
- `${C.green}${apyStr}${C.reset} ` +
528
- `${C.bright}│${C.reset}`
529
- );
530
- }
531
-
532
- console.log(` ${C.bright}└${'─'.repeat(94)}${C.bright}│${C.reset}`);
533
- console.log();
534
-
535
- // Summary
536
- const avgApy = shown.reduce((sum, v) => sum + (v.apy || 0), 0) / shown.filter(v => v.apy !== null).length;
537
- console.log(` ${C.dim}Total staked (shown): ${C.reset}${C.green}${totalStaked.toFixed(2)} AETH${C.reset} ${C.dim}Avg APY: ${C.reset}${avgApy.toFixed(1)}% ${C.dim}Top stake: ${C.reset}${maxStake.toFixed(2)} AETH${C.reset}`);
538
- console.log(` ${C.dim}Run with ${C.cyan}--json${C.reset}${C.dim} for raw data, ${C.cyan}--limit 20${C.reset}${C.dim} for top 20${C.reset}`);
539
- console.log();
540
- }
541
-
542
- async function validatorsRank(opts) {
543
- const rpc = opts.rpc;
544
- const limit = Math.min(opts.limit || 50, 200);
545
-
546
- if (!opts.asJson) {
547
- console.log(`${C.dim}Fetching validator leaderboard from ${rpc}...${C.reset}`);
548
- }
549
-
550
- const [rawValidators, epochInfo, supply] = await Promise.all([
551
- fetchValidators(rpc),
552
- fetchEpochInfo(rpc),
553
- fetchSupply(rpc),
554
- ]);
555
-
556
- if (rawValidators.length === 0) {
557
- if (opts.asJson) {
558
- console.log(JSON.stringify({ rpc, validators: [], total: 0, error: 'No validator data returned from RPC' }, null, 2));
559
- } else {
560
- console.log(`\n ${C.yellow}⚠ No validator data returned from RPC.${C.reset}`);
561
- console.log(` ${C.dim} RPC: ${rpc}${C.reset}`);
562
- console.log(` ${C.dim} Check that your validator is running and the RPC endpoint is accessible.${C.reset}\n`);
563
- }
564
- return;
565
- }
566
-
567
- let validators = rawValidators.map(normaliseValidator);
568
-
569
- // Estimate APY if not provided
570
- if (supply && !supply.error) {
571
- const totalStake = Number(supply.total_staked || supply.total || 0);
572
- const rewardsPerEpoch = Number(epochInfo?.rewards_per_epoch || '2000000000');
573
- if (totalStake > 0 && rewardsPerEpoch > 0) {
574
- const apyEstimate = (rewardsPerEpoch / totalStake) * 73;
575
- validators = validators.map(v => {
576
- if (v.apy === null || v.apy === undefined) {
577
- return { ...v, apy: apyEstimate };
578
- }
579
- return v;
580
- });
581
- }
582
- }
583
-
584
- opts.limit = limit;
585
-
586
- if (opts.asJson) {
587
- const ranked = [...validators]
588
- .sort((a, b) => b.stakeAeth - a.stakeAeth)
589
- .filter(v => !opts.tier || v.tier === opts.tier)
590
- .slice(0, limit)
591
- .map((v, i) => ({ rank: i + 1, ...v }));
592
- console.log(JSON.stringify({ rpc, validators: ranked, total: ranked.length, fetched_at: new Date().toISOString() }, null, 2));
593
- } else {
594
- renderRankTable(validators, opts);
595
- }
596
- }
597
-
598
- // ---------------------------------------------------------------------------
599
- // Entry point
600
- // ---------------------------------------------------------------------------
601
-
602
- async function main() {
603
- const opts = parseArgs();
604
-
605
- if (opts.subcmd === 'list') {
606
- await validatorsList(opts);
607
- } else if (opts.subcmd === 'rank') {
608
- await validatorsRank(opts);
609
- } else {
610
- console.log(`\n ${C.red}Unknown subcommand:${C.reset} ${opts.subcmd}`);
611
- console.log(` ${C.dim}Usage: aether validators list [--tier full] [--sort stake] [--json]${C.reset}\n`);
612
- process.exit(1);
613
- }
614
- }
615
-
616
- main().catch(err => {
617
- console.error(`\n${C.red}✗ Validators command failed:${C.reset} ${err.message}`);
618
- console.error(` ${C.dim}Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
619
- process.exit(1);
620
- });
621
-
622
- module.exports = { validatorsListCommand: main };
623
-
624
- if (require.main === module) {
625
- main();
626
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli validators
4
+ *
5
+ * List and manage validators on the Aether network.
6
+ * Fully wired to @jellylegsai/aether-sdk for real blockchain RPC calls.
7
+ *
8
+ * Usage:
9
+ * aether validators list [--tier full|lite|observer] [--json]
10
+ * aether validators info <address> [--json]
11
+ * aether validators top [--limit 10] [--json]
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+ const readline = require('readline');
18
+
19
+ // Import SDK for real blockchain RPC calls
20
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
21
+ const aether = require(sdkPath);
22
+
23
+ // Import UI framework for consistent branding
24
+ const { BRANDING, C, indicators, drawBox, drawTable,
25
+ success, error, warning, info, code, highlight, value,
26
+ startSpinner, stopSpinner, progressBar, progressBarColored,
27
+ formatHelp } = require('../lib/ui');
28
+
29
+ const CLI_VERSION = '2.0.0';
30
+
31
+ // ============================================================================
32
+ // SDK Setup
33
+ // ============================================================================
34
+
35
+ function getDefaultRpc() {
36
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
37
+ }
38
+
39
+ function createClient(rpcUrl) {
40
+ return new aether.AetherClient({ rpcUrl });
41
+ }
42
+
43
+ // ============================================================================
44
+ // Config Helpers
45
+ // ============================================================================
46
+
47
+ function getAetherDir() {
48
+ return path.join(os.homedir(), '.aether');
49
+ }
50
+
51
+ function getConfigPath() {
52
+ return path.join(getAetherDir(), 'config.json');
53
+ }
54
+
55
+ function loadConfig() {
56
+ if (!fs.existsSync(getConfigPath())) {
57
+ return { defaultWallet: null };
58
+ }
59
+ try {
60
+ return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
61
+ } catch {
62
+ return { defaultWallet: null };
63
+ }
64
+ }
65
+
66
+ // ============================================================================
67
+ // Format Helpers
68
+ // ============================================================================
69
+
70
+ function formatAether(lamports) {
71
+ if (!lamports || lamports === '0') return '0 AETH';
72
+ const aeth = Number(lamports) / 1e9;
73
+ if (aeth >= 1_000_000) {
74
+ return (aeth / 1_000_000).toFixed(2) + 'M AETH';
75
+ }
76
+ if (aeth >= 1_000) {
77
+ return (aeth / 1_000).toFixed(2) + 'K AETH';
78
+ }
79
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
80
+ }
81
+
82
+ function formatNumber(n) {
83
+ if (n === null || n === undefined) return 'N/A';
84
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
85
+ }
86
+
87
+ function shortAddress(addr) {
88
+ if (!addr || addr.length < 16) return addr || 'unknown';
89
+ return addr.slice(0, 8) + '...' + addr.slice(-8);
90
+ }
91
+
92
+ function formatPercent(val) {
93
+ if (val === undefined || val === null) return 'N/A';
94
+ const pct = Number(val);
95
+ if (isNaN(pct)) return 'N/A';
96
+ return pct.toFixed(2) + '%';
97
+ }
98
+
99
+ function formatAPY(apy) {
100
+ if (!apy && apy !== 0) return `${C.dim}N/A${C.reset}`;
101
+ const val = Number(apy);
102
+ if (isNaN(val)) return `${C.dim}N/A${C.reset}`;
103
+ if (val >= 10) return `${C.green}${val.toFixed(2)}%${C.reset}`;
104
+ if (val >= 5) return `${C.yellow}${val.toFixed(2)}%${C.reset}`;
105
+ return `${C.cyan}${val.toFixed(2)}%${C.reset}`;
106
+ }
107
+
108
+ // ============================================================================
109
+ // Fetch Validators via SDK
110
+ // ============================================================================
111
+
112
+ async function fetchValidators(rpcUrl) {
113
+ const client = createClient(rpcUrl);
114
+
115
+ try {
116
+ const validators = await client.getValidators();
117
+ if (!Array.isArray(validators)) return [];
118
+
119
+ return validators.map(v => ({
120
+ address: v.vote_account || v.pubkey || v.address || v.identity || 'unknown',
121
+ identity: v.identity || v.node_pubkey || v.address,
122
+ stake: v.stake_lamports || v.activated_stake || v.stake || 0,
123
+ commission: v.commission || v.commission_bps || 0,
124
+ apy: v.apy || v.return_rate || v.estimated_apy || 0,
125
+ name: v.name || v.moniker || v.identity_name || 'Unknown',
126
+ tier: v.tier || 'unknown',
127
+ active: v.active !== false && v.delinquent !== true,
128
+ delinquent: v.delinquent === true,
129
+ skipRate: v.skip_rate || 0,
130
+ uptime: v.uptime || 0,
131
+ lastVote: v.last_vote || v.last_vote_slot || 0,
132
+ version: v.version || 'unknown',
133
+ website: v.website || null,
134
+ details: v.details || null,
135
+ }));
136
+ } catch (err) {
137
+ return [];
138
+ }
139
+ }
140
+
141
+ async function fetchValidatorInfo(rpcUrl, address) {
142
+ const validators = await fetchValidators(rpcUrl);
143
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
144
+ return validators.find(v =>
145
+ v.address === address ||
146
+ v.address === rawAddr ||
147
+ v.identity === address ||
148
+ v.identity === rawAddr
149
+ ) || null;
150
+ }
151
+
152
+ // ============================================================================
153
+ // Validator Status Indicator
154
+ // ============================================================================
155
+
156
+ function getStatusIndicator(validator) {
157
+ if (validator.delinquent) {
158
+ return `${C.red}● DELINQUENT${C.reset}`;
159
+ }
160
+ if (validator.active) {
161
+ return `${C.green}● ACTIVE${C.reset}`;
162
+ }
163
+ return `${C.yellow}● INACTIVE${C.reset}`;
164
+ }
165
+
166
+ function getTierBadge(tier) {
167
+ const tiers = {
168
+ full: `${C.magenta}[FULL]${C.reset}`,
169
+ lite: `${C.cyan}[LITE]${C.reset}`,
170
+ observer: `${C.dim}[OBSERVER]${C.reset}`,
171
+ };
172
+ return tiers[tier?.toLowerCase()] || `${C.dim}[${tier?.toUpperCase() || 'UNKNOWN'}]${C.reset}`;
173
+ }
174
+
175
+ // ============================================================================
176
+ // List Command
177
+ // ============================================================================
178
+
179
+ async function listCommand(opts) {
180
+ if (!opts.json) {
181
+ console.log(BRANDING.header(CLI_VERSION));
182
+ console.log();
183
+ console.log(drawBox(
184
+ `${C.bright}VALIDATORS${C.reset} — Active validators on the Aether network`,
185
+ { style: 'double', title: 'NETWORK', titleColor: C.cyan }
186
+ ));
187
+ console.log();
188
+ startSpinner('Fetching validators from ' + shortAddress(opts.rpc));
189
+ }
190
+
191
+ const validators = await fetchValidators(opts.rpc);
192
+
193
+ if (!opts.json) {
194
+ stopSpinner(true, `Found ${validators.length} validators`);
195
+ }
196
+
197
+ // Filter by tier if specified
198
+ let filtered = validators;
199
+ if (opts.tier && opts.tier !== 'all') {
200
+ filtered = validators.filter(v => v.tier?.toLowerCase() === opts.tier.toLowerCase());
201
+ }
202
+
203
+ // Sort by stake (descending)
204
+ filtered.sort((a, b) => b.stake - a.stake);
205
+
206
+ if (filtered.length === 0) {
207
+ if (opts.json) {
208
+ console.log(JSON.stringify({
209
+ success: false,
210
+ error: 'No validators found',
211
+ rpc: opts.rpc,
212
+ tier_filter: opts.tier,
213
+ }, null, 2));
214
+ } else {
215
+ console.log(`\n ${warning('No validators found.')}`);
216
+ if (opts.tier) {
217
+ console.log(` ${C.dim}Tier filter: ${opts.tier}${C.reset}`);
218
+ }
219
+ console.log(` ${C.dim}Check your RPC endpoint: ${opts.rpc}${C.reset}\n`);
220
+ }
221
+ return;
222
+ }
223
+
224
+ // Calculate stats
225
+ const totalStake = filtered.reduce((sum, v) => sum + Number(v.stake), 0);
226
+ const activeCount = filtered.filter(v => v.active && !v.delinquent).length;
227
+ const delinquentCount = filtered.filter(v => v.delinquent).length;
228
+ const avgAPY = filtered.length > 0
229
+ ? filtered.reduce((sum, v) => sum + Number(v.apy || 0), 0) / filtered.length
230
+ : 0;
231
+
232
+ if (opts.json) {
233
+ console.log(JSON.stringify({
234
+ success: true,
235
+ count: filtered.length,
236
+ total_validators: validators.length,
237
+ tier_filter: opts.tier || 'all',
238
+ stats: {
239
+ total_stake_lamports: totalStake,
240
+ total_stake_aeth: totalStake / 1e9,
241
+ active_count: activeCount,
242
+ delinquent_count: delinquentCount,
243
+ average_apy: avgAPY,
244
+ },
245
+ validators: filtered.map((v, i) => ({
246
+ rank: i + 1,
247
+ address: v.address,
248
+ name: v.name,
249
+ tier: v.tier,
250
+ stake_lamports: v.stake,
251
+ stake_aeth: v.stake / 1e9,
252
+ commission: v.commission,
253
+ apy: v.apy,
254
+ active: v.active,
255
+ delinquent: v.delinquent,
256
+ skip_rate: v.skipRate,
257
+ uptime: v.uptime,
258
+ })),
259
+ rpc: opts.rpc,
260
+ timestamp: new Date().toISOString(),
261
+ }, null, 2));
262
+ return;
263
+ }
264
+
265
+ // Pretty output
266
+ console.log();
267
+ console.log(drawBox(
268
+ `${C.dim}Network Stats${C.reset}\n\n` +
269
+ ` ${C.cyan}Total Validators:${C.reset} ${C.bright}${formatNumber(filtered.length)}${C.reset}\n` +
270
+ ` ${C.green}Active:${C.reset} ${C.bright}${formatNumber(activeCount)}${C.reset} ` +
271
+ `${C.red}Delinquent:${C.reset} ${C.bright}${formatNumber(delinquentCount)}${C.reset}\n` +
272
+ ` ${C.yellow}Total Stake:${C.reset} ${C.bright}${formatAether(totalStake)}${C.reset}\n` +
273
+ ` ${C.magenta}Avg APY:${C.reset} ${formatAPY(avgAPY)}`,
274
+ { style: 'single', borderColor: C.dim }
275
+ ));
276
+ console.log();
277
+
278
+ // Build table
279
+ const headers = ['#', 'Status', 'Name', 'Address', 'Tier', 'Stake', 'APY', 'Comm'];
280
+ const rows = filtered.slice(0, opts.limit).map((v, i) => {
281
+ const status = v.delinquent ? indicators.error : (v.active ? indicators.success : indicators.warning);
282
+ const name = (v.name || 'Unknown').slice(0, 16).padEnd(16);
283
+ const addr = shortAddress(v.address);
284
+ const tier = getTierBadge(v.tier);
285
+ const stake = formatAether(v.stake);
286
+ const apy = formatAPY(v.apy);
287
+ const comm = formatPercent(v.commission / 100); // Assuming basis points
288
+ return [`${i + 1}`, status, name, addr, tier, stake, apy, comm];
289
+ });
290
+
291
+ console.log(drawTable(headers, rows, {
292
+ borderStyle: 'single',
293
+ headerColor: C.cyan + C.bright,
294
+ borderColor: C.dim,
295
+ }));
296
+
297
+ if (filtered.length > opts.limit) {
298
+ console.log(`\n ${C.dim}... and ${filtered.length - opts.limit} more validators${C.reset}`);
299
+ }
300
+
301
+ console.log(`\n ${C.dim}To stake:${C.reset} ${code('aether stake --validator <address> --amount <aeth>')}`);
302
+ console.log(` ${C.dim}For details:${C.reset} ${code('aether validators info <address>')}`);
303
+ console.log();
304
+ }
305
+
306
+ // ============================================================================
307
+ // Info Command
308
+ // ============================================================================
309
+
310
+ async function infoCommand(opts) {
311
+ if (!opts.address) {
312
+ if (!opts.json) {
313
+ console.log(`\n ${error('Validator address required.')}`);
314
+ console.log(` ${C.dim}Usage: aether validators info <address>${C.reset}\n`);
315
+ } else {
316
+ console.log(JSON.stringify({ error: 'Validator address required' }, null, 2));
317
+ }
318
+ return;
319
+ }
320
+
321
+ if (!opts.json) {
322
+ console.log(BRANDING.header(CLI_VERSION));
323
+ console.log();
324
+ startSpinner('Fetching validator info');
325
+ }
326
+
327
+ const validator = await fetchValidatorInfo(opts.rpc, opts.address);
328
+
329
+ if (!opts.json) {
330
+ stopSpinner(!!validator, validator ? 'Validator found' : 'Validator not found');
331
+ }
332
+
333
+ if (!validator) {
334
+ if (opts.json) {
335
+ console.log(JSON.stringify({
336
+ success: false,
337
+ error: 'Validator not found',
338
+ address: opts.address,
339
+ rpc: opts.rpc,
340
+ }, null, 2));
341
+ } else {
342
+ console.log(`\n ${error('Validator not found:')} ${opts.address}`);
343
+ console.log(` ${C.dim}Check the address and try again.${C.reset}\n`);
344
+ }
345
+ return;
346
+ }
347
+
348
+ if (opts.json) {
349
+ console.log(JSON.stringify({
350
+ success: true,
351
+ validator: {
352
+ address: validator.address,
353
+ identity: validator.identity,
354
+ name: validator.name,
355
+ tier: validator.tier,
356
+ stake_lamports: validator.stake,
357
+ stake_aeth: validator.stake / 1e9,
358
+ commission: validator.commission,
359
+ apy: validator.apy,
360
+ active: validator.active,
361
+ delinquent: validator.delinquent,
362
+ skip_rate: validator.skipRate,
363
+ uptime: validator.uptime,
364
+ last_vote: validator.lastVote,
365
+ version: validator.version,
366
+ website: validator.website,
367
+ details: validator.details,
368
+ },
369
+ rpc: opts.rpc,
370
+ timestamp: new Date().toISOString(),
371
+ }, null, 2));
372
+ return;
373
+ }
374
+
375
+ // Pretty output
376
+ const statusLine = getStatusIndicator(validator);
377
+ const uptimeBar = validator.uptime
378
+ ? progressBarColored(validator.uptime, 100, 30)
379
+ : `${C.dim}N/A${C.reset}`;
380
+
381
+ console.log();
382
+ console.log(drawBox(
383
+ `${C.bright}VALIDATOR DETAILS${C.reset}\n\n` +
384
+ ` ${C.cyan}Status:${C.reset} ${statusLine}\n` +
385
+ ` ${C.cyan}Name:${C.reset} ${C.bright}${validator.name || 'Unknown'}${C.reset}\n` +
386
+ ` ${C.cyan}Tier:${C.reset} ${getTierBadge(validator.tier)}\n\n` +
387
+ ` ${C.cyan}Address:${C.reset} ${C.bright}${validator.address}${C.reset}\n` +
388
+ ` ${C.cyan}Identity:${C.reset} ${validator.identity || 'N/A'}\n\n` +
389
+ ` ${C.cyan}Stake:${C.reset} ${C.bright}${formatAether(validator.stake)}${C.reset}\n` +
390
+ ` ${C.cyan}APY:${C.reset} ${formatAPY(validator.apy)}\n` +
391
+ ` ${C.cyan}Commission:${C.reset} ${formatPercent(validator.commission / 100)}\n\n` +
392
+ ` ${C.cyan}Uptime:${C.reset} ${uptimeBar}\n` +
393
+ ` ${C.cyan}Skip Rate:${C.reset} ${formatPercent(validator.skipRate)}\n` +
394
+ ` ${C.cyan}Version:${C.reset} ${validator.version || 'unknown'}`,
395
+ { style: 'double', title: validator.name?.toUpperCase() || 'VALIDATOR', titleColor: C.cyan }
396
+ ));
397
+
398
+ if (validator.website) {
399
+ console.log(`\n ${C.dim}Website:${C.reset} ${C.blue}${validator.website}${C.reset}`);
400
+ }
401
+ if (validator.details) {
402
+ console.log(`\n ${C.dim}Details:${C.reset} ${validator.details}`);
403
+ }
404
+
405
+ console.log(`\n ${C.dim}To stake:${C.reset} ${code(`aether stake --validator ${validator.address} --amount <aeth>`)}`);
406
+ console.log();
407
+ }
408
+
409
+ // ============================================================================
410
+ // Top Command
411
+ // ============================================================================
412
+
413
+ async function topCommand(opts) {
414
+ if (!opts.json) {
415
+ console.log(BRANDING.header(CLI_VERSION));
416
+ console.log();
417
+ console.log(drawBox(
418
+ `${C.bright}TOP VALIDATORS${C.reset} — Highest stake validators`,
419
+ { style: 'double', title: 'LEADERBOARD', titleColor: C.yellow }
420
+ ));
421
+ console.log();
422
+ startSpinner('Fetching top validators');
423
+ }
424
+
425
+ const validators = await fetchValidators(opts.rpc);
426
+
427
+ // Sort by stake and take top N
428
+ validators.sort((a, b) => b.stake - a.stake);
429
+ const topValidators = validators.slice(0, opts.limit);
430
+
431
+ if (!opts.json) {
432
+ stopSpinner(topValidators.length > 0, `Found ${topValidators.length} validators`);
433
+ }
434
+
435
+ if (topValidators.length === 0) {
436
+ if (opts.json) {
437
+ console.log(JSON.stringify({ error: 'No validators found' }, null, 2));
438
+ } else {
439
+ console.log(`\n ${warning('No validators found.')}\n`);
440
+ }
441
+ return;
442
+ }
443
+
444
+ // Calculate total stake for percentages
445
+ const totalNetworkStake = validators.reduce((sum, v) => sum + Number(v.stake), 0);
446
+
447
+ if (opts.json) {
448
+ console.log(JSON.stringify({
449
+ success: true,
450
+ top_count: topValidators.length,
451
+ total_network_stake: totalNetworkStake,
452
+ validators: topValidators.map((v, i) => ({
453
+ rank: i + 1,
454
+ address: v.address,
455
+ name: v.name,
456
+ stake_lamports: v.stake,
457
+ stake_aeth: v.stake / 1e9,
458
+ stake_percentage: totalNetworkStake > 0 ? (v.stake / totalNetworkStake * 100).toFixed(2) : 0,
459
+ apy: v.apy,
460
+ commission: v.commission,
461
+ active: v.active,
462
+ })),
463
+ rpc: opts.rpc,
464
+ timestamp: new Date().toISOString(),
465
+ }, null, 2));
466
+ return;
467
+ }
468
+
469
+ console.log();
470
+ console.log(` ${C.dim}Network:${C.reset} ${C.bright}${formatAether(totalNetworkStake)}${C.reset} total stake`);
471
+ console.log(` ${C.dim}Showing top ${opts.limit} validators${C.reset}\n`);
472
+
473
+ // Build leaderboard table
474
+ const headers = ['Rank', 'Validator', 'Stake', '% Network', 'APY'];
475
+ const rows = topValidators.map((v, i) => {
476
+ const rank = i + 1;
477
+ const rankIcon = rank === 1 ? `${C.yellow}🥇${C.reset}` :
478
+ rank === 2 ? `${C.dim}🥈${C.reset}` :
479
+ rank === 3 ? `${C.brightYellow}🥉${C.reset}` :
480
+ `${C.dim}${rank}${C.reset}`;
481
+ const name = (v.name || shortAddress(v.address)).slice(0, 20);
482
+ const stake = formatAether(v.stake);
483
+ const pct = totalNetworkStake > 0 ? (v.stake / totalNetworkStake * 100).toFixed(2) + '%' : '0%';
484
+ const apy = formatAPY(v.apy);
485
+ return [rankIcon, name, stake, pct, apy];
486
+ });
487
+
488
+ console.log(drawTable(headers, rows, {
489
+ borderStyle: 'single',
490
+ headerColor: C.yellow + C.bright,
491
+ borderColor: C.dim,
492
+ }));
493
+
494
+ console.log();
495
+ }
496
+
497
+ // ============================================================================
498
+ // Show Help
499
+ // ============================================================================
500
+
501
+ function showHelp() {
502
+ console.log(BRANDING.header(CLI_VERSION));
503
+
504
+ console.log(`\n ${C.bright}AETHER VALIDATORS${C.reset} Network validator management\n`);
505
+
506
+ console.log(` ${C.cyan}◆ LIST${C.reset} ${code('aether validators list [--tier <tier>] [--json]')}`);
507
+ console.log(` ${C.dim}Show all validators, optionally filtered by tier${C.reset}`);
508
+
509
+ console.log(`\n ${C.cyan}◆ INFO${C.reset} ${code('aether validators info <address> [--json]')}`);
510
+ console.log(` ${C.dim}Detailed information about a specific validator${C.reset}`);
511
+
512
+ console.log(`\n ${C.cyan}◆ TOP${C.reset} ${code('aether validators top [--limit 10] [--json]')}`);
513
+ console.log(` ${C.dim}Show top validators by stake amount${C.reset}`);
514
+
515
+ console.log(`\n ${C.bright}OPTIONS${C.reset}`);
516
+ console.log(` ${code('--tier <tier>')} Filter by tier: full, lite, observer`);
517
+ console.log(` ${code('--limit <n>')} Show top N validators (default: 15)`);
518
+ console.log(` ${code('--rpc <url>')} Custom RPC endpoint`);
519
+ console.log(` ${code('--json')} Output JSON for scripting`);
520
+ console.log(` ${code('--help')} Show this help message`);
521
+
522
+ console.log(`\n ${C.bright}EXAMPLES${C.reset}`);
523
+ console.log(` ${C.dim}$${C.reset} ${code('aether validators list')}`);
524
+ console.log(` ${C.dim}$${C.reset} ${code('aether validators list --tier full --json')}`);
525
+ console.log(` ${C.dim}$${C.reset} ${code('aether validators info ATHxxx...')}`);
526
+ console.log(` ${C.dim}$${C.reset} ${code('aether validators top --limit 20')}`);
527
+
528
+ console.log(`\n ${C.bright}SDK METHODS${C.reset}`);
529
+ console.log(` ${C.dim}client.getValidators() → GET /v1/validators${C.reset}`);
530
+ console.log();
531
+ }
532
+
533
+ // ============================================================================
534
+ // CLI Args Parser
535
+ // ============================================================================
536
+
537
+ function parseArgs() {
538
+ const rawArgs = process.argv.slice(3);
539
+
540
+ // Determine subcommand - first arg if not a flag
541
+ let subcmd = 'list';
542
+ let allArgs = rawArgs;
543
+
544
+ if (rawArgs.length > 0 && !rawArgs[0].startsWith('-')) {
545
+ subcmd = rawArgs[0];
546
+ allArgs = rawArgs.slice(1);
547
+ }
548
+
549
+ const opts = {
550
+ subcmd,
551
+ rpc: getDefaultRpc(),
552
+ json: false,
553
+ tier: null,
554
+ limit: 15,
555
+ address: null,
556
+ };
557
+
558
+ // Parse flags
559
+ for (let i = 0; i < allArgs.length; i++) {
560
+ const arg = allArgs[i];
561
+ if (arg === '--rpc' || arg === '-r') {
562
+ opts.rpc = allArgs[++i];
563
+ } else if (arg === '--json' || arg === '-j') {
564
+ opts.json = true;
565
+ } else if (arg === '--tier' || arg === '-t') {
566
+ opts.tier = allArgs[++i];
567
+ } else if (arg === '--limit' || arg === '-l') {
568
+ const val = parseInt(allArgs[++i], 10);
569
+ if (!isNaN(val) && val > 0) opts.limit = val;
570
+ } else if (arg === '--help' || arg === '-h') {
571
+ opts.subcmd = 'help';
572
+ } else if (!arg.startsWith('-') && subcmd === 'info') {
573
+ // For info command, non-flag arg is the address
574
+ opts.address = arg;
575
+ }
576
+ }
577
+
578
+ // For info command, also check if address was passed directly
579
+ if (subcmd === 'info' && !opts.address) {
580
+ const firstArg = allArgs.find(a => !a.startsWith('-'));
581
+ if (firstArg) opts.address = firstArg;
582
+ }
583
+
584
+ return opts;
585
+ }
586
+
587
+ // ============================================================================
588
+ // Main Entry Point
589
+ // ============================================================================
590
+
591
+ async function validatorsCommand() {
592
+ const opts = parseArgs();
593
+
594
+ switch (opts.subcmd) {
595
+ case 'list':
596
+ await listCommand(opts);
597
+ break;
598
+ case 'info':
599
+ await infoCommand(opts);
600
+ break;
601
+ case 'top':
602
+ await topCommand(opts);
603
+ break;
604
+ case 'help':
605
+ case '--help':
606
+ case '-h':
607
+ showHelp();
608
+ break;
609
+ default:
610
+ console.log(`\n ${error('Unknown subcommand:')} ${opts.subcmd}`);
611
+ console.log(` ${C.dim}Run 'aether validators --help' for usage.${C.reset}\n`);
612
+ process.exit(1);
613
+ }
614
+ }
615
+
616
+ module.exports = { validatorsCommand };
617
+
618
+ if (require.main === module) {
619
+ validatorsCommand().catch(err => {
620
+ console.error(`\n${error('Validators command failed:')} ${err.message}\n`);
621
+ process.exit(1);
622
+ });
623
+ }