aether-hub 1.2.2 → 1.2.4

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,371 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli status
4
+ *
5
+ * Single-command dashboard: epoch + network + supply + validator + rewards
6
+ * Gives a full node/network overview in one shot — no need to run multiple commands.
7
+ *
8
+ * Usage:
9
+ * aether status Show full status dashboard
10
+ * aether status --json JSON output for scripting/monitoring
11
+ * aether status --rpc <url> Query a specific RPC endpoint
12
+ * aether status --validator Include local validator info
13
+ * aether status --compact One-line summary
14
+ *
15
+ * Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
16
+ */
17
+
18
+ const http = require('http');
19
+ const https = require('https');
20
+ const os = require('os');
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
+ cyan: '\x1b[36m',
31
+ magenta: '\x1b[35m',
32
+ };
33
+
34
+ const CLI_VERSION = '1.0.0';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ function getDefaultRpc() {
41
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
42
+ }
43
+
44
+ function httpRequest(rpcUrl, pathStr, timeoutMs = 10000) {
45
+ return new Promise((resolve, reject) => {
46
+ const url = new URL(pathStr, rpcUrl);
47
+ const lib = url.protocol === 'https:' ? https : http;
48
+ const req = lib.request({
49
+ hostname: url.hostname,
50
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
51
+ path: url.pathname + url.search,
52
+ method: 'GET',
53
+ timeout: timeoutMs,
54
+ headers: { 'Content-Type': 'application/json' },
55
+ }, (res) => {
56
+ let data = '';
57
+ res.on('data', (chunk) => data += chunk);
58
+ res.on('end', () => {
59
+ try { resolve(JSON.parse(data)); }
60
+ catch { resolve({ raw: data }); }
61
+ });
62
+ });
63
+ req.on('error', reject);
64
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout after ' + timeoutMs + 'ms')); });
65
+ req.end();
66
+ });
67
+ }
68
+
69
+ function httpPost(rpcUrl, pathStr, body, timeoutMs = 10000) {
70
+ return new Promise((resolve, reject) => {
71
+ const url = new URL(pathStr, rpcUrl);
72
+ const lib = url.protocol === 'https:' ? https : http;
73
+ const bodyStr = JSON.stringify(body);
74
+ const req = lib.request({
75
+ hostname: url.hostname,
76
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
77
+ path: url.pathname + url.search,
78
+ method: 'POST',
79
+ timeout: timeoutMs,
80
+ headers: {
81
+ 'Content-Type': 'application/json',
82
+ 'Content-Length': Buffer.byteLength(bodyStr),
83
+ },
84
+ }, (res) => {
85
+ let data = '';
86
+ res.on('data', (chunk) => data += chunk);
87
+ res.on('end', () => {
88
+ try { resolve(JSON.parse(data)); }
89
+ catch { resolve(data); }
90
+ });
91
+ });
92
+ req.on('error', reject);
93
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout after ' + timeoutMs + 'ms')); });
94
+ req.end();
95
+ });
96
+ }
97
+
98
+ function formatAether(lamports) {
99
+ const aeth = (Number(lamports) / 1e9).toFixed(4);
100
+ return aeth + ' AETH';
101
+ }
102
+
103
+ function loadConfig() {
104
+ const fs = require('fs');
105
+ const path = require('path');
106
+ const aetherDir = path.join(os.homedir(), '.aether');
107
+ const cfgPath = path.join(aetherDir, 'config.json');
108
+ if (!fs.existsSync(cfgPath)) return { defaultWallet: null };
109
+ try { return JSON.parse(fs.readFileSync(cfgPath, 'utf8')); }
110
+ catch { return { defaultWallet: null }; }
111
+ }
112
+
113
+ function loadIdentity() {
114
+ const fs = require('fs');
115
+ const path = require('path');
116
+ const idPath = path.join(os.homedir(), '.aether', 'validator-identity.json');
117
+ if (!fs.existsSync(idPath)) return null;
118
+ try { return JSON.parse(fs.readFileSync(idPath, 'utf8')); }
119
+ catch { return null; }
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Status command
124
+ // ---------------------------------------------------------------------------
125
+
126
+ async function statusCommand() {
127
+ const args = process.argv.slice(3); // skip "aether status"
128
+ const isJson = args.includes('--json') || args.includes('-j');
129
+ const isCompact = args.includes('--compact');
130
+ const includeValidator = args.includes('--validator');
131
+ const rpcIdx = args.findIndex(a => a === '--rpc');
132
+ const rpc = rpcIdx !== -1 && args[rpcIdx + 1] ? args[rpcIdx + 1] : getDefaultRpc();
133
+
134
+ const errors = {};
135
+ const data = {};
136
+
137
+ // Fetch all data in parallel
138
+ const promises = [
139
+ fetchEpochInfo(rpc).then(d => { data.epoch = d; }).catch(e => { errors.epoch = e.message; }),
140
+ fetchSupplyInfo(rpc).then(d => { data.supply = d; }).catch(e => { errors.supply = e.message; }),
141
+ fetchNetworkInfo(rpc).then(d => { data.network = d; }).catch(e => { errors.network = e.message; }),
142
+ fetchVersionInfo(rpc).then(d => { data.version = d; }).catch(e => { errors.version = e.message; }),
143
+ ];
144
+
145
+ await Promise.all(promises);
146
+
147
+ // Validator identity (local file)
148
+ data.validator = loadIdentity();
149
+
150
+ // Rewards for default wallet (optional)
151
+ const config = loadConfig();
152
+ if (config.defaultWallet) {
153
+ data.defaultWallet = config.defaultWallet;
154
+ try {
155
+ data.rewards = await fetchRewardsSummary(rpc, config.defaultWallet);
156
+ } catch (e) {
157
+ errors.rewards = e.message;
158
+ }
159
+ }
160
+
161
+ if (isJson) {
162
+ console.log(JSON.stringify({ rpc, errors: Object.keys(errors).length ? errors : undefined, ...data }, null, 2));
163
+ return;
164
+ }
165
+
166
+ if (isCompact) {
167
+ printCompact(data, errors);
168
+ return;
169
+ }
170
+
171
+ printDashboard(data, errors, includeValidator);
172
+ }
173
+
174
+ async function fetchEpochInfo(rpc) {
175
+ const [epochResp, slotResp] = await Promise.all([
176
+ httpPost(rpc, '/v1/epoch/info', { jsonrpc: '2.0', id: 1, method: 'getEpochInfo' }),
177
+ httpPost(rpc, '/v1Slot', { jsonrpc: '2.0', id: 1, method: 'getSlot' }),
178
+ ]);
179
+
180
+ const epoch = epochResp?.result || {};
181
+ const currentSlot = slotResp?.result || epoch.currentSlot || 0;
182
+ const slotsInEpoch = epoch.slotsInEpoch || 432000;
183
+ const slotIndex = currentSlot % slotsInEpoch;
184
+ const epochProgress = slotsInEpoch > 0 ? (slotIndex / slotsInEpoch * 100).toFixed(1) : '0';
185
+
186
+ // Estimate time remaining (assuming 400ms slots)
187
+ const slotsRemaining = slotsInEpoch - slotIndex;
188
+ const secsRemaining = Math.round(slotsRemaining * 0.4);
189
+ const minsRemaining = Math.round(secsRemaining / 60);
190
+ const timeStr = minsRemaining >= 60
191
+ ? `${Math.floor(minsRemaining / 60)}h ${minsRemaining % 60}m`
192
+ : `${minsRemaining}m`;
193
+
194
+ return {
195
+ epoch: epoch.epoch || 0,
196
+ absoluteSlot: currentSlot,
197
+ slotIndex,
198
+ slotsInEpoch,
199
+ progress: epochProgress,
200
+ timeRemaining: timeStr,
201
+ totalSlots: epoch.totalSlots || 0,
202
+ };
203
+ }
204
+
205
+ async function fetchSupplyInfo(rpc) {
206
+ const resp = await httpPost(rpc, '/v1/supply', { jsonrpc: '2.0', id: 1, method: 'getSupply' });
207
+ const supply = resp?.result?.value || {};
208
+ const total = BigInt(supply.total || 0);
209
+ const circulating = BigInt(supply.circulating || 0);
210
+ const nonCirculating = BigInt(supply.nonCirculating || 0);
211
+
212
+ return {
213
+ total: total.toString(),
214
+ totalFormatted: formatAether(total.toString()),
215
+ circulating: circulating.toString(),
216
+ circulatingFormatted: formatAether(circulating.toString()),
217
+ nonCirculating: nonCirculating.toString(),
218
+ nonCirculatingFormatted: formatAether(nonCirculating.toString()),
219
+ };
220
+ }
221
+
222
+ async function fetchNetworkInfo(rpc) {
223
+ const [slotResp, blockResp, peersResp] = await Promise.all([
224
+ httpPost(rpc, '/v1Slot', { jsonrpc: '2.0', id: 1, method: 'getSlot' }),
225
+ httpPost(rpc, '/v1Block', { jsonrpc: '2.0', id: 1, method: 'getBlockTime', params: [0] }),
226
+ httpPost(rpc, '/v1Peers', { jsonrpc: '2.0', id: 1, method: 'getClusterPeers' }),
227
+ ]);
228
+
229
+ const blockHeight = slotResp?.result || 0;
230
+ const blockTime = blockResp?.result || null;
231
+ const peers = Array.isArray(peersResp?.result) ? peersResp.result : [];
232
+
233
+ return {
234
+ blockHeight,
235
+ blockTime,
236
+ peerCount: peers.length,
237
+ peers: peers.slice(0, 5), // first 5 for detail
238
+ };
239
+ }
240
+
241
+ async function fetchVersionInfo(rpc) {
242
+ try {
243
+ const resp = await httpPost(rpc, '/v1Version', { jsonrpc: '2.0', id: 1, method: 'getVersion' });
244
+ return resp?.result || {};
245
+ } catch {
246
+ return {};
247
+ }
248
+ }
249
+
250
+ async function fetchRewardsSummary(rpc, address) {
251
+ // Fetch stake accounts for wallet, then fetch rewards for each
252
+ const allAccountsResp = await httpPost(rpc, '/v1Stake/accounts', {
253
+ jsonrpc: '2.0', id: 1, method: 'getStakeAccounts', params: [address],
254
+ }).catch(() => null);
255
+
256
+ const stakeAccounts = (allAccountsResp?.result?.value || [])
257
+ .filter(a => a.owner && (!Array.isArray(a.owner) || a.owner.length > 0))
258
+ .map(a => a.pubkey || a);
259
+
260
+ if (stakeAccounts.length === 0) return null;
261
+
262
+ const rewardsResults = await Promise.all(
263
+ stakeAccounts.slice(0, 10).map(async (sa) => {
264
+ try {
265
+ const resp = await httpPost(rpc, '/v1Stake/rewards', {
266
+ jsonrpc: '2.0', id: 1, method: 'getStakeRewards', params: [sa],
267
+ });
268
+ const rewards = resp?.result?.rewards || [];
269
+ let total = BigInt(0);
270
+ for (const r of rewards) {
271
+ total += BigInt(r.estimatedReward || 0);
272
+ }
273
+ return { stakeAccount: sa, estimatedRewards: total.toString(), estimatedRewardsFormatted: formatAether(total.toString()) };
274
+ } catch {
275
+ return { stakeAccount: sa, estimatedRewards: '0', estimatedRewardsFormatted: '0 AETH' };
276
+ }
277
+ })
278
+ );
279
+
280
+ let totalRewards = BigInt(0);
281
+ for (const r of rewardsResults) {
282
+ totalRewards += BigInt(r.estimatedRewards);
283
+ }
284
+
285
+ return {
286
+ address,
287
+ totalRewards: totalRewards.toString(),
288
+ totalRewardsFormatted: formatAether(totalRewards.toString()),
289
+ activeAccounts: rewardsResults.filter(r => BigInt(r.estimatedRewards) > 0n).length,
290
+ totalAccounts: rewardsResults.length,
291
+ };
292
+ }
293
+
294
+ function printDashboard(data, errors, includeValidator) {
295
+ const { epoch, supply, network, version, validator, rewards, defaultWallet } = data;
296
+
297
+ console.log(`\n${C.bright}${C.cyan} ╔══════════════════════════════════════════════════════════╗${C.reset}`);
298
+ console.log(`${C.bright}${C.cyan} ║ AETHER STATUS DASHBOARD ║${C.reset}`);
299
+ console.log(`${C.bright}${C.cyan} ╚══════════════════════════════════════════════════════════╝${C.reset}\n`);
300
+
301
+ // Epoch row
302
+ if (epoch) {
303
+ console.log(` ${C.bright}Epoch${C.reset} ${C.cyan}E${epoch.epoch}${C.reset} │ Slot ${C.bright}${epoch.absoluteSlot.toLocaleString()}${C.reset} (${epoch.progress}%) │ ${epoch.timeRemaining} remaining`);
304
+ } else {
305
+ console.log(` ${C.red}✗ Epoch info unavailable${errors.epoch ? ': ' + errors.epoch : ''}${C.reset}`);
306
+ }
307
+
308
+ // Network row
309
+ if (network) {
310
+ const peerStr = network.peerCount > 0 ? `${C.green}${network.peerCount} peers${C.reset}` : `${C.yellow}no peers${C.reset}`;
311
+ console.log(` ${C.bright}Network${C.reset} │ Block ${C.bright}${network.blockHeight.toLocaleString()}${C.reset} │ ${peerStr}`);
312
+ } else {
313
+ console.log(` ${C.red}✗ Network info unavailable${errors.network ? ': ' + errors.network : ''}${C.reset}`);
314
+ }
315
+
316
+ // Supply row
317
+ if (supply) {
318
+ console.log(` ${C.bright}Supply${C.reset} │ Total ${C.cyan}${supply.totalFormatted}${C.reset} │ Circulating ${C.green}${supply.circulatingFormatted}${C.reset}`);
319
+ console.log(` │ Staked (non-circulating) ${C.yellow}${supply.nonCirculatingFormatted}${C.reset}`);
320
+ } else {
321
+ console.log(` ${C.red}✗ Supply info unavailable${errors.supply ? ': ' + errors.supply : ''}${C.reset}`);
322
+ }
323
+
324
+ // Version row
325
+ if (version && Object.keys(version).length > 0) {
326
+ console.log(` ${C.bright}Version${C.reset} │ ${C.dim}${JSON.stringify(version)}${C.reset}`);
327
+ }
328
+
329
+ // Validator row
330
+ if (includeValidator && validator) {
331
+ const identity = validator.identity || validator.nodeKey || 'unknown';
332
+ const shortId = identity.length > 16 ? identity.substring(0, 16) + '...' : identity;
333
+ const stake = validator.delegatedStake ? formatAether(validator.delegatedStake) : 'unknown';
334
+ console.log(` ${C.bright}Validator${C.reset} │ ${C.magenta}${shortId}${C.reset} │ Stake: ${stake}`);
335
+ } else if (includeValidator && !validator) {
336
+ console.log(` ${C.bright}Validator${C.reset} │ ${C.yellow}No validator identity found (run aether init)${C.reset}`);
337
+ }
338
+
339
+ // Rewards row
340
+ if (rewards && defaultWallet) {
341
+ const shortAddr = defaultWallet.length > 16 ? defaultWallet.substring(0, 16) + '...' : defaultWallet;
342
+ console.log(` ${C.bright}Rewards${C.reset} │ ${C.green}${rewards.totalRewardsFormatted}${C.reset} est. │ Wallet: ${C.dim}${shortAddr}${C.reset}`);
343
+ }
344
+
345
+ console.log(` ${C.dim}RPC: ${rpc || getDefaultRpc()}${C.reset}\n`);
346
+ }
347
+
348
+ function printCompact(data, errors) {
349
+ const parts = [];
350
+ if (data.epoch) parts.push(`E${data.epoch.epoch}`);
351
+ if (data.network) parts.push(`blk ${data.network.blockHeight}`);
352
+ if (data.network) parts.push(`p${data.network.peerCount}`);
353
+ if (data.supply) parts.push(`total ${data.supply.totalFormatted}`);
354
+ if (data.rewards) parts.push(`rwd ${data.rewards.totalRewardsFormatted}`);
355
+ if (Object.keys(errors).length > 0) parts.push(`err:${Object.keys(errors).join(',')}`);
356
+ console.log(parts.join(' │ '));
357
+ }
358
+
359
+ // ---------------------------------------------------------------------------
360
+ // Entry point
361
+ // ---------------------------------------------------------------------------
362
+
363
+ module.exports = { statusCommand };
364
+
365
+ // Run directly
366
+ if (require.main === module) {
367
+ statusCommand().catch(err => {
368
+ console.error(`${C.red}✗ Status command failed:${C.reset} ${err.message}`);
369
+ process.exit(1);
370
+ });
371
+ }