aether-hub 1.2.0 → 1.2.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/commands/broadcast.js +323 -0
- package/commands/epoch.js +357 -0
- package/commands/status.js +371 -0
- package/commands/supply.js +437 -0
- package/commands/validator-info.js +640 -0
- package/index.js +38 -1
- package/package.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli broadcast
|
|
4
|
+
*
|
|
5
|
+
* Broadcast a signed transaction to the Aether network.
|
|
6
|
+
* Accepts a base58-encoded transaction signature or a raw JSON payload.
|
|
7
|
+
* Useful for submitting offline-constructed transactions.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aether broadcast --tx <signature> Broadcast by tx signature
|
|
11
|
+
* aether broadcast --json <payload> Broadcast raw JSON tx payload
|
|
12
|
+
* aether broadcast --file <path> Read tx from a JSON file
|
|
13
|
+
* aether broadcast --rpc <url> Use a specific RPC endpoint
|
|
14
|
+
* aether broadcast --json-output Output result as JSON
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
* aether broadcast --tx 5abcdef... # Submit pre-signed tx
|
|
18
|
+
* aether broadcast --json '{"type":"Transfer",...}'
|
|
19
|
+
* aether broadcast --file ./unsigned_tx.json
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const https = require('https');
|
|
25
|
+
const http = require('http');
|
|
26
|
+
|
|
27
|
+
// ANSI colours
|
|
28
|
+
const C = {
|
|
29
|
+
reset: '\x1b[0m',
|
|
30
|
+
bright: '\x1b[1m',
|
|
31
|
+
dim: '\x1b[2m',
|
|
32
|
+
red: '\x1b[31m',
|
|
33
|
+
green: '\x1b[32m',
|
|
34
|
+
yellow: '\x1b[33m',
|
|
35
|
+
cyan: '\x1b[36m',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CLI_VERSION = '1.0.0';
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Config
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function getDefaultRpc() {
|
|
45
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Argument parsing
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function parseArgs() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
const opts = {
|
|
55
|
+
rpc: getDefaultRpc(),
|
|
56
|
+
signature: null,
|
|
57
|
+
jsonPayload: null,
|
|
58
|
+
filePath: null,
|
|
59
|
+
asJson: false,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < args.length; i++) {
|
|
63
|
+
if (args[i] === '--tx' || args[i] === '-t') {
|
|
64
|
+
opts.signature = args[++i];
|
|
65
|
+
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
66
|
+
opts.jsonPayload = args[++i];
|
|
67
|
+
} else if (args[i] === '--file' || args[i] === '-f') {
|
|
68
|
+
opts.filePath = args[++i];
|
|
69
|
+
} else if (args[i] === '--rpc' || args[i] === '-r') {
|
|
70
|
+
opts.rpc = args[++i];
|
|
71
|
+
} else if (args[i] === '--json-output') {
|
|
72
|
+
opts.asJson = true;
|
|
73
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
74
|
+
showHelp();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return opts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function showHelp() {
|
|
83
|
+
console.log(`
|
|
84
|
+
${C.bright}${C.cyan}aether-cli broadcast${C.reset} - Broadcast a Signed Transaction
|
|
85
|
+
|
|
86
|
+
${C.bright}Usage:${C.reset}
|
|
87
|
+
aether-cli broadcast --tx <signature> Broadcast by base58 signature
|
|
88
|
+
aether-cli broadcast --json <payload> Broadcast inline JSON payload
|
|
89
|
+
aether-cli broadcast --file <path> Read tx from a JSON file
|
|
90
|
+
aether-cli broadcast --rpc <url> Override default RPC
|
|
91
|
+
aether-cli broadcast --json-output JSON output for scripting
|
|
92
|
+
|
|
93
|
+
${C.bright}Examples:${C.reset}
|
|
94
|
+
aether-cli broadcast --tx 5abcdef123456... # Submit by signature
|
|
95
|
+
aether-cli broadcast --json '{"type":"Transfer","data":{...}}'
|
|
96
|
+
aether-cli broadcast --file ./my_tx.json # Read and broadcast from file
|
|
97
|
+
`.trim());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// HTTP helpers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
function httpRequest(rpcUrl, pathStr, method = 'GET', body = null, timeoutMs = 15000) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const url = new URL(pathStr, rpcUrl);
|
|
107
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
108
|
+
const bodyStr = body ? JSON.stringify(body) : null;
|
|
109
|
+
|
|
110
|
+
const reqOptions = {
|
|
111
|
+
hostname: url.hostname,
|
|
112
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
113
|
+
path: url.pathname + url.search,
|
|
114
|
+
method,
|
|
115
|
+
timeout: timeoutMs,
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {}),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const req = lib.request(reqOptions, (res) => {
|
|
123
|
+
let data = '';
|
|
124
|
+
res.on('data', (chunk) => (data += chunk));
|
|
125
|
+
res.on('end', () => {
|
|
126
|
+
try { resolve(JSON.parse(data)); }
|
|
127
|
+
catch { resolve({ raw: data }); }
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
req.on('error', reject);
|
|
132
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
133
|
+
if (bodyStr) req.write(bodyStr);
|
|
134
|
+
req.end();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function httpPost(rpcUrl, pathStr, body, timeoutMs = 15000) {
|
|
139
|
+
return httpRequest(rpcUrl, pathStr, 'POST', body, timeoutMs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Validate transaction payload
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
function validateTxPayload(tx) {
|
|
147
|
+
const errors = [];
|
|
148
|
+
|
|
149
|
+
if (!tx) {
|
|
150
|
+
errors.push('Transaction payload is null or empty');
|
|
151
|
+
return errors;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Must have signer
|
|
155
|
+
if (!tx.signer && !tx.from && !tx.pubkey) {
|
|
156
|
+
errors.push('Missing signer field (signer | from | pubkey)');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Must have tx_type or type
|
|
160
|
+
if (!tx.tx_type && !tx.type) {
|
|
161
|
+
errors.push('Missing tx_type or type field');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Must have payload
|
|
165
|
+
if (!tx.payload && !tx.data) {
|
|
166
|
+
errors.push('Missing payload or data field');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return errors;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Broadcast logic
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async function broadcast({ rpc, signature, jsonPayload, filePath, asJson }) {
|
|
177
|
+
// Build the tx object from inputs (priority: signature > file > inline JSON)
|
|
178
|
+
let tx = null;
|
|
179
|
+
|
|
180
|
+
if (signature) {
|
|
181
|
+
// Signature-only broadcast: POST /v1/tx with { signature }
|
|
182
|
+
tx = { signature };
|
|
183
|
+
} else if (filePath) {
|
|
184
|
+
// Read from file
|
|
185
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
186
|
+
if (!fs.existsSync(absPath)) {
|
|
187
|
+
throw new Error(`File not found: ${absPath}`);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
tx = JSON.parse(fs.readFileSync(absPath, 'utf8'));
|
|
191
|
+
} catch {
|
|
192
|
+
throw new Error(`Invalid JSON in file: ${absPath}`);
|
|
193
|
+
}
|
|
194
|
+
} else if (jsonPayload) {
|
|
195
|
+
try {
|
|
196
|
+
tx = JSON.parse(jsonPayload);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error('Invalid JSON payload provided with --json');
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
throw new Error('No transaction provided. Use --tx, --json, or --file');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Validate the transaction has required fields
|
|
205
|
+
if (!signature) {
|
|
206
|
+
const validationErrors = validateTxPayload(tx);
|
|
207
|
+
if (validationErrors.length > 0) {
|
|
208
|
+
throw new Error('Invalid transaction payload:\n ' + validationErrors.join('\n '));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!asJson) {
|
|
213
|
+
console.log(`\n${C.bright}${C.cyan}── Broadcast Transaction ─────────────────────────────────────${C.reset}`);
|
|
214
|
+
console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
|
|
215
|
+
if (signature) {
|
|
216
|
+
console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${signature}${C.reset}`);
|
|
217
|
+
} else {
|
|
218
|
+
const txType = tx.tx_type || tx.type || 'Unknown';
|
|
219
|
+
const signer = tx.signer || tx.from || tx.pubkey || 'unknown';
|
|
220
|
+
console.log(` ${C.dim}Type:${C.reset} ${C.cyan}${txType}${C.reset}`);
|
|
221
|
+
console.log(` ${C.dim}Signer:${C.reset} ${C.cyan}${signer}${C.reset}`);
|
|
222
|
+
}
|
|
223
|
+
console.log();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Submit the transaction
|
|
227
|
+
let result;
|
|
228
|
+
let latencyMs;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const start = Date.now();
|
|
232
|
+
result = await httpPost(rpc, '/v1/tx', tx);
|
|
233
|
+
latencyMs = Date.now() - start;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (asJson) {
|
|
236
|
+
console.log(JSON.stringify({
|
|
237
|
+
success: false,
|
|
238
|
+
error: err.message,
|
|
239
|
+
rpc,
|
|
240
|
+
cli_version: CLI_VERSION,
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
}, null, 2));
|
|
243
|
+
} else {
|
|
244
|
+
console.log(` ${C.red}✗ Network error:${C.reset} ${err.message}`);
|
|
245
|
+
console.log(` ${C.dim} Check that your RPC is accessible: ${rpc}${C.reset}`);
|
|
246
|
+
}
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const success = result && !result.error && result.accepted !== false;
|
|
251
|
+
|
|
252
|
+
if (asJson) {
|
|
253
|
+
console.log(JSON.stringify({
|
|
254
|
+
success,
|
|
255
|
+
accepted: result?.accepted ?? null,
|
|
256
|
+
signature: result?.signature ?? result?.tx_signature ?? signature ?? null,
|
|
257
|
+
slot: result?.slot ?? null,
|
|
258
|
+
error: result?.error ?? null,
|
|
259
|
+
rpc,
|
|
260
|
+
latency_ms: latencyMs,
|
|
261
|
+
cli_version: CLI_VERSION,
|
|
262
|
+
timestamp: new Date().toISOString(),
|
|
263
|
+
}, null, 2));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (success) {
|
|
268
|
+
const sig = result?.signature ?? result?.tx_signature ?? signature ?? 'unknown';
|
|
269
|
+
console.log(`${C.green}✓ Transaction accepted!${C.reset}`);
|
|
270
|
+
console.log(` ${C.green}★${C.reset} ${C.bright}Signature:${C.reset} ${sig}`);
|
|
271
|
+
if (result?.slot) {
|
|
272
|
+
console.log(` ${C.dim} Slot:${C.reset} ${result.slot}`);
|
|
273
|
+
}
|
|
274
|
+
console.log(` ${C.dim} Latency:${C.reset} ${latencyMs}ms`);
|
|
275
|
+
console.log(` ${C.dim} RPC:${C.reset} ${rpc}`);
|
|
276
|
+
console.log();
|
|
277
|
+
} else {
|
|
278
|
+
const errMsg = result?.error || 'Transaction rejected by network';
|
|
279
|
+
console.log(`${C.red}✗ Transaction rejected${C.reset}`);
|
|
280
|
+
if (result?.error) {
|
|
281
|
+
console.log(` ${C.red} Error:${C.reset} ${result.error}`);
|
|
282
|
+
}
|
|
283
|
+
if (result?.logs) {
|
|
284
|
+
console.log(` ${C.dim} Logs:${C.reset}`);
|
|
285
|
+
for (const log of result.logs) {
|
|
286
|
+
console.log(` ${C.dim}${log}${C.reset}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
console.log(` ${C.dim} Latency:${C.reset} ${latencyMs}ms`);
|
|
290
|
+
console.log(` ${C.dim} RPC:${C.reset} ${rpc}`);
|
|
291
|
+
console.log();
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Main
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
async function main() {
|
|
301
|
+
const opts = parseArgs();
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
await broadcast(opts);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (opts.asJson) {
|
|
307
|
+
console.log(JSON.stringify({
|
|
308
|
+
success: false,
|
|
309
|
+
error: err.message,
|
|
310
|
+
rpc: opts.rpc,
|
|
311
|
+
cli_version: CLI_VERSION,
|
|
312
|
+
timestamp: new Date().toISOString(),
|
|
313
|
+
}, null, 2));
|
|
314
|
+
} else {
|
|
315
|
+
console.log(`\n ${C.red}✗ ${err.message}${C.reset}\n`);
|
|
316
|
+
}
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main();
|
|
322
|
+
|
|
323
|
+
module.exports = { broadcastCommand: main };
|
|
@@ -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
|
+
}
|