@jellylegsai/aether-cli 1.8.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/aether-cli-1.0.0.tgz +0 -0
  4. package/aether-cli-1.8.0.tgz +0 -0
  5. package/aether-hub-1.0.5.tgz +0 -0
  6. package/aether-hub-1.1.8.tgz +0 -0
  7. package/aether-hub-1.2.1.tgz +0 -0
  8. package/commands/account.js +280 -0
  9. package/commands/apy.js +499 -0
  10. package/commands/balance.js +241 -0
  11. package/commands/blockhash.js +181 -0
  12. package/commands/broadcast.js +387 -0
  13. package/commands/claim.js +490 -0
  14. package/commands/config.js +851 -0
  15. package/commands/delegations.js +582 -0
  16. package/commands/doctor.js +769 -0
  17. package/commands/emergency.js +667 -0
  18. package/commands/epoch.js +275 -0
  19. package/commands/fees.js +276 -0
  20. package/commands/index.js +78 -0
  21. package/commands/info.js +495 -0
  22. package/commands/init.js +816 -0
  23. package/commands/install.js +666 -0
  24. package/commands/kyc.js +272 -0
  25. package/commands/logs.js +315 -0
  26. package/commands/monitor.js +431 -0
  27. package/commands/multisig.js +701 -0
  28. package/commands/network.js +429 -0
  29. package/commands/nft.js +857 -0
  30. package/commands/ping.js +266 -0
  31. package/commands/price.js +253 -0
  32. package/commands/rewards.js +931 -0
  33. package/commands/sdk-test.js +477 -0
  34. package/commands/sdk.js +656 -0
  35. package/commands/slot.js +155 -0
  36. package/commands/snapshot.js +470 -0
  37. package/commands/stake-info.js +139 -0
  38. package/commands/stake-positions.js +205 -0
  39. package/commands/stake.js +516 -0
  40. package/commands/stats.js +396 -0
  41. package/commands/status.js +327 -0
  42. package/commands/supply.js +391 -0
  43. package/commands/tps.js +238 -0
  44. package/commands/transfer.js +495 -0
  45. package/commands/tx-history.js +346 -0
  46. package/commands/unstake.js +597 -0
  47. package/commands/validator-info.js +657 -0
  48. package/commands/validator-register.js +593 -0
  49. package/commands/validator-start.js +323 -0
  50. package/commands/validator-status.js +227 -0
  51. package/commands/validators.js +626 -0
  52. package/commands/wallet.js +1570 -0
  53. package/index.js +593 -0
  54. package/lib/errors.js +398 -0
  55. package/package.json +76 -0
  56. package/sdk/README.md +210 -0
  57. package/sdk/index.js +1639 -0
  58. package/sdk/package.json +34 -0
  59. package/sdk/rpc.js +254 -0
  60. package/sdk/test.js +85 -0
  61. package/test/doctor.test.js +76 -0
  62. package/validator-identity.json +4 -0
@@ -0,0 +1,626 @@
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
+ }