aether-hub 1.2.2 → 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/status.js +371 -0
- package/commands/supply.js +437 -437
- package/index.js +16 -0
- 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,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
|
+
}
|