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.
- package/commands/validators.js +173 -2
- package/package.json +1 -1
package/commands/validators.js
CHANGED
|
@@ -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
|
|
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`);
|