aether-hub 1.0.6 → 1.1.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.
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli network - Aether Network Status
4
+ *
5
+ * Queries the Aether network for broad health metrics:
6
+ * - Network-wide slot, block height, slot production
7
+ * - Connected peers and their info
8
+ * - Consensus status and epoch info
9
+ * - TPS estimates from the network
10
+ *
11
+ * Usage:
12
+ * aether-cli network # Interactive summary view
13
+ * aether-cli network --json # JSON output for scripting
14
+ * aether-cli network --rpc <url> # Query a specific RPC endpoint
15
+ * aether-cli network --peers # Detailed peer list
16
+ * aether-cli network --epoch # Current epoch and consensus info
17
+ */
18
+
19
+ const http = require('http');
20
+ const https = require('https');
21
+
22
+ // ANSI colours
23
+ const C = {
24
+ reset: '\x1b[0m',
25
+ bright: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ red: '\x1b[31m',
28
+ green: '\x1b[32m',
29
+ yellow: '\x1b[33m',
30
+ blue: '\x1b[34m',
31
+ cyan: '\x1b[36m',
32
+ magenta: '\x1b[35m',
33
+ white: '\x1b[37m',
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, options = {}) {
43
+ return new Promise((resolve, reject) => {
44
+ const url = new URL(path, rpcUrl);
45
+ const isHttps = url.protocol === 'https:';
46
+ const lib = isHttps ? https : http;
47
+
48
+ const reqOptions = {
49
+ hostname: url.hostname,
50
+ port: url.port || (isHttps ? 443 : 80),
51
+ path: url.pathname + url.search,
52
+ method: 'GET',
53
+ timeout: 5000,
54
+ headers: { 'Content-Type': 'application/json' },
55
+ };
56
+
57
+ const req = lib.request(reqOptions, (res) => {
58
+ let data = '';
59
+ res.on('data', (chunk) => (data += chunk));
60
+ res.on('end', () => {
61
+ try {
62
+ resolve(JSON.parse(data));
63
+ } catch {
64
+ resolve({ raw: data });
65
+ }
66
+ });
67
+ });
68
+
69
+ req.on('error', reject);
70
+ req.on('timeout', () => {
71
+ req.destroy();
72
+ reject(new Error('Request timeout'));
73
+ });
74
+
75
+ req.end();
76
+ });
77
+ }
78
+
79
+ function httpPost(rpcUrl, path, body) {
80
+ return new Promise((resolve, reject) => {
81
+ const url = new URL(path, rpcUrl);
82
+ const isHttps = url.protocol === 'https:';
83
+ const lib = isHttps ? https : http;
84
+ const bodyStr = JSON.stringify(body);
85
+
86
+ const req = lib.request({
87
+ hostname: url.hostname,
88
+ port: url.port || (isHttps ? 443 : 80),
89
+ path: url.pathname + url.search,
90
+ method: 'POST',
91
+ timeout: 5000,
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ 'Content-Length': Buffer.byteLength(bodyStr),
95
+ },
96
+ }, (res) => {
97
+ let data = '';
98
+ res.on('data', (chunk) => (data += chunk));
99
+ res.on('end', () => {
100
+ try { resolve(JSON.parse(data)); }
101
+ catch { resolve(data); }
102
+ });
103
+ });
104
+
105
+ req.on('error', reject);
106
+ req.on('timeout', () => {
107
+ req.destroy();
108
+ reject(new Error('Request timeout'));
109
+ });
110
+
111
+ req.write(bodyStr);
112
+ req.end();
113
+ });
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Argument parsing
118
+ // ---------------------------------------------------------------------------
119
+
120
+ function parseArgs() {
121
+ const args = process.argv.slice(2);
122
+ const options = {
123
+ rpc: DEFAULT_RPC,
124
+ showPeers: false,
125
+ showEpoch: false,
126
+ asJson: false,
127
+ };
128
+
129
+ for (let i = 0; i < args.length; i++) {
130
+ if (args[i] === '--rpc' || args[i] === '-r') {
131
+ options.rpc = args[++i];
132
+ } else if (args[i] === '--peers' || args[i] === '-p') {
133
+ options.showPeers = true;
134
+ } else if (args[i] === '--epoch' || args[i] === '-e') {
135
+ options.showEpoch = true;
136
+ } else if (args[i] === '--json' || args[i] === '-j') {
137
+ options.asJson = true;
138
+ } else if (args[i] === '--help' || args[i] === '-h') {
139
+ showHelp();
140
+ process.exit(0);
141
+ }
142
+ }
143
+
144
+ return options;
145
+ }
146
+
147
+ function showHelp() {
148
+ console.log(`
149
+ ${C.bright}${C.cyan}aether-cli network${C.reset} - Aether Network Status
150
+
151
+ ${C.bright}Usage:${C.reset}
152
+ aether-cli network [options]
153
+
154
+ ${C.bright}Options:${C.reset}
155
+ -r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC} or $AETHER_RPC)
156
+ -p, --peers Show detailed peer list
157
+ -e, --epoch Show epoch and consensus information
158
+ -j, --json Output raw JSON (good for scripting)
159
+ -h, --help Show this help message
160
+
161
+ ${C.bright}Examples:${C.reset}
162
+ aether-cli network # Summary view
163
+ aether-cli network --json # JSON output
164
+ aether-cli network --rpc http://api.testnet.aether.network
165
+ aether-cli network --peers # Detailed peer list
166
+ aether-cli network --epoch # Epoch/consensus info
167
+ `.trim());
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Network data fetchers
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /** GET /v1/slot — current network slot */
175
+ async function getSlot(rpc) {
176
+ try {
177
+ const res = await httpRequest(rpc, '/v1/slot');
178
+ return res.slot ?? res.root_slot ?? null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ /** GET /v1/block_height — current network block height */
185
+ async function getBlockHeight(rpc) {
186
+ try {
187
+ const res = await httpRequest(rpc, '/v1/block_height');
188
+ return res.block_height ?? null;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ /** GET /v1/validators — list of validators / peers */
195
+ async function getValidators(rpc) {
196
+ try {
197
+ const res = await httpRequest(rpc, '/v1/validators');
198
+ if (res.validators && Array.isArray(res.validators)) {
199
+ return res.validators;
200
+ }
201
+ if (Array.isArray(res)) return res;
202
+ return [];
203
+ } catch {
204
+ return [];
205
+ }
206
+ }
207
+
208
+ /** GET /v1/epoch — current epoch and consensus info */
209
+ async function getEpoch(rpc) {
210
+ try {
211
+ const res = await httpRequest(rpc, '/v1/epoch');
212
+ return res;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+
218
+ /** POST /v1/slot_production — slot production stats */
219
+ async function getSlotProduction(rpc) {
220
+ try {
221
+ // Try slot production endpoint
222
+ const res = await httpPost(rpc, '/v1/slot_production', {});
223
+ return res;
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+
229
+ /** GET /v1/tps — TPS estimate from network */
230
+ async function getTPS(rpc) {
231
+ try {
232
+ const res = await httpRequest(rpc, '/v1/tps');
233
+ return res.tps ?? res.tps_avg ?? res.transactions_per_second ?? null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ /** GET /v1/supply — token supply info */
240
+ async function getSupply(rpc) {
241
+ try {
242
+ const res = await httpRequest(rpc, '/v1/supply');
243
+ return res;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Output formatters
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function formatNumber(n) {
254
+ if (n === null || n === undefined) return 'N/A';
255
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
256
+ }
257
+
258
+ function peerHealthIcon(score) {
259
+ if (score === undefined || score === null) return `${C.dim}●${C.reset}`;
260
+ if (score >= 80) return `${C.green}●${C.reset}`;
261
+ if (score >= 50) return `${C.yellow}●${C.reset}`;
262
+ return `${C.red}●${C.reset}`;
263
+ }
264
+
265
+ function uptimeString(seconds) {
266
+ if (!seconds) return `${C.dim}unknown${C.reset}`;
267
+ const d = Math.floor(seconds / 86400);
268
+ const h = Math.floor((seconds % 86400) / 3600);
269
+ const m = Math.floor((seconds % 3600) / 60);
270
+ const parts = [];
271
+ if (d > 0) parts.push(`${d}d`);
272
+ if (h > 0) parts.push(`${h}h`);
273
+ if (m > 0) parts.push(`${m}m`);
274
+ return parts.length > 0 ? parts.join(' ') : `${seconds}s`;
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Main renderers
279
+ // ---------------------------------------------------------------------------
280
+
281
+ function renderSummary(data, rpc) {
282
+ const { slot, blockHeight, peerCount, tps, supply, epochData } = data;
283
+ const now = new Date().toLocaleTimeString();
284
+
285
+ console.log(`
286
+ ${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════╗${C.reset}
287
+ ${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER NETWORK STATUS${C.reset}${C.cyan} ║${C.reset}
288
+ ${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════╝${C.reset}
289
+ `);
290
+ console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
291
+ console.log(` ${C.dim}Updated:${C.reset} ${now}`);
292
+ console.log();
293
+
294
+ // Network health
295
+ const isHealthy = slot !== null && blockHeight !== null;
296
+ const healthIcon = isHealthy ? `${C.green}● HEALTHY${C.reset}` : `${C.red}● UNHEALTHY${C.reset}`;
297
+ console.log(` Network ${healthIcon}`);
298
+ console.log();
299
+
300
+ // Key metrics
301
+ console.log(` ${C.bright}┌─────────────────────────────────────────────────────────────────┐${C.reset}`);
302
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Current Slot${C.reset} ${C.bright}│${C.reset} ${C.green}${formatNumber(slot).padEnd(20)}${C.reset}${C.bright}│${C.reset}`);
303
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Block Height${C.reset} ${C.bright}│${C.reset} ${C.blue}${formatNumber(blockHeight).padEnd(20)}${C.reset}${C.bright}│${C.reset}`);
304
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Active Peers${C.reset} ${C.bright}│${C.reset} ${C.magenta}${formatNumber(peerCount).padEnd(20)}${C.reset}${C.bright}│${C.reset}`);
305
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Network TPS${C.reset} ${C.bright}│${C.reset} ${tps !== null ? (tps > 0 ? `${C.green}` : `${C.yellow}`) + tps.toFixed(2).padEnd(20) : `${C.dim}N/A`.padEnd(20)}${C.reset}${C.bright}│${C.reset}`);
306
+ console.log(` ${C.bright}└─────────────────────────────────────────────────────────────────┘${C.reset}`);
307
+ console.log();
308
+
309
+ // Epoch info if available
310
+ if (epochData && (epochData.epoch !== undefined || epochData.absolute_slot !== undefined)) {
311
+ console.log(` ${C.bright}── Epoch / Consensus ──────────────────────────────────────────${C.reset}`);
312
+ const ep = epochData.epoch !== undefined ? epochData.epoch : '?';
313
+ const slotIndex = epochData.slot_index !== undefined ? epochData.slot_index : '?';
314
+ const slotsInEpoch = epochData.slots_in_epoch !== undefined ? epochData.slots_in_epoch : '?';
315
+ const progress = slotsInEpoch !== '?' && slotsInEpoch > 0
316
+ ? ((slotIndex / slotsInEpoch) * 100).toFixed(1) + '%'
317
+ : '?';
318
+
319
+ console.log(` ${C.dim}Epoch:${C.reset} ${C.bright}${ep}${C.reset} ${C.dim}Slot in epoch:${C.reset} ${slotIndex}/${slotsInEpoch} ${C.dim}(${progress})${C.reset}`);
320
+ if (epochData.absolute_slot !== undefined) {
321
+ console.log(` ${C.dim}Absolute slot:${C.reset} ${formatNumber(epochData.absolute_slot)}`);
322
+ }
323
+ if (epochData.block_height !== undefined) {
324
+ console.log(` ${C.dim}Block height:${C.reset} ${formatNumber(epochData.block_height)}`);
325
+ }
326
+ console.log();
327
+ }
328
+
329
+ // Supply if available
330
+ if (supply && !supply.error) {
331
+ console.log(` ${C.bright}── Token Supply ───────────────────────────────────────────────${C.reset}`);
332
+ if (supply.total !== undefined) {
333
+ const totalAETH = (supply.total / 1e9).toFixed(2);
334
+ console.log(` ${C.dim}Total supply:${C.reset} ${C.green}${formatNumber(supply.total)} lamports${C.reset} ${C.dim}(${totalAETH} AETH)${C.reset}`);
335
+ }
336
+ if (supply.circulating !== undefined) {
337
+ const circAETH = (supply.circulating / 1e9).toFixed(2);
338
+ console.log(` ${C.dim}Circulating:${C.reset} ${formatNumber(supply.circulating)} lamports ${C.dim}(${circAETH} AETH)${C.reset}`);
339
+ }
340
+ console.log();
341
+ }
342
+
343
+ console.log(` ${C.dim}Tip: --peers for peer list | --epoch for consensus | --json for raw data${C.reset}`);
344
+ console.log();
345
+ }
346
+
347
+ function renderPeers(peers, rpc) {
348
+ console.log();
349
+ console.log(`${C.bright}${C.cyan}── Peer List ─────────────────────────────────────────────────${C.reset}`);
350
+ console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
351
+ console.log();
352
+
353
+ if (!peers || peers.length === 0) {
354
+ console.log(` ${C.yellow}⚠ No peer information available from this RPC.${C.reset}`);
355
+ console.log(` ${C.dim} Peers may not be exposed by your validator's RPC configuration.${C.reset}`);
356
+ console.log();
357
+ return;
358
+ }
359
+
360
+ console.log(` ${C.bright}┌────────────────────────────────────────────────────────────────────────┐${C.reset}`);
361
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}#${C.reset} ${C.cyan}Validator Address${C.reset} ${C.cyan}Tier${C.reset} ${C.cyan}Score${C.reset} ${C.cyan}Uptime${C.reset} ${C.bright}│${C.reset}`);
362
+ console.log(` ${C.bright}├${C.reset}${'-'.repeat(78)}${C.bright}│${C.reset}`);
363
+
364
+ peers.slice(0, 50).forEach((peer, i) => {
365
+ const num = (i + 1).toString().padStart(2);
366
+ const addr = (peer.address || peer.pubkey || peer.id || 'unknown').slice(0, 32).padEnd(34);
367
+ const tier = (peer.tier || peer.node_type || '?').toUpperCase().padEnd(6).slice(0, 6);
368
+ const score = peer.score !== undefined ? peer.score : (peer.uptime !== undefined ? Math.round(peer.uptime * 100) : null);
369
+ const scoreStr = score !== null ? `${score}%` : '?';
370
+ const uptime = uptimeString(peer.uptime_seconds || peer.uptime);
371
+ const scoreColor = score === null ? C.dim : score >= 80 ? C.green : score >= 50 ? C.yellow : C.red;
372
+
373
+ console.log(
374
+ ` ${C.bright}│${C.reset} ${C.dim}${num}${C.reset} ${addr} ${tier.padEnd(6)} ${scoreColor}${(scoreStr + '%').padEnd(7)}${C.reset} ${C.dim}${uptime}${C.reset} ${C.bright}│${C.reset}`
375
+ );
376
+ });
377
+
378
+ if (peers.length > 50) {
379
+ console.log(` ${C.bright}│${C.reset} ${C.dim}... and ${peers.length - 50} more peers (use --json for full list)${C.reset}`.padEnd(80) + `${C.bright}│${C.reset}`);
380
+ }
381
+
382
+ console.log(` ${C.bright}└────────────────────────────────────────────────────────────────────────┘${C.reset}`);
383
+ console.log();
384
+ console.log(` ${C.dim}Total peers: ${peers.length}${C.reset}`);
385
+ console.log();
386
+ }
387
+
388
+ function renderEpoch(epochData, rpc) {
389
+ console.log();
390
+ console.log(`${C.bright}${C.cyan}── Epoch / Consensus ────────────────────────────────────────${C.reset}`);
391
+ console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
392
+ console.log();
393
+
394
+ if (!epochData) {
395
+ console.log(` ${C.yellow}⚠ Epoch information not available.${C.reset}`);
396
+ console.log(` ${C.dim} Is your validator fully synced?${C.reset}`);
397
+ console.log();
398
+ return;
399
+ }
400
+
401
+ const t = (label, val) => console.log(` ${C.dim}${label}:${C.reset} ${val !== undefined && val !== null ? C.bright + val : C.dim + 'N/A'}${C.reset}`);
402
+
403
+ console.log(` ${C.bright}┌─────────────────────────────────────────────────────┐${C.reset}`);
404
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}Epoch${C.reset}${' '.repeat(45)}${C.bright}│${C.reset}`);
405
+ const ep = epochData.epoch !== undefined ? epochData.epoch : '?';
406
+ console.log(` ${C.bright}│${C.reset} ${C.green}${(ep + '').padEnd(53)}${C.reset}${C.bright}│${C.reset}`);
407
+ console.log(` ${C.bright}├${C.reset}${'─'.repeat(58)}${C.bright}│${C.reset}`);
408
+
409
+ if (epochData.slots_in_epoch !== undefined) {
410
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Slots in epoch${C.reset}`.padEnd(53) + `${C.bright}│${C.reset}`);
411
+ console.log(` ${C.bright}│${C.reset} ${C.bright}${formatNumber(epochData.slots_in_epoch).padEnd(53)}${C.reset}${C.bright}│${C.reset}`);
412
+ }
413
+
414
+ if (epochData.slot_index !== undefined) {
415
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Current slot in epoch${C.reset}`.padEnd(53) + `${C.bright}│${C.reset}`);
416
+ const progress = epochData.slots_in_epoch > 0
417
+ ? `${epochData.slot_index} / ${epochData.slots_in_epoch} (${((epochData.slot_index / epochData.slots_in_epoch) * 100).toFixed(1)}%)`
418
+ : `${epochData.slot_index}`;
419
+ console.log(` ${C.bright}│${C.reset} ${C.bright}${progress.padEnd(53)}${C.reset}${C.bright}│${C.reset}`);
420
+ }
421
+
422
+ if (epochData.absolute_slot !== undefined) {
423
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Absolute slot${C.reset}`.padEnd(53) + `${C.bright}│${C.reset}`);
424
+ console.log(` ${C.bright}│${C.reset} ${C.bright}${formatNumber(epochData.absolute_slot).padEnd(53)}${C.reset}${C.bright}│${C.reset}`);
425
+ }
426
+
427
+ if (epochData.block_height !== undefined) {
428
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Block height${C.reset}`.padEnd(53) + `${C.bright}│${C.reset}`);
429
+ console.log(` ${C.bright}│${C.reset} ${C.bright}${formatNumber(epochData.block_height).padEnd(53)}${C.reset}${C.bright}│${C.reset}`);
430
+ }
431
+
432
+ if (epochData.epoch_schedule) {
433
+ const es = epochData.epoch_schedule;
434
+ if (es.first_normal_epoch !== undefined) {
435
+ console.log(` ${C.bright}│${C.reset} ${C.dim}First normal epoch${C.reset}`.padEnd(53) + `${C.bright}│${C.reset}`);
436
+ console.log(` ${C.bright}│${C.reset} ${C.bright}${es.first_normal_epoch.padEnd(53)}${C.reset}${C.bright}│${C.reset}`);
437
+ }
438
+ }
439
+
440
+ console.log(` ${C.bright}└─────────────────────────────────────────────────────┘${C.reset}`);
441
+ console.log();
442
+ }
443
+
444
+ // ---------------------------------------------------------------------------
445
+ // Main
446
+ // ---------------------------------------------------------------------------
447
+
448
+ async function main() {
449
+ const opts = parseArgs();
450
+ const rpc = opts.rpc;
451
+
452
+ if (!opts.asJson) {
453
+ console.log(`\n${C.cyan}Querying Aether network...${C.reset} ${C.dim}(${rpc})${C.reset}\n`);
454
+ }
455
+
456
+ // Fetch all data in parallel
457
+ const [slot, blockHeight, validators, epochData, tps, supply] = await Promise.all([
458
+ getSlot(rpc),
459
+ getBlockHeight(rpc),
460
+ getValidators(rpc),
461
+ getEpoch(rpc),
462
+ getTPS(rpc),
463
+ getSupply(rpc),
464
+ ]);
465
+
466
+ const peerCount = Array.isArray(validators) ? validators.length : 0;
467
+
468
+ const data = {
469
+ slot,
470
+ blockHeight,
471
+ peerCount,
472
+ tps,
473
+ supply,
474
+ epochData,
475
+ validators,
476
+ rpc,
477
+ fetchedAt: new Date().toISOString(),
478
+ };
479
+
480
+ if (opts.asJson) {
481
+ console.log(JSON.stringify(data, null, 2));
482
+ return;
483
+ }
484
+
485
+ if (opts.showPeers) {
486
+ renderPeers(validators, rpc);
487
+ } else if (opts.showEpoch) {
488
+ renderEpoch(epochData, rpc);
489
+ } else {
490
+ renderSummary(data, rpc);
491
+ }
492
+ }
493
+
494
+ module.exports = { main, networkCommand: main };
495
+
496
+ if (require.main === module) {
497
+ main().catch((err) => {
498
+ console.error(`\n${C.red}✗ Network command failed:${C.reset} ${err.message}`);
499
+ console.error(` ${C.dim}Check that your validator is running and RPC is accessible.${C.reset}`);
500
+ console.error(` ${C.dim}Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
501
+ process.exit(1);
502
+ });
503
+ }
@@ -737,29 +737,6 @@ async function stakeWallet(rl) {
737
737
  return;
738
738
  }
739
739
 
740
- // Derive the full wallet object (secret key needed for signing)
741
- let keyPair;
742
- try {
743
- // Re-derive from public key stored in wallet file
744
- // The secret key isn't stored — we'd need the mnemonic to re-derive.
745
- // For signing, the CLI requires the wallet to have been created/imported in this session.
746
- // We use bs58 decoded publicKey + nacl key derivation from stored entropy.
747
- // Since we store only publicKey, we need the secret key for signing.
748
- // Workaround: accept a --sign-with <secretkeybase58> flag for now, or
749
- // require the wallet to be "active" via a session.
750
- // For simplicity, derive a keypair using a stored seed phrase approach.
751
- // The wallet.json only has public_key. We need nacl sign keypair.
752
- // Let's require the secret key be provided for stake/transfer.
753
- console.log(` ${C.red}✗ Signing requires the wallet secret key.${C.reset}`);
754
- console.log(` ${C.dim}The wallet must be created/imported in this session to access the secret key.${C.reset}`);
755
- console.log(` ${C.dim}For staking, use the JS SDK's offline signing flow instead.${C.reset}`);
756
- console.log(` ${C.dim}See: aether-cli sdk js${C.reset}\n`);
757
- return;
758
- } catch (e) {
759
- console.log(` ${C.red}✗ Failed to load wallet keys: ${e.message}${C.reset}\n`);
760
- return;
761
- }
762
-
763
740
  // Prompt for missing values interactively
764
741
  if (!validator) {
765
742
  console.log(` ${C.cyan}Enter validator address:${C.reset}`);
@@ -784,6 +761,30 @@ async function stakeWallet(rl) {
784
761
  console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
785
762
  console.log();
786
763
 
764
+ // Ask for mnemonic to derive signing keypair
765
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
766
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
767
+ console.log();
768
+
769
+ let keyPair;
770
+ try {
771
+ keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
772
+ } catch (e) {
773
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
774
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
775
+ return;
776
+ }
777
+
778
+ // Verify the derived address matches the wallet
779
+ const derivedAddress = formatAddress(keyPair.publicKey);
780
+ if (derivedAddress !== address) {
781
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
782
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
783
+ console.log(` ${C.dim} Expected: ${address}${C.reset}`);
784
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
785
+ return;
786
+ }
787
+
787
788
  const confirm = await question(rl, ` ${C.yellow}Confirm stake? [y/N]${C.reset} > ${C.reset}`);
788
789
  if (!confirm.trim().toLowerCase().startsWith('y')) {
789
790
  console.log(` ${C.dim}Cancelled.${C.reset}\n`);
@@ -894,6 +895,30 @@ async function transferWallet(rl) {
894
895
  console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
895
896
  console.log();
896
897
 
898
+ // Ask for mnemonic to derive signing keypair
899
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
900
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
901
+ console.log();
902
+
903
+ let keyPair;
904
+ try {
905
+ keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
906
+ } catch (e) {
907
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
908
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
909
+ return;
910
+ }
911
+
912
+ // Verify the derived address matches the wallet
913
+ const derivedAddress = formatAddress(keyPair.publicKey);
914
+ if (derivedAddress !== address) {
915
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
916
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
917
+ console.log(` ${C.dim} Expected: ${address}${C.reset}`);
918
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
919
+ return;
920
+ }
921
+
897
922
  const confirm = await question(rl, ` ${C.yellow}Confirm transfer? [y/N]${C.reset} > ${C.reset}`);
898
923
  if (!confirm.trim().toLowerCase().startsWith('y')) {
899
924
  console.log(` ${C.dim}Cancelled.${C.reset}\n`);
package/index.js CHANGED
@@ -14,6 +14,7 @@ const { monitorLoop } = require('./commands/monitor');
14
14
  const { logsCommand } = require('./commands/logs');
15
15
  const { sdkCommand } = require('./commands/sdk');
16
16
  const { walletCommand } = require('./commands/wallet');
17
+ const { networkCommand } = require('./commands/network');
17
18
  const readline = require('readline');
18
19
 
19
20
  // CLI version
@@ -55,10 +56,11 @@ async function showMenu() {
55
56
  console.log(' ' + TIER_COLORS.FULL + '3)' + TIER_COLORS.reset + ' 📊 Monitor — Watch live validator stats');
56
57
  console.log(' ' + TIER_COLORS.FULL + '4)' + TIER_COLORS.reset + ' 📋 Logs — Tail and colourise validator logs');
57
58
  console.log(' ' + TIER_COLORS.FULL + '5)' + TIER_COLORS.reset + ' 📦 SDK — Get SDK links and install tools');
58
- console.log(' ' + TIER_COLORS.FULL + '6)' + TIER_COLORS.reset + ' Help Show all commands\n');
59
+ console.log(' ' + TIER_COLORS.FULL + '6)' + TIER_COLORS.reset + ' 🌐 Network Aether network status (slot, peers, TPS)');
60
+ console.log(' ' + TIER_COLORS.FULL + '7)' + TIER_COLORS.reset + ' ❓ Help — Show all commands\n');
59
61
  console.log(' ' + TIER_COLORS.reset + ' Type a number or command name. Press Ctrl+C to exit.\n');
60
62
 
61
- const VALID_CHOICES = ['1', '2', '3', '4', '5', '6', 'doctor', 'init', 'monitor', 'logs', 'sdk', 'help'];
63
+ const VALID_CHOICES = ['1', '2', '3', '4', '5', '6', '7', 'doctor', 'init', 'monitor', 'logs', 'sdk', 'network', 'help'];
62
64
 
63
65
  while (true) {
64
66
  const answer = (await prompt(` > `)).trim().toLowerCase();
@@ -98,7 +100,14 @@ async function showMenu() {
98
100
  return;
99
101
  }
100
102
 
101
- if (answer === '6' || answer === 'help') {
103
+ if (answer === '6' || answer === 'network') {
104
+ rl.close();
105
+ const { networkCommand } = require('./commands/network');
106
+ networkCommand();
107
+ return;
108
+ }
109
+
110
+ if (answer === '7' || answer === 'help') {
102
111
  showHelp();
103
112
  console.log(" Press Ctrl+C to exit or select an option above.\n");
104
113
  continue;
@@ -192,6 +201,13 @@ const COMMANDS = {
192
201
  process.argv = originalArgv;
193
202
  },
194
203
  },
204
+ network: {
205
+ description: 'Aether network status — slot, block height, peers, TPS, epoch info',
206
+ handler: () => {
207
+ const { networkCommand } = require('./commands/network');
208
+ networkCommand();
209
+ },
210
+ },
195
211
  history: {
196
212
  description: 'Transaction history for an address — alias for tx history',
197
213
  handler: () => {
@@ -270,6 +286,8 @@ Validator CLI v${VERSION}
270
286
  console.log(' aether-cli validator start # Start validator node');
271
287
  console.log(' aether-cli validator status # Check validator status');
272
288
  console.log(' aether-cli wallet balance # Query AETH balance');
289
+ console.log(' aether-cli network # Network status, peers, slot info');
290
+ console.log(' aether-cli network --peers # Detailed peer list');
273
291
  console.log(' aether-cli tx history # Show transaction history');
274
292
  console.log(' aether-cli --version # Show version');
275
293
  console.log('\nDocumentation: https://github.com/jelly-legs-ai/Jelly-legs-unsteady-workshop');
@@ -282,13 +300,10 @@ Validator CLI v${VERSION}
282
300
  function parseArgs() {
283
301
  const args = process.argv.slice(2);
284
302
 
285
- // Handle flags
303
+ // Handle version flag
286
304
  if (args.includes('--version') || args.includes('-v')) {
287
305
  return 'version';
288
306
  }
289
- if (args.includes('--help') || args.includes('-h')) {
290
- return 'help';
291
- }
292
307
 
293
308
  // No args → interactive menu
294
309
  if (args.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-hub",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "AeTHer Validator CLI — tiered validators (Full/Lite/Observer), system checks, onboarding, and node management",
5
5
  "main": "index.js",
6
6
  "bin": {