@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,597 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli unstake
4
+ *
5
+ * Unstake AETH from a validator — deactivate a stake account and begin cooldown.
6
+ * Fully wired to @jellylegsai/aether-sdk for real blockchain RPC calls.
7
+ *
8
+ * Usage:
9
+ * aether unstake --address <wallet> [--account <stakeAcct>] [--amount <aeth>]
10
+ * aether unstake --address ATHxxx... --account Stakexxx... --amount 100
11
+ * aether unstake --address ATHxxx... --json
12
+ *
13
+ * SDK wired to:
14
+ * - client.getSlot() → GET /v1/slot
15
+ * - client.getStakePositions() → GET /v1/stake/<addr>
16
+ * - client.getAccountInfo() → GET /v1/account/<addr>
17
+ * - client.sendTransaction() → POST /v1/transaction
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const os = require('os');
23
+ const readline = require('readline');
24
+ const nacl = require('tweetnacl');
25
+ const bs58 = require('bs58').default;
26
+ const bip39 = require('bip39');
27
+
28
+ // Import SDK for real blockchain RPC calls
29
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
30
+ const aether = require(sdkPath);
31
+
32
+ // ANSI colours
33
+ const C = {
34
+ reset: '\x1b[0m',
35
+ bright: '\x1b[1m',
36
+ dim: '\x1b[2m',
37
+ red: '\x1b[31m',
38
+ green: '\x1b[32m',
39
+ yellow: '\x1b[33m',
40
+ cyan: '\x1b[36m',
41
+ magenta: '\x1b[35m',
42
+ };
43
+
44
+ const CLI_VERSION = '1.0.0';
45
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
46
+
47
+ // ============================================================================
48
+ // SDK Setup
49
+ // ============================================================================
50
+
51
+ function getDefaultRpc() {
52
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
53
+ }
54
+
55
+ function createClient(rpcUrl) {
56
+ return new aether.AetherClient({ rpcUrl });
57
+ }
58
+
59
+ // ============================================================================
60
+ // Config & Wallet
61
+ // ============================================================================
62
+
63
+ function getAetherDir() {
64
+ return path.join(os.homedir(), '.aether');
65
+ }
66
+
67
+ function getConfigPath() {
68
+ return path.join(getAetherDir(), 'config.json');
69
+ }
70
+
71
+ function loadConfig() {
72
+ if (!fs.existsSync(getConfigPath())) {
73
+ return { defaultWallet: null };
74
+ }
75
+ try {
76
+ return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
77
+ } catch {
78
+ return { defaultWallet: null };
79
+ }
80
+ }
81
+
82
+ function loadWallet(address) {
83
+ const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
84
+ if (!fs.existsSync(fp)) return null;
85
+ try {
86
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ // ============================================================================
93
+ // Crypto Helpers
94
+ // ============================================================================
95
+
96
+ function deriveKeypair(mnemonic) {
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
+ function formatAddress(publicKey) {
110
+ return 'ATH' + bs58.encode(publicKey);
111
+ }
112
+
113
+ function signTransaction(tx, secretKey) {
114
+ const txBytes = Buffer.from(JSON.stringify(tx));
115
+ const sig = nacl.sign.detached(txBytes, secretKey);
116
+ return bs58.encode(sig);
117
+ }
118
+
119
+ // ============================================================================
120
+ // Format Helpers
121
+ // ============================================================================
122
+
123
+ function formatAether(lamports) {
124
+ if (!lamports || lamports === '0') return '0 AETH';
125
+ const aeth = Number(lamports) / 1e9;
126
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
127
+ }
128
+
129
+ function shortAddress(addr) {
130
+ if (!addr || addr.length < 16) return addr || 'unknown';
131
+ return addr.slice(0, 8) + '...' + addr.slice(-8);
132
+ }
133
+
134
+ // ============================================================================
135
+ // Argument Parsing
136
+ // ============================================================================
137
+
138
+ function parseArgs() {
139
+ const args = process.argv.slice(2);
140
+ const opts = {
141
+ address: null,
142
+ stakeAccount: null,
143
+ amount: null,
144
+ rpc: getDefaultRpc(),
145
+ json: false,
146
+ dryRun: false,
147
+ force: false,
148
+ };
149
+
150
+ for (let i = 0; i < args.length; i++) {
151
+ const arg = args[i];
152
+ if (arg === '--address' || arg === '-a') {
153
+ opts.address = args[++i];
154
+ } else if (arg === '--account' || arg === '-s') {
155
+ opts.stakeAccount = args[++i];
156
+ } else if (arg === '--amount' || arg === '-m') {
157
+ const val = parseFloat(args[++i]);
158
+ if (!isNaN(val) && val > 0) {
159
+ opts.amount = val;
160
+ }
161
+ } else if (arg === '--rpc' || arg === '-r') {
162
+ opts.rpc = args[++i];
163
+ } else if (arg === '--json' || arg === '-j') {
164
+ opts.json = true;
165
+ } else if (arg === '--dry-run') {
166
+ opts.dryRun = true;
167
+ } else if (arg === '--force' || arg === '-f') {
168
+ opts.force = true;
169
+ } else if (arg === '--help' || arg === '-h') {
170
+ opts.help = true;
171
+ }
172
+ }
173
+
174
+ return opts;
175
+ }
176
+
177
+ function showHelp() {
178
+ console.log(`
179
+ ${C.bright}${C.cyan}aether-cli unstake${C.reset} — Unstake AETH from a validator
180
+
181
+ ${C.bright}USAGE${C.reset}
182
+ aether unstake --address <wallet> [--account <stakeAcct>] [--amount <aeth>]
183
+
184
+ ${C.bright}REQUIRED${C.reset}
185
+ --address <wallet> Wallet address with the stake account
186
+
187
+ ${C.bright}OPTIONS${C.reset}
188
+ --account <addr> Specific stake account to deactivate
189
+ --amount <aeth> Amount to unstake (default: full stake)
190
+ --rpc <url> RPC endpoint (default: $AETHER_RPC or localhost:8899)
191
+ --json Output JSON for scripting
192
+ --dry-run Preview unstake without submitting
193
+ --force Skip confirmation prompts
194
+ --help Show this help
195
+
196
+ ${C.bright}SDK METHODS USED${C.reset}
197
+ client.getSlot() → GET /v1/slot
198
+ client.getStakePositions() → GET /v1/stake/<addr>
199
+ client.getAccountInfo() → GET /v1/account/<addr>
200
+ client.sendTransaction() → POST /v1/transaction
201
+
202
+ ${C.bright}EXAMPLES${C.reset}
203
+ aether unstake --address ATHxxx...
204
+ aether unstake --address ATHxxx... --account Stakexxx... --amount 500
205
+ aether unstake --address ATHxxx... --json --dry-run
206
+
207
+ ${C.bright}NOTES${C.reset}
208
+ • Unstaking begins a cooldown period (typically 1-2 epochs)
209
+ • During cooldown, stake is "deactivating" and earns reduced rewards
210
+ • Once cooldown completes, use 'aether claim' to withdraw to wallet
211
+ `);
212
+ }
213
+
214
+ // ============================================================================
215
+ // Readline Helpers
216
+ // ============================================================================
217
+
218
+ function createRl() {
219
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
220
+ }
221
+
222
+ function question(rl, q) {
223
+ return new Promise((res) => rl.question(q, res));
224
+ }
225
+
226
+ async function askMnemonic(rl, promptText) {
227
+ console.log(`\n${C.cyan}${promptText}${C.reset}`);
228
+ console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
229
+ const raw = await question(rl, ` > ${C.reset}`);
230
+ return raw.trim().toLowerCase();
231
+ }
232
+
233
+ // ============================================================================
234
+ // Fetch Stake Accounts via SDK
235
+ // ============================================================================
236
+
237
+ async function fetchStakeAccounts(rpcUrl, walletAddress) {
238
+ const client = createClient(rpcUrl);
239
+ const rawAddr = walletAddress.startsWith('ATH') ? walletAddress.slice(3) : walletAddress;
240
+
241
+ try {
242
+ const stakePositions = await client.getStakePositions(rawAddr);
243
+ if (!Array.isArray(stakePositions)) return [];
244
+
245
+ return stakePositions.map(s => ({
246
+ address: s.pubkey || s.publicKey || s.account || s.stake_account,
247
+ validator: s.validator || s.delegate || s.vote_account,
248
+ lamports: s.lamports || s.stake_lamports || s.amount || 0,
249
+ status: s.status || s.state || 'active',
250
+ activationEpoch: s.activation_epoch || s.activationEpoch,
251
+ deactivationEpoch: s.deactivation_epoch || s.deactivationEpoch,
252
+ })).filter(s => s.address);
253
+ } catch (err) {
254
+ return [];
255
+ }
256
+ }
257
+
258
+ // ============================================================================
259
+ // Main Unstake Logic
260
+ // ============================================================================
261
+
262
+ async function unstakeCommand() {
263
+ const opts = parseArgs();
264
+ const rl = createRl();
265
+
266
+ if (opts.help) {
267
+ showHelp();
268
+ rl.close();
269
+ return;
270
+ }
271
+
272
+ // Resolve wallet address
273
+ if (!opts.address) {
274
+ const cfg = loadConfig();
275
+ opts.address = cfg.defaultWallet;
276
+ }
277
+
278
+ if (!opts.address) {
279
+ console.log(`\n ${C.red}✗ No wallet address provided.${C.reset}`);
280
+ console.log(` ${C.dim}Usage: aether unstake --address <addr> [--account <stakeAcct>]${C.reset}\n`);
281
+ rl.close();
282
+ process.exit(1);
283
+ }
284
+
285
+ const wallet = loadWallet(opts.address);
286
+ if (!wallet) {
287
+ console.log(`\n ${C.red}✗ Wallet not found locally:${C.reset} ${opts.address}`);
288
+ console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
289
+ rl.close();
290
+ process.exit(1);
291
+ }
292
+
293
+ const client = createClient(opts.rpc);
294
+
295
+ // Fetch stake accounts via SDK
296
+ if (!opts.json) {
297
+ console.log(`\n${C.bright}${C.cyan}── Unstake AETH ──────────────────────────────────────────${C.reset}\n`);
298
+ console.log(` ${C.dim}Fetching stake accounts via SDK...${C.reset}`);
299
+ }
300
+
301
+ const stakeAccounts = await fetchStakeAccounts(opts.rpc, opts.address);
302
+
303
+ if (stakeAccounts.length === 0) {
304
+ if (opts.json) {
305
+ console.log(JSON.stringify({
306
+ success: false,
307
+ error: 'No active stake accounts found',
308
+ address: opts.address,
309
+ suggestion: 'Stake AETH first with: aether stake --validator <addr> --amount <aeth>',
310
+ }, null, 2));
311
+ } else {
312
+ console.log(`\n ${C.yellow}⚠ No active stake accounts found.${C.reset}`);
313
+ console.log(` ${C.dim} Stake AETH first: aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
314
+ }
315
+ rl.close();
316
+ process.exit(1);
317
+ }
318
+
319
+ // Filter for active stakes only (can't unstake already deactivating)
320
+ const activeStakes = stakeAccounts.filter(s =>
321
+ s.status === 'active' && !s.deactivationEpoch
322
+ );
323
+
324
+ if (activeStakes.length === 0) {
325
+ if (opts.json) {
326
+ console.log(JSON.stringify({
327
+ success: false,
328
+ error: 'No active stake positions to unstake',
329
+ address: opts.address,
330
+ stake_accounts: stakeAccounts,
331
+ }, null, 2));
332
+ } else {
333
+ console.log(`\n ${C.yellow}⚠ No active stake positions to unstake.${C.reset}`);
334
+ console.log(` ${C.dim} Current status:${C.reset}`);
335
+ stakeAccounts.forEach(s => {
336
+ const status = s.deactivationEpoch ? 'deactivating' : s.status;
337
+ console.log(` ${shortAddress(s.address)} → ${status}`);
338
+ });
339
+ console.log();
340
+ }
341
+ rl.close();
342
+ process.exit(1);
343
+ }
344
+
345
+ // Select stake account
346
+ let selectedStake = null;
347
+
348
+ if (opts.stakeAccount) {
349
+ // User specified a stake account
350
+ selectedStake = activeStakes.find(s =>
351
+ s.address === opts.stakeAccount ||
352
+ s.address.endsWith(opts.stakeAccount)
353
+ );
354
+ if (!selectedStake) {
355
+ console.log(`\n ${C.red}✗ Stake account not found or not active:${C.reset} ${opts.stakeAccount}`);
356
+ console.log(` ${C.dim}Active stake accounts:${C.reset}`);
357
+ activeStakes.forEach((s, i) => {
358
+ console.log(` ${i + 1}) ${shortAddress(s.address)} → ${formatAether(s.lamports)}`);
359
+ });
360
+ console.log();
361
+ rl.close();
362
+ process.exit(1);
363
+ }
364
+ } else if (activeStakes.length === 1) {
365
+ // Only one active stake, use it
366
+ selectedStake = activeStakes[0];
367
+ } else {
368
+ // Multiple active stakes - prompt user to select
369
+ if (opts.json) {
370
+ console.log(JSON.stringify({
371
+ success: false,
372
+ error: 'Multiple active stake accounts found. Use --account to specify.',
373
+ address: opts.address,
374
+ active_stakes: activeStakes.map(s => ({
375
+ address: s.address,
376
+ validator: s.validator,
377
+ lamports: s.lamports,
378
+ aeth: formatAether(s.lamports),
379
+ })),
380
+ }, null, 2));
381
+ rl.close();
382
+ process.exit(1);
383
+ }
384
+
385
+ console.log(`\n ${C.bright}Multiple active stake accounts found:${C.reset}\n`);
386
+ activeStakes.forEach((s, i) => {
387
+ const val = s.validator ? shortAddress(s.validator) : 'unknown';
388
+ console.log(` ${C.green}${i + 1})${C.reset} ${shortAddress(s.address)}`);
389
+ console.log(` Validator: ${C.cyan}${val}${C.reset} Amount: ${C.bright}${formatAether(s.lamports)}${C.reset}`);
390
+ });
391
+ console.log();
392
+
393
+ const choice = await question(rl, ` ${C.cyan}Select account [1-${activeStakes.length}]:${C.reset} `);
394
+ const idx = parseInt(choice.trim(), 10) - 1;
395
+
396
+ if (isNaN(idx) || idx < 0 || idx >= activeStakes.length) {
397
+ console.log(`\n ${C.red}✗ Invalid selection.${C.reset}\n`);
398
+ rl.close();
399
+ process.exit(1);
400
+ }
401
+
402
+ selectedStake = activeStakes[idx];
403
+ }
404
+
405
+ // Determine unstake amount
406
+ let unstakeLamports = selectedStake.lamports;
407
+ if (opts.amount) {
408
+ const requestedLamports = Math.round(opts.amount * 1e9);
409
+ if (requestedLamports > selectedStake.lamports) {
410
+ console.log(`\n ${C.red}✗ Requested amount exceeds staked balance.${C.reset}`);
411
+ console.log(` ${C.dim} Requested: ${formatAether(requestedLamports)}${C.reset}`);
412
+ console.log(` ${C.dim} Staked: ${formatAether(selectedStake.lamports)}${C.reset}\n`);
413
+ rl.close();
414
+ process.exit(1);
415
+ }
416
+ unstakeLamports = requestedLamports;
417
+ }
418
+
419
+ // Display summary
420
+ if (!opts.json) {
421
+ console.log(`\n ${C.green}★${C.reset} Wallet: ${C.bright}${opts.address}${C.reset}`);
422
+ console.log(` ${C.green}★${C.reset} Stake acct: ${C.bright}${shortAddress(selectedStake.address)}${C.reset}`);
423
+ console.log(` ${C.green}★${C.reset} Validator: ${C.bright}${shortAddress(selectedStake.validator || 'unknown')}${C.reset}`);
424
+ console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${opts.amount ? formatAether(unstakeLamports) : 'FULL STAKE'}${C.reset}`);
425
+ console.log(` ${C.dim}(${unstakeLamports.toLocaleString()} lamports)${C.reset}`);
426
+ console.log();
427
+ }
428
+
429
+ if (opts.dryRun) {
430
+ if (opts.json) {
431
+ console.log(JSON.stringify({
432
+ dry_run: true,
433
+ wallet: opts.address,
434
+ stake_account: selectedStake.address,
435
+ validator: selectedStake.validator,
436
+ unstake_lamports: unstakeLamports,
437
+ unstake_aeth: formatAether(unstakeLamports),
438
+ current_stake_lamports: selectedStake.lamports,
439
+ current_stake_aeth: formatAether(selectedStake.lamports),
440
+ rpc: opts.rpc,
441
+ cli_version: CLI_VERSION,
442
+ timestamp: new Date().toISOString(),
443
+ }, null, 2));
444
+ } else {
445
+ console.log(` ${C.yellow}⚠ Dry run mode - no transaction submitted${C.reset}\n`);
446
+ }
447
+ rl.close();
448
+ return;
449
+ }
450
+
451
+ // Get mnemonic for signing
452
+ if (!opts.json) {
453
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}\n`);
454
+ }
455
+
456
+ let keyPair;
457
+ try {
458
+ const mnemonic = await askMnemonic(rl, 'Enter your wallet passphrase to sign the unstake');
459
+ keyPair = deriveKeypair(mnemonic);
460
+
461
+ // Verify derived address matches
462
+ const derivedAddress = formatAddress(keyPair.publicKey);
463
+ if (derivedAddress !== opts.address) {
464
+ if (opts.json) {
465
+ console.log(JSON.stringify({ success: false, error: 'Passphrase mismatch' }, null, 2));
466
+ } else {
467
+ console.log(`\n ${C.red}✗ Passphrase mismatch!${C.reset}`);
468
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
469
+ console.log(` ${C.dim} Expected: ${opts.address}${C.reset}\n`);
470
+ }
471
+ rl.close();
472
+ process.exit(1);
473
+ }
474
+ } catch (e) {
475
+ if (opts.json) {
476
+ console.log(JSON.stringify({ success: false, error: e.message }, null, 2));
477
+ } else {
478
+ console.log(`\n ${C.red}✗ Failed to derive keypair:${C.reset} ${e.message}\n`);
479
+ }
480
+ rl.close();
481
+ process.exit(1);
482
+ }
483
+
484
+ // Confirm transaction
485
+ if (!opts.json && !opts.force) {
486
+ const confirm = await question(rl, ` ${C.yellow}Confirm unstake? [y/N]${C.reset} > `);
487
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
488
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
489
+ rl.close();
490
+ return;
491
+ }
492
+ console.log();
493
+ }
494
+
495
+ rl.close();
496
+
497
+ // Build unstake transaction
498
+ const rawWalletAddr = opts.address.startsWith('ATH') ? opts.address.slice(3) : opts.address;
499
+
500
+ // Fetch current slot via SDK
501
+ let currentSlot = 0;
502
+ try {
503
+ currentSlot = await client.getSlot();
504
+ } catch (e) {
505
+ // Continue with slot 0
506
+ }
507
+
508
+ const tx = {
509
+ signer: rawWalletAddr,
510
+ tx_type: 'Unstake',
511
+ payload: {
512
+ type: 'Unstake',
513
+ data: {
514
+ stake_account: selectedStake.address,
515
+ amount: unstakeLamports,
516
+ validator: selectedStake.validator,
517
+ },
518
+ },
519
+ fee: 5000,
520
+ slot: currentSlot,
521
+ timestamp: Math.floor(Date.now() / 1000),
522
+ };
523
+
524
+ // Sign transaction
525
+ tx.signature = signTransaction(tx, keyPair.secretKey);
526
+
527
+ if (!opts.json) {
528
+ console.log(` ${C.dim}Submitting unstake via SDK to ${opts.rpc}...${C.reset}`);
529
+ }
530
+
531
+ // Submit via SDK
532
+ try {
533
+ const result = await client.sendTransaction(tx);
534
+
535
+ if (result.error) {
536
+ throw new Error(result.error.message || JSON.stringify(result.error));
537
+ }
538
+
539
+ if (opts.json) {
540
+ console.log(JSON.stringify({
541
+ success: true,
542
+ wallet: opts.address,
543
+ stake_account: selectedStake.address,
544
+ validator: selectedStake.validator,
545
+ unstake_lamports: unstakeLamports,
546
+ unstake_aeth: formatAether(unstakeLamports),
547
+ tx_signature: result.signature || result.txid,
548
+ slot: result.slot || currentSlot,
549
+ rpc: opts.rpc,
550
+ cli_version: CLI_VERSION,
551
+ timestamp: new Date().toISOString(),
552
+ }, null, 2));
553
+ } else {
554
+ console.log(`\n ${C.green}✓ Unstake transaction submitted!${C.reset}\n`);
555
+ console.log(` ${C.dim}Stake Account:${C.reset} ${shortAddress(selectedStake.address)}`);
556
+ console.log(` ${C.dim}Amount: ${C.reset}${C.bright}${formatAether(unstakeLamports)}${C.reset}`);
557
+ if (result.signature || result.txid) {
558
+ console.log(` ${C.dim}Signature: ${C.reset}${C.cyan}${(result.signature || result.txid).slice(0, 40)}...${C.reset}`);
559
+ }
560
+ console.log(` ${C.dim}Slot: ${C.reset}${result.slot || currentSlot}`);
561
+ console.log();
562
+ console.log(` ${C.yellow}⚠ Cooldown period started${C.reset}`);
563
+ console.log(` ${C.dim} Your stake is now deactivating. Rewards will be reduced during cooldown.${C.reset}`);
564
+ console.log(` ${C.dim} Check status: aether stake-positions --address ${opts.address}${C.reset}`);
565
+ console.log(` ${C.dim} After cooldown: aether claim --address ${opts.address}${C.reset}\n`);
566
+ }
567
+ } catch (err) {
568
+ if (opts.json) {
569
+ console.log(JSON.stringify({
570
+ success: false,
571
+ error: err.message,
572
+ wallet: opts.address,
573
+ stake_account: selectedStake.address,
574
+ }, null, 2));
575
+ } else {
576
+ console.log(`\n ${C.red}✗ Unstake failed:${C.reset} ${err.message}\n`);
577
+ console.log(` ${C.dim}Common causes:${C.reset}`);
578
+ console.log(` • Stake account already deactivating`);
579
+ console.log(` • Insufficient balance for transaction fee`);
580
+ console.log(` • RPC endpoint not accepting transactions\n`);
581
+ }
582
+ process.exit(1);
583
+ }
584
+ }
585
+
586
+ // ============================================================================
587
+ // Entry Point
588
+ // ============================================================================
589
+
590
+ module.exports = { unstakeCommand };
591
+
592
+ if (require.main === module) {
593
+ unstakeCommand().catch(err => {
594
+ console.error(`\n${C.red}✗ Unstake command failed:${C.reset} ${err.message}\n`);
595
+ process.exit(1);
596
+ });
597
+ }