aether-hub 1.1.0 → 1.1.1

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,326 @@
1
+ /**
2
+ * aether-cli validators - Aether Validator Registry
3
+ *
4
+ * List and explore validators available for staking.
5
+ * aether validators list — Show all active validators
6
+ * aether validators list --json — JSON output for scripting
7
+ * aether validators list --tier <full|lite|observer> — Filter by tier
8
+ *
9
+ * @see docs/MINING_VALIDATOR_TOOLS.md for spec
10
+ */
11
+
12
+ const http = require('http');
13
+ const https = require('https');
14
+
15
+ // ANSI colours
16
+ const C = {
17
+ reset: '\x1b[0m',
18
+ bright: '\x1b[1m',
19
+ dim: '\x1b[2m',
20
+ red: '\x1b[31m',
21
+ green: '\x1b[32m',
22
+ yellow: '\x1b[33m',
23
+ blue: '\x1b[34m',
24
+ cyan: '\x1b[36m',
25
+ magenta: '\x1b[35m',
26
+ };
27
+
28
+ const DEFAULT_RPC = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // HTTP helpers (mirrors network.js patterns)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function httpRequest(rpcUrl, path) {
35
+ return new Promise((resolve, reject) => {
36
+ const url = new URL(path, rpcUrl);
37
+ const isHttps = url.protocol === 'https:';
38
+ const lib = isHttps ? https : http;
39
+
40
+ const req = lib.request({
41
+ hostname: url.hostname,
42
+ port: url.port || (isHttps ? 443 : 80),
43
+ path: url.pathname + url.search,
44
+ method: 'GET',
45
+ timeout: 8000,
46
+ headers: { 'Content-Type': 'application/json' },
47
+ }, (res) => {
48
+ let data = '';
49
+ res.on('data', (chunk) => (data += chunk));
50
+ res.on('end', () => {
51
+ try { resolve(JSON.parse(data)); }
52
+ catch { resolve({ raw: data }); }
53
+ });
54
+ });
55
+
56
+ req.on('error', reject);
57
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
58
+ req.end();
59
+ });
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Argument parsing
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function parseArgs() {
67
+ const args = process.argv.slice(3); // skip 'validators' and subcommand
68
+ const opts = {
69
+ rpc: DEFAULT_RPC,
70
+ tier: null,
71
+ asJson: false,
72
+ sort: 'stake', // 'stake' | 'name' | 'uptime'
73
+ limit: 50,
74
+ };
75
+
76
+ for (let i = 0; i < args.length; i++) {
77
+ if (args[i] === '--rpc' || args[i] === '-r') {
78
+ opts.rpc = args[++i];
79
+ } else if (args[i] === '--tier' || args[i] === '-t') {
80
+ opts.tier = args[++i]?.toLowerCase();
81
+ } else if (args[i] === '--json' || args[i] === '-j') {
82
+ opts.asJson = true;
83
+ } else if (args[i] === '--sort' || args[i] === '-s') {
84
+ opts.sort = args[++i]?.toLowerCase();
85
+ } else if (args[i] === '--limit' || args[i] === '-l') {
86
+ opts.limit = parseInt(args[++i], 10) || 50;
87
+ } else if (args[i] === '--help' || args[i] === '-h') {
88
+ showHelp();
89
+ process.exit(0);
90
+ }
91
+ }
92
+
93
+ return opts;
94
+ }
95
+
96
+ function showHelp() {
97
+ console.log(`
98
+ ${C.bright}${C.cyan}aether-cli validators${C.reset} - Aether Validator Registry
99
+
100
+ ${C.bright}Usage:${C.reset}
101
+ aether validators list [options]
102
+
103
+ ${C.bright}Options:${C.reset}
104
+ -r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC} or $AETHER_RPC)
105
+ -t, --tier <tier> Filter by tier: full, lite, observer
106
+ -s, --sort <field> Sort by: stake (default), name, uptime
107
+ -l, --limit <n> Max validators to show (default: 50)
108
+ -j, --json Output raw JSON
109
+ -h, --help Show this help
110
+
111
+ ${C.bright}Examples:${C.reset}
112
+ aether validators list
113
+ aether validators list --tier full
114
+ aether validators list --sort stake --limit 20
115
+ aether validators list --json
116
+ `.trim());
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Data fetching
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /** Fetch validators list from RPC */
124
+ async function fetchValidators(rpc) {
125
+ try {
126
+ const res = await httpRequest(rpc, '/v1/validators');
127
+ if (Array.isArray(res)) return res;
128
+ if (res.validators && Array.isArray(res.validators)) return res.validators;
129
+ return [];
130
+ } catch (err) {
131
+ return [];
132
+ }
133
+ }
134
+
135
+ /** Fetch epoch info for context */
136
+ async function fetchEpoch(rpc) {
137
+ try {
138
+ const res = await httpRequest(rpc, '/v1/epoch');
139
+ return res;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Formatting helpers
147
+ // ---------------------------------------------------------------------------
148
+
149
+ function formatAether(lamports) {
150
+ if (!lamports && lamports !== 0) return '?';
151
+ const aeth = lamports / 1e9;
152
+ if (aeth === 0) return '0';
153
+ return aeth.toFixed(2).replace(/\.?0+$/, '');
154
+ }
155
+
156
+ function formatPct(n) {
157
+ if (n === undefined || n === null) return '?';
158
+ return n.toFixed(1) + '%';
159
+ }
160
+
161
+ function tierBadge(tier) {
162
+ const map = {
163
+ full: `${C.cyan}FULL${C.reset}`,
164
+ lite: `${C.yellow}LITE${C.reset}`,
165
+ observer: `${C.green}OBS${C.reset}`,
166
+ };
167
+ return map[tier?.toLowerCase()] || `${C.dim}?${C.reset}`;
168
+ }
169
+
170
+ function scoreColor(score) {
171
+ if (score === undefined || score === null) return C.dim;
172
+ if (score >= 90) return C.green;
173
+ if (score >= 70) return C.yellow;
174
+ return C.red;
175
+ }
176
+
177
+ function shortenAddr(addr, len = 16) {
178
+ if (!addr) return '?';
179
+ if (addr.length <= len) return addr;
180
+ return addr.slice(0, 8) + '…' + addr.slice(-6);
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Renderers
185
+ // ---------------------------------------------------------------------------
186
+
187
+ function renderList(validators, epochData, opts, rpc) {
188
+ const filtered = opts.tier
189
+ ? validators.filter(v => (v.tier || v.node_type || '').toLowerCase() === opts.tier)
190
+ : validators;
191
+
192
+ // Sort
193
+ if (opts.sort === 'name') {
194
+ filtered.sort((a, b) => (a.name || a.address || '').localeCompare(b.name || b.address || ''));
195
+ } else if (opts.sort === 'uptime') {
196
+ filtered.sort((a, b) => (b.uptime || b.score || 0) - (a.uptime || a.score || 0));
197
+ } else {
198
+ // Default: by stake (descending)
199
+ filtered.sort((a, b) => (b.stake || b.stake_amount || 0) - (a.stake || a.stake_amount || 0));
200
+ }
201
+
202
+ const shown = filtered.slice(0, opts.limit);
203
+ const totalStake = validators.reduce((sum, v) => sum + (v.stake || v.stake_amount || 0), 0);
204
+ const networkScore = validators.reduce((sum, v) => sum + (v.score || v.uptime || 0) * (v.stake || 1), 0) / (totalStake || 1);
205
+
206
+ console.log();
207
+ console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════════╗${C.reset}`);
208
+ console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER VALIDATOR REGISTRY${C.reset}${C.cyan} ║${C.reset}`);
209
+ console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════════╝${C.reset}`);
210
+ console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
211
+ console.log(` ${C.dim}Total validators:${C.reset} ${C.bright}${validators.length}${C.reset}`);
212
+ if (opts.tier) console.log(` ${C.dim}Filtered by tier:${C.reset} ${C.bright}${opts.tier}${C.reset}`);
213
+ console.log();
214
+
215
+ // Summary stats
216
+ console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────────┐${C.reset}`);
217
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Total Stake${C.reset} ${C.bright}│${C.reset} ${C.green}${formatAether(totalStake).padEnd(20)} AETH${C.reset}`.padEnd(80) + `${C.bright}│${C.reset}`);
218
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Network Uptime${C.reset} ${C.bright}│${C.reset} ${scoreColor(networkScore)}${formatPct(networkScore).padEnd(20)}${C.reset}`.padEnd(80) + `${C.bright}│${C.reset}`);
219
+ if (epochData && epochData.epoch !== undefined) {
220
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Current Epoch${C.reset} ${C.bright}│${C.reset} ${C.green}${epochData.epoch}${C.reset}`.padEnd(80) + `${C.bright}│${C.reset}`);
221
+ }
222
+ console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────────┘${C.reset}`);
223
+ console.log();
224
+
225
+ if (shown.length === 0) {
226
+ console.log(` ${C.yellow}⚠ No validators found${C.reset}${opts.tier ? ` for tier "${opts.tier}"` : ''}.`);
227
+ console.log(` ${C.dim}Try without --tier filter or check RPC connectivity.${C.reset}`);
228
+ console.log();
229
+ return;
230
+ }
231
+
232
+ // Table header
233
+ console.log(` ${C.bright}┌────┬────────────────────────┬────────┬─────────┬────────┬─────────────┐${C.reset}`);
234
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}#${C.reset} ${C.cyan}Validator${C.reset} ${C.cyan}Tier${C.reset} ${C.cyan}Stake${C.reset} ${C.cyan}Uptime${C.reset} ${C.cyan}Commission${C.reset} ${C.bright}│${C.reset}`);
235
+ console.log(` ${C.bright}├────┼────────────────────────┼────────┼─────────┼────────┼─────────────┤${C.reset}`);
236
+
237
+ for (let i = 0; i < shown.length; i++) {
238
+ const v = shown[i];
239
+ const num = (i + 1).toString().padStart(3);
240
+ const addr = shortenAddr(v.address || v.pubkey || v.id);
241
+ const tierStr = tierBadge(v.tier || v.node_type);
242
+ const stake = formatAether(v.stake || v.stake_amount || 0);
243
+ const uptime = formatPct(v.uptime || v.score);
244
+ const commission = formatPct(v.commission || v.fee);
245
+ const row = ` ${C.bright}│${C.reset} ${C.dim}${num}${C.reset} ${addr.padEnd(24)} ${tierStr.padEnd(8)} ${stake.padEnd(9)} ${scoreColor(v.uptime || v.score)}${uptime.padEnd(9)}${C.reset} ${C.dim}${commission}${C.reset} ${C.bright}│${C.reset}`;
246
+ console.log(row);
247
+ }
248
+
249
+ console.log(` ${C.bright}└────┴────────────────────────┴────────┴─────────┴────────┴─────────────┘${C.reset}`);
250
+ console.log();
251
+
252
+ if (filtered.length > opts.limit) {
253
+ console.log(` ${C.dim}Showing ${opts.limit} of ${filtered.length} validators. Use --limit to see more.${C.reset}`);
254
+ }
255
+
256
+ console.log();
257
+ console.log(` ${C.dim}Stake to a validator:${C.reset}`);
258
+ console.log(` ${C.cyan}aether stake --validator <address> --amount <aeth>${C.reset}`);
259
+ console.log(` ${C.cyan}aether stake --validator <address> --amount <aeth> --dry-run${C.reset} ${C.dim}(preview)${C.reset}`);
260
+ console.log();
261
+ }
262
+
263
+ function renderJson(validators, epochData, opts, rpc) {
264
+ const out = {
265
+ rpc,
266
+ fetchedAt: new Date().toISOString(),
267
+ total: validators.length,
268
+ epoch: epochData?.epoch ?? null,
269
+ validators: validators.map(v => ({
270
+ address: v.address || v.pubkey || v.id,
271
+ name: v.name || null,
272
+ tier: v.tier || v.node_type || null,
273
+ stake: v.stake || v.stake_amount || 0,
274
+ stakeAETH: formatAether(v.stake || v.stake_amount || 0),
275
+ uptime: v.uptime ?? v.score ?? null,
276
+ uptimePct: formatPct(v.uptime ?? v.score),
277
+ commission: v.commission ?? v.fee ?? null,
278
+ commissionPct: formatPct(v.commission ?? v.fee),
279
+ lastSeen: v.last_seen || v.lastActive || null,
280
+ version: v.version || v.clientVersion || null,
281
+ })),
282
+ };
283
+ console.log(JSON.stringify(out, null, 2));
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Main
288
+ // ---------------------------------------------------------------------------
289
+
290
+ async function validatorsListCommand() {
291
+ const opts = parseArgs();
292
+ const rpc = opts.rpc;
293
+
294
+ if (!opts.asJson) {
295
+ console.log(`\n${C.cyan}Fetching validators...${C.reset} ${C.dim}(${rpc})${C.reset}`);
296
+ }
297
+
298
+ const [validators, epochData] = await Promise.all([
299
+ fetchValidators(rpc),
300
+ fetchEpoch(rpc),
301
+ ]);
302
+
303
+ if (validators.length === 0 && !opts.asJson) {
304
+ console.log(`\n ${C.yellow}⚠ No validator data returned from RPC.${C.reset}`);
305
+ console.log(` ${C.dim} Is your validator running and fully synced?${C.reset}`);
306
+ console.log(` ${C.dim} Check: aether-cli network${C.reset}`);
307
+ console.log(` ${C.dim} Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
308
+ return;
309
+ }
310
+
311
+ if (opts.asJson) {
312
+ renderJson(validators, epochData, opts, rpc);
313
+ } else {
314
+ renderList(validators, epochData, opts, rpc);
315
+ }
316
+ }
317
+
318
+ module.exports = { validatorsListCommand };
319
+
320
+ if (require.main === module) {
321
+ validatorsListCommand().catch((err) => {
322
+ console.error(`\n${C.red}✗ Validators command failed:${C.reset} ${err.message}`);
323
+ console.error(` ${C.dim}Check that your validator is running and RPC is accessible.${C.reset}\n`);
324
+ process.exit(1);
325
+ });
326
+ }