aether-hub 1.2.4 → 1.2.6

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,508 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli tx-history
4
+ *
5
+ * Fetch and display transaction history for an Aether wallet address.
6
+ * Shows recent transactions with type, amount, timestamp, fee, and status.
7
+ *
8
+ * Usage:
9
+ * aether tx history --address <addr> [--limit <n>] [--json] [--rpc <url>]
10
+ * aether history --address <addr> [--limit <n>] [--json]
11
+ *
12
+ * Examples:
13
+ * aether tx history --address ATHxxx
14
+ * aether tx history --address ATHxxx --limit 50 --json
15
+ * aether history --address ATHxxx --rpc https://rpc.aether.io
16
+ */
17
+
18
+ const https = require('https');
19
+ const http = require('http');
20
+
21
+ // ANSI colours
22
+ const C = {
23
+ reset: '\x1b[0m',
24
+ bright: '\x1b[1m',
25
+ dim: '\x1b[2m',
26
+ red: '\x1b[31m',
27
+ green: '\x1b[32m',
28
+ yellow: '\x1b[33m',
29
+ cyan: '\x1b[36m',
30
+ magenta: '\x1b[35m',
31
+ };
32
+
33
+ const CLI_VERSION = '1.0.0';
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Config
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function getDefaultRpc() {
40
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // HTTP helpers
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function httpRequest(rpcUrl, path, timeoutMs = 10000) {
48
+ return new Promise((resolve, reject) => {
49
+ const url = new URL(path, rpcUrl);
50
+ const lib = url.protocol === 'https:' ? https : http;
51
+ const req = lib.request({
52
+ hostname: url.hostname,
53
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
54
+ path: url.pathname + url.search,
55
+ method: 'GET',
56
+ timeout: timeoutMs,
57
+ headers: { 'Content-Type': 'application/json' },
58
+ }, (res) => {
59
+ let data = '';
60
+ res.on('data', (chunk) => data += chunk);
61
+ res.on('end', () => {
62
+ try { resolve(JSON.parse(data)); }
63
+ catch { resolve({ raw: data }); }
64
+ });
65
+ });
66
+ req.on('error', reject);
67
+ req.on('timeout', () => { req.destroy(); reject(new Error(`Request timeout after ${timeoutMs}ms`)); });
68
+ req.end();
69
+ });
70
+ }
71
+
72
+ function httpPost(rpcUrl, path, body, timeoutMs = 15000) {
73
+ return new Promise((resolve, reject) => {
74
+ const url = new URL(path, rpcUrl);
75
+ const lib = url.protocol === 'https:' ? https : http;
76
+ const bodyStr = JSON.stringify(body);
77
+ const req = lib.request({
78
+ hostname: url.hostname,
79
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
80
+ path: url.pathname + url.search,
81
+ method: 'POST',
82
+ timeout: timeoutMs,
83
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
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({ raw: data }); }
90
+ });
91
+ });
92
+ req.on('error', reject);
93
+ req.on('timeout', () => { req.destroy(); reject(new Error(`Request timeout after ${timeoutMs}ms`)); });
94
+ req.write(bodyStr);
95
+ req.end();
96
+ });
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Argument parsing
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function parseArgs() {
104
+ const args = process.argv.slice(2);
105
+ const result = { address: null, limit: 20, json: false, rpc: null };
106
+
107
+ for (let i = 0; i < args.length; i++) {
108
+ const arg = args[i];
109
+ if ((arg === '--address' || arg === '-a') && args[i + 1] && !args[i + 1].startsWith('-')) {
110
+ result.address = args[i + 1];
111
+ i++;
112
+ } else if ((arg === '--limit' || arg === '-l') && args[i + 1] && !args[i + 1].startsWith('-')) {
113
+ result.limit = parseInt(args[i + 1], 10);
114
+ i++;
115
+ } else if (arg === '--json' || arg === '--json-output') {
116
+ result.json = true;
117
+ } else if (arg === '--rpc' && args[i + 1] && !args[i + 1].startsWith('-')) {
118
+ result.rpc = args[i + 1];
119
+ i++;
120
+ } else if (arg === '--help' || arg === '-h') {
121
+ result.help = true;
122
+ }
123
+ }
124
+
125
+ return result;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Formatting helpers
130
+ // ---------------------------------------------------------------------------
131
+
132
+ function formatAether(lamports) {
133
+ if (lamports == null) return '—';
134
+ const aeth = lamports / 1e9;
135
+ if (aeth === 0) return '0 AETH';
136
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
137
+ }
138
+
139
+ function formatTimestamp(slot, blockTime) {
140
+ if (!blockTime) return '—';
141
+ try {
142
+ const d = new Date(blockTime * 1000);
143
+ return d.toISOString().replace('T', ' ').slice(0, 19);
144
+ } catch {
145
+ return String(blockTime);
146
+ }
147
+ }
148
+
149
+ function shortAddress(addr) {
150
+ if (!addr) return '—';
151
+ if (addr.length <= 16) return addr;
152
+ return addr.slice(0, 8) + '…' + addr.slice(-6);
153
+ }
154
+
155
+ function formatTxType(type) {
156
+ const t = (type || 'unknown').toLowerCase();
157
+ if (t.includes('transfer') || t.includes('send') || t.includes('payment')) return { label: 'TRANSFER', color: C.cyan };
158
+ if (t.includes('stake') || t.includes('delegate')) return { label: 'STAKE', color: C.green };
159
+ if (t.includes('unstake') || t.includes('deactivate')) return { label: 'UNSTAKE', color: C.yellow };
160
+ if (t.includes('reward') || t.includes('mint')) return { label: 'REWARD', color: C.magenta };
161
+ if (t.includes('vote')) return { label: 'VOTE', color: C.bright };
162
+ if (t.includes('create') || t.includes('initialize')) return { label: 'CREATE', color: C.bright };
163
+ if (t.includes('burn')) return { label: 'BURN', color: C.red };
164
+ return { label: type ? type.toUpperCase().slice(0, 10) : 'TX', color: C.dim };
165
+ }
166
+
167
+ function formatStatus(status) {
168
+ if (!status) return { label: '—', color: C.dim };
169
+ const s = status.toLowerCase();
170
+ if (s === 'success' || s === 'finalized' || s === 'confirmed') {
171
+ return { label: '✓ OK', color: C.green };
172
+ }
173
+ if (s === 'pending' || s === 'processing') {
174
+ return { label: '⏳', color: C.yellow };
175
+ }
176
+ if (s === 'failed') {
177
+ return { label: '✗ FAIL', color: C.red };
178
+ }
179
+ return { label: status.slice(0, 8), color: C.dim };
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Core RPC calls
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /**
187
+ * Fetch confirmed transaction signatures for an address using getSignaturesForAddress.
188
+ */
189
+ async function fetchTxSignatures(rpcUrl, address, limit) {
190
+ const body = {
191
+ jsonrpc: '2.0',
192
+ id: 1,
193
+ method: 'getSignaturesForAddress',
194
+ params: [
195
+ address,
196
+ { limit },
197
+ ],
198
+ };
199
+ return httpPost(rpcUrl, '/', body);
200
+ }
201
+
202
+ /**
203
+ * Fetch a specific confirmed transaction by signature.
204
+ */
205
+ async function fetchTx(rpcUrl, signature) {
206
+ const body = {
207
+ jsonrpc: '2.0',
208
+ id: 1,
209
+ method: 'getTransaction',
210
+ params: [
211
+ signature,
212
+ { encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 },
213
+ ],
214
+ };
215
+ return httpPost(rpcUrl, '/', body);
216
+ }
217
+
218
+ /**
219
+ * Parse a transaction result into a normalized display object.
220
+ */
221
+ function parseTransaction(txResult, sigInfo) {
222
+ const blockTime = sigInfo.blockTime || txResult.blockTime;
223
+ const slot = sigInfo.slot;
224
+ const status = sigInfo.err ? 'failed' : (sigInfo.confirmationStatus || 'confirmed');
225
+
226
+ let txType = 'unknown';
227
+ let amount = 0;
228
+ let fee = txResult.meta?.fee || 0;
229
+ let fromAddr = null;
230
+ let toAddr = null;
231
+ let memo = null;
232
+
233
+ try {
234
+ const msg = txResult.transaction?.message;
235
+ if (msg) {
236
+ // Parse instructions for transfer/stake types
237
+ const instructions = msg.instructions || [];
238
+ for (const ix of instructions) {
239
+ const programId = ix.programId || (ix.parsed && ix.parsed.info && ix.parsed.type);
240
+ // Native transfer
241
+ if (ix.parsed && ix.parsed.type === 'transfer') {
242
+ txType = 'transfer';
243
+ const info = ix.parsed.info;
244
+ fromAddr = info.source || info.from;
245
+ toAddr = info.destination || info.to;
246
+ amount = info.lamports || info.amount || 0;
247
+ } else if (ix.parsed && ix.parsed.type === 'stake') {
248
+ txType = 'stake';
249
+ const info = ix.parsed.info;
250
+ fromAddr = info.from || info.funder;
251
+ toAddr = info.validator;
252
+ amount = info.lamports || info.amount || 0;
253
+ } else if (ix.parsed && ix.parsed.type === 'withdrawStake') {
254
+ txType = 'unstake';
255
+ const info = ix.parsed.info;
256
+ toAddr = info.destination || info.withdrawer;
257
+ amount = info.lamports || info.amount || 0;
258
+ } else if (ix.parsed && ix.parsed.type === 'vote') {
259
+ txType = 'vote';
260
+ } else if (ix.parsed && ix.parsed.type === 'initialize') {
261
+ txType = 'initialize';
262
+ } else if (ix.parsed && ix.parsed.type === 'createAccount') {
263
+ txType = 'create';
264
+ } else if (ix.parsed && ix.parsed.type === 'approve') {
265
+ txType = 'stake';
266
+ const info = ix.parsed.info || {};
267
+ fromAddr = info.from || info.owner;
268
+ toAddr = info.stake;
269
+ amount = info.amount || info.lamports || 0;
270
+ } else if (ix.parsed && ix.parsed.type === 'delegate') {
271
+ txType = 'stake';
272
+ const info = ix.parsed.info || {};
273
+ fromAddr = info.stake || info.from;
274
+ toAddr = info.validator;
275
+ amount = info.lamports || 0;
276
+ } else if (ix.parsed && ix.parsed.type === 'withdraw') {
277
+ txType = 'unstake';
278
+ const info = ix.parsed.info || {};
279
+ toAddr = info.destination;
280
+ amount = info.lamports || 0;
281
+ }
282
+ // Check memo
283
+ if (ix.memo) memo = ix.memo;
284
+ }
285
+
286
+ // Fallback: try legacy instructions if no parsed instructions
287
+ if (!instructions.length || instructions.every(ix => !ix.parsed)) {
288
+ for (const ix of instructions) {
289
+ if (ix.data === 'AAAA' || ix.data === '2ugJ4ELK3wW9qNXH' || !ix.data) {
290
+ txType = 'transfer';
291
+ }
292
+ }
293
+ }
294
+
295
+ // Compute fee
296
+ if (txResult.meta) {
297
+ fee = txResult.meta.fee || 0;
298
+ if (txResult.meta.postBalances && txResult.meta.preBalances) {
299
+ // Try to detect native transfer from balance changes
300
+ for (let i = 0; i < txResult.meta.postBalances.length; i++) {
301
+ const diff = txResult.meta.postBalances[i] - txResult.meta.preBalances[i];
302
+ if (diff < 0) {
303
+ amount = Math.abs(diff);
304
+ if (!fromAddr) fromAddr = msg.accountKeys?.[i];
305
+ } else if (diff > 0 && amount === 0) {
306
+ if (!toAddr) toAddr = msg.accountKeys?.[i];
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
312
+ } catch (e) {
313
+ // Parsing failed — use defaults
314
+ }
315
+
316
+ return {
317
+ signature: sigInfo.signature || sigInfo.signatures?.[0],
318
+ slot,
319
+ blockTime,
320
+ status,
321
+ type: txType,
322
+ amount,
323
+ fee,
324
+ from: fromAddr,
325
+ to: toAddr,
326
+ memo,
327
+ };
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Display
332
+ // ---------------------------------------------------------------------------
333
+
334
+ function displayTxTable(txs) {
335
+ // Header
336
+ console.log(
337
+ `\n${C.bright}${C.cyan} ╔══════════════════════════════════════════════════════════════════════════════════════════╗${C.reset}\n` +
338
+ ` ║${C.bright} ${C.cyan}Transaction History${C.reset}${C.bright} ║${C.reset}\n` +
339
+ ` ${C.cyan}╚══════════════════════════════════════════════════════════════════════════════════════════${C.reset}`
340
+ );
341
+
342
+ if (txs.length === 0) {
343
+ console.log(`\n ${C.yellow}No transactions found for this address.${C.reset}\n`);
344
+ return;
345
+ }
346
+
347
+ console.log(
348
+ ` ${C.dim}┌────┬──────────────────────┬──────────┬───────────────┬────────────┬────────┬───────────┐${C.reset}\n` +
349
+ ` ${C.dim}│ # │ ${C.reset}Timestamp ${C.dim}│ ${C.reset}Type ${C.dim}│ ${C.reset}Amount ${C.dim}│ ${C.reset}From ${C.dim}│ ${C.reset}To ${C.dim}│ ${C.reset}Status ${C.dim}│${C.reset}`
350
+ );
351
+
352
+ for (let i = 0; i < txs.length; i++) {
353
+ const tx = txs[i];
354
+ const num = String(i + 1).padStart(3);
355
+ const time = formatTimestamp(tx.blockTime).slice(0, 19).padEnd(19);
356
+ const typeInfo = formatTxType(tx.type);
357
+ const type = typeInfo.label.padEnd(10);
358
+ const amt = tx.amount > 0 ? formatAether(tx.amount).padEnd(13) : '—'.padEnd(13);
359
+ const from = shortAddress(tx.from || '').padEnd(11);
360
+ const to = shortAddress(tx.to || '').padEnd(11);
361
+ const statusInfo = formatStatus(tx.status);
362
+ const status = statusInfo.label;
363
+
364
+ const bgAlt = (i % 2 === 0) ? '' : C.dim;
365
+ const reset = C.reset + bgAlt;
366
+
367
+ console.log(
368
+ ` ${bgAlt}${C.dim}├────┼──────────────────────┼──────────┼───────────────┼────────────┼────────┼───────────┤${reset}`
369
+ );
370
+ console.log(
371
+ ` ${bgAlt}${C.dim}│${reset} ${num} ${bgAlt}${C.dim}│ ${reset}${time} ${bgAlt}${C.dim}│ ${reset}${typeInfo.color}${type}${reset} ${bgAlt}${C.dim}│ ${reset}${amt} ${bgAlt}${C.dim}│ ${reset}${from} ${bgAlt}${C.dim}│ ${reset}${to} ${bgAlt}${C.dim}│ ${reset}${statusInfo.color}${status}${reset}${bgAlt} │${reset}`
372
+ );
373
+ }
374
+
375
+ console.log(
376
+ ` ${C.dim}└────┴──────────────────────┴──────────┴───────────────┴────────────┴────────┴───────────┘${C.reset}`
377
+ );
378
+
379
+ // Summary
380
+ const totalVolume = txs.reduce((sum, tx) => sum + tx.amount, 0);
381
+ const successCount = txs.filter(tx => tx.status !== 'failed').length;
382
+ console.log(`\n ${C.dim} ${txs.length} transactions · Total volume: ${C.reset}${C.bright}${formatAether(totalVolume)}${C.reset} ${C.dim}· Success: ${C.reset}${C.green}${successCount}/${txs.length}${C.reset} ${C.dim}· Failed: ${C.reset}${C.red}${(txs.length - successCount)}${C.reset}\n`);
383
+ }
384
+
385
+ function displayJson(txs, meta) {
386
+ console.log(JSON.stringify({
387
+ address: meta.address,
388
+ rpc: meta.rpc,
389
+ limit: meta.limit,
390
+ transaction_count: txs.length,
391
+ total_volume_lamports: txs.reduce((sum, tx) => sum + tx.amount, 0),
392
+ transactions: txs.map(tx => ({
393
+ signature: tx.signature,
394
+ slot: tx.slot,
395
+ timestamp: tx.blockTime ? new Date(tx.blockTime * 1000).toISOString() : null,
396
+ type: tx.type,
397
+ amount_lamports: tx.amount,
398
+ amount_aeth: (tx.amount / 1e9).toFixed(9),
399
+ fee_lamports: tx.fee,
400
+ from: tx.from,
401
+ to: tx.to,
402
+ memo: tx.memo,
403
+ status: tx.status,
404
+ })),
405
+ }, null, 2));
406
+ }
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // Main
410
+ // ---------------------------------------------------------------------------
411
+
412
+ async function main() {
413
+ const opts = parseArgs();
414
+
415
+ if (opts.help) {
416
+ console.log(`
417
+ ${C.bright}${C.cyan}tx-history${C.reset} — Fetch and display transaction history for an Aether address
418
+
419
+ ${C.bright}USAGE${C.reset}
420
+ aether tx history --address <addr> [--limit <n>] [--json] [--rpc <url>]
421
+ aether history --address <addr> [--limit <n>] [--json]
422
+
423
+ ${C.bright}OPTIONS${C.reset}
424
+ --address <addr> Aether wallet address (ATH...)
425
+ --limit <n> Max transactions to fetch (default: 20, max: 100)
426
+ --json Output raw JSON for scripting
427
+ --rpc <url> RPC endpoint (default: AETHER_RPC or http://127.0.0.1:8899)
428
+ --help Show this help
429
+
430
+ ${C.bright}EXAMPLES${C.reset}
431
+ aether tx history --address ATH3abc... --limit 50
432
+ aether tx history --address ATH3abc... --json
433
+ aether history --address ATH3abc... --rpc https://mainnet.aether.io
434
+ `);
435
+ return;
436
+ }
437
+
438
+ if (!opts.address) {
439
+ console.log(` ${C.red}✗ Missing --address${C.reset}\n`);
440
+ console.log(` Usage: aether tx history --address <addr> [--limit <n>] [--json]\n`);
441
+ process.exit(1);
442
+ }
443
+
444
+ const rpcUrl = opts.rpc || getDefaultRpc();
445
+ const limit = Math.min(Math.max(1, opts.limit || 20), 100);
446
+
447
+ if (!opts.json) {
448
+ console.log(`\n${C.bright}${C.cyan} Tx History${C.reset} · ${C.dim}address:${C.reset} ${opts.address} ${C.dim}· ${C.dim}limit:${C.reset} ${limit} ${C.dim}· ${C.dim}rpc:${C.reset} ${rpcUrl}\n`);
449
+ }
450
+
451
+ try {
452
+ // Step 1: Get transaction signatures
453
+ const sigsResult = await fetchTxSignatures(rpcUrl, opts.address, limit);
454
+
455
+ if (sigsResult.error) {
456
+ throw new Error(sigsResult.error.message || JSON.stringify(sigsResult.error));
457
+ }
458
+
459
+ const signatures = Array.isArray(sigsResult.result) ? sigsResult.result : [];
460
+
461
+ if (signatures.length === 0) {
462
+ if (!opts.json) {
463
+ displayTxTable([]);
464
+ } else {
465
+ displayJson([], { address: opts.address, rpc: rpcUrl, limit });
466
+ }
467
+ return;
468
+ }
469
+
470
+ // Step 2: Fetch each transaction in parallel (up to 10 at a time)
471
+ const txResults = [];
472
+ const BATCH = 10;
473
+
474
+ for (let i = 0; i < signatures.length; i += BATCH) {
475
+ const batch = signatures.slice(i, i + BATCH);
476
+ const batchPromises = batch.map(sig => fetchTx(rpcUrl, sig.signature).catch(err => ({ error: err.message })));
477
+ const batchResults = await Promise.all(batchPromises);
478
+ txResults.push(...batchResults);
479
+ }
480
+
481
+ // Step 3: Parse and normalize
482
+ const txs = txResults
483
+ .map((res, idx) => {
484
+ if (res.error) return null;
485
+ try {
486
+ return parseTransaction(res.result || {}, signatures[idx] || {});
487
+ } catch {
488
+ return null;
489
+ }
490
+ })
491
+ .filter(Boolean);
492
+
493
+ if (opts.json) {
494
+ displayJson(txs, { address: opts.address, rpc: rpcUrl, limit });
495
+ } else {
496
+ displayTxTable(txs);
497
+ }
498
+
499
+ } catch (err) {
500
+ console.log(`\n ${C.red}✗ Failed to fetch transaction history:${C.reset} ${err.message}\n`);
501
+ if (err.stack && !opts.json) {
502
+ console.log(` ${C.dim}${err.stack.split('\n').slice(0, 3).join('\n ')}${C.reset}\n`);
503
+ }
504
+ process.exit(1);
505
+ }
506
+ }
507
+
508
+ main();
package/index.js CHANGED
@@ -28,6 +28,8 @@ const { supplyCommand } = require('./commands/supply');
28
28
  const { statusCommand } = require('./commands/status');
29
29
  const { broadcastCommand } = require('./commands/broadcast');
30
30
  const { apyCommand } = require('./commands/apy');
31
+ const { statsCommand } = require('./commands/stats');
32
+ const { txHistoryCommand } = require('./commands/tx-history');
31
33
  const readline = require('readline');
32
34
 
33
35
  // CLI version
@@ -235,11 +237,13 @@ const COMMANDS = {
235
237
  tx: {
236
238
  description: 'Transaction history — aether tx history --address <addr> [--limit 20] [--json]',
237
239
  handler: () => {
238
- const { walletCommand } = require('./commands/wallet');
239
- const originalArgv = process.argv;
240
- process.argv = [...originalArgv.slice(0, 2), 'wallet', 'history', ...originalArgv.slice(3)];
241
- walletCommand();
242
- process.argv = originalArgv;
240
+ txHistoryCommand();
241
+ },
242
+ },
243
+ 'tx-history': {
244
+ description: 'Transaction history for an address — aether tx-history --address <addr> [--limit 20] [--json]',
245
+ handler: () => {
246
+ txHistoryCommand();
243
247
  },
244
248
  },
245
249
  network: {
@@ -252,11 +256,7 @@ const COMMANDS = {
252
256
  history: {
253
257
  description: 'Transaction history for an address — alias for tx history',
254
258
  handler: () => {
255
- const { walletCommand } = require('./commands/wallet');
256
- const originalArgv = process.argv;
257
- process.argv = [...originalArgv.slice(0, 2), 'wallet', 'history', ...originalArgv.slice(3)];
258
- walletCommand();
259
- process.argv = originalArgv;
259
+ txHistoryCommand();
260
260
  },
261
261
  },
262
262
  validator: {
@@ -299,7 +299,7 @@ const COMMANDS = {
299
299
  },
300
300
  },
301
301
  rewards: {
302
- description: 'View staking rewards — aether rewards list | rewards summary | rewards pending | rewards claim',
302
+ description: 'View staking rewards — aether rewards list | summary | pending | claim | compound',
303
303
  handler: () => {
304
304
  rewardsCommand();
305
305
  },
@@ -353,6 +353,12 @@ const COMMANDS = {
353
353
  validatorsListCommand();
354
354
  },
355
355
  },
356
+ stats: {
357
+ description: 'Wallet stats dashboard — balance, stake positions, recent txs — aether stats --address <addr> [--compact] [--json]',
358
+ handler: () => {
359
+ statsCommand();
360
+ },
361
+ },
356
362
  price: {
357
363
  description: 'AETH/USD price — aether price [--pair AETH/USD] [--json] [--source coingecko]',
358
364
  handler: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-hub",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "AeTHer Validator CLI — tiered validators (Full/Lite/Observer), system checks, onboarding, and node management",
5
5
  "main": "index.js",
6
6
  "bin": {