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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/commands/doctor.js +720 -0
- package/commands/init.js +685 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/sdk.js +381 -0
- package/commands/validator-start.js +290 -0
- package/commands/validator-status.js +268 -0
- package/index.js +275 -0
- package/package.json +51 -0
- package/test/doctor.test.js +76 -0
|
@@ -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
|
+
};
|