@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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/aether-cli-1.0.0.tgz +0 -0
- package/aether-cli-1.8.0.tgz +0 -0
- package/aether-hub-1.0.5.tgz +0 -0
- package/aether-hub-1.1.8.tgz +0 -0
- package/aether-hub-1.2.1.tgz +0 -0
- package/commands/account.js +280 -0
- package/commands/apy.js +499 -0
- package/commands/balance.js +241 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +387 -0
- package/commands/claim.js +490 -0
- package/commands/config.js +851 -0
- package/commands/delegations.js +582 -0
- package/commands/doctor.js +769 -0
- package/commands/emergency.js +667 -0
- package/commands/epoch.js +275 -0
- package/commands/fees.js +276 -0
- package/commands/index.js +78 -0
- package/commands/info.js +495 -0
- package/commands/init.js +816 -0
- package/commands/install.js +666 -0
- package/commands/kyc.js +272 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/multisig.js +701 -0
- package/commands/network.js +429 -0
- package/commands/nft.js +857 -0
- package/commands/ping.js +266 -0
- package/commands/price.js +253 -0
- package/commands/rewards.js +931 -0
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +656 -0
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +470 -0
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +205 -0
- package/commands/stake.js +516 -0
- package/commands/stats.js +396 -0
- package/commands/status.js +327 -0
- package/commands/supply.js +391 -0
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +346 -0
- package/commands/unstake.js +597 -0
- package/commands/validator-info.js +657 -0
- package/commands/validator-register.js +593 -0
- package/commands/validator-start.js +323 -0
- package/commands/validator-status.js +227 -0
- package/commands/validators.js +626 -0
- package/commands/wallet.js +1570 -0
- package/index.js +593 -0
- package/lib/errors.js +398 -0
- package/package.json +76 -0
- package/sdk/README.md +210 -0
- package/sdk/index.js +1639 -0
- package/sdk/package.json +34 -0
- package/sdk/rpc.js +254 -0
- package/sdk/test.js +85 -0
- package/test/doctor.test.js +76 -0
- 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
|
+
}
|