aether-hub 1.2.6 → 1.2.8

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/stats.js CHANGED
@@ -1,419 +1,419 @@
1
- #!/usr/bin/env node
2
- /**
3
- * aether-cli stats
4
- *
5
- * Comprehensive wallet stats dashboard:
6
- * - Token balance (AETH + lamports)
7
- * - Active stake positions (validator, amount, status)
8
- * - Recent transactions (last 5)
9
- * - Estimated rewards accrued
10
- *
11
- * Usage:
12
- * aether stats --address <addr> Full stats dashboard
13
- * aether stats --address <addr> --json JSON output for scripting
14
- * aether stats --address <addr> --compact One-line summary
15
- *
16
- * Requires AETHER_RPC env var or local node (default: http://127.0.0.1:8899)
17
- */
18
-
19
- const http = require('http');
20
- const https = require('https');
21
- const os = require('os');
22
- const path = require('path');
23
- const fs = require('fs');
24
- const bs58 = require('bs58').default;
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
- bold: '\x1b[1m',
37
- };
38
-
39
- // ---------------------------------------------------------------------------
40
- // Paths & config
41
- // ---------------------------------------------------------------------------
42
-
43
- function getAetherDir() {
44
- return path.join(os.homedir(), '.aether');
45
- }
46
-
47
- function getConfigPath() {
48
- return path.join(getAetherDir(), 'config.json');
49
- }
50
-
51
- function getWalletsDir() {
52
- return path.join(getAetherDir(), 'wallets');
53
- }
54
-
55
- function loadConfig() {
56
- const p = getConfigPath();
57
- if (!fs.existsSync(p)) return { defaultWallet: null };
58
- try {
59
- return JSON.parse(fs.readFileSync(p, 'utf8'));
60
- } catch {
61
- return { defaultWallet: null };
62
- }
63
- }
64
-
65
- function loadWallet(address) {
66
- const fp = path.join(getWalletsDir(), `${address}.json`);
67
- if (!fs.existsSync(fp)) return null;
68
- return JSON.parse(fs.readFileSync(fp, 'utf8'));
69
- }
70
-
71
- // ---------------------------------------------------------------------------
72
- // RPC helpers
73
- // ---------------------------------------------------------------------------
74
-
75
- function getDefaultRpc() {
76
- return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
77
- }
78
-
79
- function httpRequest(rpcUrl, pathStr) {
80
- return new Promise((resolve, reject) => {
81
- const url = new URL(pathStr, rpcUrl);
82
- const lib = url.protocol === 'https:' ? https : http;
83
- const req = lib.request({
84
- hostname: url.hostname,
85
- port: url.port || (url.protocol === 'https:' ? 443 : 80),
86
- path: url.pathname + url.search,
87
- method: 'GET',
88
- timeout: 8000,
89
- headers: { 'Content-Type': 'application/json' },
90
- }, (res) => {
91
- let data = '';
92
- res.on('data', (chunk) => data += chunk);
93
- res.on('end', () => {
94
- try { resolve(JSON.parse(data)); }
95
- catch { resolve({ raw: data }); }
96
- });
97
- });
98
- req.on('error', reject);
99
- req.end();
100
- });
101
- }
102
-
103
- function httpPost(rpcUrl, pathStr, body) {
104
- return new Promise((resolve, reject) => {
105
- const url = new URL(pathStr, rpcUrl);
106
- const lib = url.protocol === 'https:' ? https : http;
107
- const bodyStr = JSON.stringify(body);
108
- const req = lib.request({
109
- hostname: url.hostname,
110
- port: url.port || (url.protocol === 'https:' ? 443 : 80),
111
- path: url.pathname + url.search,
112
- method: 'POST',
113
- timeout: 8000,
114
- headers: {
115
- 'Content-Type': 'application/json',
116
- 'Content-Length': Buffer.byteLength(bodyStr),
117
- },
118
- }, (res) => {
119
- let data = '';
120
- res.on('data', (chunk) => data += chunk);
121
- res.on('end', () => {
122
- try { resolve(JSON.parse(data)); }
123
- catch { resolve(data); }
124
- });
125
- });
126
- req.on('error', reject);
127
- req.write(bodyStr);
128
- req.end();
129
- });
130
- }
131
-
132
- // ---------------------------------------------------------------------------
133
- // Formatting helpers
134
- // ---------------------------------------------------------------------------
135
-
136
- /**
137
- * Format lamports as AETH string (1 AETH = 1e9 lamports)
138
- */
139
- function formatAether(lamports) {
140
- if (lamports === undefined || lamports === null) return '0 AETH';
141
- const aeth = lamports / 1e9;
142
- if (aeth === 0) return '0 AETH';
143
- return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
144
- }
145
-
146
- /**
147
- * Format a timestamp as relative time ("2h ago", "3d ago")
148
- */
149
- function relativeTime(timestamp) {
150
- if (!timestamp) return 'unknown';
151
- const now = Math.floor(Date.now() / 1000);
152
- const diff = now - timestamp;
153
- if (diff < 60) return `${diff}s ago`;
154
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
155
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
156
- return `${Math.floor(diff / 86400)}d ago`;
157
- }
158
-
159
- /**
160
- * Truncate a signature or hash for display
161
- */
162
- function truncate(str, chars = 8) {
163
- if (!str || typeof str !== 'string') return '—';
164
- if (str.length <= chars * 2 + 3) return str;
165
- return str.slice(0, chars) + '…' + str.slice(-chars);
166
- }
167
-
168
- // ---------------------------------------------------------------------------
169
- // Fetch wallet stats from RPC
170
- // ---------------------------------------------------------------------------
171
-
172
- async function fetchWalletStats(address, rpcUrl) {
173
- const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
174
-
175
- // Fetch account, transactions, and stake positions in parallel
176
- const [account, txs, stakeAccounts] = await Promise.all([
177
- httpRequest(rpcUrl, `/v1/account/${rawAddr}`).catch(() => null),
178
- httpRequest(rpcUrl, `/v1/tx?address=${encodeURIComponent(rawAddr)}&limit=5`).catch(() => null),
179
- httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`).catch(() => null),
180
- ]);
181
-
182
- return { account, txs, stakeAccounts };
183
- }
184
-
185
- // ---------------------------------------------------------------------------
186
- // Render dashboard
187
- // ---------------------------------------------------------------------------
188
-
189
- function renderDashboard(address, stats, opts) {
190
- const { account, txs, stakeAccounts } = stats;
191
- const { compact, asJson } = opts;
192
- const rpcUrl = opts.rpcUrl;
193
-
194
- if (asJson) {
195
- const stakeList = stakeAccounts && !stakeAccounts.error
196
- ? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
197
- : [];
198
- const txList = txs && !txs.error
199
- ? (Array.isArray(txs) ? txs : txs.transactions || [])
200
- : [];
201
-
202
- const out = {
203
- address,
204
- rpc: rpcUrl,
205
- balance: account && !account.error ? {
206
- lamports: account.lamports || 0,
207
- aeth: formatAether(account.lamports || 0),
208
- } : null,
209
- stake_positions: stakeList.map((sa) => ({
210
- stake_account: sa.stake_account || sa.address || 'unknown',
211
- validator: sa.validator || 'unknown',
212
- amount: sa.amount || 0,
213
- aeth: formatAether(sa.amount || 0),
214
- status: sa.status || 'active',
215
- created_epoch: sa.created_epoch || null,
216
- })),
217
- recent_txs: txList.map((tx) => ({
218
- type: tx.tx_type || tx.type || 'Unknown',
219
- signature: tx.signature || tx.id || tx.tx_signature || null,
220
- timestamp: tx.timestamp || null,
221
- relative_time: relativeTime(tx.timestamp),
222
- payload: tx.payload?.data || {},
223
- fee: tx.fee || 0,
224
- })),
225
- fetched_at: new Date().toISOString(),
226
- };
227
- console.log(JSON.stringify(out, null, 2));
228
- return;
229
- }
230
-
231
- const lamports = (account && !account.error) ? (account.lamports || 0) : null;
232
- const stakeList = stakeAccounts && !stakeAccounts.error
233
- ? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
234
- : [];
235
- const txList = txs && !txs.error
236
- ? (Array.isArray(txs) ? txs : txs.transactions || [])
237
- : [];
238
-
239
- if (compact) {
240
- // One-line summary
241
- const bal = lamports !== null ? formatAether(lamports) : 'unknown';
242
- const stakes = stakeList.length;
243
- const recent = txList.length > 0 ? (txList[0].tx_type || txList[0].type || '?') : 'none';
244
- console.log(`${C.bright}${address}${C.reset} bal:${C.green}${bal}${C.reset} stakes:${stakes} last:${recent} txs:${txList.length}`);
245
- return;
246
- }
247
-
248
- // Full dashboard
249
- console.log(`\n${C.bright}${C.cyan}── Wallet Stats ─────────────────────────────────────────${C.reset}`);
250
- console.log(` ${C.green}★${C.reset} ${C.bright}${address}${C.reset}`);
251
- console.log(` ${C.dim}RPC: ${rpcUrl}${C.reset}`);
252
- console.log();
253
-
254
- // Balance section
255
- console.log(` ${C.bright}Balance${C.reset}`);
256
- if (lamports !== null) {
257
- console.log(` ${C.green}${formatAether(lamports)}${C.reset} ${C.dim}(${lamports} lamports)${C.reset}`);
258
- if (account.owner) {
259
- const ownerStr = Array.isArray(account.owner)
260
- ? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
261
- : account.owner;
262
- console.log(` ${C.dim}Owner: ${ownerStr}${C.reset}`);
263
- }
264
- if (account.rent_epoch !== undefined) {
265
- console.log(` ${C.dim}Rent epoch: ${account.rent_epoch}${C.reset}`);
266
- }
267
- } else {
268
- console.log(` ${C.yellow}⚠ Could not fetch balance (account may not exist)${C.reset}`);
269
- }
270
- console.log();
271
-
272
- // Stake positions section
273
- console.log(` ${C.bright}Stake Positions (${stakeList.length})${C.reset}`);
274
- if (stakeList.length === 0) {
275
- console.log(` ${C.dim}No active stake positions.${C.reset}`);
276
- } else {
277
- const statusColors = {
278
- active: C.green,
279
- inactive: C.yellow,
280
- activating: C.yellow,
281
- deactivating: C.red,
282
- unknown: C.dim,
283
- };
284
- for (const sa of stakeList) {
285
- const status = sa.status || 'unknown';
286
- const color = statusColors[status] || C.dim;
287
- const amount = sa.amount ? formatAether(sa.amount) : '0 AETH';
288
- const validator = sa.validator || 'unknown';
289
- console.log(` ${C.dim}┌─${C.reset}`);
290
- console.log(` │ ${C.bright}Validator:${C.reset} ${validator}`);
291
- console.log(` │ ${C.bright}Amount:${C.reset} ${color}${amount}${C.reset}`);
292
- console.log(` │ ${C.bright}Status:${C.reset} ${color}${status}${C.reset}`);
293
- if (sa.stake_account) {
294
- console.log(` │ ${C.bright}Stake acct:${C.reset} ${truncate(sa.stake_account)}`);
295
- }
296
- console.log(` ${C.dim}└${C.reset}`);
297
- }
298
- }
299
- console.log();
300
-
301
- // Recent transactions section
302
- console.log(` ${C.bright}Recent Transactions (${txList.length})${C.reset}`);
303
- if (txList.length === 0) {
304
- console.log(` ${C.dim}No transactions yet.${C.reset}`);
305
- } else {
306
- const typeColors = {
307
- Transfer: C.cyan,
308
- Stake: C.green,
309
- Unstake: C.yellow,
310
- ClaimRewards: C.magenta,
311
- CreateNFT: C.red,
312
- MintNFT: C.red,
313
- TransferNFT: C.cyan,
314
- UpdateMetadata: C.yellow,
315
- Unknown: C.dim,
316
- };
317
- for (const tx of txList) {
318
- const txType = tx.tx_type || tx.type || 'Unknown';
319
- const color = typeColors[txType] || C.dim;
320
- const sig = tx.signature || tx.id || tx.tx_signature || '—';
321
- const ts = tx.timestamp ? relativeTime(tx.timestamp) : 'unknown';
322
-
323
- console.log(` ${C.dim}┌─ ${ts}${C.reset} ${C.bright}${color}${txType}${C.reset} sig:${truncate(sig)}`);
324
- if (tx.payload && tx.payload.data) {
325
- const d = tx.payload.data;
326
- if (d.recipient) console.log(` │ ${C.dim}→ to: ${d.recipient}${C.reset}`);
327
- if (d.amount) console.log(` │ ${C.dim}amount: ${formatAether(d.amount)}${C.reset}`);
328
- if (d.validator) console.log(` │ ${C.dim}validator: ${d.validator}${C.reset}`);
329
- }
330
- if (tx.fee !== undefined && tx.fee > 0) {
331
- console.log(` │ ${C.dim}fee: ${tx.fee} lamports${C.reset}`);
332
- }
333
- console.log(` ${C.dim}└${C.reset}`);
334
- }
335
- }
336
- console.log();
337
- }
338
-
339
- // ---------------------------------------------------------------------------
340
- // Main command
341
- // ---------------------------------------------------------------------------
342
-
343
- function statsCommand() {
344
- const args = process.argv.slice(3);
345
-
346
- // Parse flags
347
- let address = null;
348
- let compact = false;
349
- let asJson = false;
350
-
351
- for (let i = 0; i < args.length; i++) {
352
- if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
353
- address = args[i + 1];
354
- i++;
355
- } else if (args[i] === '--compact' || args[i] === '-c') {
356
- compact = true;
357
- } else if (args[i] === '--json' || args[i] === '-j') {
358
- asJson = true;
359
- } else if (args[i] === '--help' || args[i] === '-h') {
360
- console.log(`
361
- ${C.bright}Aether Wallet Stats${C.reset}
362
- ${C.dim}Comprehensive wallet overview: balance, stakes, recent transactions.${C.reset}
363
-
364
- ${C.bright}Usage:${C.reset}
365
- aether stats --address <addr> Full dashboard
366
- aether stats --address <addr> --json JSON output
367
- aether stats --address <addr> --compact One-line summary
368
-
369
- ${C.bright}Options:${C.reset}
370
- -a, --address <addr> Wallet address (or set default)
371
- -j, --json JSON output
372
- -c, --compact One-line summary
373
- -h, --help Show this help
374
- `);
375
- return;
376
- }
377
- }
378
-
379
- // Resolve address
380
- if (!address) {
381
- const cfg = loadConfig();
382
- address = cfg.defaultWallet;
383
- }
384
-
385
- if (!address) {
386
- console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
387
- console.log(` ${C.dim}Usage: aether stats --address <address> [--compact] [--json]${C.reset}\n`);
388
- process.exit(1);
389
- }
390
-
391
- // Verify wallet file exists
392
- const wallet = loadWallet(address);
393
- if (!wallet) {
394
- console.log(` ${C.red}✗ Wallet not found locally:${C.reset} ${address}`);
395
- console.log(` ${C.dim}Check your wallets: ${C.cyan}aether wallet list${C.reset}\n`);
396
- process.exit(1);
397
- }
398
-
399
- const rpcUrl = getDefaultRpc();
400
-
401
- if (!asJson) {
402
- console.log(` ${C.dim}Fetching stats from ${rpcUrl}...${C.reset}`);
403
- }
404
-
405
- // Fetch and render
406
- (async () => {
407
- try {
408
- const stats = await fetchWalletStats(address, rpcUrl);
409
- renderDashboard(address, stats, { compact, asJson, rpcUrl });
410
- } catch (err) {
411
- console.log(` ${C.red}✗ Failed to fetch wallet stats:${C.reset} ${err.message}`);
412
- console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
413
- console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
414
- process.exit(1);
415
- }
416
- })();
417
- }
418
-
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli stats
4
+ *
5
+ * Comprehensive wallet stats dashboard:
6
+ * - Token balance (AETH + lamports)
7
+ * - Active stake positions (validator, amount, status)
8
+ * - Recent transactions (last 5)
9
+ * - Estimated rewards accrued
10
+ *
11
+ * Usage:
12
+ * aether stats --address <addr> Full stats dashboard
13
+ * aether stats --address <addr> --json JSON output for scripting
14
+ * aether stats --address <addr> --compact One-line summary
15
+ *
16
+ * Requires AETHER_RPC env var or local node (default: http://127.0.0.1:8899)
17
+ */
18
+
19
+ const http = require('http');
20
+ const https = require('https');
21
+ const os = require('os');
22
+ const path = require('path');
23
+ const fs = require('fs');
24
+ const bs58 = require('bs58').default;
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
+ bold: '\x1b[1m',
37
+ };
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Paths & config
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function getAetherDir() {
44
+ return path.join(os.homedir(), '.aether');
45
+ }
46
+
47
+ function getConfigPath() {
48
+ return path.join(getAetherDir(), 'config.json');
49
+ }
50
+
51
+ function getWalletsDir() {
52
+ return path.join(getAetherDir(), 'wallets');
53
+ }
54
+
55
+ function loadConfig() {
56
+ const p = getConfigPath();
57
+ if (!fs.existsSync(p)) return { defaultWallet: null };
58
+ try {
59
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
60
+ } catch {
61
+ return { defaultWallet: null };
62
+ }
63
+ }
64
+
65
+ function loadWallet(address) {
66
+ const fp = path.join(getWalletsDir(), `${address}.json`);
67
+ if (!fs.existsSync(fp)) return null;
68
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // RPC helpers
73
+ // ---------------------------------------------------------------------------
74
+
75
+ function getDefaultRpc() {
76
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
77
+ }
78
+
79
+ function httpRequest(rpcUrl, pathStr) {
80
+ return new Promise((resolve, reject) => {
81
+ const url = new URL(pathStr, rpcUrl);
82
+ const lib = url.protocol === 'https:' ? https : http;
83
+ const req = lib.request({
84
+ hostname: url.hostname,
85
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
86
+ path: url.pathname + url.search,
87
+ method: 'GET',
88
+ timeout: 8000,
89
+ headers: { 'Content-Type': 'application/json' },
90
+ }, (res) => {
91
+ let data = '';
92
+ res.on('data', (chunk) => data += chunk);
93
+ res.on('end', () => {
94
+ try { resolve(JSON.parse(data)); }
95
+ catch { resolve({ raw: data }); }
96
+ });
97
+ });
98
+ req.on('error', reject);
99
+ req.end();
100
+ });
101
+ }
102
+
103
+ function httpPost(rpcUrl, pathStr, body) {
104
+ return new Promise((resolve, reject) => {
105
+ const url = new URL(pathStr, rpcUrl);
106
+ const lib = url.protocol === 'https:' ? https : http;
107
+ const bodyStr = JSON.stringify(body);
108
+ const req = lib.request({
109
+ hostname: url.hostname,
110
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
111
+ path: url.pathname + url.search,
112
+ method: 'POST',
113
+ timeout: 8000,
114
+ headers: {
115
+ 'Content-Type': 'application/json',
116
+ 'Content-Length': Buffer.byteLength(bodyStr),
117
+ },
118
+ }, (res) => {
119
+ let data = '';
120
+ res.on('data', (chunk) => data += chunk);
121
+ res.on('end', () => {
122
+ try { resolve(JSON.parse(data)); }
123
+ catch { resolve(data); }
124
+ });
125
+ });
126
+ req.on('error', reject);
127
+ req.write(bodyStr);
128
+ req.end();
129
+ });
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Formatting helpers
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * Format lamports as AETH string (1 AETH = 1e9 lamports)
138
+ */
139
+ function formatAether(lamports) {
140
+ if (lamports === undefined || lamports === null) return '0 AETH';
141
+ const aeth = lamports / 1e9;
142
+ if (aeth === 0) return '0 AETH';
143
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
144
+ }
145
+
146
+ /**
147
+ * Format a timestamp as relative time ("2h ago", "3d ago")
148
+ */
149
+ function relativeTime(timestamp) {
150
+ if (!timestamp) return 'unknown';
151
+ const now = Math.floor(Date.now() / 1000);
152
+ const diff = now - timestamp;
153
+ if (diff < 60) return `${diff}s ago`;
154
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
155
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
156
+ return `${Math.floor(diff / 86400)}d ago`;
157
+ }
158
+
159
+ /**
160
+ * Truncate a signature or hash for display
161
+ */
162
+ function truncate(str, chars = 8) {
163
+ if (!str || typeof str !== 'string') return '—';
164
+ if (str.length <= chars * 2 + 3) return str;
165
+ return str.slice(0, chars) + '…' + str.slice(-chars);
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Fetch wallet stats from RPC
170
+ // ---------------------------------------------------------------------------
171
+
172
+ async function fetchWalletStats(address, rpcUrl) {
173
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
174
+
175
+ // Fetch account, transactions, and stake positions in parallel
176
+ const [account, txs, stakeAccounts] = await Promise.all([
177
+ httpRequest(rpcUrl, `/v1/account/${rawAddr}`).catch(() => null),
178
+ httpRequest(rpcUrl, `/v1/tx?address=${encodeURIComponent(rawAddr)}&limit=5`).catch(() => null),
179
+ httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`).catch(() => null),
180
+ ]);
181
+
182
+ return { account, txs, stakeAccounts };
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Render dashboard
187
+ // ---------------------------------------------------------------------------
188
+
189
+ function renderDashboard(address, stats, opts) {
190
+ const { account, txs, stakeAccounts } = stats;
191
+ const { compact, asJson } = opts;
192
+ const rpcUrl = opts.rpcUrl;
193
+
194
+ if (asJson) {
195
+ const stakeList = stakeAccounts && !stakeAccounts.error
196
+ ? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
197
+ : [];
198
+ const txList = txs && !txs.error
199
+ ? (Array.isArray(txs) ? txs : txs.transactions || [])
200
+ : [];
201
+
202
+ const out = {
203
+ address,
204
+ rpc: rpcUrl,
205
+ balance: account && !account.error ? {
206
+ lamports: account.lamports || 0,
207
+ aeth: formatAether(account.lamports || 0),
208
+ } : null,
209
+ stake_positions: stakeList.map((sa) => ({
210
+ stake_account: sa.stake_account || sa.address || 'unknown',
211
+ validator: sa.validator || 'unknown',
212
+ amount: sa.amount || 0,
213
+ aeth: formatAether(sa.amount || 0),
214
+ status: sa.status || 'active',
215
+ created_epoch: sa.created_epoch || null,
216
+ })),
217
+ recent_txs: txList.map((tx) => ({
218
+ type: tx.tx_type || tx.type || 'Unknown',
219
+ signature: tx.signature || tx.id || tx.tx_signature || null,
220
+ timestamp: tx.timestamp || null,
221
+ relative_time: relativeTime(tx.timestamp),
222
+ payload: tx.payload?.data || {},
223
+ fee: tx.fee || 0,
224
+ })),
225
+ fetched_at: new Date().toISOString(),
226
+ };
227
+ console.log(JSON.stringify(out, null, 2));
228
+ return;
229
+ }
230
+
231
+ const lamports = (account && !account.error) ? (account.lamports || 0) : null;
232
+ const stakeList = stakeAccounts && !stakeAccounts.error
233
+ ? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
234
+ : [];
235
+ const txList = txs && !txs.error
236
+ ? (Array.isArray(txs) ? txs : txs.transactions || [])
237
+ : [];
238
+
239
+ if (compact) {
240
+ // One-line summary
241
+ const bal = lamports !== null ? formatAether(lamports) : 'unknown';
242
+ const stakes = stakeList.length;
243
+ const recent = txList.length > 0 ? (txList[0].tx_type || txList[0].type || '?') : 'none';
244
+ console.log(`${C.bright}${address}${C.reset} bal:${C.green}${bal}${C.reset} stakes:${stakes} last:${recent} txs:${txList.length}`);
245
+ return;
246
+ }
247
+
248
+ // Full dashboard
249
+ console.log(`\n${C.bright}${C.cyan}── Wallet Stats ─────────────────────────────────────────${C.reset}`);
250
+ console.log(` ${C.green}★${C.reset} ${C.bright}${address}${C.reset}`);
251
+ console.log(` ${C.dim}RPC: ${rpcUrl}${C.reset}`);
252
+ console.log();
253
+
254
+ // Balance section
255
+ console.log(` ${C.bright}Balance${C.reset}`);
256
+ if (lamports !== null) {
257
+ console.log(` ${C.green}${formatAether(lamports)}${C.reset} ${C.dim}(${lamports} lamports)${C.reset}`);
258
+ if (account.owner) {
259
+ const ownerStr = Array.isArray(account.owner)
260
+ ? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
261
+ : account.owner;
262
+ console.log(` ${C.dim}Owner: ${ownerStr}${C.reset}`);
263
+ }
264
+ if (account.rent_epoch !== undefined) {
265
+ console.log(` ${C.dim}Rent epoch: ${account.rent_epoch}${C.reset}`);
266
+ }
267
+ } else {
268
+ console.log(` ${C.yellow}⚠ Could not fetch balance (account may not exist)${C.reset}`);
269
+ }
270
+ console.log();
271
+
272
+ // Stake positions section
273
+ console.log(` ${C.bright}Stake Positions (${stakeList.length})${C.reset}`);
274
+ if (stakeList.length === 0) {
275
+ console.log(` ${C.dim}No active stake positions.${C.reset}`);
276
+ } else {
277
+ const statusColors = {
278
+ active: C.green,
279
+ inactive: C.yellow,
280
+ activating: C.yellow,
281
+ deactivating: C.red,
282
+ unknown: C.dim,
283
+ };
284
+ for (const sa of stakeList) {
285
+ const status = sa.status || 'unknown';
286
+ const color = statusColors[status] || C.dim;
287
+ const amount = sa.amount ? formatAether(sa.amount) : '0 AETH';
288
+ const validator = sa.validator || 'unknown';
289
+ console.log(` ${C.dim}┌─${C.reset}`);
290
+ console.log(` │ ${C.bright}Validator:${C.reset} ${validator}`);
291
+ console.log(` │ ${C.bright}Amount:${C.reset} ${color}${amount}${C.reset}`);
292
+ console.log(` │ ${C.bright}Status:${C.reset} ${color}${status}${C.reset}`);
293
+ if (sa.stake_account) {
294
+ console.log(` │ ${C.bright}Stake acct:${C.reset} ${truncate(sa.stake_account)}`);
295
+ }
296
+ console.log(` ${C.dim}└${C.reset}`);
297
+ }
298
+ }
299
+ console.log();
300
+
301
+ // Recent transactions section
302
+ console.log(` ${C.bright}Recent Transactions (${txList.length})${C.reset}`);
303
+ if (txList.length === 0) {
304
+ console.log(` ${C.dim}No transactions yet.${C.reset}`);
305
+ } else {
306
+ const typeColors = {
307
+ Transfer: C.cyan,
308
+ Stake: C.green,
309
+ Unstake: C.yellow,
310
+ ClaimRewards: C.magenta,
311
+ CreateNFT: C.red,
312
+ MintNFT: C.red,
313
+ TransferNFT: C.cyan,
314
+ UpdateMetadata: C.yellow,
315
+ Unknown: C.dim,
316
+ };
317
+ for (const tx of txList) {
318
+ const txType = tx.tx_type || tx.type || 'Unknown';
319
+ const color = typeColors[txType] || C.dim;
320
+ const sig = tx.signature || tx.id || tx.tx_signature || '—';
321
+ const ts = tx.timestamp ? relativeTime(tx.timestamp) : 'unknown';
322
+
323
+ console.log(` ${C.dim}┌─ ${ts}${C.reset} ${C.bright}${color}${txType}${C.reset} sig:${truncate(sig)}`);
324
+ if (tx.payload && tx.payload.data) {
325
+ const d = tx.payload.data;
326
+ if (d.recipient) console.log(` │ ${C.dim}→ to: ${d.recipient}${C.reset}`);
327
+ if (d.amount) console.log(` │ ${C.dim}amount: ${formatAether(d.amount)}${C.reset}`);
328
+ if (d.validator) console.log(` │ ${C.dim}validator: ${d.validator}${C.reset}`);
329
+ }
330
+ if (tx.fee !== undefined && tx.fee > 0) {
331
+ console.log(` │ ${C.dim}fee: ${tx.fee} lamports${C.reset}`);
332
+ }
333
+ console.log(` ${C.dim}└${C.reset}`);
334
+ }
335
+ }
336
+ console.log();
337
+ }
338
+
339
+ // ---------------------------------------------------------------------------
340
+ // Main command
341
+ // ---------------------------------------------------------------------------
342
+
343
+ function statsCommand() {
344
+ const args = process.argv.slice(3);
345
+
346
+ // Parse flags
347
+ let address = null;
348
+ let compact = false;
349
+ let asJson = false;
350
+
351
+ for (let i = 0; i < args.length; i++) {
352
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
353
+ address = args[i + 1];
354
+ i++;
355
+ } else if (args[i] === '--compact' || args[i] === '-c') {
356
+ compact = true;
357
+ } else if (args[i] === '--json' || args[i] === '-j') {
358
+ asJson = true;
359
+ } else if (args[i] === '--help' || args[i] === '-h') {
360
+ console.log(`
361
+ ${C.bright}Aether Wallet Stats${C.reset}
362
+ ${C.dim}Comprehensive wallet overview: balance, stakes, recent transactions.${C.reset}
363
+
364
+ ${C.bright}Usage:${C.reset}
365
+ aether stats --address <addr> Full dashboard
366
+ aether stats --address <addr> --json JSON output
367
+ aether stats --address <addr> --compact One-line summary
368
+
369
+ ${C.bright}Options:${C.reset}
370
+ -a, --address <addr> Wallet address (or set default)
371
+ -j, --json JSON output
372
+ -c, --compact One-line summary
373
+ -h, --help Show this help
374
+ `);
375
+ return;
376
+ }
377
+ }
378
+
379
+ // Resolve address
380
+ if (!address) {
381
+ const cfg = loadConfig();
382
+ address = cfg.defaultWallet;
383
+ }
384
+
385
+ if (!address) {
386
+ console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
387
+ console.log(` ${C.dim}Usage: aether stats --address <address> [--compact] [--json]${C.reset}\n`);
388
+ process.exit(1);
389
+ }
390
+
391
+ // Verify wallet file exists
392
+ const wallet = loadWallet(address);
393
+ if (!wallet) {
394
+ console.log(` ${C.red}✗ Wallet not found locally:${C.reset} ${address}`);
395
+ console.log(` ${C.dim}Check your wallets: ${C.cyan}aether wallet list${C.reset}\n`);
396
+ process.exit(1);
397
+ }
398
+
399
+ const rpcUrl = getDefaultRpc();
400
+
401
+ if (!asJson) {
402
+ console.log(` ${C.dim}Fetching stats from ${rpcUrl}...${C.reset}`);
403
+ }
404
+
405
+ // Fetch and render
406
+ (async () => {
407
+ try {
408
+ const stats = await fetchWalletStats(address, rpcUrl);
409
+ renderDashboard(address, stats, { compact, asJson, rpcUrl });
410
+ } catch (err) {
411
+ console.log(` ${C.red}✗ Failed to fetch wallet stats:${C.reset} ${err.message}`);
412
+ console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
413
+ console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
414
+ process.exit(1);
415
+ }
416
+ })();
417
+ }
418
+
419
419
  module.exports = { statsCommand };