@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,1570 @@
1
+ /**
2
+ * aether-cli wallet
3
+ *
4
+ * Aether wallet management:
5
+ * aether wallet create — Create new BIP39 wallet or import existing
6
+ * aether wallet list — List all wallets
7
+ * aether wallet import — Import wallet from mnemonic
8
+ * aether wallet default — Show/set default wallet
9
+ * aether wallet connect — Connect wallet via browser verification
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const readline = require('readline');
16
+ const crypto = require('crypto');
17
+ const { execSync } = require('child_process');
18
+ const bip39 = require('bip39');
19
+ const nacl = require('tweetnacl');
20
+ const bs58 = require('bs58').default;
21
+
22
+ // Import SDK for blockchain RPC calls
23
+ const aether = require('../sdk');
24
+
25
+ // Destructure SDK functions for convenience
26
+ const { createClient } = aether;
27
+
28
+ // ANSI colours
29
+ const C = {
30
+ reset: '\x1b[0m',
31
+ bright: '\x1b[1m',
32
+ green: '\x1b[32m',
33
+ yellow: '\x1b[33m',
34
+ cyan: '\x1b[36m',
35
+ red: '\x1b[31m',
36
+ dim: '\x1b[2m',
37
+ magenta: '\x1b[35m',
38
+ };
39
+
40
+ // CLI version for session files
41
+ const CLI_VERSION = '1.0.3';
42
+
43
+ // Derivation path for Aether wallets
44
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Paths
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function getAetherDir() {
51
+ return path.join(os.homedir(), '.aether');
52
+ }
53
+
54
+ function getWalletsDir() {
55
+ return path.join(getAetherDir(), 'wallets');
56
+ }
57
+
58
+ function getSessionsDir() {
59
+ return path.join(getAetherDir(), 'sessions');
60
+ }
61
+
62
+ function getConfigPath() {
63
+ return path.join(getAetherDir(), 'config.json');
64
+ }
65
+
66
+ function ensureDirs() {
67
+ const wd = getWalletsDir();
68
+ if (!fs.existsSync(wd)) fs.mkdirSync(wd, { recursive: true });
69
+ const sd = getSessionsDir();
70
+ if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
71
+ }
72
+
73
+ function loadConfig() {
74
+ const p = getConfigPath();
75
+ if (!fs.existsSync(p)) return { defaultWallet: null, version: 1 };
76
+ try {
77
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
78
+ } catch {
79
+ return { defaultWallet: null, version: 1 };
80
+ }
81
+ }
82
+
83
+ function saveConfig(cfg) {
84
+ ensureDirs();
85
+ fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Crypto helpers
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Derive an Ed25519 keypair from a BIP39 seed.
94
+ * BIP39 seed → 64-byte seed → TweetNaCl keypair
95
+ */
96
+ function deriveKeypair(mnemonic, derivationPath) {
97
+ if (!bip39.validateMnemonic(mnemonic)) {
98
+ throw new Error('Invalid mnemonic phrase.');
99
+ }
100
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
101
+ const seed32 = seedBuffer.slice(0, 32);
102
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
103
+ return {
104
+ publicKey: Buffer.from(keyPair.publicKey),
105
+ secretKey: Buffer.from(keyPair.secretKey),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Format Aether address: ATH + base58check of public key.
111
+ */
112
+ function formatAddress(publicKey) {
113
+ return 'ATH' + bs58.encode(publicKey);
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Session management helpers
118
+ // ---------------------------------------------------------------------------
119
+
120
+ function sessionFilePath(token) {
121
+ return path.join(getSessionsDir(), `${token}.json`);
122
+ }
123
+
124
+ /** Generate a UUID v4 session token */
125
+ function generateSessionToken() {
126
+ return crypto.randomUUID();
127
+ }
128
+
129
+ /**
130
+ * Save session to ~/.aether/sessions/<uuid>.json
131
+ * Fields: wallet_address, created_at, expires_at, verified, cli_version
132
+ */
133
+ function saveSession(token, wallet_address, expires_in_minutes = 10) {
134
+ ensureDirs();
135
+ const now = new Date();
136
+ const expires_at = new Date(now.getTime() + expires_in_minutes * 60 * 1000);
137
+ const session = {
138
+ wallet_address,
139
+ created_at: now.toISOString(),
140
+ expires_at: expires_at.toISOString(),
141
+ verified: false,
142
+ cli_version: CLI_VERSION,
143
+ };
144
+ fs.writeFileSync(sessionFilePath(token), JSON.stringify(session, null, 2));
145
+ return session;
146
+ }
147
+
148
+ /** Load a session, or return null if missing or expired */
149
+ function getSession(token) {
150
+ const fp = sessionFilePath(token);
151
+ if (!fs.existsSync(fp)) return null;
152
+ try {
153
+ const session = JSON.parse(fs.readFileSync(fp, 'utf8'));
154
+ if (new Date(session.expires_at) < new Date()) return null;
155
+ return session;
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /** Mark a session as verified */
162
+ function markSessionVerified(token) {
163
+ const session = getSession(token);
164
+ if (!session) return false;
165
+ session.verified = true;
166
+ fs.writeFileSync(sessionFilePath(token), JSON.stringify(session, null, 2));
167
+ return true;
168
+ }
169
+
170
+ /** Delete a session file */
171
+ function deleteSession(token) {
172
+ const fp = sessionFilePath(token);
173
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
174
+ }
175
+
176
+ /**
177
+ * Poll ~/.aether/sessions/<token>.json every 2 seconds.
178
+ * Resolves when verified=true OR session expired/timeout.
179
+ * Returns { verified: boolean, reason?: 'expired' | 'timeout' }
180
+ */
181
+ async function pollForVerification(token, timeout_ms = 600000) {
182
+ const interval_ms = 2000;
183
+ const max_retries = Math.floor(timeout_ms / interval_ms);
184
+
185
+ for (let i = 0; i < max_retries; i++) {
186
+ const session = getSession(token);
187
+ if (session && session.verified) {
188
+ return { verified: true };
189
+ }
190
+ if (!session) {
191
+ return { verified: false, reason: 'expired' };
192
+ }
193
+ await new Promise((res) => setTimeout(res, interval_ms));
194
+ }
195
+ return { verified: false, reason: 'timeout' };
196
+ }
197
+
198
+ /** Get the site URL from env var or default */
199
+ function getSiteUrl() {
200
+ return process.env.AETHER_SITE_URL || 'https://jelly-legs-ai.github.io';
201
+ }
202
+
203
+ /** Open URL in the default browser (cross-platform) */
204
+ function openBrowser(url) {
205
+ const platform = os.platform();
206
+ try {
207
+ if (platform === 'win32') {
208
+ execSync(`start "" "${url}"`, { shell: 'cmd' });
209
+ } else if (platform === 'darwin') {
210
+ execSync(`open "${url}"`);
211
+ } else {
212
+ execSync(`xdg-open "${url}"`);
213
+ }
214
+ return true;
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Wallet file helpers
222
+ // ---------------------------------------------------------------------------
223
+
224
+ function walletFilePath(address) {
225
+ return path.join(getWalletsDir(), `${address}.json`);
226
+ }
227
+
228
+ function loadWallet(address) {
229
+ const fp = walletFilePath(address);
230
+ if (!fs.existsSync(fp)) return null;
231
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
232
+ }
233
+
234
+ function saveWalletFile(address, publicKey) {
235
+ ensureDirs();
236
+ const data = {
237
+ version: 1,
238
+ address,
239
+ public_key: bs58.encode(publicKey),
240
+ created_at: new Date().toISOString(),
241
+ derivation_path: DERIVATION_PATH,
242
+ };
243
+ fs.writeFileSync(walletFilePath(address), JSON.stringify(data, null, 2));
244
+ return data;
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Readline helpers
249
+ // ---------------------------------------------------------------------------
250
+
251
+ function createRl() {
252
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
253
+ }
254
+
255
+ function question(rl, q) {
256
+ return new Promise((res) => rl.question(q, res));
257
+ }
258
+
259
+ async function askMnemonic(rl, questionText) {
260
+ console.log(`\n${C.cyan}${questionText}${C.reset}`);
261
+ console.log(`${C.dim}Enter your ${C.bright}12 or 24${C.reset}${C.dim}-word mnemonic phrase, one space-separated line:${C.reset}`);
262
+ const raw = await question(rl, ` > ${C.reset}`);
263
+ return raw.trim().toLowerCase();
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // CREATE WALLET
268
+ // ---------------------------------------------------------------------------
269
+
270
+ async function createWallet(rl) {
271
+ console.log(`\n${C.bright}${C.cyan}── Wallet Creation ─────────────────────────────────────${C.reset}`);
272
+ console.log(` ${C.green}1)${C.reset} Create new wallet — generates a fresh 12-word mnemonic`);
273
+ console.log(` ${C.green}2)${C.reset} Import existing — enter your own mnemonic to restore\n`);
274
+
275
+ const choice = await question(rl, ` Choose [1/2]: ${C.reset}`);
276
+
277
+ let mnemonic;
278
+ if (choice.trim() === '1') {
279
+ mnemonic = bip39.generateMnemonic(128);
280
+ } else if (choice.trim() === '2') {
281
+ mnemonic = await askMnemonic(rl, 'Importing existing wallet');
282
+ if (!bip39.validateMnemonic(mnemonic)) {
283
+ console.log(`\n ${C.red}✗ Invalid BIP39 mnemonic.${C.reset} Please check your word list and try again.`);
284
+ return;
285
+ }
286
+ } else {
287
+ console.log(`\n ${C.red}✗ Invalid choice.${C.reset} Run \`aether wallet create\` again.`);
288
+ return;
289
+ }
290
+
291
+ let keyPair;
292
+ try {
293
+ keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
294
+ } catch (e) {
295
+ console.log(`\n ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
296
+ return;
297
+ }
298
+
299
+ const address = formatAddress(keyPair.publicKey);
300
+
301
+ if (choice.trim() === '1') {
302
+ const words = mnemonic.split(' ');
303
+ console.log(`\n`);
304
+ console.log(`${C.red}${C.bright}╔═══════════════════════════════════════════════════════════════╗${C.reset}`);
305
+ console.log(`${C.red}${C.bright}║ YOUR WALLET PASSPHRASE ║${C.reset}`);
306
+ console.log(`${C.red}${C.bright}╚═══════════════════════════════════════════════════════════════╝${C.reset}`);
307
+ console.log(`\n${C.yellow} Write these words down. They cannot be recovered.${C.reset}`);
308
+ console.log(`${C.yellow} No copy is stored. If you lose them, your wallet is UNRECOVERABLE.${C.reset}\n`);
309
+ console.log(` ${C.bright}1.${C.reset} ${words[0].padEnd(15)} ${C.bright}5.${C.reset} ${words[4].padEnd(15)} ${C.bright}9.${C.reset} ${words[8]}`);
310
+ console.log(` ${C.bright}2.${C.reset} ${words[1].padEnd(15)} ${C.bright}6.${C.reset} ${words[5].padEnd(15)} ${C.bright}10.${C.reset} ${words[9]}`);
311
+ console.log(` ${C.bright}3.${C.reset} ${words[2].padEnd(15)} ${C.bright}7.${C.reset} ${words[6].padEnd(15)} ${C.bright}11.${C.reset} ${words[10]}`);
312
+ console.log(` ${C.bright}4.${C.reset} ${words[3].padEnd(15)} ${C.bright}8.${C.reset} ${words[7].padEnd(15)} ${C.bright}12.${C.reset} ${words[11]}`);
313
+ console.log(`\n`);
314
+ await question(rl, ` ${C.cyan}Press Enter when you have saved your passphrase.${C.reset}\n`);
315
+ }
316
+
317
+ const walletData = saveWalletFile(address, keyPair.publicKey);
318
+ const cfg = loadConfig();
319
+ cfg.defaultWallet = address;
320
+ saveConfig(cfg);
321
+
322
+ console.log(`${C.green}✓ Wallet created:${C.reset} ${C.bright}${address}${C.reset}`);
323
+ console.log(`${C.dim} Saved to:${C.reset} ${walletFilePath(address)}`);
324
+ console.log(`${C.green}✓ Set as default wallet.${C.reset}\n`);
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // LIST WALLETS
329
+ // ---------------------------------------------------------------------------
330
+
331
+ async function listWallets(rl) {
332
+ ensureDirs();
333
+ const cfg = loadConfig();
334
+ const defaultWallet = cfg.defaultWallet;
335
+
336
+ let files;
337
+ try {
338
+ files = fs.readdirSync(getWalletsDir()).filter((f) => f.endsWith('.json'));
339
+ } catch {
340
+ files = [];
341
+ }
342
+
343
+ if (files.length === 0) {
344
+ console.log(`\n ${C.dim}No wallets found. Create one with:${C.reset}`);
345
+ console.log(` ${C.cyan}aether wallet create${C.reset}\n`);
346
+ return;
347
+ }
348
+
349
+ console.log(`\n${C.bright}${C.cyan}── Aether Wallets ─────────────────────────────────────────${C.reset}\n`);
350
+ console.log(` ${C.dim}Location: ${getWalletsDir()}${C.reset}\n`);
351
+
352
+ const wallets = files
353
+ .map((f) => {
354
+ try {
355
+ return JSON.parse(fs.readFileSync(path.join(getWalletsDir(), f), 'utf8'));
356
+ } catch {
357
+ return null;
358
+ }
359
+ })
360
+ .filter(Boolean);
361
+
362
+ wallets.sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
363
+
364
+ for (const w of wallets) {
365
+ const isDefault = w.address === defaultWallet;
366
+ const marker = isDefault ? ` ${C.green}★ default${C.reset}` : '';
367
+ const date = w.created_at ? new Date(w.created_at).toLocaleDateString() : 'unknown';
368
+ console.log(` ${C.bright}${w.address}${C.reset}${marker}`);
369
+ console.log(` ${C.dim} Created: ${date} | ${w.derivation_path}${C.reset}`);
370
+ console.log();
371
+ }
372
+
373
+ if (defaultWallet) {
374
+ console.log(` ${C.green}★${C.reset} = default wallet (used for signing transactions)\n`);
375
+ }
376
+ }
377
+
378
+ // ---------------------------------------------------------------------------
379
+ // IMPORT WALLET
380
+ // ---------------------------------------------------------------------------
381
+
382
+ async function importWallet(rl) {
383
+ const mnemonic = await askMnemonic(rl, 'Importing wallet from mnemonic');
384
+
385
+ if (!bip39.validateMnemonic(mnemonic)) {
386
+ const words = mnemonic.split(/\s+/);
387
+ if (words.length !== 12 && words.length !== 24) {
388
+ console.log(`\n ${C.red}✗ Invalid word count:${C.reset} got ${words.length}, expected 12 or 24.`);
389
+ return;
390
+ }
391
+ console.log(`\n ${C.red}✗ Invalid BIP39 mnemonic.${C.reset} Please check your word list and try again.`);
392
+ return;
393
+ }
394
+
395
+ let keyPair;
396
+ try {
397
+ keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
398
+ } catch (e) {
399
+ console.log(`\n ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
400
+ return;
401
+ }
402
+
403
+ const address = formatAddress(keyPair.publicKey);
404
+
405
+ if (loadWallet(address)) {
406
+ console.log(`\n ${C.yellow}⚠ Wallet already exists:${C.reset} ${address}`);
407
+ console.log(` ${C.dim}No new file created.${C.reset}\n`);
408
+ return;
409
+ }
410
+
411
+ const walletData = saveWalletFile(address, keyPair.publicKey);
412
+ const cfg = loadConfig();
413
+ cfg.defaultWallet = address;
414
+ saveConfig(cfg);
415
+
416
+ console.log(`\n${C.green}✓ Wallet imported:${C.reset} ${C.bright}${address}${C.reset}`);
417
+ console.log(`${C.dim} Saved to:${C.reset} ${walletFilePath(address)}`);
418
+ console.log(`${C.green}✓ Set as default wallet.${C.reset}\n`);
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // DEFAULT WALLET
423
+ // ---------------------------------------------------------------------------
424
+
425
+ async function defaultWallet(rl) {
426
+ const cfg = loadConfig();
427
+ const defaultAddr = cfg.defaultWallet;
428
+
429
+ const args = process.argv.slice(4);
430
+ if (args.includes('--set') || args.includes('-s')) {
431
+ const setIdx = args.indexOf('--set') !== -1 ? args.indexOf('--set') : args.indexOf('-s');
432
+ const address = args[setIdx + 1];
433
+ if (!address) {
434
+ console.log(`\n ${C.red}Usage:${C.reset} aether wallet default --set <address>\n`);
435
+ return;
436
+ }
437
+ const w = loadWallet(address);
438
+ if (!w) {
439
+ console.log(`\n ${C.red}✗ Wallet not found:${C.reset} ${address}`);
440
+ return;
441
+ }
442
+ cfg.defaultWallet = address;
443
+ saveConfig(cfg);
444
+ console.log(`\n${C.green}✓ Default wallet set to:${C.reset} ${address}\n`);
445
+ return;
446
+ }
447
+
448
+ console.log(`\n${C.bright}${C.cyan}── Default Wallet ─────────────────────────────────────────${C.reset}\n`);
449
+ if (!defaultAddr) {
450
+ console.log(` ${C.dim}No default wallet set.${C.reset}`);
451
+ console.log(` ${C.dim}Usage: aether wallet default --set <address>${C.reset}\n`);
452
+ return;
453
+ }
454
+
455
+ const w = loadWallet(defaultAddr);
456
+ if (w) {
457
+ console.log(` ${C.green}★${C.reset} ${C.bright}${defaultAddr}${C.reset}`);
458
+ console.log(` ${C.dim} Created: ${new Date(w.created_at).toLocaleString()}${C.reset}`);
459
+ console.log(` ${C.dim} Derivation: ${w.derivation_path}${C.reset}\n`);
460
+ } else {
461
+ console.log(` ${C.yellow}⚠ Default wallet file missing, but config references:${C.reset}`);
462
+ console.log(` ${defaultAddr}\n`);
463
+ console.log(` ${C.dim}Run:${C.reset} aether wallet default --set <address> ${C.dim}to update.${C.reset}\n`);
464
+ }
465
+ }
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // CONNECT WALLET
469
+ // Generates a session token, opens browser to verify page, polls until done.
470
+ // ---------------------------------------------------------------------------
471
+
472
+ async function connectWallet(rl) {
473
+ console.log(`\n${C.bright}${C.cyan}── Wallet Connect ────────────────────────────────────────${C.reset}\n`);
474
+
475
+ // Resolve wallet address: --address flag or default
476
+ const args = process.argv.slice(4);
477
+ let address = null;
478
+ const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
479
+ if (addrIdx !== -1 && args[addrIdx + 1]) {
480
+ address = args[addrIdx + 1];
481
+ }
482
+ if (!address) {
483
+ const cfg = loadConfig();
484
+ address = cfg.defaultWallet;
485
+ }
486
+
487
+ if (!address) {
488
+ console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
489
+ console.log(` ${C.dim}Usage:${C.reset} aether wallet connect --address <address>`);
490
+ console.log(` ${C.dim}Or set a default:${C.reset} aether wallet default --set <address>\n`);
491
+ return;
492
+ }
493
+
494
+ const wallet = loadWallet(address);
495
+ if (!wallet) {
496
+ console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}`);
497
+ console.log(` ${C.dim}Check your wallets with:${C.reset} aether wallet list\n`);
498
+ return;
499
+ }
500
+
501
+ // Generate session token and save session
502
+ const token = generateSessionToken();
503
+ saveSession(token, address, 10);
504
+
505
+ // Build verification URL
506
+ const siteUrl = getSiteUrl();
507
+ const verifyUrl = `${siteUrl}/wallet/verify?token=${token}&address=${encodeURIComponent(address)}`;
508
+
509
+ console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
510
+ console.log(` ${C.dim} Session expires in 10 minutes${C.reset}`);
511
+ console.log();
512
+
513
+ // Open browser
514
+ const opened = openBrowser(verifyUrl);
515
+ if (opened) {
516
+ console.log(` ${C.green}✓${C.reset} Opened verification page in browser.`);
517
+ console.log(` ${C.dim} ${verifyUrl}${C.reset}`);
518
+ } else {
519
+ console.log(` ${C.yellow}⚠ Could not open browser automatically.${C.reset}`);
520
+ console.log(` ${C.cyan}Open this URL manually:${C.reset}`);
521
+ console.log(` ${C.dim} ${verifyUrl}${C.reset}`);
522
+ }
523
+
524
+ console.log();
525
+ console.log(` ${C.yellow}⏳ Waiting for verification...${C.reset} (Ctrl+C to cancel)`);
526
+ console.log(` ${C.dim} Polling every 2s, timeout after 10 minutes${C.reset}`);
527
+
528
+ // Poll for verification (blocking, async)
529
+ const result = await pollForVerification(token, 600000);
530
+
531
+ if (result.verified) {
532
+ console.log(`\n${C.green}✓ Wallet verified and connected!${C.reset}`);
533
+ console.log(` ${C.green}★${C.reset} ${address}`);
534
+ deleteSession(token);
535
+ console.log();
536
+ return;
537
+ }
538
+
539
+ if (result.reason === 'expired') {
540
+ console.log(`\n ${C.red}✗ Session expired.${C.reset} Please run ${C.cyan}aether wallet connect${C.reset} again.\n`);
541
+ } else {
542
+ console.log(`\n ${C.red}✗ Verification timed out (10 minutes).${C.reset} Please run ${C.cyan}aether wallet connect${C.reset} again.\n`);
543
+ }
544
+ deleteSession(token);
545
+ process.exit(1);
546
+ }
547
+
548
+ // ---------------------------------------------------------------------------
549
+ // BALANCE
550
+ // Query chain RPC GET /v1/account/<addr> for real AETH balance using SDK
551
+ // ---------------------------------------------------------------------------
552
+
553
+ function getDefaultRpc() {
554
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
555
+ }
556
+
557
+ /**
558
+ * Format lamports as AETH string (1 AETH = 1e9 lamports)
559
+ */
560
+ function formatAether(lamports) {
561
+ const aeth = lamports / 1e9;
562
+ if (aeth === 0) return '0 AETH';
563
+ // Show up to 4 decimal places, stripping trailing zeros
564
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
565
+ }
566
+
567
+ async function balanceWallet(rl) {
568
+ console.log(`\n${C.bright}${C.cyan}── Wallet Balance ───────────────────────────────────────${C.reset}\n`);
569
+
570
+ // Resolve wallet address: --address flag or default
571
+ const args = process.argv.slice(4);
572
+ let address = null;
573
+ const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
574
+ if (addrIdx !== -1 && args[addrIdx + 1]) {
575
+ address = args[addrIdx + 1];
576
+ }
577
+ if (!address) {
578
+ const cfg = loadConfig();
579
+ address = cfg.defaultWallet;
580
+ }
581
+
582
+ if (!address) {
583
+ console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
584
+ console.log(` ${C.dim}Usage:${C.reset} aether wallet balance --address <address>`);
585
+ console.log(` ${C.dim}Or set a default:${C.reset} aether wallet default --set <address>\n`);
586
+ return;
587
+ }
588
+
589
+ const rpcUrl = getDefaultRpc();
590
+ console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
591
+ console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
592
+ console.log();
593
+
594
+ try {
595
+ // Use SDK for real blockchain RPC call
596
+ const client = new aether.AetherClient({ rpcUrl });
597
+ // Strip ATH prefix if present for API call
598
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
599
+ const account = await client.getAccountInfo(rawAddr);
600
+
601
+ if (!account || account.error) {
602
+ console.log(` ${C.yellow}⚠ Account not found on chain or RPC error.${C.reset}`);
603
+ console.log(` ${C.dim} This is normal for new wallets with 0 balance.${C.reset}`);
604
+ console.log(` ${C.dim} RPC response: ${JSON.stringify(account?.error || account)}${C.reset}\n`);
605
+ return;
606
+ }
607
+
608
+ const lamports = account.lamports || 0;
609
+ console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(lamports)}${C.reset}`);
610
+ console.log(` ${C.dim} Raw: ${lamports} lamports${C.reset}`);
611
+ console.log();
612
+
613
+ if (account.owner) {
614
+ const ownerStr = Array.isArray(account.owner)
615
+ ? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
616
+ : account.owner;
617
+ console.log(` ${C.dim} Owner: ${ownerStr}${C.reset}`);
618
+ }
619
+ if (account.rent_epoch !== undefined) {
620
+ console.log(` ${C.dim} Rent epoch: ${account.rent_epoch}${C.reset}`);
621
+ }
622
+ console.log();
623
+ } catch (err) {
624
+ console.log(` ${C.red}✗ Failed to fetch balance:${C.reset} ${err.message}`);
625
+ console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
626
+ console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
627
+ }
628
+ }
629
+
630
+ // ---------------------------------------------------------------------------
631
+ // Transaction helpers using SDK
632
+ // ---------------------------------------------------------------------------
633
+
634
+ /**
635
+ * Sign a transaction using the wallet's secret key.
636
+ * Returns a base58-encoded 64-byte signature.
637
+ */
638
+ function signTransaction(tx, secretKey) {
639
+ const txBytes = Buffer.from(JSON.stringify(tx));
640
+ const sig = nacl.sign.detached(txBytes, secretKey);
641
+ return bs58.encode(sig);
642
+ }
643
+
644
+ /**
645
+ * Submit a transaction using SDK client.sendTransaction()
646
+ * @param {Object} tx - Transaction object (must have signature set)
647
+ * @param {string} rpcUrl - RPC endpoint
648
+ * @returns {Promise<Object>} Transaction result
649
+ */
650
+ async function submitViaSDK(tx, rpcUrl) {
651
+ const client = new aether.AetherClient({ rpcUrl });
652
+ return client.sendTransaction(tx);
653
+ }
654
+
655
+ // ---------------------------------------------------------------------------
656
+ // STAKE
657
+ // Submit a Stake transaction via POST /v1/tx
658
+ // ---------------------------------------------------------------------------
659
+
660
+ async function stakeWallet(rl) {
661
+ console.log(`\n${C.bright}${C.cyan}── Stake AETH ─────────────────────────────────────────────${C.reset}\n`);
662
+
663
+ // Resolve wallet address
664
+ const args = process.argv.slice(4);
665
+ let address = null;
666
+ let validator = null;
667
+ let amountStr = null;
668
+
669
+ for (let i = 0; i < args.length; i++) {
670
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
671
+ address = args[i + 1];
672
+ }
673
+ if ((args[i] === '--validator' || args[i] === '-v') && args[i + 1]) {
674
+ validator = args[i + 1];
675
+ }
676
+ if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
677
+ amountStr = args[i + 1];
678
+ }
679
+ }
680
+
681
+ if (!address) {
682
+ const cfg = loadConfig();
683
+ address = cfg.defaultWallet;
684
+ }
685
+
686
+ if (!address) {
687
+ console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
688
+ console.log(` ${C.dim}Usage: aether stake --address <addr> --validator <val> --amount <aeth>${C.reset}\n`);
689
+ return;
690
+ }
691
+
692
+ const wallet = loadWallet(address);
693
+ if (!wallet) {
694
+ console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
695
+ return;
696
+ }
697
+
698
+ // Prompt for missing values interactively
699
+ if (!validator) {
700
+ console.log(` ${C.cyan}Enter validator address:${C.reset}`);
701
+ validator = await question(rl, ` Validator > ${C.reset}`);
702
+ }
703
+
704
+ if (!amountStr) {
705
+ console.log(` ${C.cyan}Enter amount in AETH:${C.reset}`);
706
+ amountStr = await question(rl, ` Amount (AETH) > ${C.reset}`);
707
+ }
708
+
709
+ const amount = parseFloat(amountStr);
710
+ if (isNaN(amount) || amount <= 0) {
711
+ console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
712
+ return;
713
+ }
714
+
715
+ const lamports = Math.round(amount * 1e9);
716
+
717
+ console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
718
+ console.log(` ${C.green}★${C.reset} Validator: ${C.bright}${validator}${C.reset}`);
719
+ console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
720
+ console.log();
721
+
722
+ // Ask for mnemonic to derive signing keypair
723
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
724
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
725
+ console.log();
726
+
727
+ let keyPair;
728
+ try {
729
+ keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
730
+ } catch (e) {
731
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
732
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
733
+ return;
734
+ }
735
+
736
+ // Verify the derived address matches the wallet
737
+ const derivedAddress = formatAddress(keyPair.publicKey);
738
+ if (derivedAddress !== address) {
739
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
740
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
741
+ console.log(` ${C.dim} Expected: ${address}${C.reset}`);
742
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
743
+ return;
744
+ }
745
+
746
+ const confirm = await question(rl, ` ${C.yellow}Confirm stake? [y/N]${C.reset} > ${C.reset}`);
747
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
748
+ console.log(` ${C.dim}Cancelled.${C.reset}\n`);
749
+ return;
750
+ }
751
+
752
+ // Fetch current slot via SDK for the transaction
753
+ const rpcUrl = getDefaultRpc();
754
+ let currentSlot = 0;
755
+ try {
756
+ currentSlot = await new aether.AetherClient({ rpcUrl }).getSlot();
757
+ } catch (e) {
758
+ // Continue with slot 0 if RPC unavailable
759
+ }
760
+
761
+ // Build the transaction
762
+ const tx = {
763
+ signer: address.startsWith('ATH') ? address.slice(3) : address,
764
+ tx_type: 'Stake',
765
+ payload: {
766
+ type: 'Stake',
767
+ data: {
768
+ validator,
769
+ amount: lamports,
770
+ },
771
+ },
772
+ fee: 5000,
773
+ slot: currentSlot,
774
+ timestamp: Math.floor(Date.now() / 1000),
775
+ };
776
+
777
+ // Sign transaction with wallet secret key
778
+ tx.signature = signTransaction(tx, keyPair.secretKey);
779
+
780
+ console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
781
+
782
+ try {
783
+ // Submit via SDK (real RPC POST /v1/transaction)
784
+ const result = await submitViaSDK(tx, rpcUrl);
785
+
786
+ if (result.error) {
787
+ console.log(`\n ${C.red}✗ Transaction failed:${C.reset} ${result.error}\n`);
788
+ process.exit(1);
789
+ }
790
+
791
+ const sig = result.signature || result.tx_signature || result.txid || result.id || JSON.stringify(result);
792
+ console.log(`\n${C.green}✓ Stake transaction submitted!${C.reset}`);
793
+ console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${sig}${C.reset}`);
794
+ console.log(` ${C.dim}Slot:${C.reset} ${result.slot || currentSlot}`);
795
+ console.log(` ${C.dim}SDK: sendTransaction()${C.reset}`);
796
+ console.log(` ${C.dim}Check: aether delegations list --address ${address}${C.reset}\n`);
797
+ } catch (err) {
798
+ console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
799
+ console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
800
+ process.exit(1);
801
+ }
802
+ }
803
+
804
+ // ---------------------------------------------------------------------------
805
+ // TRANSFER
806
+ // Submit a Transfer transaction via POST /v1/tx
807
+ // ---------------------------------------------------------------------------
808
+
809
+ async function transferWallet(rl) {
810
+ console.log(`\n${C.bright}${C.cyan}── Transfer AETH ─────────────────────────────────────────${C.reset}\n`);
811
+
812
+ const args = process.argv.slice(4);
813
+ let address = null;
814
+ let recipient = null;
815
+ let amountStr = null;
816
+
817
+ for (let i = 0; i < args.length; i++) {
818
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
819
+ address = args[i + 1];
820
+ }
821
+ if ((args[i] === '--to' || args[i] === '-t') && args[i + 1]) {
822
+ recipient = args[i + 1];
823
+ }
824
+ if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
825
+ amountStr = args[i + 1];
826
+ }
827
+ }
828
+
829
+ if (!address) {
830
+ const cfg = loadConfig();
831
+ address = cfg.defaultWallet;
832
+ }
833
+
834
+ if (!address) {
835
+ console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
836
+ console.log(` ${C.dim}Usage: aether transfer --to <addr> --amount <aeth>${C.reset}\n`);
837
+ return;
838
+ }
839
+
840
+ const wallet = loadWallet(address);
841
+ if (!wallet) {
842
+ console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
843
+ return;
844
+ }
845
+
846
+ // Prompt for missing values interactively
847
+ if (!recipient) {
848
+ console.log(` ${C.cyan}Enter recipient address:${C.reset}`);
849
+ recipient = await question(rl, ` Recipient > ${C.reset}`);
850
+ }
851
+
852
+ if (!amountStr) {
853
+ console.log(` ${C.cyan}Enter amount in AETH:${C.reset}`);
854
+ amountStr = await question(rl, ` Amount (AETH) > ${C.reset}`);
855
+ }
856
+
857
+ const amount = parseFloat(amountStr);
858
+ if (isNaN(amount) || amount <= 0) {
859
+ console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
860
+ return;
861
+ }
862
+
863
+ const lamports = Math.round(amount * 1e9);
864
+
865
+ console.log(` ${C.green}★${C.reset} From: ${C.bright}${address}${C.reset}`);
866
+ console.log(` ${C.green}★${C.reset} To: ${C.bright}${recipient}${C.reset}`);
867
+ console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
868
+ console.log();
869
+
870
+ // Ask for mnemonic to derive signing keypair
871
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
872
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
873
+ console.log();
874
+
875
+ let keyPair;
876
+ try {
877
+ keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
878
+ } catch (e) {
879
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
880
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
881
+ return;
882
+ }
883
+
884
+ // Verify the derived address matches the wallet
885
+ const derivedAddress = formatAddress(keyPair.publicKey);
886
+ if (derivedAddress !== address) {
887
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
888
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
889
+ console.log(` ${C.dim} Expected: ${address}${C.reset}`);
890
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
891
+ return;
892
+ }
893
+
894
+ const confirm = await question(rl, ` ${C.yellow}Confirm transfer? [y/N]${C.reset} > ${C.reset}`);
895
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
896
+ console.log(` ${C.dim}Cancelled.${C.reset}\n`);
897
+ return;
898
+ }
899
+
900
+ // Fetch current slot via SDK
901
+ const rpcUrl = getDefaultRpc();
902
+ let currentSlot = 0;
903
+ try {
904
+ currentSlot = await new aether.AetherClient({ rpcUrl }).getSlot();
905
+ } catch (e) {
906
+ // Continue with slot 0
907
+ }
908
+
909
+ // Build transfer transaction
910
+ const tx = {
911
+ signer: address.startsWith('ATH') ? address.slice(3) : address,
912
+ tx_type: 'Transfer',
913
+ payload: {
914
+ type: 'Transfer',
915
+ data: {
916
+ recipient: recipient.startsWith('ATH') ? recipient.slice(3) : recipient,
917
+ amount: lamports,
918
+ nonce: Math.floor(Math.random() * 0xffffffff),
919
+ },
920
+ },
921
+ fee: 5000,
922
+ slot: currentSlot,
923
+ timestamp: Math.floor(Date.now() / 1000),
924
+ };
925
+
926
+ // Sign transaction
927
+ tx.signature = signTransaction(tx, keyPair.secretKey);
928
+
929
+ console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
930
+
931
+ try {
932
+ // Submit via SDK
933
+ const result = await submitViaSDK(tx, rpcUrl);
934
+
935
+ if (result.error) {
936
+ console.log(`\n ${C.red}✗ Transaction failed:${C.reset} ${result.error}\n`);
937
+ process.exit(1);
938
+ }
939
+
940
+ const sig = result.signature || result.tx_signature || result.txid || result.id || JSON.stringify(result);
941
+ console.log(`\n${C.green}✓ Transfer transaction submitted!${C.reset}`);
942
+ console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${sig}${C.reset}`);
943
+ console.log(` ${C.dim}Slot:${C.reset} ${result.slot || currentSlot}`);
944
+ console.log(` ${C.dim}SDK: sendTransaction()${C.reset}`);
945
+ console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
946
+ } catch (err) {
947
+ console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
948
+ console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
949
+ process.exit(1);
950
+ }
951
+ }
952
+
953
+ // ---------------------------------------------------------------------------
954
+ // TX HISTORY
955
+ // Fetch and display recent transactions for an address using SDK
956
+ // ---------------------------------------------------------------------------
957
+
958
+ async function txHistory(rl) {
959
+ console.log(`\n${C.bright}${C.cyan}── Transaction History ────────────────────────────────────${C.reset}\n`);
960
+
961
+ const args = process.argv.slice(4);
962
+ let address = null;
963
+ let limit = 20;
964
+ let asJson = false;
965
+
966
+ const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
967
+ if (addrIdx !== -1 && args[addrIdx + 1]) {
968
+ address = args[addrIdx + 1];
969
+ }
970
+
971
+ const limitIdx = args.findIndex((a) => a === '--limit' || a === '-l');
972
+ if (limitIdx !== -1 && args[limitIdx + 1]) {
973
+ limit = parseInt(args[limitIdx + 1], 10);
974
+ if (isNaN(limit) || limit < 1 || limit > 100) {
975
+ console.log(` ${C.red}✗ --limit must be between 1 and 100.${C.reset}\n`);
976
+ return;
977
+ }
978
+ }
979
+
980
+ asJson = args.includes('--json') || args.includes('-j');
981
+
982
+ if (!address) {
983
+ const cfg = loadConfig();
984
+ address = cfg.defaultWallet;
985
+ }
986
+
987
+ if (!address) {
988
+ console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
989
+ console.log(` ${C.dim}Usage: aether tx history --address <addr> [--limit 20] [--json]${C.reset}\n`);
990
+ return;
991
+ }
992
+
993
+ const rpcUrl = getDefaultRpc();
994
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
995
+
996
+ if (!asJson) {
997
+ console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
998
+ console.log(` ${C.dim} RPC: ${rpcUrl} Limit: ${limit}${C.reset}`);
999
+ console.log();
1000
+ }
1001
+
1002
+ try {
1003
+ // Create SDK client
1004
+ const client = createClient(rpcUrl);
1005
+
1006
+ // Fetch account info first (for context)
1007
+ const account = await client.getAccount(rawAddr);
1008
+
1009
+ // Fetch transactions for this address using SDK
1010
+ const txs = await client.getRecentTransactions(rawAddr, limit);
1011
+
1012
+ if (asJson) {
1013
+ const out = {
1014
+ address,
1015
+ rpc: rpcUrl,
1016
+ account: account && !account.error ? {
1017
+ lamports: account.lamports,
1018
+ owner: account.owner,
1019
+ } : null,
1020
+ transactions: txs && !txs.error ? (Array.isArray(txs) ? txs : txs.transactions || []) : [],
1021
+ fetched_at: new Date().toISOString(),
1022
+ };
1023
+ console.log(JSON.stringify(out, null, 2));
1024
+ return;
1025
+ }
1026
+
1027
+ if (!account || account.error) {
1028
+ console.log(` ${C.yellow}⚠ Account not found on chain.${C.reset}`);
1029
+ } else {
1030
+ console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(account.lamports || 0)}${C.reset}`);
1031
+ if (account.owner) {
1032
+ const ownerStr = Array.isArray(account.owner)
1033
+ ? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
1034
+ : account.owner;
1035
+ console.log(` ${C.dim} Owner: ${ownerStr}${C.reset}`);
1036
+ }
1037
+ console.log();
1038
+ }
1039
+
1040
+ if (!txs || txs.error) {
1041
+ console.log(` ${C.yellow}⚠ No transaction history available.${C.reset}`);
1042
+ console.log(` ${C.dim} RPC response: ${JSON.stringify(txs?.error || txs)}${C.reset}`);
1043
+ console.log(` ${C.dim} (New wallets with 0 txs will return empty results)${C.reset}\n`);
1044
+ return;
1045
+ }
1046
+
1047
+ const txList = Array.isArray(txs) ? txs : txs.transactions || [];
1048
+ console.log(` ${C.bright}Recent Transactions (${txList.length})${C.reset}\n`);
1049
+
1050
+ if (txList.length === 0) {
1051
+ console.log(` ${C.dim} No transactions found for this address.${C.reset}`);
1052
+ console.log(` ${C.dim} This is normal for new wallets.${C.reset}\n`);
1053
+ return;
1054
+ }
1055
+
1056
+ const typeColors = {
1057
+ Transfer: C.cyan,
1058
+ Stake: C.green,
1059
+ Unstake: C.yellow,
1060
+ ClaimRewards: C.magenta,
1061
+ CreateNFT: C.red,
1062
+ MintNFT: C.red,
1063
+ TransferNFT: C.cyan,
1064
+ UpdateMetadata: C.yellow,
1065
+ };
1066
+
1067
+ for (const tx of txList) {
1068
+ const txType = tx.tx_type || tx.type || 'Unknown';
1069
+ const color = typeColors[txType] || C.reset;
1070
+ const ts = tx.timestamp
1071
+ ? new Date(tx.timestamp * 1000).toISOString()
1072
+ : 'unknown';
1073
+ const sig = tx.signature || tx.id || tx.tx_signature || '—';
1074
+ const sigShort = sig.length > 20 ? sig.slice(0, 8) + '…' + sig.slice(-8) : sig;
1075
+
1076
+ console.log(` ${C.dim}┌─ ${ts}${C.reset}`);
1077
+ console.log(` │ ${C.bright}${color}${txType}${C.reset} ${C.dim}sig:${C.reset} ${sigShort}`);
1078
+ if (tx.payload && tx.payload.data) {
1079
+ const d = tx.payload.data;
1080
+ if (d.recipient) console.log(` │ ${C.dim} → to: ${d.recipient}${C.reset}`);
1081
+ if (d.amount) console.log(` │ ${C.dim} amount: ${formatAether(d.amount)}${C.reset}`);
1082
+ if (d.validator) console.log(` │ ${C.dim} validator: ${d.validator}${C.reset}`);
1083
+ if (d.stake_account) console.log(` │ ${C.dim} stake_acct: ${d.stake_account}${C.reset}`);
1084
+ }
1085
+ if (tx.fee !== undefined && tx.fee > 0) {
1086
+ console.log(` │ ${C.dim} fee: ${tx.fee} lamports${C.reset}`);
1087
+ }
1088
+ console.log(` ${C.dim}└${C.reset}`);
1089
+ console.log();
1090
+ }
1091
+ console.log();
1092
+ } catch (err) {
1093
+ console.log(` ${C.red}✗ Failed to fetch transaction history:${C.reset} ${err.message}`);
1094
+ console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
1095
+ console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
1096
+ }
1097
+ }
1098
+
1099
+ // ---------------------------------------------------------------------------
1100
+ // ---------------------------------------------------------------------------
1101
+ // EXPORT WALLET
1102
+ // Export wallet data for backup — public data by default, mnemonic with --mnemonic flag
1103
+ // ---------------------------------------------------------------------------
1104
+
1105
+ async function exportWallet(rl) {
1106
+ console.log(`\n${C.bright}${C.cyan}── Wallet Export ─────────────────────────────────────────${C.reset}\n`);
1107
+
1108
+ const args = process.argv.slice(4);
1109
+ let address = null;
1110
+ let asJson = false;
1111
+ let includeMnemonic = false;
1112
+
1113
+ const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
1114
+ if (addrIdx !== -1 && args[addrIdx + 1]) {
1115
+ address = args[addrIdx + 1];
1116
+ }
1117
+
1118
+ asJson = args.includes('--json') || args.includes('-j');
1119
+ includeMnemonic = args.includes('--mnemonic') || args.includes('-m');
1120
+
1121
+ if (!address) {
1122
+ const cfg = loadConfig();
1123
+ address = cfg.defaultWallet;
1124
+ }
1125
+
1126
+ if (!address) {
1127
+ console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
1128
+ console.log(` ${C.dim}Usage: aether wallet export --address <addr> [--mnemonic] [--json]${C.reset}\n`);
1129
+ return;
1130
+ }
1131
+
1132
+ const walletData = loadWallet(address);
1133
+ if (!walletData) {
1134
+ console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}`);
1135
+ console.log(` ${C.dim} Available wallets: ${C.cyan}aether wallet list${C.reset}\n`);
1136
+ return;
1137
+ }
1138
+
1139
+ const exportData = {
1140
+ version: walletData.version || 1,
1141
+ address: walletData.address,
1142
+ public_key: walletData.public_key,
1143
+ derivation_path: walletData.derivation_path,
1144
+ created_at: walletData.created_at,
1145
+ source: 'aether-cli',
1146
+ };
1147
+
1148
+ // Mnemonic export requires interactive confirmation for security
1149
+ if (includeMnemonic) {
1150
+ console.log(` ${C.yellow}⚠ WARNING: You are about to export your SECRET MNEMONIC.${C.reset}`);
1151
+ console.log(` ${C.dim} Anyone with this phrase can access your funds.${C.reset}\n`);
1152
+ const confirmed = await question(rl, ` Type ${C.bright}EXPORT${C.reset} to confirm: `);
1153
+ if (confirmed.trim().toUpperCase() !== 'EXPORT') {
1154
+ console.log(`\n ${C.dim}Aborted. Mnemonic not exported.${C.reset}\n`);
1155
+ return;
1156
+ }
1157
+ console.log(` ${C.green}✓ Confirmed${C.reset}\n`);
1158
+
1159
+ // Re-derive mnemonic from keypair is NOT possible — we must ask for it
1160
+ console.log(` ${C.dim}The CLI cannot retrieve your mnemonic from the stored keypair.${C.reset}`);
1161
+ console.log(` ${C.dim}If you have a backup of your mnemonic, enter it below to include it.${C.reset}`);
1162
+ console.log(` ${C.dim}Otherwise, press Enter to export public data only.${C.reset}\n`);
1163
+ const mnemonicInput = await question(rl, ` ${C.cyan}Mnemonic (or Enter to skip):${C.reset} `);
1164
+ const mnemonic = mnemonicInput.trim();
1165
+ if (mnemonic && bip39.validateMnemonic(mnemonic)) {
1166
+ exportData.mnemonic = mnemonic;
1167
+ } else if (mnemonic) {
1168
+ console.log(` ${C.red}✗ Invalid mnemonic phrase. Skipping.${C.reset}`);
1169
+ }
1170
+ }
1171
+
1172
+ if (asJson) {
1173
+ console.log(JSON.stringify(exportData, null, 2));
1174
+ return;
1175
+ }
1176
+
1177
+ // Human-readable output
1178
+ console.log(` ${C.green}★${C.reset} Wallet exported successfully\n`);
1179
+ console.log(` ${C.dim}Address:${C.reset} ${exportData.address}`);
1180
+ console.log(` ${C.dim}Public key:${C.reset} ${exportData.public_key}`);
1181
+ console.log(` ${C.dim}Derivation path:${C.reset} ${exportData.derivation_path}`);
1182
+ console.log(` ${C.dim}Created:${C.reset} ${exportData.created_at ? new Date(exportData.created_at).toLocaleString() : 'unknown'}`);
1183
+ if (exportData.mnemonic) {
1184
+ console.log();
1185
+ console.log(` ${C.yellow}★ MNEMONIC (keep this secret!):${C.reset}`);
1186
+ console.log(` ${C.bright}${exportData.mnemonic}${C.reset}`);
1187
+ }
1188
+ console.log();
1189
+ console.log(` ${C.dim}Export saved to stdout in JSON format with:${C.reset}`);
1190
+ console.log(` ${C.cyan}aether wallet export --address ${address} --json${C.reset}`);
1191
+ console.log();
1192
+ }
1193
+
1194
+ // STAKE POSITIONS
1195
+ // Query and display current stake positions/delegations for a wallet
1196
+ // ---------------------------------------------------------------------------
1197
+
1198
+ async function stakePositions(rl) {
1199
+ console.log(`\n${C.bright}${C.cyan}── Stake Positions ──────────────────────────────────────${C.reset}\n`);
1200
+
1201
+ const args = process.argv.slice(4);
1202
+ let address = null;
1203
+ let asJson = false;
1204
+
1205
+ const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
1206
+ if (addrIdx !== -1 && args[addrIdx + 1]) {
1207
+ address = args[addrIdx + 1];
1208
+ }
1209
+
1210
+ asJson = args.includes('--json') || args.includes('-j');
1211
+
1212
+ if (!address) {
1213
+ const cfg = loadConfig();
1214
+ address = cfg.defaultWallet;
1215
+ }
1216
+
1217
+ if (!address) {
1218
+ console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
1219
+ console.log(` ${C.dim}Usage: aether wallet stake-positions --address <addr> [--json]${C.reset}\n`);
1220
+ return;
1221
+ }
1222
+
1223
+ const rpcUrl = getDefaultRpc();
1224
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
1225
+
1226
+ if (!asJson) {
1227
+ console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
1228
+ console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
1229
+ console.log();
1230
+ }
1231
+
1232
+ try {
1233
+ // Create SDK client for real blockchain calls
1234
+ const client = createClient(rpcUrl);
1235
+
1236
+ // Fetch stake accounts for this address using SDK (real RPC GET /v1/stake/<addr>)
1237
+ const stakeAccounts = await client.getStakeAccounts(rawAddr);
1238
+
1239
+ if (asJson) {
1240
+ const out = {
1241
+ address,
1242
+ rpc: rpcUrl,
1243
+ stake_accounts: stakeAccounts.map(acc => ({
1244
+ stake_account: acc.pubkey || acc.publicKey || acc.account || acc.address,
1245
+ validator: acc.validator || acc.voter || acc.vote_account,
1246
+ stake_lamports: acc.stake_lamports || acc.lamports || 0,
1247
+ stake_aeth: (acc.stake_lamports || acc.lamports || 0) / 1e9,
1248
+ status: acc.status || acc.state || 'unknown',
1249
+ activation_epoch: acc.activation_epoch,
1250
+ deactivation_epoch: acc.deactivation_epoch,
1251
+ rewards_earned: acc.rewards_earned || 0,
1252
+ })),
1253
+ total_staked_lamports: stakeAccounts.reduce((sum, acc) => sum + (acc.stake_lamports || acc.lamports || 0), 0),
1254
+ fetched_at: new Date().toISOString(),
1255
+ };
1256
+ console.log(JSON.stringify(out, null, 2));
1257
+ return;
1258
+ }
1259
+
1260
+ if (!stakeAccounts || stakeAccounts.length === 0) {
1261
+ console.log(` ${C.yellow}⚠ No active stake positions found.${C.reset}`);
1262
+ console.log(` ${C.dim} This wallet has not delegated to any validators.${C.reset}`);
1263
+ console.log(` ${C.dim} Stake AETH with: ${C.cyan}aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
1264
+ return;
1265
+ }
1266
+
1267
+ let totalStaked = 0;
1268
+ let activeCount = 0;
1269
+ let deactivatingCount = 0;
1270
+ let inactiveCount = 0;
1271
+
1272
+ console.log(` ${C.bright}Stake Positions (${stakeAccounts.length})${C.reset}\n`);
1273
+
1274
+ const statusColors = {
1275
+ active: C.green,
1276
+ activating: C.cyan,
1277
+ deactivating: C.yellow,
1278
+ inactive: C.dim,
1279
+ };
1280
+
1281
+ for (const acc of stakeAccounts) {
1282
+ const stakeAcct = acc.pubkey || acc.publicKey || acc.account || 'unknown';
1283
+ const validator = acc.validator || acc.voter || acc.vote_account || 'unknown';
1284
+ const lamports = acc.stake_lamports || acc.lamports || 0;
1285
+ const aeth = lamports / 1e9;
1286
+ const status = (acc.status || acc.state || 'unknown').toLowerCase();
1287
+ const rewards = acc.rewards_earned || 0;
1288
+
1289
+ totalStaked += lamports;
1290
+
1291
+ if (status === 'active') activeCount++;
1292
+ else if (status === 'deactivating' || status === 'deactivated') deactivatingCount++;
1293
+ else inactiveCount++;
1294
+
1295
+ const statusColor = statusColors[status] || C.reset;
1296
+ const shortAcct = stakeAcct.length > 20 ? stakeAcct.slice(0, 8) + '…' + stakeAcct.slice(-8) : stakeAcct;
1297
+ const shortVal = validator.length > 20 ? validator.slice(0, 8) + '…' + validator.slice(-8) : validator;
1298
+
1299
+ console.log(` ${C.dim}┌─ ${C.bright}${statusColor}${status.toUpperCase()}${C.reset}`);
1300
+ console.log(` │ ${C.dim}Stake acct:${C.reset} ${shortAcct}`);
1301
+ console.log(` │ ${C.dim}Validator:${C.reset} ${shortVal}`);
1302
+ console.log(` │ ${C.dim}Staked:${C.reset} ${C.bright}${aeth.toFixed(4)} AETH${C.reset} (${lamports.toLocaleString()} lamports)`);
1303
+ if (rewards > 0) {
1304
+ console.log(` │ ${C.dim}Rewards:${C.reset} ${C.green}+${(rewards / 1e9).toFixed(4)} AETH${C.reset}`);
1305
+ }
1306
+ if (acc.activation_epoch !== undefined) {
1307
+ console.log(` │ ${C.dim}Activated:${C.reset} epoch ${acc.activation_epoch}`);
1308
+ }
1309
+ if (acc.deactivation_epoch !== undefined) {
1310
+ console.log(` │ ${C.dim}Deactivates:${C.reset} epoch ${acc.deactivation_epoch}`);
1311
+ }
1312
+ console.log(` ${C.dim}└${C.reset}`);
1313
+ console.log();
1314
+ }
1315
+
1316
+ console.log(` ${C.bright}Summary:${C.reset}`);
1317
+ console.log(` ${C.dim} Total staked:${C.reset} ${C.bright}${(totalStaked / 1e9).toFixed(4)} AETH${C.reset} (${totalStaked.toLocaleString()} lamports)`);
1318
+ console.log(` ${C.green} ● Active:${C.reset} ${activeCount} ${C.yellow}● Deactivating:${C.reset} ${deactivatingCount} ${C.dim}● Inactive:${C.reset} ${inactiveCount}`);
1319
+ console.log();
1320
+
1321
+ } catch (err) {
1322
+ console.log(` ${C.red}✗ Failed to fetch stake positions:${C.reset} ${err.message}`);
1323
+ console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
1324
+ console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
1325
+ }
1326
+ }
1327
+
1328
+ // ---------------------------------------------------------------------------
1329
+ // UNSTAKE
1330
+ // Submit an Unstake transaction via POST /v1/tx to deactivate stake
1331
+ // ---------------------------------------------------------------------------
1332
+
1333
+ async function unstakeWallet(rl) {
1334
+ console.log(`\n${C.bright}${C.cyan}── Unstake AETH ──────────────────────────────────────────${C.reset}\n`);
1335
+
1336
+ const args = process.argv.slice(4);
1337
+ let address = null;
1338
+ let stakeAccount = null;
1339
+ let amountStr = null;
1340
+
1341
+ for (let i = 0; i < args.length; i++) {
1342
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
1343
+ address = args[i + 1];
1344
+ }
1345
+ if ((args[i] === '--account' || args[i] === '-s') && args[i + 1]) {
1346
+ stakeAccount = args[i + 1];
1347
+ }
1348
+ if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
1349
+ amountStr = args[i + 1];
1350
+ }
1351
+ }
1352
+
1353
+ if (!address) {
1354
+ const cfg = loadConfig();
1355
+ address = cfg.defaultWallet;
1356
+ }
1357
+
1358
+ if (!address) {
1359
+ console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
1360
+ console.log(` ${C.dim}Usage: aether unstake --account <stakeAcct> [--amount <aeth>] [--address <addr>]${C.reset}\n`);
1361
+ return;
1362
+ }
1363
+
1364
+ const wallet = loadWallet(address);
1365
+ if (!wallet) {
1366
+ console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
1367
+ return;
1368
+ }
1369
+
1370
+ // Resolve stake account: --account flag, or query chain for first active stake
1371
+ if (!stakeAccount) {
1372
+ const rpcUrl = getDefaultRpc();
1373
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
1374
+
1375
+ let stakeAccounts = [];
1376
+ try {
1377
+ // Use SDK for real blockchain call
1378
+ const client = createClient(rpcUrl);
1379
+ stakeAccounts = await client.getStakeAccounts(rawAddr);
1380
+ } catch { /* no stake accounts */ }
1381
+
1382
+ if (stakeAccounts.length === 0) {
1383
+ console.log(` ${C.red}✗ No active stake accounts found for this wallet.${C.reset}`);
1384
+ console.log(` ${C.dim}Use ${C.cyan}--account <stakeAcct>${C.reset} ${C.dim}to specify a stake account.${C.reset}`);
1385
+ console.log(` ${C.dim}Check delegations: aether delegations list --address ${address}${C.reset}\n`);
1386
+ return;
1387
+ }
1388
+
1389
+ // Default to first active stake account
1390
+ const active = stakeAccounts.find(s => !s.deactivation_epoch && (s.status === 'active' || s.state === 'active'));
1391
+ stakeAccount = active
1392
+ ? (active.pubkey || active.publicKey || active.account)
1393
+ : (stakeAccounts[0].pubkey || stakeAccounts[0].publicKey || stakeAccounts[0].account);
1394
+
1395
+ console.log(` ${C.cyan}Using stake account:${C.reset} ${C.bright}${stakeAccount}${C.reset}`);
1396
+ console.log(` ${C.dim}(override with ${C.cyan}--account <stakeAcct>${C.reset}${C.dim})${C.reset}\n`);
1397
+ }
1398
+
1399
+ // Resolve amount: --amount flag, or prompt if partial unstake supported
1400
+ // If no amount provided, unstake entire stake
1401
+ let lamports = null;
1402
+ if (amountStr) {
1403
+ const amount = parseFloat(amountStr);
1404
+ if (isNaN(amount) || amount <= 0) {
1405
+ console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
1406
+ return;
1407
+ }
1408
+ lamports = Math.round(amount * 1e9);
1409
+ }
1410
+
1411
+ console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
1412
+ console.log(` ${C.green}★${C.reset} Stake acct: ${C.bright}${stakeAccount}${C.reset}`);
1413
+ if (lamports !== null) {
1414
+ console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${(lamports / 1e9).toFixed(4)} AETH${C.reset} (${lamports} lamports)`);
1415
+ } else {
1416
+ console.log(` ${C.green}★${C.reset} Amount: ${C.bright}FULL STAKE${C.reset}`);
1417
+ }
1418
+ console.log();
1419
+
1420
+ // Ask for mnemonic to derive signing keypair
1421
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
1422
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
1423
+ console.log();
1424
+
1425
+ let keyPair;
1426
+ try {
1427
+ keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
1428
+ } catch (e) {
1429
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
1430
+ console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
1431
+ return;
1432
+ }
1433
+
1434
+ // Verify the derived address matches the wallet
1435
+ const derivedAddress = formatAddress(keyPair.publicKey);
1436
+ if (derivedAddress !== address) {
1437
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
1438
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
1439
+ console.log(` ${C.dim} Expected: ${address}${C.reset}\n`);
1440
+ return;
1441
+ }
1442
+
1443
+ const confirm = await question(rl, ` ${C.yellow}Confirm unstake? [y/N]${C.reset} > ${C.reset}`);
1444
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
1445
+ console.log(` ${C.dim}Cancelled.${C.reset}\n`);
1446
+ return;
1447
+ }
1448
+
1449
+ // Fetch current slot via SDK
1450
+ const rpcUrl = getDefaultRpc();
1451
+ let currentSlot = 0;
1452
+ try {
1453
+ currentSlot = await new aether.AetherClient({ rpcUrl }).getSlot();
1454
+ } catch (e) {
1455
+ // Continue with slot 0
1456
+ }
1457
+
1458
+ // Build the unstake transaction
1459
+ const txData = {
1460
+ type: 'Unstake',
1461
+ data: {
1462
+ stake_account: stakeAccount,
1463
+ },
1464
+ };
1465
+ if (lamports !== null) {
1466
+ txData.data.amount = lamports;
1467
+ }
1468
+
1469
+ const tx = {
1470
+ signer: address.startsWith('ATH') ? address.slice(3) : address,
1471
+ tx_type: 'Unstake',
1472
+ payload: txData,
1473
+ fee: 5000,
1474
+ slot: currentSlot,
1475
+ timestamp: Math.floor(Date.now() / 1000),
1476
+ };
1477
+
1478
+ // Sign transaction
1479
+ tx.signature = signTransaction(tx, keyPair.secretKey);
1480
+
1481
+ console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
1482
+
1483
+ try {
1484
+ // Submit via SDK
1485
+ const result = await submitViaSDK(tx, rpcUrl);
1486
+
1487
+ if (result.error) {
1488
+ console.log(`\n ${C.red}✗ Unstake failed:${C.reset} ${result.error}\n`);
1489
+ process.exit(1);
1490
+ }
1491
+
1492
+ const sig = result.signature || result.tx_signature || result.txid || result.id || JSON.stringify(result);
1493
+ console.log(`\n${C.green}✓ Unstake transaction submitted!${C.reset}`);
1494
+ console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${sig}${C.reset}`);
1495
+ console.log(` ${C.dim}Slot:${C.reset} ${result.slot || currentSlot}`);
1496
+ console.log(` ${C.dim}SDK: sendTransaction()${C.reset}`);
1497
+ console.log(` ${C.dim}Stake will deactivate over the next epoch.${C.reset}`);
1498
+ console.log(` ${C.dim}Check status: aether delegations list --address ${address}${C.reset}\n`);
1499
+ } catch (err) {
1500
+ console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
1501
+ console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
1502
+ process.exit(1);
1503
+ }
1504
+ }
1505
+
1506
+ // ---------------------------------------------------------------------------
1507
+ // Main dispatcher
1508
+ // ---------------------------------------------------------------------------
1509
+
1510
+ async function walletCommand() {
1511
+ // CLI: argv = [node, index.js, wallet, <subcmd>]
1512
+ let subcmd = process.argv[2];
1513
+ if (subcmd === 'wallet.js' || subcmd === 'wallet') {
1514
+ subcmd = process.argv[3];
1515
+ }
1516
+
1517
+ const rl = createRl();
1518
+ try {
1519
+ if (!subcmd || subcmd === 'create') {
1520
+ await createWallet(rl);
1521
+ } else if (subcmd === 'list') {
1522
+ await listWallets(rl);
1523
+ } else if (subcmd === 'import') {
1524
+ await importWallet(rl);
1525
+ } else if (subcmd === 'default') {
1526
+ await defaultWallet(rl);
1527
+ } else if (subcmd === 'connect') {
1528
+ await connectWallet(rl);
1529
+ } else if (subcmd === 'balance') {
1530
+ await balanceWallet(rl);
1531
+ } else if (subcmd === 'stake') {
1532
+ await stakeWallet(rl);
1533
+ } else if (subcmd === 'stake-positions') {
1534
+ await stakePositions(rl);
1535
+ } else if (subcmd === 'export') {
1536
+ await exportWallet(rl);
1537
+ } else if (subcmd === 'unstake') {
1538
+ await unstakeWallet(rl);
1539
+ } else if (subcmd === 'transfer') {
1540
+ await transferWallet(rl);
1541
+ } else if (subcmd === 'history' || subcmd === 'tx') {
1542
+ await txHistory(rl);
1543
+ } else {
1544
+ console.log(`\n ${C.red}Unknown wallet subcommand:${C.reset} ${subcmd}`);
1545
+ console.log(`\n Usage:`);
1546
+ console.log(` ${C.cyan}aether wallet create${C.reset} Create new or import wallet`);
1547
+ console.log(` ${C.cyan}aether wallet list${C.reset} List all wallets`);
1548
+ console.log(` ${C.cyan}aether wallet import${C.reset} Import wallet from mnemonic`);
1549
+ console.log(` ${C.cyan}aether wallet export${C.reset} Export wallet data (pubkey, address) — --mnemonic to include phrase`);
1550
+ console.log(` ${C.cyan}aether wallet default${C.reset} Show/set default wallet`);
1551
+ console.log(` ${C.cyan}aether wallet connect${C.reset} Connect wallet via browser verification`);
1552
+ console.log(` ${C.cyan}aether wallet balance${C.reset} Query chain balance for an address`);
1553
+ console.log(` ${C.cyan}aether wallet stake${C.reset} Stake AETH to a validator`);
1554
+ console.log(` ${C.cyan}aether wallet stake-positions${C.reset} Show current stake delegations and rewards`);
1555
+ console.log(` ${C.cyan}aether wallet unstake${C.reset} Unstake AETH — deactivate a stake account`);
1556
+ console.log(` ${C.cyan}aether wallet transfer${C.reset} Transfer AETH to another address`);
1557
+ console.log(` ${C.cyan}aether wallet history${C.reset} Show recent transactions for an address`);
1558
+ console.log();
1559
+ process.exit(1);
1560
+ }
1561
+ } finally {
1562
+ rl.close();
1563
+ }
1564
+ }
1565
+
1566
+ module.exports = { walletCommand };
1567
+
1568
+ if (require.main === module) {
1569
+ walletCommand();
1570
+ }