@jellylegsai/aether-cli 1.8.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/aether-cli-1.0.0.tgz +0 -0
  4. package/aether-cli-1.8.0.tgz +0 -0
  5. package/aether-hub-1.0.5.tgz +0 -0
  6. package/aether-hub-1.1.8.tgz +0 -0
  7. package/aether-hub-1.2.1.tgz +0 -0
  8. package/commands/account.js +280 -0
  9. package/commands/apy.js +499 -0
  10. package/commands/balance.js +241 -0
  11. package/commands/blockhash.js +181 -0
  12. package/commands/broadcast.js +387 -0
  13. package/commands/claim.js +490 -0
  14. package/commands/config.js +851 -0
  15. package/commands/delegations.js +582 -0
  16. package/commands/doctor.js +769 -0
  17. package/commands/emergency.js +667 -0
  18. package/commands/epoch.js +275 -0
  19. package/commands/fees.js +276 -0
  20. package/commands/index.js +78 -0
  21. package/commands/info.js +495 -0
  22. package/commands/init.js +816 -0
  23. package/commands/install.js +666 -0
  24. package/commands/kyc.js +272 -0
  25. package/commands/logs.js +315 -0
  26. package/commands/monitor.js +431 -0
  27. package/commands/multisig.js +701 -0
  28. package/commands/network.js +429 -0
  29. package/commands/nft.js +857 -0
  30. package/commands/ping.js +266 -0
  31. package/commands/price.js +253 -0
  32. package/commands/rewards.js +931 -0
  33. package/commands/sdk-test.js +477 -0
  34. package/commands/sdk.js +656 -0
  35. package/commands/slot.js +155 -0
  36. package/commands/snapshot.js +470 -0
  37. package/commands/stake-info.js +139 -0
  38. package/commands/stake-positions.js +205 -0
  39. package/commands/stake.js +516 -0
  40. package/commands/stats.js +396 -0
  41. package/commands/status.js +327 -0
  42. package/commands/supply.js +391 -0
  43. package/commands/tps.js +238 -0
  44. package/commands/transfer.js +495 -0
  45. package/commands/tx-history.js +346 -0
  46. package/commands/unstake.js +597 -0
  47. package/commands/validator-info.js +657 -0
  48. package/commands/validator-register.js +593 -0
  49. package/commands/validator-start.js +323 -0
  50. package/commands/validator-status.js +227 -0
  51. package/commands/validators.js +626 -0
  52. package/commands/wallet.js +1570 -0
  53. package/index.js +593 -0
  54. package/lib/errors.js +398 -0
  55. package/package.json +76 -0
  56. package/sdk/README.md +210 -0
  57. package/sdk/index.js +1639 -0
  58. package/sdk/package.json +34 -0
  59. package/sdk/rpc.js +254 -0
  60. package/sdk/test.js +85 -0
  61. package/test/doctor.test.js +76 -0
  62. package/validator-identity.json +4 -0
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli supply
4
+ *
5
+ * Display Aether network token supply metrics:
6
+ * - Total supply of AETH (all accounts + locked/escrow)
7
+ * - Circulating supply (liquid, tradeable tokens)
8
+ * - Staked supply (locked in stake accounts)
9
+ * - Burned supply (tokens sent to burn address / invalid addresses)
10
+ *
11
+ * Usage:
12
+ * aether supply Show supply overview
13
+ * aether supply --json JSON output for scripting/monitoring
14
+ * aether supply --rpc <url> Query a specific RPC endpoint
15
+ * aether supply --verbose Show breakdown by account type
16
+ *
17
+ * Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
18
+ */
19
+
20
+ const path = require('path');
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
+ bold: '\x1b[1m',
33
+ };
34
+
35
+ const CLI_VERSION = '1.0.0';
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // SDK Import - Real blockchain RPC calls via @jellylegsai/aether-sdk
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
42
+ const aether = require(sdkPath);
43
+
44
+ function getDefaultRpc() {
45
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
46
+ }
47
+
48
+ /** Create SDK client */
49
+ function createClient(rpcUrl) {
50
+ return new aether.AetherClient({ rpcUrl });
51
+ }
52
+
53
+ function formatAether(lamports) {
54
+ const aeth = Number(lamports) / 1e9;
55
+ if (aeth === 0) return '0 AETH';
56
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
57
+ }
58
+
59
+ function formatAethFull(lamports) {
60
+ return (Number(lamports) / 1e9).toFixed(6) + ' AETH';
61
+ }
62
+
63
+ function formatLargeNum(n) {
64
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Core supply fetchers using SDK
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Fetch the total supply of AETH from the chain using SDK.
73
+ * Makes real RPC call: GET /v1/supply
74
+ */
75
+ async function fetchTotalSupply(rpc) {
76
+ const client = createClient(rpc);
77
+ try {
78
+ // Primary: SDK getSupply() → GET /v1/supply
79
+ const res = await client.getSupply();
80
+ if (res && (res.total !== undefined || res.supply !== undefined)) {
81
+ return {
82
+ total: BigInt(res.total || res.supply?.total || 0),
83
+ circulating: BigInt(res.circulating || res.supply?.circulating || 0),
84
+ nonCirculating: BigInt(res.non_circulating || res.nonCirculating || res.supply?.non_circulating || 0),
85
+ source: 'rpc_v1_supply',
86
+ };
87
+ }
88
+ } catch { /* fall through */ }
89
+
90
+ // Fallback: fetch epoch info which contains total token count
91
+ try {
92
+ const epochInfo = await client.getEpochInfo();
93
+ if (epochInfo) {
94
+ const totalStaked = BigInt(epochInfo.total_staked || 0);
95
+ const rewardsPerEpoch = BigInt(epochInfo.rewards_per_epoch || '2000000000');
96
+ const currentEpoch = BigInt(epochInfo.epoch || 0);
97
+ // Rough estimate: total supply ~= minted so far + remaining allocation
98
+ // Aether has ~500M AETH max supply, minted gradually over 100 years
99
+ const maxSupply = BigInt('500000000000000000'); // 500M * 1e9
100
+ const mintedPerEpoch = rewardsPerEpoch;
101
+ const minted = mintedPerEpoch * currentEpoch;
102
+ // Some tokens are locked/vesting; assume ~30% is non-circulating
103
+ const estimatedTotal = minted < maxSupply ? minted : maxSupply;
104
+ const estimatedCirculating = estimatedTotal - BigInt(BigInt(estimatedTotal) / BigInt(3));
105
+ return {
106
+ total: estimatedTotal,
107
+ circulating: estimatedCirculating,
108
+ nonCirculating: estimatedTotal - estimatedCirculating,
109
+ source: 'epoch_info_estimate',
110
+ };
111
+ }
112
+ } catch { /* fall through */ }
113
+
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Fetch staked supply by querying stake program accounts using SDK.
119
+ * Makes real RPC call: GET /v1/validators
120
+ */
121
+ async function fetchStakedSupply(rpc) {
122
+ const client = createClient(rpc);
123
+ try {
124
+ // SDK getValidators() → GET /v1/validators
125
+ const validators = await client.getValidators();
126
+ if (validators && Array.isArray(validators)) {
127
+ let total = BigInt(0);
128
+ for (const v of validators) {
129
+ total += BigInt(v.delegated_stake || v.stake || v.delegatedStake || 0);
130
+ }
131
+ return total;
132
+ }
133
+ } catch { /* fall through */ }
134
+
135
+ try {
136
+ // Last resort: epoch info staked amount via SDK
137
+ const epochInfo = await client.getEpochInfo();
138
+ if (epochInfo && epochInfo.total_staked) {
139
+ return BigInt(epochInfo.total_staked);
140
+ }
141
+ } catch { /* fall through */ }
142
+
143
+ return BigInt(0);
144
+ }
145
+
146
+ /**
147
+ * Estimate burned supply by querying accounts at known burn/mint addresses using SDK.
148
+ * Makes real RPC calls: GET /v1/account/<address>
149
+ */
150
+ async function fetchBurnedSupply(rpc) {
151
+ const client = createClient(rpc);
152
+ const BURN_ADDRESSES = [
153
+ 'ATH1111111111111111111111111111111111111', // mint authority burn
154
+ 'ATH2222222222222222222222222222222222222', // zero authority
155
+ 'ATHburn000000000000000000000000000000', // burn address
156
+ ];
157
+
158
+ let totalBurned = BigInt(0);
159
+
160
+ for (const addr of BURN_ADDRESSES) {
161
+ try {
162
+ const rawAddr = addr.startsWith('ATH') ? addr.slice(3) : addr;
163
+ // SDK getAccountInfo() → GET /v1/account/<address>
164
+ const account = await client.getAccountInfo(rawAddr);
165
+ if (account && account.lamports !== undefined && Number(account.lamports) > 0) {
166
+ totalBurned += BigInt(account.lamports);
167
+ }
168
+ } catch { /* skip inaccessible addresses */ }
169
+ }
170
+
171
+ return totalBurned;
172
+ }
173
+
174
+ /**
175
+ * Fetch circulating supply = total - non-circulating (locked/vesting/burned).
176
+ * Non-circulating includes: burn address, escrow/staking vault, team vesting.
177
+ * Uses SDK client for RPC calls.
178
+ */
179
+ async function fetchNonCirculatingAccounts(rpc) {
180
+ const client = createClient(rpc);
181
+ try {
182
+ const res = await client._httpGet('/v1/supply/non-circulating');
183
+ if (res && !res.error && Array.isArray(res.accounts)) {
184
+ let total = BigInt(0);
185
+ for (const acct of res.accounts) {
186
+ total += BigInt(acct.lamports || 0);
187
+ }
188
+ return total;
189
+ }
190
+ } catch { /* fall through */ }
191
+
192
+ return BigInt(0);
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Render output
197
+ // ---------------------------------------------------------------------------
198
+
199
+ function renderSupplyTable(data) {
200
+ const { total, circulating, staked, burned, nonCirculating, rpc, source } = data;
201
+
202
+ const circPct = total > 0 ? ((Number(circulating) / Number(total)) * 100).toFixed(1) : '?';
203
+ const stakedPct = total > 0 ? ((Number(staked) / Number(total)) * 100).toFixed(1) : '?';
204
+ const burnedPct = total > 0 ? ((Number(burned) / Number(total)) * 100).toFixed(2) : '?';
205
+
206
+ console.log(`\n${C.bold}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
207
+ console.log(`${C.bold}${C.cyan}║ AETHER TOKEN SUPPLY ║${C.reset}`);
208
+ console.log(`${C.bold}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
209
+ console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
210
+ console.log(` ${C.dim}Source: ${source}${C.reset}\n`);
211
+
212
+ console.log(` ${C.bright}┌─ ${C.cyan}TOTAL SUPPLY${C.reset}`);
213
+ console.log(` │ ${C.bold}${formatAethFull(total)}${C.reset}`);
214
+ console.log(` │ ${C.dim}${formatLargeNum(Number(total))} lamports${C.reset}`);
215
+ console.log(` ${C.dim}└${C.reset}`);
216
+ console.log();
217
+
218
+ console.log(` ${C.bright}┌─ ${C.green}CIRCULATING SUPPLY${C.reset}`);
219
+ console.log(` │ ${C.green}${formatAethFull(circulating)}${C.reset}`);
220
+ console.log(` │ ${C.dim}${formatLargeNum(Number(circulating))} lamports${C.reset}`);
221
+ console.log(` │ ${C.green}${circPct}%${C.reset} of total supply`);
222
+ console.log(` ${C.dim}└${C.reset}`);
223
+ console.log();
224
+
225
+ console.log(` ${C.bright}┌─ ${C.yellow}STAKED SUPPLY${C.reset}`);
226
+ console.log(` │ ${C.yellow}${formatAethFull(staked)}${C.reset}`);
227
+ console.log(` │ ${C.dim}${formatLargeNum(Number(staked))} lamports${C.reset}`);
228
+ console.log(` │ ${C.yellow}${stakedPct}%${C.reset} of total supply`);
229
+ console.log(` ${C.dim}└${C.reset}`);
230
+ console.log();
231
+
232
+ if (burned > 0) {
233
+ console.log(` ${C.bright}┌─ ${C.red}BURNED / IRRECOVERABLE${C.reset}`);
234
+ console.log(` │ ${C.red}${formatAethFull(burned)}${C.reset}`);
235
+ console.log(` │ ${C.dim}${formatLargeNum(Number(burned))} lamports${C.reset}`);
236
+ console.log(` │ ${C.red}${burnedPct}%${C.reset} of total supply`);
237
+ console.log(` ${C.dim}└${C.reset}`);
238
+ console.log();
239
+ }
240
+
241
+ if (nonCirculating > 0) {
242
+ console.log(` ${C.bright}┌─ ${C.magenta}NON-CIRCULATING (LOCKED/ESCROW)${C.reset}`);
243
+ console.log(` │ ${C.magenta}${formatAethFull(nonCirculating)}${C.reset}`);
244
+ console.log(` │ ${C.dim}${formatLargeNum(Number(nonCirculating))} lamports${C.reset}`);
245
+ console.log(` ${C.dim}└${C.reset}`);
246
+ console.log();
247
+ }
248
+
249
+ // Visual bar
250
+ const barLen = 40;
251
+ const circBars = Math.round((Number(circulating) / Number(total)) * barLen);
252
+ const stakedBars = Math.round((Number(staked) / Number(total)) * barLen);
253
+ const burnedBars = Math.round((Number(burned) / Number(total)) * barLen);
254
+ const nonCircBars = Math.round((Number(nonCirculating) / Number(total)) * barLen);
255
+
256
+ console.log(` ${C.dim}Supply breakdown bar (per ${barLen} units):${C.reset}`);
257
+ const bar = [
258
+ C.green + '█'.repeat(Math.min(circBars, barLen)) + C.reset,
259
+ C.yellow + '█'.repeat(Math.min(stakedBars, Math.max(0, barLen - circBars))) + C.reset,
260
+ C.red + '█'.repeat(Math.min(burnedBars, Math.max(0, barLen - circBars - stakedBars))) + C.reset,
261
+ ].join('');
262
+ console.log(` ${bar}`);
263
+ console.log(` ${C.green}■ circulating${C.reset} ${C.yellow}■ staked${C.reset} ${C.red}■ burned${C.reset}`);
264
+ console.log();
265
+ }
266
+
267
+ /**
268
+ * Compute and display supply metrics.
269
+ */
270
+ async function showSupply(rpc, opts) {
271
+ const { asJson, verbose } = opts;
272
+
273
+ console.error(`${C.dim}Fetching supply data from ${rpc}...${C.reset}`);
274
+
275
+ // Fetch all supply components in parallel
276
+ const [totalData, staked, burned, nonCirc] = await Promise.all([
277
+ fetchTotalSupply(rpc),
278
+ fetchStakedSupply(rpc),
279
+ fetchBurnedSupply(rpc),
280
+ fetchNonCirculatingAccounts(rpc),
281
+ ]);
282
+
283
+ if (!totalData) {
284
+ const msg = `Failed to fetch supply data from ${rpc}. Ensure your node is running or set AETHER_RPC.`;
285
+ if (asJson) {
286
+ console.log(JSON.stringify({ error: msg, rpc }, null, 2));
287
+ } else {
288
+ console.log(`\n${C.red}✗ ${msg}${C.reset}\n`);
289
+ }
290
+ process.exit(1);
291
+ }
292
+
293
+ const { total, circulating, nonCirculating: ncFromSupply, source } = totalData;
294
+ // Use chain non-circulating if available, otherwise fall back to computed value
295
+ const nonCirculating = ncFromSupply > 0 ? ncFromSupply : nonCirc;
296
+
297
+ if (asJson) {
298
+ const out = {
299
+ rpc,
300
+ source,
301
+ supply: {
302
+ total: total.toString(),
303
+ total_formatted: formatAethFull(total),
304
+ circulating: circulating.toString(),
305
+ circulating_formatted: formatAethFull(circulating),
306
+ non_circulating: nonCirculating.toString(),
307
+ non_circulating_formatted: formatAethFull(nonCirculating),
308
+ staked: staked.toString(),
309
+ staked_formatted: formatAethFull(staked),
310
+ burned: burned.toString(),
311
+ burned_formatted: formatAethFull(burned),
312
+ percentages: {
313
+ circulating_pct: total > 0 ? ((Number(circulating) / Number(total)) * 100).toFixed(2) : '0',
314
+ staked_pct: total > 0 ? ((Number(staked) / Number(total)) * 100).toFixed(2) : '0',
315
+ burned_pct: total > 0 ? ((Number(burned) / Number(total)) * 100).toFixed(4) : '0',
316
+ },
317
+ },
318
+ fetched_at: new Date().toISOString(),
319
+ };
320
+ console.log(JSON.stringify(out, null, 2));
321
+ return;
322
+ }
323
+
324
+ renderSupplyTable({
325
+ total,
326
+ circulating,
327
+ staked,
328
+ burned,
329
+ nonCirculating,
330
+ rpc,
331
+ source,
332
+ });
333
+
334
+ if (verbose) {
335
+ console.log(` ${C.dim}Notes:${C.reset}`);
336
+ console.log(` ${C.dim} - Circulating = total - non-circulating (locked/escrow)${C.reset}`);
337
+ console.log(` ${C.dim} - Staked supply reflects tokens in active stake accounts${C.reset}`);
338
+ console.log(` ${C.dim} - Burned supply reflects tokens sent to irrecoverable addresses${C.reset}`);
339
+ console.log(` ${C.dim} - Percentages calculated against total supply${C.reset}`);
340
+ console.log(` ${C.dim} - Source: ${source}${C.reset}\n`);
341
+ }
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // CLI arg parsing
346
+ // ---------------------------------------------------------------------------
347
+
348
+ function parseArgs() {
349
+ return process.argv.slice(3); // [node, index.js, supply, ...]
350
+ }
351
+
352
+ async function main() {
353
+ const args = parseArgs();
354
+
355
+ let rpc = getDefaultRpc();
356
+ let asJson = false;
357
+ let verbose = false;
358
+
359
+ for (let i = 0; i < args.length; i++) {
360
+ if ((args[i] === '--rpc' || args[i] === '-r') && args[i + 1]) {
361
+ rpc = args[++i];
362
+ } else if (args[i] === '--json' || args[i] === '-j') {
363
+ asJson = true;
364
+ } else if (args[i] === '--verbose' || args[i] === '-v') {
365
+ verbose = true;
366
+ } else if (args[i] === '--help' || args[i] === '-h') {
367
+ console.log(`
368
+ ${C.cyan}Usage:${C.reset}
369
+ aether supply Show Aether token supply overview
370
+ aether supply --json JSON output for scripting/monitoring
371
+ aether supply --rpc <url> Query a specific RPC endpoint
372
+ aether supply --verbose Show detailed breakdown and notes
373
+
374
+ ${C.dim}Examples:${C.reset}
375
+ aether supply
376
+ aether supply --json --rpc https://mainnet.aether.io
377
+ AETHER_RPC=https://backup-rpc.example.com aether supply --verbose
378
+ `);
379
+ return;
380
+ }
381
+ }
382
+
383
+ await showSupply(rpc, { asJson, verbose });
384
+ }
385
+
386
+ main().catch(err => {
387
+ console.error(`${C.red}Error:${C.reset} ${err.message}\n`);
388
+ process.exit(1);
389
+ });
390
+
391
+ module.exports = { supplyCommand: main };
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli tps
4
+ *
5
+ * Monitor real-time transactions per second (TPS) on the Aether blockchain.
6
+ * Uses @jellylegsai/aether-sdk for real RPC calls to /v1/tps endpoint.
7
+ *
8
+ * Usage:
9
+ * aether tps Show current TPS
10
+ * aether tps --monitor Continuous monitoring (updates every 2s)
11
+ * aether tps --interval <sec> Custom interval for monitoring (default: 2)
12
+ * aether tps --json JSON output for scripting
13
+ * aether tps --rpc <url> Custom RPC endpoint
14
+ *
15
+ * Examples:
16
+ * aether tps # Single TPS reading
17
+ * aether tps --monitor # Live monitoring dashboard
18
+ * aether tps --monitor --interval 1 # Update every second
19
+ * aether tps --json # JSON for alerting/monitoring
20
+ */
21
+
22
+ const path = require('path');
23
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
24
+ const aether = require(sdkPath);
25
+
26
+ // ANSI colours
27
+ const C = {
28
+ reset: '\x1b[0m',
29
+ bright: '\x1b[1m',
30
+ dim: '\x1b[2m',
31
+ red: '\x1b[31m',
32
+ green: '\x1b[32m',
33
+ yellow: '\x1b[33m',
34
+ cyan: '\x1b[36m',
35
+ magenta: '\x1b[35m',
36
+ };
37
+
38
+ const CLI_VERSION = '1.0.0';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Helpers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function getDefaultRpc() {
45
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
46
+ }
47
+
48
+ function createClient(rpc) {
49
+ return new aether.AetherClient({ rpcUrl: rpc });
50
+ }
51
+
52
+ function tpsColor(tps) {
53
+ if (tps === null || tps === undefined) return C.red;
54
+ if (tps >= 1000) return C.green;
55
+ if (tps >= 100) return C.cyan;
56
+ if (tps >= 10) return C.yellow;
57
+ return C.red;
58
+ }
59
+
60
+ function tpsLabel(tps) {
61
+ if (tps === null || tps === undefined) return '✗ unreachable';
62
+ if (tps >= 1000) return `● ${tps.toLocaleString()} TPS (excellent)`;
63
+ if (tps >= 100) return `● ${tps.toLocaleString()} TPS (good)`;
64
+ if (tps >= 10) return `○ ${tps.toLocaleString()} TPS (fair)`;
65
+ return `○ ${tps.toLocaleString()} TPS (low)`;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Single TPS reading
70
+ // ---------------------------------------------------------------------------
71
+
72
+ async function getTpsOnce(rpcUrl) {
73
+ const client = createClient(rpcUrl);
74
+ const start = Date.now();
75
+ let tps = null;
76
+ let error = null;
77
+ let latencyMs = null;
78
+
79
+ try {
80
+ // Real RPC call: GET /v1/tps
81
+ tps = await client.getTPS();
82
+ latencyMs = Date.now() - start;
83
+ } catch (err) {
84
+ latencyMs = Date.now() - start;
85
+ error = err.message;
86
+ }
87
+
88
+ return { tps, error, latencyMs, rpcUrl, timestamp: new Date() };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Continuous monitoring
93
+ // ---------------------------------------------------------------------------
94
+
95
+ async function monitorTps(rpcUrl, intervalSec) {
96
+ console.log(`\n${C.bright}${C.cyan}── Aether TPS Monitor ───────────────────────────────────${C.reset}`);
97
+ console.log(` ${C.dim}RPC: ${rpcUrl}${C.reset}`);
98
+ console.log(` ${C.dim}Interval: ${intervalSec}s${C.reset}`);
99
+ console.log(` ${C.dim}Press Ctrl+C to stop${C.reset}\n`);
100
+
101
+ const history = [];
102
+ const maxHistory = 20;
103
+
104
+ process.on('SIGINT', () => {
105
+ console.log(`\n${C.dim}Monitoring stopped.${C.reset}`);
106
+ if (history.length > 1) {
107
+ const tpsValues = history.map(h => h.tps).filter(t => t !== null);
108
+ if (tpsValues.length > 0) {
109
+ const avg = Math.round(tpsValues.reduce((a, b) => a + b, 0) / tpsValues.length);
110
+ const min = Math.min(...tpsValues);
111
+ const max = Math.max(...tpsValues);
112
+ console.log(` ${C.bright}Summary:${C.reset} avg=${avg} min=${min} max=${max} samples=${history.length}`);
113
+ }
114
+ }
115
+ console.log();
116
+ process.exit(0);
117
+ });
118
+
119
+ while (true) {
120
+ const result = await getTpsOnce(rpcUrl);
121
+ history.push(result);
122
+ if (history.length > maxHistory) history.shift();
123
+
124
+ // Clear line and print
125
+ process.stdout.write('\x1b[2K\r');
126
+
127
+ const tc = tpsColor(result.tps);
128
+ const barLen = result.tps !== null ? Math.min(30, Math.floor(result.tps / 50)) : 0;
129
+ const bar = tc + '█'.repeat(barLen) + C.dim + '░'.repeat(30 - barLen) + C.reset;
130
+
131
+ const slotInfo = result.tps !== null ? ` ${C.dim}RPC latency: ${result.latencyMs}ms${C.reset}` : '';
132
+
133
+ console.log(` ${tc}${bar}${C.reset} ${tpsLabel(result.tps)}${C.reset}${slotInfo}`);
134
+
135
+ // Show trend
136
+ if (history.length >= 3) {
137
+ const recent = history.slice(-3).map(h => h.tps).filter(t => t !== null);
138
+ if (recent.length >= 2) {
139
+ const trend = recent[recent.length - 1] - recent[0];
140
+ const arrow = trend > 0 ? C.green + '▲' : trend < 0 ? C.red + '▼' : C.dim + '─';
141
+ console.log(` ${C.dim}Trend: ${arrow} ${Math.abs(trend)} TPS${C.reset}`);
142
+ }
143
+ }
144
+
145
+ await new Promise(resolve => setTimeout(resolve, intervalSec * 1000));
146
+ }
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Main command
151
+ // ---------------------------------------------------------------------------
152
+
153
+ async function tpsCommand() {
154
+ const args = process.argv.slice(2);
155
+ const asJson = args.includes('--json') || args.includes('-j');
156
+ const isMonitor = args.includes('--monitor') || args.includes('-m');
157
+ const rpcIdx = args.findIndex(a => a === '--rpc' || a === '-r');
158
+ const rpcUrl = rpcIdx !== -1 && args[rpcIdx + 1] ? args[rpcIdx + 1] : getDefaultRpc();
159
+
160
+ const intervalIdx = args.findIndex(a => a === '--interval' || a === '-i');
161
+ const intervalSec = intervalIdx !== -1 && args[intervalIdx + 1]
162
+ ? Math.max(1, parseInt(args[intervalIdx + 1], 10) || 2)
163
+ : 2;
164
+
165
+ if (isMonitor) {
166
+ await monitorTps(rpcUrl, intervalSec);
167
+ return;
168
+ }
169
+
170
+ // Single reading
171
+ const result = await getTpsOnce(rpcUrl);
172
+
173
+ if (asJson) {
174
+ console.log(JSON.stringify({
175
+ rpc: rpcUrl,
176
+ tps: result.tps,
177
+ online: result.tps !== null,
178
+ latency_ms: result.latencyMs,
179
+ error: result.error || null,
180
+ timestamp: result.timestamp.toISOString(),
181
+ cli_version: CLI_VERSION,
182
+ }, null, 2));
183
+ return;
184
+ }
185
+
186
+ console.log(`\n${C.bright}${C.cyan}── Aether Network TPS ─────────────────────────────────${C.reset}\n`);
187
+
188
+ const tc = tpsColor(result.tps);
189
+ const barLen = result.tps !== null ? Math.min(40, Math.floor(result.tps / 50)) : 0;
190
+ const bar = tc + '█'.repeat(barLen) + C.dim + '░'.repeat(40 - barLen) + C.reset;
191
+
192
+ console.log(` ${C.dim}RPC:${C.reset} ${rpcUrl}`);
193
+ console.log(` ${C.dim}Latency:${C.reset} ${result.latencyMs}ms`);
194
+ console.log();
195
+ console.log(` ${C.bright}${tc}${tpsLabel(result.tps)}${C.reset}`);
196
+ console.log();
197
+ console.log(` ${bar}`);
198
+ console.log();
199
+
200
+ if (result.error) {
201
+ console.log(` ${C.red}✗ ${result.error}${C.reset}`);
202
+ console.log();
203
+ }
204
+
205
+ // Context info
206
+ if (result.tps !== null) {
207
+ console.log(` ${C.dim}Network Health:${C.reset}`);
208
+ if (result.tps >= 1000) {
209
+ console.log(` ${C.green}● Network is handling high throughput${C.reset}`);
210
+ } else if (result.tps >= 100) {
211
+ console.log(` ${C.cyan}● Network operating normally${C.reset}`);
212
+ } else if (result.tps >= 10) {
213
+ console.log(` ${C.yellow}○ Network has low activity${C.reset}`);
214
+ } else {
215
+ console.log(` ${C.red}○ Network is idle or experiencing issues${C.reset}`);
216
+ }
217
+ }
218
+
219
+ console.log();
220
+ console.log(` ${C.dim}Run ${C.cyan}aether tps --monitor${C.reset}${C.dim} for live tracking.${C.reset}\n`);
221
+
222
+ if (result.tps === null) {
223
+ process.exit(1);
224
+ }
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Entry point
229
+ // ---------------------------------------------------------------------------
230
+
231
+ module.exports = { tpsCommand };
232
+
233
+ if (require.main === module) {
234
+ tpsCommand().catch(err => {
235
+ console.error(`\n${C.red}TPS command failed:${C.reset} ${err.message}\n`);
236
+ process.exit(1);
237
+ });
238
+ }