aether-hub 1.1.9 → 1.2.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.
@@ -96,6 +96,7 @@ function parseArgs() {
96
96
  asJson: false,
97
97
  sortBy: 'score',
98
98
  limit: 100,
99
+ rank: false,
99
100
  };
100
101
 
101
102
  for (let i = 0; i < args.length; i++) {
@@ -126,6 +127,9 @@ function parseArgs() {
126
127
  } else if (arg === '--help' || arg === '-h') {
127
128
  showHelp();
128
129
  process.exit(0);
130
+ } else if (arg === '--rank') {
131
+ opts.rank = true;
132
+ opts.subcmd = 'rank';
129
133
  }
130
134
  }
131
135
 
@@ -138,21 +142,32 @@ ${C.bright}${C.cyan}aether-cli validators${C.reset} - List and inspect Aether va
138
142
 
139
143
  ${C.bright}Usage:${C.reset}
140
144
  aether validators list [options]
145
+ aether validators rank [options] Ranked leaderboard (sorted by stake)
141
146
 
142
- ${C.bright}Options:${C.reset}
147
+ ${C.bright}Options (list):${C.reset}
143
148
  -t, --tier <type> Filter by tier: full, lite, observer
144
- -s, --sort <field> Sort by: stake (default), score, apy, uptime, name
149
+ -s, --sort <field> Sort by: stake, score, apy, uptime, name (default: score)
145
150
  -l, --limit <n> Max validators to show (default: 100, max: 500)
146
151
  -r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC} or $AETHER_RPC)
147
152
  -j, --json Output raw JSON (for scripting)
148
153
  -h, --help Show this help message
149
154
 
155
+ ${C.bright}Options (rank):${C.reset}
156
+ -t, --tier <type> Filter by tier: full, lite, observer
157
+ -l, --limit <n> Max validators to show (default: 50, max: 200)
158
+ -r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC} or $AETHER_RPC)
159
+ -j, --json Output raw JSON (for scripting)
160
+ -h, --help Show this help message
161
+
150
162
  ${C.bright}Examples:${C.reset}
151
163
  aether validators list # All validators, sorted by score
152
164
  aether validators list --tier full # Full validators only
153
165
  aether validators list --sort stake # Sort by total stake
154
166
  aether validators list --sort apy # Sort by estimated APY
155
167
  aether validators list --json # JSON for scripts
168
+ aether validators rank # Top validators by stake (leaderboard)
169
+ aether validators rank --tier full # Full validators only
170
+ aether validators rank --limit 20 # Top 20 validators
156
171
  aether validators list --rpc http://custom-rpc:8899
