aether-hub 1.2.0 → 1.2.2

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.
@@ -0,0 +1,640 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli validator-info
4
+ *
5
+ * Inspect a specific validator by identity address or name.
6
+ * Shows stake, APY, score, commission, tier, uptime, epoch performance,
7
+ * and a breakdown of delegators.
8
+ *
9
+ * Usage:
10
+ * aether validator info <addressOrName> Inspect a validator
11
+ * aether validator info <addressOrName> --json JSON output for scripting
12
+ * aether validator info <addressOrName> --rpc <url> Use specific RPC
13
+ *
14
+ * Examples:
15
+ * aether validator info ATH3mGH...
16
+ * aether validator info jellylegs --json
17
+ * aether validator info --address ATH3mGH... --rpc http://custom:8899
18
+ */
19
+
20
+ const http = require('http');
21
+ const https = require('https');
22
+
23
+ // ANSI colours
24
+ const C = {
25
+ reset: '\x1b[0m',
26
+ bright: '\x1b[1m',
27
+ dim: '\x1b[2m',
28
+ red: '\x1b[31m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ blue: '\x1b[34m',
32
+ cyan: '\x1b[36m',
33
+ magenta: '\x1b[35m',
34
+ };
35
+
36
+ const DEFAULT_RPC = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // HTTP helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function httpRequest(rpcUrl, path) {
43
+ return new Promise((resolve, reject) => {
44
+ const url = new URL(path, rpcUrl);
45
+ const lib = url.protocol === 'https:' ? https : http;
46
+ const req = lib.request({
47
+ hostname: url.hostname,
48
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
49
+ path: url.pathname + url.search,
50
+ method: 'GET',
51
+ timeout: 8000,
52
+ headers: { 'Content-Type': 'application/json' },
53
+ }, (res) => {
54
+ let data = '';
55
+ res.on('data', (chunk) => data += chunk);
56
+ res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
57
+ });
58
+ req.on('error', reject);
59
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
60
+ req.end();
61
+ });
62
+ }
63
+
64
+ function httpPost(rpcUrl, path, body) {
65
+ return new Promise((resolve, reject) => {
66
+ const url = new URL(path, rpcUrl);
67
+ const lib = url.protocol === 'https:' ? https : http;
68
+ const bodyStr = JSON.stringify(body);
69
+ const req = lib.request({
70
+ hostname: url.hostname,
71
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
72
+ path: url.pathname + url.search,
73
+ method: 'POST',
74
+ timeout: 8000,
75
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
76
+ }, (res) => {
77
+ let data = '';
78
+ res.on('data', (chunk) => data += chunk);
79
+ res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(data); } });
80
+ });
81
+ req.on('error', reject);
82
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
83
+ req.write(bodyStr);
84
+ req.end();
85
+ });
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Argument parsing
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function parseArgs() {
93
+ const raw = process.argv.slice(3); // [node, index.js, validator, info, ...]
94
+ const opts = {
95
+ rpc: DEFAULT_RPC,
96
+ target: null,
97
+ asJson: false,
98
+ };
99
+
100
+ for (let i = 0; i < raw.length; i++) {
101
+ const arg = raw[i];
102
+ if ((arg === '--rpc' || arg === '-r') && raw[i + 1] && !raw[i + 1].startsWith('-')) {
103
+ opts.rpc = raw[++i];
104
+ } else if (arg === '--json' || arg === '-j') {
105
+ opts.asJson = true;
106
+ } else if ((arg === '--address' || arg === '-a') && raw[i + 1] && !raw[i + 1].startsWith('-')) {
107
+ opts.target = raw[++i];
108
+ } else if (!arg.startsWith('-') && !opts.target) {
109
+ opts.target = arg;
110
+ }
111
+ }
112
+
113
+ return opts;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Data fetchers
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /** Fetch all validators and find the matching one */
121
+ async function fetchValidatorByIdentity(rpc, identity) {
122
+ // identity can be a full address, partial address, or name/moniker
123
+ const validators = await fetchAllValidators(rpc);
124
+ if (!validators || validators.length === 0) return null;
125
+
126
+ const isAddress = identity.startsWith('ATH');
127
+
128
+ let match = null;
129
+
130
+ if (isAddress) {
131
+ // Exact or prefix match on pubkey/identity
132
+ match = validators.find(v =>
133
+ (v.pubkey && (v.pubkey === identity || v.pubkey.startsWith(identity))) ||
134
+ (v.address && (v.address === identity || v.address.startsWith(identity))) ||
135
+ (v.identity && (v.identity === identity || v.identity.startsWith(identity)))
136
+ );
137
+ }
138
+
139
+ if (!match) {
140
+ // Try name/moniker match (case-insensitive partial)
141
+ const lower = identity.toLowerCase();
142
+ match = validators.find(v =>
143
+ (v.name && v.name.toLowerCase().includes(lower)) ||
144
+ (v.moniker && v.moniker.toLowerCase().includes(lower))
145
+ );
146
+ }
147
+
148
+ if (!match && identity.length >= 8) {
149
+ // Try prefix match on any field
150
+ match = validators.find(v => {
151
+ const pk = v.pubkey || v.address || v.identity || '';
152
+ return pk.startsWith(identity) || pk.endsWith(identity);
153
+ });
154
+ }
155
+
156
+ return match;
157
+ }
158
+
159
+ /** Fetch all validators from the network */
160
+ async function fetchAllValidators(rpc) {
161
+ try {
162
+ const res = await httpRequest(rpc, '/v1/validators');
163
+ if (res && !res.error) {
164
+ if (Array.isArray(res)) return res;
165
+ if (res.validators && Array.isArray(res.validators)) return res.validators;
166
+ if (res.accounts && Array.isArray(res.accounts)) return res.accounts;
167
+ }
168
+ const res2 = await httpPost(rpc, '/v1/validators', {});
169
+ if (res2 && !res2.error) {
170
+ if (Array.isArray(res2)) return res2;
171
+ if (res2.validators && Array.isArray(res2.validators)) return res2.validators;
172
+ }
173
+ return [];
174
+ } catch {
175
+ return [];
176
+ }
177
+ }
178
+
179
+ /** Fetch epoch info for APY and epoch calculations */
180
+ async function fetchEpochInfo(rpc) {
181
+ try {
182
+ return await httpRequest(rpc, '/v1/epoch-info');
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ /** Fetch supply for APY estimation */
189
+ async function fetchSupply(rpc) {
190
+ try {
191
+ return await httpRequest(rpc, '/v1/supply');
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ /** Fetch delegators for a specific validator */
198
+ async function fetchDelegators(rpc, validatorPubkey) {
199
+ try {
200
+ const res = await httpRequest(rpc, `/v1/validator/${encodeURIComponent(validatorPubkey)}/delegators`);
201
+ if (res && !res.error) {
202
+ return Array.isArray(res) ? res : (res.delegators || []);
203
+ }
204
+ return [];
205
+ } catch {
206
+ return [];
207
+ }
208
+ }
209
+
210
+ /** Fetch recent performance history for a validator */
211
+ async function fetchValidatorPerformance(rpc, validatorPubkey) {
212
+ try {
213
+ const res = await httpRequest(rpc, `/v1/validator/${encodeURIComponent(validatorPubkey)}/performance`);
214
+ if (res && !res.error) {
215
+ return res;
216
+ }
217
+ return null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Normalise a validator record
225
+ // ---------------------------------------------------------------------------
226
+
227
+ function normalise(v) {
228
+ const pubkey = v.pubkey || v.address || v.identity || v.id || null;
229
+ const name = v.name || v.moniker || v.label || null;
230
+ const tier = (v.tier || v.node_type || v.type || 'full').toLowerCase();
231
+ const stake = typeof v.stake === 'bigint' ? v.stake
232
+ : typeof v.stake === 'object' ? BigInt(v.stake.toString())
233
+ : BigInt(v.stake || v.delegatedStake || v.stake_lamports || v.lamports || 0);
234
+ const score = v.score !== undefined ? v.score
235
+ : v.uptime !== undefined ? Math.round(v.uptime * 100)
236
+ : null;
237
+ const apy = v.apy !== undefined ? v.apy
238
+ : v.apy_bps !== undefined ? v.apy_bps / 100
239
+ : null;
240
+ const commission = v.commission !== undefined ? v.commission
241
+ : v.commission_bps !== undefined ? v.commission_bps / 100
242
+ : null;
243
+ const version = v.version || v.agent || null;
244
+ const ip = v.ip || v.remote || null;
245
+ const lastVote = v.last_vote || v.lastVote || null;
246
+ const stakeAccounts = v.stake_accounts || v.stakeAccounts || v.delegators || null;
247
+ const activatedStake = v.activated_stake || null;
248
+ const lastEpochStake = v.last_epoch_stake || v.stake_last_epoch || null;
249
+ const credits = v.credits || v.epoch_credits || null;
250
+ const rootSlot = v.root_slot || v.rootSlot || null;
251
+ const delinquent = v.delinquent || false;
252
+
253
+ return {
254
+ pubkey, name, tier, stake, score, apy, commission,
255
+ version, ip, lastVote, stakeAccounts, activatedStake,
256
+ lastEpochStake, credits, rootSlot, delinquent,
257
+ _raw: v,
258
+ };
259
+ }
260
+
261
+ function formatAether(lamports) {
262
+ const aeth = Number(lamports) / 1e9;
263
+ if (aeth === 0) return '0 AETH';
264
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
265
+ }
266
+
267
+ function formatAethFull(lamports) {
268
+ return (Number(lamports) / 1e9).toFixed(6) + ' AETH';
269
+ }
270
+
271
+ function formatNumber(n) {
272
+ if (n === null || n === undefined) return '—';
273
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
274
+ }
275
+
276
+ function tierBadge(tier) {
277
+ if (tier === 'full') return `${C.cyan}◆ FULL${C.reset}`;
278
+ if (tier === 'lite') return `${C.yellow}◇ LITE${C.reset}`;
279
+ if (tier === 'observer') return `${C.green}○ OBSERVER${C.reset}`;
280
+ return `${C.dim}[${tier}]${C.reset}`;
281
+ }
282
+
283
+ function tierColor(tier) {
284
+ if (tier === 'full') return C.cyan;
285
+ if (tier === 'lite') return C.yellow;
286
+ if (tier === 'observer') return C.green;
287
+ return C.reset;
288
+ }
289
+
290
+ function scoreColor(score) {
291
+ if (score === null || score === undefined) return C.dim;
292
+ if (score >= 80) return C.green;
293
+ if (score >= 50) return C.yellow;
294
+ return C.red;
295
+ }
296
+
297
+ function apyColor(apy) {
298
+ if (apy === null || apy === undefined) return C.dim;
299
+ if (apy >= 8) return C.green;
300
+ if (apy >= 5) return C.cyan;
301
+ if (apy >= 2) return C.yellow;
302
+ return C.red;
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Render output
307
+ // ---------------------------------------------------------------------------
308
+
309
+ function renderHeader(v) {
310
+ const shortAddr = v.pubkey
311
+ ? v.pubkey.slice(0, 12) + '...' + v.pubkey.slice(-8)
312
+ : 'unknown';
313
+
314
+ console.log();
315
+ console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════════════╗${C.reset}`);
316
+ console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}VALIDATOR DETAILS${C.reset} ${C.dim}${shortAddr}${C.reset} ${C.bright}║${C.reset}`);
317
+ console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════════════╝${C.reset}`);
318
+ console.log();
319
+
320
+ if (v.name) {
321
+ console.log(` ${C.cyan}${C.bright}${v.name}${C.reset}`);
322
+ }
323
+ if (v.pubkey) {
324
+ console.log(` ${C.dim}${v.pubkey}${C.reset}`);
325
+ }
326
+ console.log();
327
+
328
+ // Tier + delinquent banner
329
+ const tierStr = tierBadge(v.tier);
330
+ const deliqStr = v.delinquent ? ` ${C.red}⚠ DELINQUENT${C.reset}` : '';
331
+ console.log(` ${tierStr}${deliqStr}`);
332
+ console.log();
333
+ }
334
+
335
+ function renderStats(v, epochInfo) {
336
+ const stakeFormatted = formatAether(v.stake);
337
+ const stakeAeth = Number(v.stake) / 1e9;
338
+ const score = v.score;
339
+ const apy = v.apy;
340
+ const commission = v.commission;
341
+
342
+ // APY bar (0-15% range for display)
343
+ const apyBarLen = 12;
344
+ const apyDisplay = apy !== null && apy !== undefined ? apy : 0;
345
+ const apyFillLen = Math.min(apyBarLen, Math.round((apyDisplay / 15) * apyBarLen));
346
+
347
+ // Commission bar (0-100%)
348
+ const commBarLen = 10;
349
+ const commDisplay = commission !== null && commission !== undefined ? commission : 0;
350
+ const commFillLen = Math.round((commDisplay / 100) * commBarLen);
351
+
352
+ console.log(` ${C.bright}┌───────────────────────────────────────────────────────────────────────┐${C.reset}`);
353
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Stake${C.reset} ${formatAether(v.stake).padEnd(15)} ${C.cyan}${formatAethFull(v.stake).padEnd(14)} ${C.bright}│${C.reset}`);
354
+
355
+ if (v.lastEpochStake !== null && v.lastEpochStake !== undefined) {
356
+ const lastAeth = Number(v.lastEpochStake) / 1e9;
357
+ const change = stakeAeth - lastAeth;
358
+ const changeStr = change >= 0 ? `+${change.toFixed(2)}` : change.toFixed(2);
359
+ const changeColor = change >= 0 ? C.green : C.red;
360
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Last epoch${C.reset} ${formatAethFull(v.lastEpochStake).padEnd(15)} ${changeColor}${changeStr} AETH${C.reset}`.padEnd(75) + ` ${C.bright}│${C.reset}`);
361
+ }
362
+
363
+ if (score !== null && score !== undefined) {
364
+ const sc = scoreColor(score);
365
+ const scoreLabel = score >= 80 ? 'excellent' : score >= 50 ? 'good' : score >= 20 ? 'fair' : 'poor';
366
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Score${C.reset} ${sc}${score}${C.reset}% (${scoreLabel})`.padEnd(75) + ` ${C.bright}│${C.reset}`);
367
+ }
368
+
369
+ if (apy !== null && apy !== undefined) {
370
+ const ac = apyColor(apy);
371
+ const apyBar = `${ac}${'█'.repeat(apyFillLen)}${C.dim}${'░'.repeat(apyBarLen - apyFillLen)}${C.reset}`;
372
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Est. APY${C.reset} ${ac}${apy.toFixed(2)}%${C.reset} ${apyBar}`.padEnd(75) + ` ${C.bright}│${C.reset}`);
373
+ }
374
+
375
+ if (commission !== null && commission !== undefined) {
376
+ const commBar = `${C.yellow}${'█'.repeat(commFillLen)}${C.dim}${'░'.repeat(commBarLen - commFillLen)}${C.reset}`;
377
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Commission${C.reset} ${commission.toFixed(1)}% ${commBar}`.padEnd(75) + ` ${C.bright}│${C.reset}`);
378
+ }
379
+
380
+ console.log(` ${C.bright}└───────────────────────────────────────────────────────────────────────┘${C.reset}`);
381
+ console.log();
382
+ }
383
+
384
+ function renderNodeInfo(v) {
385
+ const items = [];
386
+ if (v.version) items.push({ label: 'Version', value: v.version, color: C.cyan });
387
+ if (v.ip) items.push({ label: 'IP', value: v.ip, color: C.dim });
388
+ if (v.lastVote !== null && v.lastVote !== undefined) items.push({ label: 'Last vote', value: `slot ${formatNumber(v.lastVote)}`, color: C.cyan });
389
+ if (v.rootSlot !== null && v.rootSlot !== undefined) items.push({ label: 'Root slot', value: formatNumber(v.rootSlot), color: C.cyan });
390
+
391
+ if (items.length === 0) return;
392
+
393
+ console.log(` ${C.bright}── Node Info ──────────────────────────────────────────────${C.reset}`);
394
+ for (const item of items) {
395
+ console.log(` ${C.dim}${item.label}:${C.reset} ${item.color}${item.value}${C.reset}`);
396
+ }
397
+ console.log();
398
+ }
399
+
400
+ function renderEpochPerformance(v, epochInfo, performance) {
401
+ console.log(` ${C.bright}── Epoch Performance ─────────────────────────────────────${C.reset}`);
402
+
403
+ if (epochInfo) {
404
+ const ep = epochInfo.epoch;
405
+ const slotIdx = epochInfo.slot_index;
406
+ const slotsInEp = epochInfo.slots_in_epoch;
407
+ const progress = slotsInEp > 0 ? ((slotIdx / slotsInEp) * 100).toFixed(1) : '?';
408
+ console.log(` ${C.dim}Current epoch:${C.reset} ${C.bright}${ep}${C.reset} ${C.dim}progress: ${C.reset}${progress}%`);
409
+ if (slotsInEp) console.log(` ${C.dim}Slots in epoch:${C.reset} ${formatNumber(slotsInEp)}`);
410
+ if (slotIdx !== undefined) console.log(` ${C.dim}Current slot:${C.reset} ${formatNumber(slotIdx)}`);
411
+ console.log();
412
+ }
413
+
414
+ if (v.credits !== null && v.credits !== undefined) {
415
+ if (Array.isArray(v.credits) && v.credits.length > 0) {
416
+ const [ep, cr, pr] = v.credits.length >= 3
417
+ ? [v.credits[v.credits.length - 1], v.credits[v.credits.length - 2], v.credits[v.credits.length - 3]]
418
+ : [null, null, null];
419
+ if (ep !== null) console.log(` ${C.dim}Epoch credits:${C.reset} ${formatNumber(ep)} ${C.dim}(prev: ${formatNumber(pr)})${C.reset}`);
420
+ } else if (typeof v.credits === 'number') {
421
+ console.log(` ${C.dim}Epoch credits:${C.reset} ${formatNumber(v.credits)}`);
422
+ }
423
+ }
424
+
425
+ if (performance) {
426
+ if (performance.slots_in_epoch !== undefined) {
427
+ console.log(` ${C.dim}Epoch slots:${C.reset} ${formatNumber(performance.slots_in_epoch)}`);
428
+ }
429
+ if (performance.slots_produced !== undefined) {
430
+ const pct = performance.slots_in_epoch > 0
431
+ ? ((performance.slots_produced / performance.slots_in_epoch) * 100).toFixed(1)
432
+ : '?';
433
+ console.log(` ${C.dim}Slots produced:${C.reset} ${performance.slots_produced} / ${formatNumber(performance.slots_in_epoch)} ${C.dim}(${pct}%)${C.reset}`);
434
+ }
435
+ if (performance.credits !== undefined) {
436
+ console.log(` ${C.dim}Credits earned:${C.reset} ${formatNumber(performance.credits)}`);
437
+ }
438
+ }
439
+
440
+ console.log();
441
+ }
442
+
443
+ function renderDelegators(delegators) {
444
+ if (!delegators || delegators.length === 0) {
445
+ console.log(` ${C.bright}── Delegators ────────────────────────────────────────────${C.reset}`);
446
+ console.log(` ${C.dim}No delegator data available for this validator.${C.reset}`);
447
+ console.log();
448
+ return;
449
+ }
450
+
451
+ console.log(` ${C.bright}── Delegators (${delegators.length}) ──────────────────────────────────${C.reset}`);
452
+ console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────────────────┐${C.reset}`);
453
+ console.log(` ${C.dim}│${C.reset} ${C.cyan}Delegator${C.reset.padEnd(44)} ${C.cyan}Stake${C.reset.padEnd(18)} ${C.cyan}Status${C.reset.padEnd(12)} ${C.dim}│${C.reset}`);
454
+ console.log(` ${C.dim}├─────────────────────────────────────────────────────────────────────────┤${C.reset}`);
455
+
456
+ const sorted = [...delegators].sort((a, b) => {
457
+ const aLamports = Number(a.lamports || a.stake || 0);
458
+ const bLamports = Number(b.lamports || b.stake || 0);
459
+ return bLamports - aLamports;
460
+ }).slice(0, 20);
461
+
462
+ let totalDelegated = BigInt(0);
463
+
464
+ for (const d of sorted) {
465
+ const addr = d.address || d.pubkey || d.delegator || 'unknown';
466
+ const shortAddr = addr.slice(0, 20) + '...' + addr.slice(-12);
467
+ const lamports = BigInt(d.lamports || d.stake || 0);
468
+ totalDelegated += lamports;
469
+ const stakeFormatted = formatAether(lamports);
470
+ const status = d.status || d.activation_status || 'active';
471
+ const statusColor = status === 'active' ? C.green : status === 'pending' ? C.yellow : C.dim;
472
+
473
+ console.log(
474
+ ` ${C.dim}│${C.reset} ${shortAddr.padEnd(46)} ${stakeFormatted.padEnd(18)} ${statusColor}${(status + '').padEnd(12)}${C.reset} ${C.dim}│${C.reset}`
475
+ );
476
+ }
477
+
478
+ if (delegators.length > 20) {
479
+ console.log(` ${C.dim}│${C.reset} ${C.dim}... and ${delegators.length - 20} more delegators (use --json for full list)${C.reset}`.padEnd(77) + `${C.dim}│${C.reset}`);
480
+ }
481
+
482
+ console.log(` ${C.dim}└─────────────────────────────────────────────────────────────────────────┘${C.reset}`);
483
+ console.log(` ${C.dim}Total delegated (shown): ${formatAether(totalDelegated)}${C.reset}`);
484
+ console.log();
485
+ }
486
+
487
+ function renderRank(validators, v) {
488
+ if (!validators || !v) return;
489
+
490
+ const sorted = [...validators].sort((a, b) => {
491
+ const aStake = typeof a.stake === 'bigint' ? Number(a.stake) : Number(a.stake || 0);
492
+ const bStake = typeof b.stake === 'bigint' ? Number(b.stake) : Number(b.stake || 0);
493
+ return bStake - aStake;
494
+ });
495
+
496
+ const rank = sorted.findIndex(m => {
497
+ const mPub = m.pubkey || m.address || '';
498
+ const vPub = v.pubkey || '';
499
+ return mPub === vPub;
500
+ });
501
+
502
+ if (rank >= 0) {
503
+ const pct = ((rank + 1) / sorted.length * 100).toFixed(1);
504
+ console.log(` ${C.bright}── Network Rank ───────────────────────────────────────────${C.reset}`);
505
+ console.log(` ${C.dim}Stake rank:${C.reset} ${C.bright}#${rank + 1}${C.reset} of ${sorted.length} validators ${C.dim}(top ${pct}%)${C.reset}`);
506
+ console.log();
507
+ }
508
+ }
509
+
510
+ function renderJson(v, delegators, performance, epochInfo, validators) {
511
+ const out = {
512
+ rpc: opts.rpc,
513
+ pubkey: v.pubkey,
514
+ name: v.name,
515
+ tier: v.tier,
516
+ stake: v.stake.toString(),
517
+ stake_aeth: Number(v.stake) / 1e9,
518
+ stake_formatted: formatAether(v.stake),
519
+ score: v.score,
520
+ apy: v.apy,
521
+ commission: v.commission,
522
+ version: v.version,
523
+ ip: v.ip,
524
+ last_vote: v.lastVote,
525
+ root_slot: v.rootSlot,
526
+ delinquent: v.delinquent,
527
+ activated_stake: v.activatedStake?.toString(),
528
+ last_epoch_stake: v.lastEpochStake?.toString(),
529
+ credits: v.credits,
530
+ delegators: delegators ? delegators.map(d => ({
531
+ address: d.address || d.pubkey || d.delegator,
532
+ lamports: String(d.lamports || d.stake || 0),
533
+ stake_aeth: Number(d.lamports || d.stake || 0) / 1e9,
534
+ status: d.status || d.activation_status || 'unknown',
535
+ })) : [],
536
+ performance,
537
+ epoch_info: epochInfo,
538
+ fetched_at: new Date().toISOString(),
539
+ };
540
+
541
+ // Add rank
542
+ if (validators && v.pubkey) {
543
+ const sorted = [...validators].sort((a, b) => {
544
+ const aS = typeof a.stake === 'bigint' ? Number(a.stake) : Number(a.stake || 0);
545
+ const bS = typeof b.stake === 'bigint' ? Number(b.stake) : Number(b.stake || 0);
546
+ return bS - aS;
547
+ });
548
+ const rank = sorted.findIndex(m => (m.pubkey || m.address || '') === v.pubkey);
549
+ if (rank >= 0) out.rank = { position: rank + 1, total: sorted.length };
550
+ }
551
+
552
+ console.log(JSON.stringify(out, null, 2));
553
+ }
554
+
555
+ // ---------------------------------------------------------------------------
556
+ // Main
557
+ // ---------------------------------------------------------------------------
558
+
559
+ async function main() {
560
+ const opts = parseArgs();
561
+
562
+ if (!opts.target) {
563
+ console.log(`\n ${C.red}✗ Missing validator address or name.${C.reset}`);
564
+ console.log(`\n ${C.cyan}Usage:${C.reset}`);
565
+ console.log(` ${C.cyan}aether validator info <addressOrName>${C.reset} Inspect a validator`);
566
+ console.log(` ${C.cyan}aether validator info <addressOrName> --json${C.reset} JSON output`);
567
+ console.log(` ${C.cyan}aether validator info --address <addr>${C.reset} Use --address flag`);
568
+ console.log();
569
+ process.exit(1);
570
+ }
571
+
572
+ const rpc = opts.rpc;
573
+
574
+ if (!opts.asJson) {
575
+ console.log(`\n${C.dim}Looking up "${opts.target}" on ${rpc}...${C.reset}`);
576
+ }
577
+
578
+ // Fetch everything in parallel
579
+ const [rawValidator, allValidators, epochInfo, supply] = await Promise.all([
580
+ fetchValidatorByIdentity(rpc, opts.target),
581
+ fetchAllValidators(rpc),
582
+ fetchEpochInfo(rpc),
583
+ fetchSupply(rpc),
584
+ ]);
585
+
586
+ if (!rawValidator) {
587
+ if (opts.asJson) {
588
+ console.log(JSON.stringify({ error: 'Validator not found', query: opts.target, rpc }, null, 2));
589
+ } else {
590
+ console.log(`\n ${C.red}✗ Validator not found:${C.reset} "${opts.target}"`);
591
+ console.log(` ${C.dim}Try a full address (ATH...), partial address (first 8+ chars), or name.${C.reset}`);
592
+ console.log(` ${C.dim}List all validators: aether validators list${C.reset}\n`);
593
+ }
594
+ process.exit(1);
595
+ }
596
+
597
+ const v = normalise(rawValidator);
598
+
599
+ // Fetch delegators and performance in parallel
600
+ const [delegators, performance] = await Promise.all([
601
+ v.pubkey ? fetchDelegators(rpc, v.pubkey) : Promise.resolve([]),
602
+ v.pubkey ? fetchValidatorPerformance(rpc, v.pubkey) : Promise.resolve(null),
603
+ ]);
604
+
605
+ // Estimate APY if not available
606
+ let apyEstimated = v.apy;
607
+ if ((apyEstimated === null || apyEstimated === undefined) && supply && !supply.error && epochInfo) {
608
+ const totalStake = Number(supply.total_staked || supply.total || 0);
609
+ const rewardsPerEpoch = Number(epochInfo.rewards_per_epoch || '2000000000');
610
+ if (totalStake > 0 && rewardsPerEpoch > 0) {
611
+ const validatorStake = Number(v.stake);
612
+ const networkApy = (rewardsPerEpoch / totalStake) * 73;
613
+ if (validatorStake > 0) {
614
+ apyEstimated = networkApy;
615
+ }
616
+ }
617
+ }
618
+
619
+ const finalV = { ...v, apy: apyEstimated !== undefined ? apyEstimated : v.apy };
620
+
621
+ if (opts.asJson) {
622
+ renderJson(finalV, delegators, performance, epochInfo, allValidators);
623
+ return;
624
+ }
625
+
626
+ renderHeader(finalV);
627
+ renderStats(finalV, epochInfo);
628
+ renderNodeInfo(finalV);
629
+ renderEpochPerformance(finalV, epochInfo, performance);
630
+ renderDelegators(delegators);
631
+ renderRank(allValidators, finalV);
632
+
633
+ console.log(` ${C.dim}Fetched from: ${rpc}${C.reset}`);
634
+ console.log();
635
+ }
636
+
637
+ main().catch(err => {
638
+ console.error(`\n${C.red}✗ Validator info failed:${C.reset} ${err.message}\n`);
639
+ process.exit(1);
640
+ });
package/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  const { doctorCommand } = require('./commands/doctor');
10
10
  const { validatorStart } = require('./commands/validator-start');
11
11
  const { validatorStatus } = require('./commands/validator-status');
12
+ const { validatorInfo } = require('./commands/validator-info');
12
13
  const { init } = require('./commands/init');
13
14
  const { monitorLoop } = require('./commands/monitor');
14
15
  const { logsCommand } = require('./commands/logs');
@@ -22,6 +23,8 @@ const { rewardsCommand } = require('./commands/rewards');
22
23
  const { accountCommand } = require('./commands/account');
23
24
  const { emergencyCommand } = require('./commands/emergency');
24
25
  const { priceCommand } = require('./commands/price');
26
+ const { epochCommand } = require('./commands/epoch');
27
+ const { supplyCommand } = require('./commands/supply');
25
28
  const readline = require('readline');
26
29
 
27
30
  // CLI version
@@ -279,9 +282,12 @@ const COMMANDS = {
279
282
  case 'status':
280
283
  validatorStatus();
281
284
  break;
285
+ case 'info':
286
+ validatorInfo();
287
+ break;
282
288
  default:
283
289
  console.error(`Unknown validator command: ${subcmd}`);
284
- console.error('Valid commands: start, status');
290
+ console.error('Valid commands: start, status, info');
285
291
  process.exit(1);
286
292
  }
287
293
  },
@@ -319,6 +325,21 @@ const COMMANDS = {
319
325
  accountCommand();
320
326
  },
321
327
  },
328
+ epoch: {
329
+ description: 'Aether epoch info — current epoch, slot, time remaining, APY estimate — aether epoch [--json] [--schedule]',
330
+ handler: () => {
331
+ const { epochCommand } = require('./commands/epoch');
332
+ epochCommand();
333
+ },
334
+ },
335
+ supply: {
336
+ description: 'Aether token supply — total, circulating, staked, burned — aether supply [--json] [--verbose]',
337
+ handler: () => {
338
+ const { supplyCommand } = require('./commands/supply');
339
+ // Pass full argv so supply.js can parse its own --help etc.
340
+ supplyCommand();
341
+ },
342
+ },
322
343
  validators: {
323
344
  description: 'List active validators — aether validators list [--tier full|lite|observer] [--json]',
324
345
  handler: () => {