aether-hub 1.2.0 → 1.2.2

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,357 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli epoch
4
+ *
5
+ * Display current epoch information including timing, schedule,
6
+ * slots per epoch, and estimated staking rewards rate.
7
+ *
8
+ * Usage:
9
+ * aether epoch Show current epoch with timing breakdown
10
+ * aether epoch --json JSON output for scripting/monitoring
11
+ * aether epoch --rpc <url> Query a specific RPC endpoint
12
+ * aether epoch --schedule Show upcoming epoch schedule
13
+ *
14
+ * Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
15
+ */
16
+
17
+ const http = require('http');
18
+ const https = require('https');
19
+
20
+ // ANSI colours
21
+ const C = {
22
+ reset: '\x1b[0m',
23
+ bright: '\x1b[1m',
24
+ dim: '\x1b[2m',
25
+ red: '\x1b[31m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ cyan: '\x1b[36m',
29
+ magenta: '\x1b[35m',
30
+ };
31
+
32
+ const CLI_VERSION = '1.0.0';
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // HTTP helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function httpRequest(rpcUrl, pathStr, timeoutMs) {
39
+ timeoutMs = timeoutMs || 8000;
40
+ return new Promise(function(resolve, reject) {
41
+ const url = new URL(pathStr, rpcUrl);
42
+ const lib = url.protocol === 'https:' ? https : http;
43
+ const req = lib.request({
44
+ hostname: url.hostname,
45
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
46
+ path: url.pathname + url.search,
47
+ method: 'GET',
48
+ timeout: timeoutMs,
49
+ headers: { 'Content-Type': 'application/json' },
50
+ }, function(res) {
51
+ let data = '';
52
+ res.on('data', function(chunk) { data += chunk; });
53
+ res.on('end', function() {
54
+ try { resolve(JSON.parse(data)); }
55
+ catch { resolve({ raw: data }); }
56
+ });
57
+ });
58
+ req.on('error', reject);
59
+ req.on('timeout', function() { req.destroy(); reject(new Error('Request timeout')); });
60
+ req.end();
61
+ });
62
+ }
63
+
64
+ function httpPost(rpcUrl, pathStr, body, timeoutMs) {
65
+ timeoutMs = timeoutMs || 8000;
66
+ return new Promise(function(resolve, reject) {
67
+ const url = new URL(pathStr, rpcUrl);
68
+ const lib = url.protocol === 'https:' ? https : http;
69
+ const bodyStr = JSON.stringify(body);
70
+ const req = lib.request({
71
+ hostname: url.hostname,
72
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
73
+ path: url.pathname + url.search,
74
+ method: 'POST',
75
+ timeout: timeoutMs,
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'Content-Length': Buffer.byteLength(bodyStr),
79
+ },
80
+ }, function(res) {
81
+ let data = '';
82
+ res.on('data', function(chunk) { data += chunk; });
83
+ res.on('end', function() {
84
+ try { resolve(JSON.parse(data)); }
85
+ catch { resolve({ raw: data }); }
86
+ });
87
+ });
88
+ req.on('error', reject);
89
+ req.on('timeout', function() { req.destroy(); reject(new Error('Request timeout')); });
90
+ req.write(bodyStr);
91
+ req.end();
92
+ });
93
+ }
94
+
95
+ function getDefaultRpc() {
96
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Argument parsing
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function parseArgs() {
104
+ const args = process.argv.slice(3); // [node, index.js, epoch, ...]
105
+ return {
106
+ rpc: getDefaultRpc(),
107
+ asJson: args.indexOf('--json') !== -1 || args.indexOf('-j') !== -1,
108
+ showSchedule: args.indexOf('--schedule') !== -1 || args.indexOf('-s') !== -1,
109
+ rpcUrl: getDefaultRpc(),
110
+ };
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Fetch epoch info from RPC
115
+ // ---------------------------------------------------------------------------
116
+
117
+ async function fetchEpochInfo(rpc) {
118
+ // Try Aether-native endpoint first
119
+ try {
120
+ const epochInfo = await httpRequest(rpc, '/v1/epoch-info');
121
+ if (epochInfo && !epochInfo.error && (epochInfo.epoch !== undefined || epochInfo.current_epoch)) {
122
+ return { data: epochInfo, source: 'aether' };
123
+ }
124
+ } catch(e) {
125
+ // fall through
126
+ }
127
+
128
+ // Fallback: try Solana-compat JSON-RPC
129
+ try {
130
+ const result = await httpPost(rpc, '/', {
131
+ jsonrpc: '2.0',
132
+ id: 1,
133
+ method: 'getEpochInfo',
134
+ });
135
+ if (result && result.result) {
136
+ return { data: result.result, source: 'solana-compat' };
137
+ }
138
+ } catch(e) {
139
+ // fall through
140
+ }
141
+
142
+ // Try getEpochSchedule
143
+ try {
144
+ const schedule = await httpPost(rpc, '/', {
145
+ jsonrpc: '2.0',
146
+ id: 1,
147
+ method: 'getEpochSchedule',
148
+ });
149
+ if (schedule && schedule.result) {
150
+ return { data: schedule.result, source: 'schedule-only' };
151
+ }
152
+ } catch(e) {
153
+ // fall through
154
+ }
155
+
156
+ throw new Error('Failed to fetch epoch info from RPC. Is your validator running?');
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Format helpers
161
+ // ---------------------------------------------------------------------------
162
+
163
+ function formatAether(lamports) {
164
+ const aeth = lamports / 1e9;
165
+ if (aeth === 0) return '0 AETH';
166
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
167
+ }
168
+
169
+ function formatDuration(seconds) {
170
+ if (seconds < 0) return '\u2014';
171
+ const h = Math.floor(seconds / 3600);
172
+ const m = Math.floor((seconds % 3600) / 60);
173
+ const s = Math.floor(seconds % 60);
174
+ if (h > 24) {
175
+ const d = Math.floor(h / 24);
176
+ return d + 'd ' + (h % 24) + 'h';
177
+ }
178
+ if (h > 0) return h + 'h ' + m + 'm';
179
+ if (m > 0) return m + 'm ' + s + 's';
180
+ return s + 's';
181
+ }
182
+
183
+ function fmtPct(value, decimals) {
184
+ decimals = decimals || 1;
185
+ return (value || 0).toFixed(decimals) + '%';
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Box drawing helpers
190
+ // ---------------------------------------------------------------------------
191
+
192
+ // Box drawing characters
193
+ const BOX_H = '\u2500'; // ─
194
+ const BOX_V = '\u2502'; // │
195
+ const BOX_TL = '\u256d'; // ╭
196
+ const BOX_TR = '\u256e'; // ╮
197
+ const BOX_BL = '\u2570'; // ╰
198
+ const BOX_BR = '\u256f'; // ╯
199
+ const BOX_Cross = '\u253c'; // ┼
200
+
201
+ function makeBoxLine(chars, width) {
202
+ let s = '';
203
+ for (let i = 0; i < width; i++) s += chars;
204
+ return s;
205
+ }
206
+
207
+ function makeSectionHeader(label) {
208
+ const total = 62;
209
+ const labelWithSpaces = ' ' + label + ' ';
210
+ const remaining = total - 4 - labelWithSpaces.length; // 4 for ╼ on each side
211
+ const half = Math.floor(remaining / 2);
212
+ const left = makeBoxLine('\u2550', half);
213
+ const right = makeBoxLine('\u2550', remaining - half);
214
+ return C.bright + C.cyan + '\u256e' + left + '\u2554' + labelWithSpaces + '\u2557' + right + '\u256f' + C.reset;
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Main display
219
+ // ---------------------------------------------------------------------------
220
+
221
+ async function showEpochInfo(opts) {
222
+ const rpc = opts.rpcUrl;
223
+ const { data, source } = await fetchEpochInfo(rpc);
224
+
225
+ // Normalise fields across different RPC response formats
226
+ const epoch = data.epoch ?? data.current_epoch ?? 0;
227
+ const slotIndex = data.slotIndex ?? data.current_slot ?? 0;
228
+ const slotsInEpoch = data.slotsInEpoch ?? data.slots_per_epoch ?? 8192;
229
+ const epochProgress = slotsInEpoch > 0 ? (slotIndex / slotsInEpoch) * 100 : 0;
230
+ const absoluteSlot = data.absoluteSlot ?? data.slot ?? 0;
231
+ const totalStaked = BigInt(data.totalStaked ?? data.total_staked ?? data.stake ?? 0);
232
+ const rewardsPerEpoch = BigInt(data.rewardsPerEpoch ?? data.rewards_per_epoch ?? data.rewards ?? 0);
233
+
234
+ // Estimate seconds per slot from slot data
235
+ const epochDurationSecs = data.epochDurationSecs ?? (slotsInEpoch * 0.4); // ~400ms/slot default
236
+ const secsPerSlot = epochDurationSecs / slotsInEpoch;
237
+ const secondsIntoEpoch = slotIndex * secsPerSlot;
238
+ const secondsRemaining = (slotsInEpoch - slotIndex) * secsPerSlot;
239
+
240
+ // APY estimate: rewards per epoch / total staked * epochs per year
241
+ const epochsPerYear = 365 * 24 * 3600 / epochDurationSecs;
242
+ const apyRate = totalStaked > 0n
243
+ ? (Number(rewardsPerEpoch) / Number(totalStaked)) * epochsPerYear
244
+ : 0;
245
+ const apyBps = Math.round(apyRate * 10000);
246
+
247
+ if (opts.asJson) {
248
+ const out = {
249
+ epoch: epoch,
250
+ slotIndex: slotIndex,
251
+ slotsInEpoch: slotsInEpoch,
252
+ absoluteSlot: absoluteSlot,
253
+ epochProgress: epochProgress,
254
+ secondsIntoEpoch: Math.round(secondsIntoEpoch),
255
+ secondsRemaining: Math.round(secondsRemaining),
256
+ totalStaked: totalStaked.toString(),
257
+ totalStakedFormatted: formatAether(totalStaked),
258
+ rewardsPerEpoch: rewardsPerEpoch.toString(),
259
+ rewardsPerEpochFormatted: formatAether(rewardsPerEpoch),
260
+ estimatedApyBps: apyBps,
261
+ estimatedApy: fmtPct(apyRate),
262
+ source: source,
263
+ fetchedAt: new Date().toISOString(),
264
+ };
265
+ console.log(JSON.stringify(out, null, 2));
266
+ return;
267
+ }
268
+
269
+ // ASCII art header
270
+ console.log('');
271
+ const line1 = C.bright + C.cyan + BOX_TL + makeBoxLine(BOX_H, 60) + BOX_TR + C.reset;
272
+ console.log(line1);
273
+ const line2 = C.bright + C.cyan + BOX_V + ' AeTHer Epoch ' + epoch + ' Info ' + BOX_V + C.reset;
274
+ console.log(line2);
275
+ const line3 = C.bright + C.cyan + BOX_BL + makeBoxLine(BOX_H, 60) + BOX_BR + C.reset;
276
+ console.log(line3);
277
+ console.log('');
278
+
279
+ console.log(' ' + C.dim + 'RPC: ' + rpc + C.reset);
280
+ console.log('');
281
+
282
+ // ── Epoch timing ───────────────────────────────────────────────────────
283
+ console.log(makeSectionHeader('Epoch Timing'));
284
+
285
+ const progressBars = 40;
286
+ const filled = Math.round((epochProgress / 100) * progressBars);
287
+ const empty = progressBars - filled;
288
+ const bar = C.green + '#'.repeat(filled) + C.dim + '\u2500'.repeat(empty) + C.reset;
289
+
290
+ console.log(' ' + C.dim + ' Progress: [' + bar + '] ' + C.bright + fmtPct(epochProgress) + C.reset);
291
+ console.log(' ' + C.dim + ' Slot: ' + C.reset + C.bright + slotIndex.toLocaleString() + C.reset + ' / ' + slotsInEpoch.toLocaleString() + ' slots into epoch');
292
+ console.log(' ' + C.dim + ' Abs slot: ' + C.reset + absoluteSlot.toLocaleString());
293
+ console.log(' ' + C.dim + ' Elapsed: ' + C.reset + formatDuration(Math.round(secondsIntoEpoch)));
294
+ console.log(' ' + C.dim + ' Remaining: ' + C.reset + C.yellow + formatDuration(Math.round(secondsRemaining)) + C.reset);
295
+ console.log(' ' + C.dim + ' Duration: ' + C.reset + '~' + formatDuration(Math.round(epochDurationSecs)) + ' per epoch');
296
+ console.log('');
297
+
298
+ // ── Staking rewards ─────────────────────────────────────────────────────
299
+ console.log(makeSectionHeader('Staking Rewards'));
300
+
301
+ console.log(' ' + C.dim + ' Network stake: ' + C.reset + C.bright + formatAether(totalStaked) + C.reset);
302
+ console.log(' ' + C.dim + ' Rewards/epoch: ' + C.reset + C.green + formatAether(rewardsPerEpoch) + C.reset);
303
+ console.log(' ' + C.dim + ' Estimated APY: ' + C.reset + C.green + C.bright + fmtPct(apyRate) + C.reset + ' ' + C.dim + '(~' + (apyBps / 100).toFixed(0) + ' bps)' + C.reset);
304
+ console.log('');
305
+
306
+ // ── Epoch schedule ──────────────────────────────────────────────────────
307
+ if (opts.showSchedule) {
308
+ console.log(makeSectionHeader('Upcoming Epochs'));
309
+ const startSlotNext = absoluteSlot + (slotsInEpoch - slotIndex);
310
+ for (let i = 0; i < 5; i++) {
311
+ const e = epoch + i;
312
+ const start = startSlotNext + i * slotsInEpoch;
313
+ const end = start + slotsInEpoch - 1;
314
+ const isNext = i === 0 ? ' ' + C.green + '(next)' + C.reset : '';
315
+ console.log(' ' + C.dim + ' Epoch ' + String(e).padStart(4) + ': slots ' + start.toLocaleString() + ' \u2013 ' + end.toLocaleString() + isNext + C.reset);
316
+ }
317
+ console.log('');
318
+ }
319
+
320
+ // ── Raw data ─────────────────────────────────────────────────────────────
321
+ console.log(makeSectionHeader('Raw RPC Data'));
322
+ console.log(' ' + C.dim + ' Source: ' + source + C.reset);
323
+ const rawPreview = JSON.stringify(data).substring(0, 80);
324
+ console.log(' ' + C.dim + ' ' + rawPreview + C.reset);
325
+ console.log('');
326
+
327
+ console.log(' ' + C.dim + 'Run "aether validators list" to see validator performance for epoch ' + epoch + '.' + C.reset);
328
+ console.log(' ' + C.dim + 'Run "aether rewards list --address <addr>" to check your staking rewards.' + C.reset);
329
+ console.log('');
330
+ }
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // Main entry point
334
+ // ---------------------------------------------------------------------------
335
+
336
+ async function epochCommand() {
337
+ const opts = parseArgs();
338
+ try {
339
+ await showEpochInfo(opts);
340
+ } catch (err) {
341
+ if (opts.asJson) {
342
+ console.log(JSON.stringify({ error: err.message }, null, 2));
343
+ } else {
344
+ console.log('');
345
+ console.log(' ' + C.red + '\u2514 Error: ' + C.reset + ' ' + err.message);
346
+ console.log(' ' + C.dim + 'Set a custom RPC: AETHER_RPC=https://your-rpc-url' + C.reset);
347
+ console.log('');
348
+ }
349
+ process.exit(1);
350
+ }
351
+ }
352
+
353
+ module.exports = { epochCommand };
354
+
355
+ if (require.main === module) {
356
+ epochCommand();
357
+ }