157
172
  `.trim());
158
173
  }
@@ -465,6 +480,160 @@ async function validatorsList(opts) {
465
480
  }
466
481
  }
467
482
 
483
+ // ---------------------------------------------------------------------------
484
+ // Validators Rank — leaderboard sorted by stake
485
+ // ---------------------------------------------------------------------------
486
+
487
+ function rankBadge(rank) {
488
+ if (rank === 1) return `${C.yellow}🥇 1${C.reset}`;
489
+ if (rank === 2) return `${C.dim}🥈 2${C.reset}`;
490
+ if (rank === 3) return `${C.yellow}🥉 3${C.reset}`;
491
+ return `${C.dim}${String(rank).padStart(3)}${C.reset}`;
492
+ }
493
+
494
+ function renderRankTable(validators, opts) {
495
+ const tier = opts.tier;
496
+
497
+ // Sort by stake descending, filter by tier
498
+ const sorted = [...validators]
499
+ .sort((a, b) => b.stakeAeth - a.stakeAeth);
500
+
501
+ const filtered = tier
502
+ ? sorted.filter(v => v.tier === tier)
503
+ : sorted;
504
+
505
+ const shown = filtered.slice(0, opts.limit);
506
+ const total = filtered.length;
507
+
508
+ // Total staked across all shown validators
509
+ const totalStaked = shown.reduce((sum, v) => sum + v.stakeAeth, 0);
510
+ const maxStake = shown.length > 0 ? shown[0].stakeAeth : 1;
511
+
512
+ console.log();
513
+ console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════════════════════╗${C.reset}`);
514
+ 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}`);
515
+ console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════════════════════╝${C.reset}`);
516
+ if (tier) console.log(` ${C.dim}Tier: ${tier.toUpperCase()} RPC: ${opts.rpc}${C.reset}`);
517
+ else console.log(` ${C.dim}Sorted by stake RPC: ${opts.rpc}${C.reset}`);
518
+ console.log();
519
+ console.log(` ${C.bright}┌────────────────────────────────────────────────────────────────────────────────────────────┐${C.reset}`);
520
+ console.log(
521
+ ` ${C.bright}│${C.reset}` +
522
+ ` ${C.cyan}Rank${C.reset}`.padEnd(6) +
523
+ `${C.cyan}Validator${C.reset}`.padEnd(34) +
524
+ `${C.cyan}Tier${C.reset}`.padEnd(8) +
525
+ `${C.cyan}Stake (AETH)${C.reset}`.padEnd(16) +
526
+ `${C.cyan}Score${C.reset}`.padEnd(8) +
527
+ `${C.cyan}APY${C.reset}`.padEnd(8) +
528
+ `${C.bright}│${C.reset}`
529
+ );
530
+ console.log(` ${C.bright}├${'─'.repeat(94)}${C.bright}│${C.reset}`);
531
+
532
+ for (let i = 0; i < shown.length; i++) {
533
+ const v = shown[i];
534
+ const rank = i + 1;
535
+ const rankStr = rankBadge(rank);
536
+ const nameOrKey = v.name
537
+ ? v.name.substring(0, 20).padEnd(20)
538
+ : (v.pubkey ? v.pubkey.substring(0, 20).padEnd(20) : 'unknown'.padEnd(20));
539
+ const tierStr = tierBadge(v.tier);
540
+ const stakeFormatted = v.stakeFormatted.padEnd(14);
541
+ const scoreStr = v.score !== null && v.score !== undefined
542
+ ? `${v.score}%`.padEnd(6)
543
+ : '—'.padEnd(6);
544
+ const apyStr = v.apy !== null && v.apy !== undefined
545
+ ? `${v.apy.toFixed(1)}%`.padEnd(6)
546
+ : '—'.padEnd(6);
547
+
548
+ const scoreColor = v.score === null || v.score === undefined ? C.dim
549
+ : v.score >= 80 ? C.green
550
+ : v.score >= 50 ? C.yellow
551
+ : C.red;
552
+
553
+ // Mini bar for relative stake
554
+ const barLen = 12;
555
+ const fillLen = maxStake > 0 ? Math.round((v.stakeAeth / maxStake) * barLen) : 0;
556
+ const stakeBar = `${C.green}${'█'.repeat(fillLen)}${C.dim}${'░'.repeat(barLen - fillLen)}${C.reset}`;
557
+
558
+ console.log(
559
+ ` ${C.bright}│${C.reset}` +
560
+ ` ${rankStr} `.substring(0, 7) +
561
+ `${C.cyan}${nameOrKey}${C.reset} ` +
562
+ `${tierStr} `.substring(0, 9) +
563
+ `${stakeBar}${C.reset} ` +
564
+ `${C.green}${stakeFormatted}${C.reset} ` +
565
+ `${scoreColor}${scoreStr}${C.reset} ` +
566
+ `${C.green}${apyStr}${C.reset} ` +
567
+ `${C.bright}│${C.reset}`
568
+ );
569
+ }
570
+
571
+ console.log(` ${C.bright}└${'─'.repeat(94)}${C.bright}│${C.reset}`);
572
+ console.log();
573
+
574
+ // Summary
575
+ const avgApy = shown.reduce((sum, v) => sum + (v.apy || 0), 0) / shown.filter(v => v.apy !== null).length;
576
+ 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}`);
577
+ 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}`);
578
+ console.log();
579
+ }
580
+
581
+ async function validatorsRank(opts) {
582
+ const rpc = opts.rpc;
583
+ const limit = Math.min(opts.limit || 50, 200);
584
+
585
+ if (!opts.asJson) {
586
+ console.log(`${C.dim}Fetching validator leaderboard from ${rpc}...${C.reset}`);
587
+ }
588
+
589
+ const [rawValidators, epochInfo, supply] = await Promise.all([
590
+ fetchValidators(rpc),
591
+ fetchEpochInfo(rpc),
592
+ fetchSupply(rpc),
593
+ ]);
594
+
595
+ if (rawValidators.length === 0) {
596
+ if (opts.asJson) {
597
+ console.log(JSON.stringify({ rpc, validators: [], total: 0, error: 'No validator data returned from RPC' }, null, 2));
598
+ } else {
599
+ console.log(`\n ${C.yellow}⚠ No validator data returned from RPC.${C.reset}`);
600
+ console.log(` ${C.dim} RPC: ${rpc}${C.reset}`);
601
+ console.log(` ${C.dim} Check that your validator is running and the RPC endpoint is accessible.${C.reset}\n`);
602
+ }
603
+ return;
604
+ }
605
+
606
+ let validators = rawValidators.map(normaliseValidator);
607
+
608
+ // Estimate APY if not provided
609
+ if (supply && !supply.error) {
610
+ const totalStake = Number(supply.total_staked || supply.total || 0);
611
+ const rewardsPerEpoch = Number(epochInfo?.rewards_per_epoch || '2000000000');
612
+ if (totalStake > 0 && rewardsPerEpoch > 0) {
613
+ const apyEstimate = (rewardsPerEpoch / totalStake) * 73;
614
+ validators = validators.map(v => {
615
+ if (v.apy === null || v.apy === undefined) {
616
+ return { ...v, apy: apyEstimate };
617
+ }
618
+ return v;
619
+ });
620
+ }
621
+ }
622
+
623
+ opts.limit = limit;
624
+
625
+ if (opts.asJson) {
626
+ const ranked = [...validators]
627
+ .sort((a, b) => b.stakeAeth - a.stakeAeth)
628
+ .filter(v => !opts.tier || v.tier === opts.tier)
629
+ .slice(0, limit)
630
+ .map((v, i) => ({ rank: i + 1, ...v }));
631
+ console.log(JSON.stringify({ rpc, validators: ranked, total: ranked.length, fetched_at: new Date().toISOString() }, null, 2));
632
+ } else {
633
+ renderRankTable(validators, opts);
634
+ }
635
+ }
636
+
468
637
  // ---------------------------------------------------------------------------
469
638
  // Entry point
470
639
  // ---------------------------------------------------------------------------
@@ -474,6 +643,8 @@ async function main() {
474
643
 
475
644
  if (opts.subcmd === 'list') {
476
645
  await validatorsList(opts);
646
+ } else if (opts.subcmd === 'rank') {
647
+ await validatorsRank(opts);
477
648
  } else {
478
649
  console.log(`\n ${C.red}Unknown subcommand:${C.reset} ${opts.subcmd}`);
479
650
  console.log(` ${C.dim}Usage: aether validators list [--tier full] [--sort stake] [--json]${C.reset}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-hub",
3
- "version": "1.1.9",
3
+ "version": "1.2.0",
4
4
  "description": "AeTHer Validator CLI — tiered validators (Full/Lite/Observer), system checks, onboarding, and node management",
5
5
  "main": "index.js",
6
6
  "bin": {