aether-hub 1.0.3

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,431 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli monitor - Real-time Validator Dashboard
4
+ *
5
+ * Polls the validator RPC endpoints and displays:
6
+ * - Current slot number
7
+ * - Block height
8
+ * - Peer count
9
+ * - Transactions per second (TPS)
10
+ * - Validator health status
11
+ *
12
+ * Updates every 2 seconds with rich terminal output.
13
+ */
14
+
15
+ const http = require('http');
16
+ const https = require('https');
17
+ const os = require('os');
18
+
19
+ // ANSI colors and control codes
20
+ const colors = {
21
+ reset: '\x1b[0m',
22
+ bright: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ red: '\x1b[31m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ blue: '\x1b[34m',
28
+ magenta: '\x1b[35m',
29
+ cyan: '\x1b[36m',
30
+ white: '\x1b[37m',
31
+ bgBlue: '\x1b[44m',
32
+ bgGreen: '\x1b[42m',
33
+ bgRed: '\x1b[41m',
34
+ };
35
+
36
+ const cursor = {
37
+ up: '\x1b[A',
38
+ down: '\x1b[B',
39
+ right: '\x1b[C',
40
+ left: '\x1b[D',
41
+ hide: '\x1b[?25l',
42
+ show: '\x1b[?25h',
43
+ clear: '\x1b[2J',
44
+ clearLine: '\x1b[2K',
45
+ };
46
+
47
+ // Default RPC endpoint
48
+ const DEFAULT_RPC = 'http://127.0.0.1:8899';
49
+ const POLL_INTERVAL_MS = 2000;
50
+
51
+ // State tracking for TPS calculation
52
+ let previousSlot = null;
53
+ let previousTimestamp = null;
54
+ let tpsHistory = [];
55
+
56
+ /**
57
+ * Parse command line arguments
58
+ */
59
+ function parseArgs() {
60
+ // Handle both direct execution (node monitor.js) and via aether-cli
61
+ // Direct: argv = [node, monitor.js, --help] -> slice(2)
62
+ // Via CLI: argv = [node, index.js, monitor, --help] -> slice(3)
63
+ const args = process.argv.slice(2);
64
+ const options = {
65
+ rpc: DEFAULT_RPC,
66
+ interval: POLL_INTERVAL_MS,
67
+ };
68
+
69
+ for (let i = 0; i < args.length; i++) {
70
+ switch (args[i]) {
71
+ case '--rpc':
72
+ case '-r':
73
+ options.rpc = args[++i];
74
+ break;
75
+ case '--interval':
76
+ case '-i':
77
+ options.interval = parseInt(args[++i], 10) || POLL_INTERVAL_MS;
78
+ break;
79
+ case '--help':
80
+ case '-h':
81
+ showHelp();
82
+ process.exit(0);
83
+ }
84
+ }
85
+
86
+ return options;
87
+ }
88
+
89
+ /**
90
+ * Show help message
91
+ */
92
+ function showHelp() {
93
+ console.log(`
94
+ ${colors.bright}${colors.cyan}aether-cli monitor${colors.reset} - Real-time Validator Dashboard
95
+
96
+ ${colors.bright}Usage:${colors.reset}
97
+ aether-cli monitor [options]
98
+
99
+ ${colors.bright}Options:${colors.reset}
100
+ -r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC})
101
+ -i, --interval <ms> Poll interval in milliseconds (default: ${POLL_INTERVAL_MS})
102
+ -h, --help Show this help message
103
+
104
+ ${colors.bright}Examples:${colors.reset}
105
+ aether-cli monitor # Monitor local validator
106
+ aether-cli monitor --rpc http://api.testnet.aether.network
107
+ aether-cli monitor -i 1000 # Poll every second
108
+ `.trim());
109
+ }
110
+
111
+ /**
112
+ * Make HTTP GET request to REST endpoint
113
+ * Aether validator uses simple REST endpoints, not JSON-RPC
114
+ */
115
+ function httpRequest(endpoint) {
116
+ return new Promise((resolve, reject) => {
117
+ const baseUrl = options.rpc.endsWith('/') ? options.rpc.slice(0, -1) : options.rpc;
118
+ const url = new URL(`${baseUrl}${endpoint}`);
119
+ const isHttps = url.protocol === 'https:';
120
+ const lib = isHttps ? https : http;
121
+
122
+ const reqOptions = {
123
+ hostname: url.hostname,
124
+ port: url.port || (isHttps ? 443 : 80),
125
+ path: url.pathname + url.search,
126
+ method: 'GET',
127
+ timeout: 5000,
128
+ };
129
+
130
+ const req = lib.request(reqOptions, (res) => {
131
+ let data = '';
132
+ res.on('data', (chunk) => (data += chunk));
133
+ res.on('end', () => {
134
+ try {
135
+ const result = JSON.parse(data);
136
+ resolve(result);
137
+ } catch (e) {
138
+ resolve({ raw: data }); // Return raw if not JSON
139
+ }
140
+ });
141
+ });
142
+
143
+ req.on('error', reject);
144
+ req.on('timeout', () => {
145
+ req.destroy();
146
+ reject(new Error('Request timeout'));
147
+ });
148
+
149
+ req.end();
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Fetch slot information from /v1/slot
155
+ */
156
+ async function getSlot() {
157
+ const result = await httpRequest('/v1/slot');
158
+ return result.slot || 0;
159
+ }
160
+
161
+ /**
162
+ * Fetch block height from /v1/slot (same endpoint for now)
163
+ */
164
+ async function getBlockHeight() {
165
+ const result = await httpRequest('/v1/slot');
166
+ return result.slot || 0;
167
+ }
168
+
169
+ /**
170
+ * Fetch peer count from /v1/validators
171
+ */
172
+ async function getPeerCount() {
173
+ const result = await httpRequest('/v1/validators');
174
+ if (result.validators && Array.isArray(result.validators)) {
175
+ return result.validators.length;
176
+ }
177
+ return 0;
178
+ }
179
+
180
+ /**
181
+ * Fetch validator info (tier, consensus weight) from /v1/validator/info
182
+ */
183
+ async function getValidatorInfo() {
184
+ try {
185
+ const result = await httpRequest('/v1/validator/info');
186
+ return {
187
+ tier: result.tier || null,
188
+ consensusWeight: result.consensus_weight || null,
189
+ };
190
+ } catch (e) {
191
+ return { tier: null, consensusWeight: null };
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Fetch TPS from /v1/slot (calculate from slot progression)
197
+ */
198
+ async function getTPS() {
199
+ // TPS is calculated locally based on slot progression
200
+ return null;
201
+ }
202
+
203
+ /**
204
+ * Calculate TPS from slot progression
205
+ */
206
+ function calculateTPS(currentSlot, currentTimestamp) {
207
+ if (previousSlot === null || previousTimestamp === null) {
208
+ return null;
209
+ }
210
+
211
+ const slotDiff = currentSlot - previousSlot;
212
+ const timeDiff = (currentTimestamp - previousTimestamp) / 1000; // seconds
213
+
214
+ if (timeDiff <= 0) return 0;
215
+
216
+ const instantTPS = slotDiff / timeDiff;
217
+
218
+ // Smooth with history
219
+ tpsHistory.push(instantTPS);
220
+ if (tpsHistory.length > 5) {
221
+ tpsHistory.shift();
222
+ }
223
+
224
+ const avgTPS = tpsHistory.reduce((a, b) => a + b, 0) / tpsHistory.length;
225
+ return avgTPS;
226
+ }
227
+
228
+ /**
229
+ * Get status color based on value
230
+ */
231
+ function getStatusColor(healthy) {
232
+ return healthy ? colors.green : colors.red;
233
+ }
234
+
235
+ /**
236
+ * Format number with commas
237
+ */
238
+ function formatNumber(num) {
239
+ if (num === null || num === undefined) return 'N/A';
240
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
241
+ }
242
+
243
+ /**
244
+ * Render the dashboard
245
+ */
246
+ function renderDashboard(data, error = null) {
247
+ const now = new Date();
248
+ const timestamp = now.toLocaleTimeString();
249
+
250
+ // Clear screen and hide cursor
251
+ process.stdout.write(cursor.clear);
252
+ process.stdout.write(cursor.hide);
253
+
254
+ // Header
255
+ const tierBadge = data.tier ? `[${data.tier.toUpperCase()}]` : '';
256
+ const header = `
257
+ ${colors.bright}${colors.cyan}
258
+ ╔═══════════════════════════════════════════════════════════════╗
259
+ ║ ║
260
+ ║ ${colors.bright}AETHER NETWORK MONITOR${colors.reset}${colors.cyan} ║
261
+ ║ ${colors.dim}Real-time Validator Dashboard${colors.reset}${colors.cyan} ║
262
+ ║ ${colors.bright}${tierBadge}${colors.reset}${colors.cyan} ║
263
+ ╚═══════════════════════════════════════════════════════════════╝${colors.reset}
264
+ `.trim();
265
+
266
+ console.log(header);
267
+ console.log();
268
+
269
+ if (error) {
270
+ console.log(` ${colors.bgRed}${colors.bright} ERROR ${colors.reset} ${colors.red}${error.message}${colors.reset}`);
271
+ console.log();
272
+ console.log(` ${colors.dim}Retrying in ${options.interval / 1000}s...${colors.reset}`);
273
+ console.log();
274
+ console.log(` ${colors.dim}RPC: ${options.rpc}${colors.reset}`);
275
+ console.log(` ${colors.dim}Last update: ${timestamp}${colors.reset}`);
276
+ return;
277
+ }
278
+
279
+ const { slot, blockHeight, peerCount, tps, health, tier, consensusWeight } = data;
280
+
281
+ // Status indicator
282
+ const statusIcon = health ? `${colors.green}●${colors.reset}` : `${colors.red}●${colors.reset}`;
283
+ const statusText = health ? `${colors.green}HEALTHY${colors.reset}` : `${colors.red}UNHEALTHY${colors.reset}`;
284
+
285
+ console.log(` ${statusIcon} Status: ${statusText}`);
286
+ if (tier) {
287
+ const weightDisplay = consensusWeight !== undefined ? ` | Weight: ${consensusWeight.toFixed(2)}x` : '';
288
+ console.log(` ${colors.dim}Tier: ${tier.toUpperCase()}${weightDisplay}${colors.reset}`);
289
+ }
290
+ console.log();
291
+
292
+ // Metrics grid
293
+ console.log(` ${colors.bright}┌─────────────────────────────────────────────────────────────┐${colors.reset}`);
294
+ console.log(` ${colors.bright}│${colors.reset} ${colors.cyan}Current Slot${colors.reset}${' '.repeat(44)}${colors.bright}│${colors.reset}`);
295
+ console.log(` ${colors.bright}│${colors.reset} ${colors.bright}${colors.green}${formatNumber(slot).padEnd(52)}${colors.reset}${colors.bright}│${colors.reset}`);
296
+ console.log(` ${colors.bright}│${colors.reset} ${colors.bright}│${colors.reset}`);
297
+ console.log(` ${colors.bright}│${colors.reset} ${colors.cyan}Block Height${colors.reset}${' '.repeat(44)}${colors.bright}│${colors.reset}`);
298
+ console.log(` ${colors.bright}│${colors.reset} ${colors.bright}${colors.blue}${formatNumber(blockHeight).padEnd(52)}${colors.reset}${colors.bright}│${colors.reset}`);
299
+ console.log(` ${colors.bright}│${colors.reset} ${colors.bright}│${colors.reset}`);
300
+ console.log(` ${colors.bright}│${colors.reset} ${colors.cyan}Active Peers${colors.reset}${' '.repeat(44)}${colors.bright}│${colors.reset}`);
301
+ console.log(` ${colors.bright}│${colors.reset} ${colors.bright}${colors.magenta}${formatNumber(peerCount).padEnd(52)}${colors.reset}${colors.bright}│${colors.reset}`);
302
+ console.log(` ${colors.bright}│${colors.reset} ${colors.bright}│${colors.reset}`);
303
+ console.log(` ${colors.bright}│${colors.reset} ${colors.cyan}Transactions/sec${colors.reset}${' '.repeat(40)}${colors.bright}│${colors.reset}`);
304
+
305
+ const tpsDisplay = tps !== null ? `${tps.toFixed(2)} TPS` : 'Calculating...';
306
+ const tpsColor = tps !== null && tps > 0 ? colors.green : colors.yellow;
307
+ console.log(` ${colors.bright}│${colors.reset} ${colors.bright}${tpsColor}${tpsDisplay.padEnd(52)}${colors.reset}${colors.bright}│${colors.reset}`);
308
+ console.log(` ${colors.bright}└─────────────────────────────────────────────────────────────┘${colors.reset}`);
309
+
310
+ console.log();
311
+ console.log(` ${colors.dim}RPC: ${options.rpc}${colors.reset}`);
312
+ console.log(` ${colors.dim}Last update: ${timestamp}${colors.reset}`);
313
+ console.log();
314
+ console.log(` ${colors.dim}Press Ctrl+C to exit${colors.reset}`);
315
+ }
316
+
317
+ /**
318
+ * Main monitor loop
319
+ */
320
+ async function monitorLoop() {
321
+ let iteration = 0;
322
+ let validatorInfo = null;
323
+
324
+ while (true) {
325
+ try {
326
+ const startTime = Date.now();
327
+
328
+ // Fetch all metrics in parallel
329
+ const [slot, blockHeight, peerCountResult, validatorInfoResult] = await Promise.all([
330
+ getSlot().catch(() => null),
331
+ getBlockHeight().catch(() => null),
332
+ getPeerCount().catch(() => 0),
333
+ getValidatorInfo().catch(() => ({ tier: null, consensusWeight: null })),
334
+ ]);
335
+
336
+ // Cache validator info (doesn't change often)
337
+ if (validatorInfoResult.tier) {
338
+ validatorInfo = validatorInfoResult;
339
+ }
340
+
341
+ const currentTime = Date.now();
342
+
343
+ // Calculate TPS from slot progression
344
+ let tps = null;
345
+ if (slot !== null) {
346
+ tps = calculateTPS(slot, currentTime);
347
+ previousSlot = slot;
348
+ previousTimestamp = currentTime;
349
+ }
350
+
351
+ // Health check: validator is healthy if we got valid slot data
352
+ const health = slot !== null && blockHeight !== null;
353
+
354
+ renderDashboard({
355
+ slot: slot || 0,
356
+ blockHeight: blockHeight || 0,
357
+ peerCount: peerCountResult || 0,
358
+ tps,
359
+ health,
360
+ tier: validatorInfo?.tier,
361
+ consensusWeight: validatorInfo?.consensusWeight,
362
+ });
363
+
364
+ iteration++;
365
+
366
+ } catch (error) {
367
+ renderDashboard(null, error);
368
+ }
369
+
370
+ // Wait for next poll
371
+ await new Promise(resolve => setTimeout(resolve, options.interval));
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Handle graceful shutdown
377
+ */
378
+ function setupShutdownHandlers() {
379
+ const cleanup = () => {
380
+ // Show cursor
381
+ process.stdout.write(cursor.show);
382
+ console.log(`\n${colors.yellow}Monitor stopped.${colors.reset}\n`);
383
+ process.exit(0);
384
+ };
385
+
386
+ process.on('SIGINT', cleanup);
387
+ process.on('SIGTERM', cleanup);
388
+ process.on('exit', () => {
389
+ process.stdout.write(cursor.show);
390
+ });
391
+ }
392
+
393
+ // Global options (parsed from args)
394
+ let options;
395
+
396
+ /**
397
+ * Main entry point
398
+ */
399
+ function main() {
400
+ options = parseArgs();
401
+
402
+ // Show cursor on exit
403
+ setupShutdownHandlers();
404
+
405
+ // Print startup message
406
+ console.log(`\n${colors.cyan}Starting Aether Network Monitor...${colors.reset}`);
407
+ console.log(`${colors.dim}RPC Endpoint: ${options.rpc}${colors.reset}`);
408
+ console.log(`${colors.dim}Poll Interval: ${options.interval}ms${colors.reset}\n`);
409
+
410
+ // Start monitoring
411
+ monitorLoop().catch(err => {
412
+ console.error(`${colors.red}Fatal error: ${err.message}${colors.reset}`);
413
+ process.exit(1);
414
+ });
415
+ }
416
+
417
+ // Run if called directly
418
+ if (require.main === module) {
419
+ main();
420
+ }
421
+
422
+ // Export for testing and CLI integration
423
+ module.exports = {
424
+ monitorLoop,
425
+ getSlot,
426
+ getBlockHeight,
427
+ getPeerCount,
428
+ calculateTPS,
429
+ renderDashboard,
430
+ main,
431
+ };