@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,931 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli rewards
4
+ *
5
+ * View staking rewards earned from delegated stake accounts.
6
+ * Shows accumulated rewards, estimated APY, and claimable amounts.
7
+ *
8
+ * FULLY WIRED TO SDK - Uses @jellylegsai/aether-sdk for all blockchain calls.
9
+ * No manual HTTP - all calls go through AetherClient with real RPC.
10
+ *
11
+ * Usage:
12
+ * aether rewards list --address <addr> List all rewards per stake account
13
+ * aether rewards list --address <addr> --json JSON output for scripting
14
+ * aether rewards claim --address <addr> --account <stakeAcct> [--json]
15
+ * aether rewards summary --address <addr> One-line summary of total rewards
16
+ * aether rewards compound --address <addr> [--account <stakeAcct>] [--json] Claim and auto-re-stake
17
+ *
18
+ * Requires AETHER_RPC env var or local node running (default: http://127.0.0.1:8899)
19
+ *
20
+ * SDK Methods Used:
21
+ * - client.getStakePositions(address) → GET /v1/stake/<addr>
22
+ * - client.getRewards(address) → GET /v1/rewards/<addr>
23
+ * - client.getEpochInfo() → GET /v1/epoch
24
+ * - client.getSlot() → GET /v1/slot
25
+ * - client.sendTransaction(tx) → POST /v1/transaction
26
+ */
27
+
28
+ const path = require('path');
29
+ const readline = require('readline');
30
+ const crypto = require('crypto');
31
+ const bs58 = require('bs58').default;
32
+ const bip39 = require('bip39');
33
+ const nacl = require('tweetnacl');
34
+
35
+ // Import SDK for ALL blockchain RPC calls
36
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
37
+ const aether = require(sdkPath);
38
+
39
+ // ANSI colours
40
+ const C = {
41
+ reset: '\x1b[0m',
42
+ bright: '\x1b[1m',
43
+ dim: '\x1b[2m',
44
+ red: '\x1b[31m',
45
+ green: '\x1b[32m',
46
+ yellow: '\x1b[33m',
47
+ cyan: '\x1b[36m',
48
+ magenta: '\x1b[35m',
49
+ };
50
+
51
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
52
+ const CLI_VERSION = '1.1.0';
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // SDK Client Setup
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function getDefaultRpc() {
59
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
60
+ }
61
+
62
+ function createClient(rpcUrl) {
63
+ return new aether.AetherClient({ rpcUrl });
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Paths & config
68
+ // ---------------------------------------------------------------------------
69
+
70
+ function getAetherDir() {
71
+ return path.join(require('os').homedir(), '.aether');
72
+ }
73
+
74
+ function loadConfig() {
75
+ const p = path.join(getAetherDir(), 'config.json');
76
+ if (!require('fs').existsSync(p)) return { defaultWallet: null };
77
+ try {
78
+ return JSON.parse(require('fs').readFileSync(p, 'utf8'));
79
+ } catch {
80
+ return { defaultWallet: null };
81
+ }
82
+ }
83
+
84
+ function loadWallet(address) {
85
+ const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
86
+ if (!require('fs').existsSync(fp)) return null;
87
+ return JSON.parse(require('fs').readFileSync(fp, 'utf8'));
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Crypto helpers
92
+ // ---------------------------------------------------------------------------
93
+
94
+ function deriveKeypair(mnemonic) {
95
+ if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic');
96
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
97
+ const seed32 = seedBuffer.slice(0, 32);
98
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
99
+ return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
100
+ }
101
+
102
+ function formatAddress(publicKey) {
103
+ return 'ATH' + bs58.encode(publicKey);
104
+ }
105
+
106
+ function signTransaction(tx, secretKey) {
107
+ const txBytes = Buffer.from(JSON.stringify(tx));
108
+ const sig = nacl.sign.detached(txBytes, secretKey);
109
+ return bs58.encode(sig);
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Format helpers
114
+ // ---------------------------------------------------------------------------
115
+
116
+ function formatAether(lamports) {
117
+ if (!lamports || lamports === '0') return '0 AETH';
118
+ const aeth = Number(lamports) / 1e9;
119
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
120
+ }
121
+
122
+ function formatAethFull(lamports) {
123
+ if (!lamports) return '0.000000 AETH';
124
+ return (Number(lamports) / 1e9).toFixed(6) + ' AETH';
125
+ }
126
+
127
+ function shortAddress(addr) {
128
+ if (!addr || addr.length < 20) return addr || 'unknown';
129
+ return addr.slice(0, 8) + '...' + addr.slice(-8);
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Rewards calculation via SDK (REAL RPC CALLS)
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * Fetch stake positions and calculate rewards using SDK
138
+ * Makes real RPC calls: getStakePositions, getRewards, getEpochInfo
139
+ */
140
+ async function fetchStakeRewards(rpcUrl, stakeAddress) {
141
+ const client = createClient(rpcUrl);
142
+
143
+ try {
144
+ // Parallel SDK calls for stake data and epoch info
145
+ const [stakePositions, rewards, epochInfo] = await Promise.all([
146
+ client.getStakePositions(stakeAddress).catch(() => []),
147
+ client.getRewards(stakeAddress).catch(() => ({ total: 0, pending: 0 })),
148
+ client.getEpochInfo().catch(() => ({ epoch: 0, slotsInEpoch: 432000, slotIndex: 0 })),
149
+ ]);
150
+
151
+ // Find the specific stake account in positions
152
+ const stakeData = stakePositions.find(s =>
153
+ (s.pubkey || s.publicKey || s.account) === stakeAddress
154
+ ) || stakePositions[0] || {};
155
+
156
+ const delegatedStake = BigInt(stakeData.lamports || stakeData.stake_lamports || 0);
157
+ const activationEpoch = stakeData.activation_epoch || stakeData.activationEpoch || 0;
158
+ const deactivationEpoch = stakeData.deactivation_epoch || stakeData.deactivationEpoch || null;
159
+ const validator = stakeData.validator || stakeData.delegate || rewards.validator || 'unknown';
160
+ const stakeType = stakeData.stake_type || stakeData.type || 'delegated';
161
+
162
+ const currentEpoch = epochInfo.epoch || 0;
163
+
164
+ // Calculate active epochs
165
+ const activeFromEpoch = activationEpoch;
166
+ const activeToEpoch = deactivationEpoch || currentEpoch;
167
+ const activeEpochs = Math.max(0, activeToEpoch - activeFromEpoch);
168
+
169
+ // Get rewards from SDK response
170
+ const totalRewards = BigInt(rewards.total || rewards.pending_rewards || rewards.amount || 0);
171
+ const pendingRewards = BigInt(rewards.pending || rewards.pending_rewards || 0);
172
+
173
+ // Calculate APY from rewards data
174
+ const rewardsPerEpoch = BigInt(rewards.rewards_per_epoch || '2000000000');
175
+ const totalNetworkStake = BigInt(rewards.total_network_stake || '10000000000000');
176
+ const rewardsRate = totalNetworkStake > 0
177
+ ? Number(rewardsPerEpoch * BigInt(365)) / Number(totalNetworkStake)
178
+ : 0;
179
+ const apyBps = Math.round(rewardsRate * 10000);
180
+
181
+ return {
182
+ stakeAddress,
183
+ delegatedStake: delegatedStake.toString(),
184
+ delegatedStakeFormatted: formatAether(delegatedStake),
185
+ activationEpoch: activeFromEpoch,
186
+ deactivationEpoch,
187
+ isActive: deactivationEpoch === null,
188
+ activeEpochs,
189
+ totalRewards: totalRewards.toString(),
190
+ pendingRewards: pendingRewards.toString(),
191
+ totalRewardsFormatted: formatAether(totalRewards),
192
+ pendingRewardsFormatted: formatAether(pendingRewards),
193
+ apyBps,
194
+ validator,
195
+ stakeType,
196
+ currentEpoch,
197
+ };
198
+ } catch (err) {
199
+ return { stakeAddress, error: err.message };
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Fetch all stake accounts for a wallet using SDK
205
+ * REAL RPC CALL: GET /v1/stake/<address>
206
+ */
207
+ async function fetchWalletStakeAccounts(rpcUrl, walletAddress) {
208
+ const client = createClient(rpcUrl);
209
+
210
+ try {
211
+ const rawAddr = walletAddress.startsWith('ATH') ? walletAddress.slice(3) : walletAddress;
212
+ const stakePositions = await client.getStakePositions(rawAddr);
213
+
214
+ if (!Array.isArray(stakePositions)) return [];
215
+
216
+ return stakePositions.map(s => ({
217
+ address: s.pubkey || s.publicKey || s.account,
218
+ validator: s.validator || s.delegate,
219
+ lamports: s.lamports || s.stake_lamports || 0,
220
+ activationEpoch: s.activation_epoch || s.activationEpoch,
221
+ deactivationEpoch: s.deactivation_epoch || s.deactivationEpoch,
222
+ status: s.status || s.state || 'active',
223
+ })).filter(s => s.address);
224
+ } catch (err) {
225
+ return [];
226
+ }
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Rewards list command - FULLY WIRED TO SDK
231
+ // ---------------------------------------------------------------------------
232
+
233
+ async function rewardsList(args) {
234
+ const rpc = args.rpc || getDefaultRpc();
235
+ const isJson = args.json || false;
236
+ let address = args.address || null;
237
+
238
+ // Interactive address prompt if not provided
239
+ if (!address) {
240
+ const config = loadConfig();
241
+ const rl = createRl();
242
+ const answer = await question(rl, `\n${C.cyan}Enter wallet address (or press Enter for default): ${C.reset}`);
243
+ rl.close();
244
+
245
+ if (!answer.trim()) {
246
+ if (!config.defaultWallet) {
247
+ console.log(`\n${C.red}✗ No default wallet and no address provided.${C.reset}`);
248
+ console.log(` ${C.dim}Set a default wallet first: aether wallet default${C.reset}\n`);
249
+ return;
250
+ }
251
+ address = config.defaultWallet;
252
+ } else {
253
+ address = answer.trim();
254
+ }
255
+ }
256
+
257
+ // Validate address format
258
+ if (!address.startsWith('ATH') || address.length < 30) {
259
+ const config = loadConfig();
260
+ if (config.defaultWallet) address = config.defaultWallet;
261
+ }
262
+
263
+ console.log(`\n${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
264
+ console.log(`${C.bright}${C.cyan}║ Staking Rewards — ${shortAddress(address)} ║${C.reset}`);
265
+ console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
266
+ console.log(` ${C.dim}RPC: ${rpc}${C.reset}\n`);
267
+
268
+ // Fetch stake accounts via SDK (REAL RPC)
269
+ const stakeAccounts = await fetchWalletStakeAccounts(rpc, address);
270
+
271
+ if (stakeAccounts.length === 0) {
272
+ console.log(` ${C.yellow}⚠ No stake accounts found for this wallet.${C.reset}`);
273
+ console.log(` ${C.dim}Stake AETH first: aether stake --address ${address} --validator <val> --amount <aeth>${C.reset}\n`);
274
+ return;
275
+ }
276
+
277
+ // Fetch rewards for each stake account via SDK (REAL RPC CALLS)
278
+ const rewardsResults = await Promise.all(
279
+ stakeAccounts.map(sa => fetchStakeRewards(rpc, sa.address))
280
+ );
281
+
282
+ let totalEstimatedRewards = BigInt(0);
283
+ let totalPendingRewards = BigInt(0);
284
+ let totalDelegatedStake = BigInt(0);
285
+ let activeCount = 0;
286
+ const rows = [];
287
+
288
+ for (const result of rewardsResults) {
289
+ if (result.error) {
290
+ rows.push({ status: 'error', ...result });
291
+ continue;
292
+ }
293
+
294
+ totalEstimatedRewards += BigInt(result.totalRewards || 0);
295
+ totalPendingRewards += BigInt(result.pendingRewards || 0);
296
+ totalDelegatedStake += BigInt(result.delegatedStake || 0);
297
+ if (result.isActive) activeCount++;
298
+
299
+ rows.push(result);
300
+ }
301
+
302
+ if (isJson) {
303
+ console.log(JSON.stringify({
304
+ address,
305
+ rpc,
306
+ totalRewards: totalEstimatedRewards.toString(),
307
+ totalRewardsFormatted: formatAether(totalEstimatedRewards),
308
+ totalPendingRewards: totalPendingRewards.toString(),
309
+ totalPendingRewardsFormatted: formatAether(totalPendingRewards),
310
+ totalDelegatedStake: totalDelegatedStake.toString(),
311
+ totalDelegatedStakeFormatted: formatAether(totalDelegatedStake),
312
+ activeStakeAccounts: activeCount,
313
+ totalStakeAccounts: rows.length,
314
+ stakeAccounts: rows.map(r => ({
315
+ stakeAccount: r.stakeAddress,
316
+ validator: r.validator,
317
+ delegatedStake: r.delegatedStake,
318
+ delegatedStakeFormatted: r.delegatedStakeFormatted,
319
+ totalRewards: r.totalRewards,
320
+ totalRewardsFormatted: r.totalRewardsFormatted,
321
+ pendingRewards: r.pendingRewards,
322
+ pendingRewardsFormatted: r.pendingRewardsFormatted,
323
+ apyBps: r.apyBps,
324
+ isActive: r.isActive,
325
+ activationEpoch: r.activationEpoch,
326
+ currentEpoch: r.currentEpoch,
327
+ })),
328
+ cli_version: CLI_VERSION,
329
+ fetched_at: new Date().toISOString(),
330
+ }, null, 2));
331
+ return;
332
+ }
333
+
334
+ // ASCII table header
335
+ console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────────────────┐${C.reset}`);
336
+ console.log(` ${C.dim}│${C.reset} ${C.bright}Stake Account${C.reset} ${C.bright}Validator${C.reset} ${C.bright}Delegated${C.reset} ${C.bright}Total Rewards${C.reset} ${C.bright}APY${C.reset} ${C.dim}│${C.reset}`);
337
+ console.log(` ${C.dim}├─────────────────────────────────────────────────────────────────────────┤${C.reset}`);
338
+
339
+ for (const r of rows) {
340
+ const shortAddr = shortAddress(r.stakeAddress);
341
+ const shortVal = shortAddress(r.validator);
342
+ const delegated = r.delegatedStakeFormatted || '—';
343
+ const totalRew = r.totalRewardsFormatted || '—';
344
+ const apy = r.apyBps ? `${(r.apyBps / 100).toFixed(2)}%` : '—';
345
+ const statusColor = r.isActive ? C.green : r.deactivationEpoch ? C.yellow : C.red;
346
+ const status = r.isActive ? '●' : r.deactivationEpoch ? '○' : '✗';
347
+
348
+ console.log(
349
+ ` ${C.dim}│${C.reset} ${shortAddr.padEnd(18)} ${shortVal.padEnd(14)} ${delegated.padEnd(11)} ${totalRew.padEnd(13)} ${apy.padEnd(6)} ${statusColor}${status}${C.reset} ${C.dim}│${C.reset}`
350
+ );
351
+ }
352
+
353
+ console.log(` ${C.dim}└─────────────────────────────────────────────────────────────────────────┘${C.reset}`);
354
+ console.log();
355
+ console.log(` ${C.bright}Total Delegated:${C.reset} ${C.cyan}${formatAether(totalDelegatedStake)}${C.reset}`);
356
+ console.log(` ${C.bright}Total Rewards:${C.reset} ${C.green}${formatAether(totalEstimatedRewards)}${C.reset}`);
357
+ console.log(` ${C.bright}Pending Rewards:${C.reset} ${C.magenta}${formatAether(totalPendingRewards)}${C.reset}`);
358
+ console.log(` ${C.bright}Active Accounts:${C.reset} ${activeCount} of ${rows.length}`);
359
+ console.log();
360
+ console.log(` ${C.dim}SDK Methods: getStakePositions(), getRewards(), getEpochInfo()${C.reset}`);
361
+ console.log(` ${C.dim}Run "aether rewards claim --address ${address}" to claim pending rewards.${C.reset}\n`);
362
+ }
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Rewards summary command - SDK WIRED
366
+ // ---------------------------------------------------------------------------
367
+
368
+ async function rewardsSummary(args) {
369
+ const rpc = args.rpc || getDefaultRpc();
370
+ let address = args.address || null;
371
+
372
+ if (!address) {
373
+ const config = loadConfig();
374
+ if (!config.defaultWallet) {
375
+ console.log(`${C.red}✗ No default wallet and no address provided.${C.reset}`);
376
+ return;
377
+ }
378
+ address = config.defaultWallet;
379
+ }
380
+
381
+ // SDK calls
382
+ const stakeAccounts = await fetchWalletStakeAccounts(rpc, address);
383
+ if (stakeAccounts.length === 0) {
384
+ console.log(`${C.yellow}⚠ No stake accounts for ${shortAddress(address)}${C.reset}`);
385
+ return;
386
+ }
387
+
388
+ const results = await Promise.all(stakeAccounts.map(sa => fetchStakeRewards(rpc, sa.address)));
389
+ let totalRewards = BigInt(0);
390
+ let totalPending = BigInt(0);
391
+ let totalStake = BigInt(0);
392
+ let activeCount = 0;
393
+
394
+ for (const r of results) {
395
+ if (!r.error) {
396
+ totalRewards += BigInt(r.totalRewards || 0);
397
+ totalPending += BigInt(r.pendingRewards || 0);
398
+ totalStake += BigInt(r.delegatedStake || 0);
399
+ if (r.isActive) activeCount++;
400
+ }
401
+ }
402
+
403
+ console.log(`${C.cyan}${shortAddress(address)}${C.reset} │ Stake: ${C.cyan}${formatAether(totalStake)}${C.reset} │ Total Rewards: ${C.green}${formatAether(totalRewards)}${C.reset} │ Pending: ${C.magenta}${formatAether(totalPending)}${C.reset} │ Active: ${activeCount}/${results.length}`);
404
+ }
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Rewards pending command - SDK WIRED
408
+ // ---------------------------------------------------------------------------
409
+
410
+ async function rewardsPending(args) {
411
+ const rpc = args.rpc || getDefaultRpc();
412
+ const isJson = args.json || false;
413
+ let address = args.address || null;
414
+
415
+ const config = loadConfig();
416
+ const rl = createRl();
417
+
418
+ if (!address) {
419
+ const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
420
+ address = ans.trim();
421
+ }
422
+
423
+ if (!address) {
424
+ console.log(`\n${C.red}✗ No address provided.${C.reset}\n`);
425
+ rl.close();
426
+ return;
427
+ }
428
+
429
+ rl.close();
430
+
431
+ // SDK calls
432
+ const stakeAccounts = await fetchWalletStakeAccounts(rpc, address);
433
+ if (stakeAccounts.length === 0) {
434
+ if (isJson) {
435
+ console.log(JSON.stringify({ address, pending: [], total_pending: '0', sdk_version: CLI_VERSION }, null, 2));
436
+ } else {
437
+ console.log(`\n${C.red}✗ No stake accounts found for ${address}${C.reset}\n`);
438
+ }
439
+ return;
440
+ }
441
+
442
+ const results = [];
443
+ let totalPending = BigInt(0);
444
+
445
+ // SDK calls for each stake account
446
+ for (const sa of stakeAccounts) {
447
+ const rd = await fetchStakeRewards(rpc, sa.address);
448
+ if (!rd.error) {
449
+ const pending = BigInt(rd.pendingRewards || 0);
450
+ totalPending += pending;
451
+ results.push({
452
+ stake_account: sa.address,
453
+ validator: sa.validator || rd.validator || 'unknown',
454
+ delegated_stake: rd.delegatedStakeFormatted || '0',
455
+ pending_rewards: rd.pendingRewardsFormatted || '0',
456
+ pending_lamports: pending.toString(),
457
+ apy_bps: rd.apyBps || 0,
458
+ is_active: rd.isActive,
459
+ });
460
+ }
461
+ }
462
+
463
+ if (isJson) {
464
+ console.log(JSON.stringify({
465
+ address,
466
+ rpc,
467
+ total_pending: totalPending.toString(),
468
+ total_pending_formatted: formatAether(totalPending.toString()),
469
+ accounts: results,
470
+ cli_version: CLI_VERSION,
471
+ fetched_at: new Date().toISOString(),
472
+ }, null, 2));
473
+ return;
474
+ }
475
+
476
+ console.log(`\n${C.bright}${C.cyan}╔══════════════════════════════════════════════════════════════╗${C.reset}`);
477
+ console.log(`${C.bright}${C.cyan}║ Pending Staking Rewards (SDK-Wired) ║${C.reset}`);
478
+ console.log(`${C.bright}${C.cyan}╚══════════════════════════════════════════════════════════════╝${C.reset}\n`);
479
+ console.log(` ${C.dim}Wallet:${C.reset} ${C.bright}${address}${C.reset}`);
480
+ console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
481
+ console.log();
482
+ console.log(` ${C.yellow}Stake Account${C.reset.padEnd(48)} ${C.yellow}Pending${C.reset} ${C.yellow}APY${C.reset}`);
483
+ console.log(` ${C.dim}${'─'.repeat(72)}${C.reset}`);
484
+
485
+ for (const r of results) {
486
+ const shortSa = shortAddress(r.stake_account);
487
+ console.log(` ${C.cyan}${shortSa}${C.reset.padEnd(52)} ${C.green}${r.pending_rewards.padStart(12)}${C.reset} ${(r.apy_bps / 100).toFixed(2)}%`);
488
+ }
489
+
490
+ console.log(` ${C.dim}${'─'.repeat(72)}${C.reset}`);
491
+ console.log(` ${C.bright}TOTAL PENDING${C.reset.padEnd(52)} ${C.magenta}${formatAethFull(totalPending.toString()).padStart(12)}${C.reset}`);
492
+ console.log();
493
+ console.log(` ${C.dim}SDK: getStakePositions(), getRewards()${C.reset}`);
494
+ console.log(` ${C.dim}Run ${C.cyan}aether rewards claim --address ${address}${C.dim} to claim.${C.reset}\n`);
495
+ }
496
+
497
+ // ---------------------------------------------------------------------------
498
+ // Rewards claim command - SDK WIRED with sendTransaction
499
+ // ---------------------------------------------------------------------------
500
+
501
+ async function rewardsClaim(args) {
502
+ const rpc = args.rpc || getDefaultRpc();
503
+ const isJson = args.json || false;
504
+ let address = args.address || null;
505
+ let stakeAccount = args.account || null;
506
+
507
+ const config = loadConfig();
508
+ const rl = createRl();
509
+
510
+ if (!address) {
511
+ const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
512
+ address = ans.trim();
513
+ }
514
+
515
+ if (!stakeAccount) {
516
+ // SDK call to fetch stake accounts
517
+ const stakeAccounts = await fetchWalletStakeAccounts(rpc, address);
518
+ if (stakeAccounts.length === 0) {
519
+ console.log(`\n${C.red}✗ No stake accounts found for this wallet.${C.reset}\n`);
520
+ rl.close();
521
+ return;
522
+ }
523
+ if (stakeAccounts.length === 1) {
524
+ stakeAccount = stakeAccounts[0].address;
525
+ } else {
526
+ console.log(`\n${C.cyan}Select stake account:${C.reset}`);
527
+ stakeAccounts.forEach((sa, i) => {
528
+ console.log(` ${i + 1}) ${shortAddress(sa.address)} → ${shortAddress(sa.validator || 'unknown')}`);
529
+ });
530
+ const ans = await question(rl, `${C.cyan}Enter number: ${C.reset}`);
531
+ const idx = parseInt(ans.trim()) - 1;
532
+ if (idx < 0 || idx >= stakeAccounts.length) {
533
+ console.log(`\n${C.red}Invalid selection.${C.reset}\n`);
534
+ rl.close();
535
+ return;
536
+ }
537
+ stakeAccount = stakeAccounts[idx].address;
538
+ }
539
+ }
540
+
541
+ // Load wallet for signing
542
+ const wallet = loadWallet(address);
543
+ if (!wallet) {
544
+ console.log(`\n${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
545
+ console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
546
+ rl.close();
547
+ return;
548
+ }
549
+
550
+ console.log(`\n${C.bright}${C.cyan}╔════════════════════════════════════════╗${C.reset}`);
551
+ console.log(`${C.bright}${C.cyan}║ Claim Staking Rewards ║${C.reset}`);
552
+ console.log(`${C.bright}${C.cyan}╚════════════════════════════════════════╝${C.reset}\n`);
553
+ console.log(` ${C.dim}Wallet:${C.reset} ${address}`);
554
+ console.log(` ${C.dim}Stake Account:${C.reset} ${stakeAccount}`);
555
+
556
+ // SDK call to fetch current rewards
557
+ const client = createClient(rpc);
558
+ const rewardData = await fetchStakeRewards(rpc, stakeAccount);
559
+ if (rewardData.error) {
560
+ console.log(`\n${C.red}✗ Failed to fetch stake account: ${rewardData.error}${C.reset}\n`);
561
+ rl.close();
562
+ return;
563
+ }
564
+
565
+ console.log(` ${C.dim}Delegated Stake:${C.reset} ${rewardData.delegatedStakeFormatted}`);
566
+ console.log(` ${C.dim}Est. Pending Rewards:${C.reset} ${C.green}${rewardData.pendingRewardsFormatted}${C.reset}`);
567
+ console.log(` ${C.dim}Validator:${C.reset} ${rewardData.validator}`);
568
+ console.log(` ${C.dim}APY:${C.reset} ${(rewardData.apyBps / 100).toFixed(2)}%`);
569
+
570
+ const pendingRewards = BigInt(rewardData.pendingRewards || 0);
571
+ if (pendingRewards === BigInt(0)) {
572
+ console.log(`\n${C.yellow}⚠ No rewards accumulated yet.${C.reset}\n`);
573
+ rl.close();
574
+ return;
575
+ }
576
+
577
+ const confirm = await question(rl, `\n ${C.yellow}Claim ${rewardData.pendingRewardsFormatted}? [y/N]${C.reset} > `);
578
+ if (confirm.trim().toLowerCase() !== 'y') {
579
+ console.log(`${C.dim}Cancelled.${C.reset}\n`);
580
+ rl.close();
581
+ return;
582
+ }
583
+
584
+ // Ask for mnemonic to derive signing keypair
585
+ let keypair;
586
+ try {
587
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign the claim');
588
+ keypair = deriveKeypair(mnemonic);
589
+
590
+ // Verify derived address matches
591
+ const derivedAddress = formatAddress(keypair.publicKey);
592
+ if (derivedAddress !== address) {
593
+ console.log(`\n${C.red}✗ Passphrase mismatch!${C.reset}`);
594
+ console.log(` ${C.dim}Derived: ${derivedAddress}${C.reset}`);
595
+ console.log(` ${C.dim}Expected: ${address}${C.reset}\n`);
596
+ rl.close();
597
+ return;
598
+ }
599
+ } catch (err) {
600
+ console.log(`\n${C.red}✗ Failed to derive keypair: ${err.message}${C.reset}\n`);
601
+ rl.close();
602
+ return;
603
+ }
604
+
605
+ // Build claim transaction for SDK
606
+ const tx = {
607
+ signer: address.startsWith('ATH') ? address.slice(3) : address,
608
+ tx_type: 'ClaimRewards',
609
+ payload: {
610
+ type: 'ClaimRewards',
611
+ data: {
612
+ stake_account: stakeAccount,
613
+ lamports: pendingRewards.toString(),
614
+ },
615
+ },
616
+ fee: 5000,
617
+ slot: await client.getSlot().catch(() => 0),
618
+ timestamp: Math.floor(Date.now() / 1000),
619
+ };
620
+
621
+ // Sign transaction
622
+ tx.signature = signTransaction(tx, keypair.secretKey);
623
+
624
+ console.log(`\n ${C.dim}Submitting via SDK to ${rpc}...${C.reset}`);
625
+
626
+ // SDK call: sendTransaction (REAL RPC POST /v1/transaction)
627
+ try {
628
+ const result = await client.sendTransaction(tx);
629
+
630
+ if (result.error) {
631
+ throw new Error(result.error.message || JSON.stringify(result.error));
632
+ }
633
+
634
+ if (isJson) {
635
+ console.log(JSON.stringify({
636
+ success: true,
637
+ address,
638
+ stake_account: stakeAccount,
639
+ claimed_lamports: pendingRewards.toString(),
640
+ claimed_formatted: rewardData.pendingRewardsFormatted,
641
+ tx_signature: result.signature || result.txid,
642
+ slot: result.slot,
643
+ rpc,
644
+ cli_version: CLI_VERSION,
645
+ timestamp: new Date().toISOString(),
646
+ }, null, 2));
647
+ } else {
648
+ console.log(`\n${C.green}✓ Rewards claimed successfully!${C.reset}`);
649
+ console.log(` ${C.dim}TX Signature: ${C.cyan}${result.signature || result.txid}${C.reset}`);
650
+ console.log(` ${C.dim}Amount Claimed: ${C.green}${rewardData.pendingRewardsFormatted}${C.reset}`);
651
+ console.log(` ${C.dim}Slot: ${result.slot}${C.reset}`);
652
+ console.log(` ${C.dim}SDK Method: sendTransaction()${C.reset}`);
653
+ console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
654
+ }
655
+ } catch (err) {
656
+ if (isJson) {
657
+ console.log(JSON.stringify({ success: false, error: err.message, address, stake_account: stakeAccount }, null, 2));
658
+ } else {
659
+ console.log(`\n${C.red}✗ Failed to submit claim transaction: ${err.message}${C.reset}`);
660
+ console.log(` ${C.dim}The rewards are accumulated on-chain and can be claimed later.${C.reset}\n`);
661
+ }
662
+ }
663
+
664
+ rl.close();
665
+ }
666
+
667
+ // ---------------------------------------------------------------------------
668
+ // Rewards compound command - SDK WIRED
669
+ // ---------------------------------------------------------------------------
670
+
671
+ async function rewardsCompound(args) {
672
+ const rpc = args.rpc || getDefaultRpc();
673
+ const isJson = args.json || false;
674
+ let address = args.address || null;
675
+ let stakeAccount = args.account || null;
676
+
677
+ const config = loadConfig();
678
+ const rl = createRl();
679
+
680
+ if (!address) {
681
+ const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
682
+ address = ans.trim();
683
+ }
684
+
685
+ if (!address) {
686
+ console.log(`\n${C.red}✗ No address provided.${C.reset}\n`);
687
+ rl.close();
688
+ return;
689
+ }
690
+
691
+ // Load wallet for signing
692
+ const wallet = loadWallet(address);
693
+ if (!wallet) {
694
+ console.log(`\n${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
695
+ console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
696
+ rl.close();
697
+ return;
698
+ }
699
+
700
+ // SDK call to fetch stake accounts
701
+ let stakeAccounts = await fetchWalletStakeAccounts(rpc, address);
702
+ if (stakeAccounts.length === 0) {
703
+ console.log(`\n${C.red}✗ No stake accounts found for this wallet.${C.reset}\n`);
704
+ rl.close();
705
+ return;
706
+ }
707
+
708
+ // If --account specified, filter to that one
709
+ if (stakeAccount) {
710
+ stakeAccounts = stakeAccounts.filter(sa => sa.address === stakeAccount);
711
+ if (stakeAccounts.length === 0) {
712
+ console.log(`\n${C.red}✗ Stake account not found: ${stakeAccount}${C.reset}\n`);
713
+ rl.close();
714
+ return;
715
+ }
716
+ }
717
+
718
+ console.log(`\n${C.bright}${C.cyan}╔══════════════════════════════════════════════════════════════╗${C.reset}`);
719
+ console.log(`${C.bright}${C.cyan}║ Compound Staking Rewards (SDK-Wired) ║${C.reset}`);
720
+ console.log(`${C.bright}${C.cyan}╚══════════════════════════════════════════════════════════════╝${C.reset}\n`);
721
+ console.log(` ${C.dim}Wallet:${C.reset} ${C.bright}${address}${C.reset}`);
722
+ console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
723
+ console.log(` ${C.dim}Stake accounts to process:${C.reset} ${stakeAccounts.length}\n`);
724
+
725
+ // Ask for mnemonic upfront
726
+ console.log(`${C.yellow} ⚠ Compound requires your wallet passphrase to sign transactions.${C.reset}`);
727
+ let keypair;
728
+ try {
729
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase:');
730
+ keypair = deriveKeypair(mnemonic);
731
+
732
+ // Verify address matches
733
+ const derivedAddress = formatAddress(keypair.publicKey);
734
+ if (derivedAddress !== address) {
735
+ console.log(`\n${C.red}✗ Passphrase mismatch.${C.reset}`);
736
+ console.log(` ${C.dim}Derived: ${derivedAddress}${C.reset}`);
737
+ console.log(` ${C.dim}Expected: ${address}${C.reset}\n`);
738
+ rl.close();
739
+ return;
740
+ }
741
+ } catch (err) {
742
+ console.log(`\n${C.red}✗ Failed to derive keypair: ${err.message}${C.reset}\n`);
743
+ rl.close();
744
+ return;
745
+ }
746
+
747
+ const client = createClient(rpc);
748
+ const compoundResults = [];
749
+ let totalCompounded = BigInt(0);
750
+ let successCount = 0;
751
+
752
+ for (const sa of stakeAccounts) {
753
+ console.log(` ${C.dim}Processing:${C.reset} ${shortAddress(sa.address)}`);
754
+
755
+ try {
756
+ // SDK call to fetch rewards
757
+ const rewardData = await fetchStakeRewards(rpc, sa.address);
758
+ if (rewardData.error) {
759
+ console.log(` ${C.red}✗ Failed to fetch: ${rewardData.error}${C.reset}`);
760
+ compoundResults.push({ stake_account: sa.address, status: 'error', error: rewardData.error });
761
+ continue;
762
+ }
763
+
764
+ const estimatedRewards = BigInt(rewardData.pendingRewards || 0);
765
+ if (estimatedRewards === BigInt(0)) {
766
+ console.log(` ${C.yellow}⚠ No rewards to compound${C.reset}`);
767
+ compoundResults.push({ stake_account: sa.address, status: 'no_rewards', rewards: '0' });
768
+ continue;
769
+ }
770
+
771
+ console.log(` ${C.dim}Rewards:${C.reset} ${rewardData.pendingRewardsFormatted} → ${shortAddress(sa.validator || rewardData.validator || 'unknown')}`);
772
+
773
+ // Build compound transaction (ClaimRewards + Stake in one)
774
+ const tx = {
775
+ signer: address.startsWith('ATH') ? address.slice(3) : address,
776
+ tx_type: 'CompoundRewards',
777
+ payload: {
778
+ type: 'CompoundRewards',
779
+ data: {
780
+ stake_account: sa.address,
781
+ lamports: estimatedRewards.toString(),
782
+ validator: sa.validator || rewardData.validator,
783
+ },
784
+ },
785
+ fee: 5000,
786
+ slot: await client.getSlot().catch(() => 0),
787
+ timestamp: Math.floor(Date.now() / 1000),
788
+ };
789
+
790
+ // Sign transaction
791
+ tx.signature = signTransaction(tx, keypair.secretKey);
792
+
793
+ // SDK call: sendTransaction
794
+ const result = await client.sendTransaction(tx);
795
+
796
+ if (result.signature || result.txid || result.success) {
797
+ console.log(` ${C.green}✓ Compounded${C.reset}`);
798
+ totalCompounded += estimatedRewards;
799
+ successCount++;
800
+ compoundResults.push({
801
+ stake_account: sa.address,
802
+ status: 'compounded',
803
+ rewards: estimatedRewards.toString(),
804
+ rewards_formatted: rewardData.pendingRewardsFormatted,
805
+ tx: result.signature || result.txid,
806
+ });
807
+ } else {
808
+ console.log(` ${C.red}✗ Failed: ${result.error || 'Unknown error'}${C.reset}`);
809
+ compoundResults.push({ stake_account: sa.address, status: 'failed', error: result.error });
810
+ }
811
+ } catch (err) {
812
+ console.log(` ${C.red}✗ Error: ${err.message}${C.reset}`);
813
+ compoundResults.push({ stake_account: sa.address, status: 'error', error: err.message });
814
+ }
815
+ console.log();
816
+ }
817
+
818
+ rl.close();
819
+
820
+ // Summary
821
+ console.log(`${C.bright}${C.cyan}╔══════════════════════════════════════════════════════════════╗${C.reset}`);
822
+ console.log(`${C.bright}${C.cyan}║ Compound Summary ║${C.reset}`);
823
+ console.log(`${C.bright}${C.cyan}╚══════════════════════════════════════════════════════════════╝${C.reset}\n`);
824
+ console.log(` ${C.dim}Accounts processed:${C.reset} ${stakeAccounts.length}`);
825
+ console.log(` ${C.green}✓ Successful:${C.reset} ${successCount}`);
826
+ console.log(` ${C.dim}Total compounded:${C.reset} ${C.green}${formatAether(totalCompounded.toString())}${C.reset}`);
827
+ console.log(` ${C.dim}SDK: getStakePositions(), getRewards(), sendTransaction()${C.reset}\n`);
828
+
829
+ if (isJson) {
830
+ console.log(JSON.stringify({
831
+ address,
832
+ rpc,
833
+ total_compounded_lamports: totalCompounded.toString(),
834
+ total_compounded_formatted: formatAether(totalCompounded.toString()),
835
+ accounts_processed: stakeAccounts.length,
836
+ successful: successCount,
837
+ results: compoundResults,
838
+ cli_version: CLI_VERSION,
839
+ timestamp: new Date().toISOString(),
840
+ }, null, 2));
841
+ }
842
+ }
843
+
844
+ // ---------------------------------------------------------------------------
845
+ // Parse CLI args
846
+ // ---------------------------------------------------------------------------
847
+
848
+ function parseArgs() {
849
+ const rawArgs = process.argv.slice(3);
850
+ const subcmd = rawArgs[0] || 'list';
851
+ const allArgs = rawArgs.slice(1);
852
+
853
+ const rpcIndex = allArgs.findIndex(a => a === '--rpc');
854
+ const rpc = rpcIndex !== -1 ? allArgs[rpcIndex + 1] : getDefaultRpc();
855
+
856
+ const parsed = {
857
+ subcmd,
858
+ rpc,
859
+ json: allArgs.includes('--json') || allArgs.includes('-j'),
860
+ address: null,
861
+ account: null,
862
+ };
863
+
864
+ const addrIdx = allArgs.findIndex(a => a === '--address' || a === '-a');
865
+ if (addrIdx !== -1 && allArgs[addrIdx + 1]) parsed.address = allArgs[addrIdx + 1];
866
+
867
+ const acctIdx = allArgs.findIndex(a => a === '--account' || a === '-s');
868
+ if (acctIdx !== -1 && allArgs[acctIdx + 1]) parsed.account = allArgs[acctIdx + 1];
869
+
870
+ return parsed;
871
+ }
872
+
873
+ function createRl() {
874
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
875
+ }
876
+
877
+ function question(rl, q) {
878
+ return new Promise((res) => rl.question(q, res));
879
+ }
880
+
881
+ async function askMnemonic(rl, prompt) {
882
+ console.log(`\n${C.cyan}${prompt}${C.reset}`);
883
+ console.log(`${C.dim}Enter your 12 or 24-word passphrase, one space-separated line:${C.reset}`);
884
+ const raw = await question(rl, ` > ${C.reset}`);
885
+ return raw.trim().toLowerCase();
886
+ }
887
+
888
+ // ---------------------------------------------------------------------------
889
+ // Main entry point
890
+ // ---------------------------------------------------------------------------
891
+
892
+ async function main() {
893
+ const args = parseArgs();
894
+
895
+ switch (args.subcmd) {
896
+ case 'list':
897
+ await rewardsList(args);
898
+ break;
899
+ case 'summary':
900
+ await rewardsSummary(args);
901
+ break;
902
+ case 'pending':
903
+ await rewardsPending(args);
904
+ break;
905
+ case 'claim':
906
+ await rewardsClaim(args);
907
+ break;
908
+ case 'compound':
909
+ await rewardsCompound(args);
910
+ break;
911
+ default:
912
+ console.log(`\n${C.cyan}Usage:${C.reset}`);
913
+ console.log(` aether rewards list --address <addr> List all staking rewards (SDK-wired)`);
914
+ console.log(` aether rewards summary --address <addr> One-line rewards summary`);
915
+ console.log(` aether rewards pending --address <addr> Show pending rewards`);
916
+ console.log(` aether rewards claim --address <addr> [--account <stakeAcct>] Claim rewards`);
917
+ console.log(` aether rewards compound --address <addr> [--account <stakeAcct>] Claim and re-stake`);
918
+ console.log();
919
+ console.log(` ${C.dim}--json Output as JSON`);
920
+ console.log(` --rpc <url> Use specific RPC endpoint${C.reset}`);
921
+ console.log();
922
+ console.log(` ${C.green}✓ Fully wired to @jellylegsai/aether-sdk${C.reset}`);
923
+ console.log(` ${C.dim}SDK: getStakePositions(), getRewards(), getEpochInfo(), sendTransaction()${C.reset}\n`);
924
+ }
925
+ }
926
+
927
+ main().catch(err => {
928
+ console.error(`\n${C.red}Error running rewards command:${C.reset}`, err.message, '\n');
929
+ });
930
+
931
+ module.exports = { rewardsCommand: main };