aether-hub 1.1.1 → 1.1.3

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.
@@ -1,600 +1,600 @@
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
- * Usage:
9
- * aether rewards list --address <addr> List all rewards per stake account
10
- * aether rewards list --address <addr> --json JSON output for scripting
11
- * aether rewards claim --address <addr> --account <stakeAcct> [--json]
12
- * aether rewards summary --address <addr> One-line summary of total rewards
13
- *
14
- * Requires AETHER_RPC env var or local node running (default: http://127.0.0.1:8899)
15
- */
16
-
17
- const http = require('http');
18
- const https = require('https');
19
- const readline = require('readline');
20
- const crypto = require('crypto');
21
- const bs58 = require('bs58').default;
22
- const bip39 = require('bip39');
23
- const nacl = require('tweetnacl');
24
-
25
- // ANSI colours
26
- const C = {
27
- reset: '\x1b[0m',
28
- bright: '\x1b[1m',
29
- dim: '\x1b[2m',
30
- red: '\x1b[31m',
31
- green: '\x1b[32m',
32
- yellow: '\x1b[33m',
33
- cyan: '\x1b[36m',
34
- magenta: '\x1b[35m',
35
- };
36
-
37
- const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
38
- const CLI_VERSION = '1.0.5';
39
-
40
- // ---------------------------------------------------------------------------
41
- // Paths & config
42
- // ---------------------------------------------------------------------------
43
-
44
- function getAetherDir() {
45
- return require('path').join(require('os').homedir(), '.aether');
46
- }
47
-
48
- function loadConfig() {
49
- const p = require('path').join(getAetherDir(), 'config.json');
50
- if (!require('fs').existsSync(p)) return { defaultWallet: null };
51
- try {
52
- return JSON.parse(require('fs').readFileSync(p, 'utf8'));
53
- } catch {
54
- return { defaultWallet: null };
55
- }
56
- }
57
-
58
- function loadWallet(address) {
59
- const fp = require('path').join(getAetherDir(), 'wallets', `${address}.json`);
60
- if (!require('fs').existsSync(fp)) return null;
61
- return JSON.parse(require('fs').readFileSync(fp, 'utf8'));
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // Crypto helpers
66
- // ---------------------------------------------------------------------------
67
-
68
- function deriveKeypair(mnemonic) {
69
- if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic');
70
- const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
71
- const seed32 = seedBuffer.slice(0, 32);
72
- const keyPair = nacl.sign.keyPair.fromSeed(seed32);
73
- return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
74
- }
75
-
76
- function formatAddress(publicKey) {
77
- return 'ATH' + bs58.encode(publicKey);
78
- }
79
-
80
- // ---------------------------------------------------------------------------
81
- // HTTP helpers
82
- // ---------------------------------------------------------------------------
83
-
84
- function httpRequest(rpcUrl, path) {
85
- return new Promise((resolve, reject) => {
86
- const url = new URL(path, rpcUrl);
87
- const lib = url.protocol === 'https:' ? https : http;
88
- const req = lib.request({
89
- hostname: url.hostname,
90
- port: url.port || (url.protocol === 'https:' ? 443 : 80),
91
- path: url.pathname + url.search,
92
- method: 'GET',
93
- timeout: 8000,
94
- headers: { 'Content-Type': 'application/json' },
95
- }, (res) => {
96
- let data = '';
97
- res.on('data', (chunk) => data += chunk);
98
- res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
99
- });
100
- req.on('error', reject);
101
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
102
- req.end();
103
- });
104
- }
105
-
106
- function httpPost(rpcUrl, path, body) {
107
- return new Promise((resolve, reject) => {
108
- const url = new URL(path, rpcUrl);
109
- const lib = url.protocol === 'https:' ? https : http;
110
- const bodyStr = JSON.stringify(body);
111
- const req = lib.request({
112
- hostname: url.hostname,
113
- port: url.port || (url.protocol === 'https:' ? 443 : 80),
114
- path: url.pathname + url.search,
115
- method: 'POST',
116
- timeout: 15000,
117
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
118
- }, (res) => {
119
- let data = '';
120
- res.on('data', (chunk) => data += chunk);
121
- res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
122
- });
123
- req.on('error', reject);
124
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
125
- req.write(bodyStr);
126
- req.end();
127
- });
128
- }
129
-
130
- function getDefaultRpc() {
131
- return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
132
- }
133
-
134
- function formatAether(lamports) {
135
- const aeth = lamports / 1e9;
136
- if (aeth === 0) return '0 AETH';
137
- return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
138
- }
139
-
140
- function formatAethFull(lamports) {
141
- return (lamports / 1e9).toFixed(6) + ' AETH';
142
- }
143
-
144
- // ---------------------------------------------------------------------------
145
- // Rewards calculation helpers
146
- // ---------------------------------------------------------------------------
147
-
148
- /**
149
- * Fetch stake account info and compute rewards from epoch history.
150
- * Uses the stake account's delegated stake + activation/deactivation epochs
151
- * to estimate rewards accrued.
152
- */
153
- async function fetchStakeRewards(rpc, stakeAddress) {
154
- try {
155
- // Fetch stake account data
156
- const stakeData = await httpRequest(rpc, `/v1/stake-account/${stakeAddress}`);
157
- if (!stakeData || stakeData.error) {
158
- return { stakeAddress, error: stakeData.error || 'Failed to fetch stake account' };
159
- }
160
-
161
- const delegatedStake = BigInt(stakeData.delegated_stake || 0);
162
- const activationEpoch = stakeData.activation_epoch || 0;
163
- const deactivationEpoch = stakeData.deactivation_epoch || null;
164
- const stakeType = stakeData.stake_type || 'unknown';
165
-
166
- // Fetch epoch info for current epoch
167
- const epochInfo = await httpRequest(rpc, '/v1/epoch-info');
168
- const currentEpoch = epochInfo.epoch || 0;
169
- const rewardsPerEpoch = BigInt(epochInfo.rewards_per_epoch || '2000000000'); // default ~2 AETH
170
-
171
- // Calculate active epochs
172
- const activeFromEpoch = activationEpoch;
173
- const activeToEpoch = deactivationEpoch || currentEpoch;
174
- const activeEpochs = Math.max(0, activeToEpoch - activeFromEpoch);
175
-
176
- // Rewards accrue proportional to stake share (simplified — assumes network-wide pool)
177
- // APY is estimated from rewards_per_epoch vs total staked
178
- const totalNetworkStake = BigInt(epochInfo.total_staked || '1000000000000'); // fallback
179
- const rewardsRate = Number(rewardsPerEpoch * BigInt(365)) / Number(totalNetworkStake);
180
- const apyBps = Math.round(rewardsRate * 10000); // basis points
181
-
182
- // Compute estimated rewards accumulated
183
- const stakeAeth = Number(delegatedStake) / 1e9;
184
- const epochDuration = 432000; // seconds per epoch (approx 3.5 days on Aether)
185
- const yearEpochs = Math.round(31557600 / epochDuration); // ~73 epochs/year
186
- const estimatedAnnualRewards = stakeAeth * (apyBps / 10000);
187
- const estimatedRewards = (estimatedAnnualRewards / yearEpochs) * activeEpochs;
188
-
189
- return {
190
- stakeAddress,
191
- delegatedStake: delegatedStake.toString(),
192
- delegatedStakeFormatted: formatAether(delegatedStake),
193
- activationEpoch: activeFromEpoch,
194
- deactivationEpoch,
195
- isActive: deactivationEpoch === null,
196
- activeEpochs,
197
- estimatedRewards: Math.round(estimatedRewards * 1e9),
198
- estimatedRewardsFormatted: formatAether(Math.round(estimatedRewards * 1e9)),
199
- apyBps,
200
- stakeType,
201
- };
202
- } catch (err) {
203
- return { stakeAddress, error: err.message };
204
- }
205
- }
206
-
207
- /**
208
- * Fetch all stake accounts for a wallet address.
209
- * Returns array of stake account pubkeys from wallet's session data.
210
- */
211
- async function fetchWalletStakeAccounts(walletAddress) {
212
- const sessionsDir = require('path').join(getAetherDir(), 'sessions');
213
- if (!require('fs').existsSync(sessionsDir)) return [];
214
-
215
- const files = require('fs').readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
216
- const stakeAccounts = [];
217
-
218
- for (const file of files) {
219
- try {
220
- const session = JSON.parse(require('fs').readFileSync(require('path').join(sessionsDir, file), 'utf8'));
221
- if (session.wallet_address === walletAddress && session.stake_account) {
222
- stakeAccounts.push(session.stake_account);
223
- }
224
- } catch {}
225
- }
226
-
227
- return stakeAccounts;
228
- }
229
-
230
- /**
231
- * Fetch wallet info from chain (account data)
232
- */
233
- async function fetchAccountInfo(rpc, address) {
234
- try {
235
- return await httpRequest(rpc, `/v1/account/${address}`);
236
- } catch {
237
- return null;
238
- }
239
- }
240
-
241
- // ---------------------------------------------------------------------------
242
- // Rewards list command
243
- // ---------------------------------------------------------------------------
244
-
245
- async function rewardsList(args) {
246
- const rpc = args.rpc || getDefaultRpc();
247
- const isJson = args.json || false;
248
- let address = args.address || null;
249
-
250
- // Interactive address prompt if not provided
251
- if (!address) {
252
- const config = loadConfig();
253
- const rl = createRl();
254
- const answer = await question(rl, `\n${C.cyan}Enter wallet address (or press Enter for default): ${C.reset}`);
255
- rl.close();
256
-
257
- if (!answer.trim()) {
258
- if (!config.defaultWallet) {
259
- console.log(`\n${C.red}✗ No default wallet and no address provided.${C.reset}`);
260
- console.log(` ${C.dim}Set a default wallet first: aether wallet default${C.reset}\n`);
261
- return;
262
- }
263
- address = config.defaultWallet;
264
- } else {
265
- address = answer.trim();
266
- }
267
- }
268
-
269
- // Validate address format (ATH...)
270
- if (!address.startsWith('ATH') || address.length < 30) {
271
- // Try loading from config if it looks like a nickname
272
- const config = loadConfig();
273
- if (config.defaultWallet) address = config.defaultWallet;
274
- }
275
-
276
- console.log(`\n${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
277
- console.log(`${C.bright}${C.cyan}║ Staking Rewards — ${address.substring(0, 12)}... ║${C.reset}`);
278
- console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
279
-
280
- // Fetch stake accounts for this wallet
281
- const stakeAccounts = await fetchWalletStakeAccounts(address);
282
-
283
- if (stakeAccounts.length === 0) {
284
- console.log(` ${C.yellow}⚠ No stake accounts found for this wallet.${C.reset}`);
285
- console.log(` ${C.dim}Stake AETH first: aether stake --address ${address} --validator <val> --amount <aeth>${C.reset}\n`);
286
- return;
287
- }
288
-
289
- // Fetch rewards for each stake account
290
- const rewardsResults = await Promise.all(
291
- stakeAccounts.map(sa => fetchStakeRewards(rpc, sa))
292
- );
293
-
294
- let totalEstimatedRewards = BigInt(0);
295
- let totalDelegatedStake = BigInt(0);
296
- let activeCount = 0;
297
- const rows = [];
298
-
299
- for (const result of rewardsResults) {
300
- if (result.error) {
301
- rows.push({ status: 'error', ...result });
302
- continue;
303
- }
304
-
305
- totalEstimatedRewards += BigInt(result.estimatedRewards);
306
- totalDelegatedStake += BigInt(result.delegatedStake);
307
- if (result.isActive) activeCount++;
308
-
309
- rows.push(result);
310
- }
311
-
312
- if (isJson) {
313
- console.log(JSON.stringify({
314
- address,
315
- totalEstimatedRewards: totalEstimatedRewards.toString(),
316
- totalEstimatedRewardsFormatted: formatAether(totalEstimatedRewards),
317
- totalDelegatedStake: totalDelegatedStake.toString(),
318
- totalDelegatedStakeFormatted: formatAether(totalDelegatedStake),
319
- activeStakeAccounts: activeCount,
320
- totalStakeAccounts: rows.length,
321
- stakeAccounts: rows,
322
- }, null, 2));
323
- return;
324
- }
325
-
326
- // ASCII table header
327
- console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────┐${C.reset}`);
328
- console.log(` ${C.dim}│${C.reset} ${C.bright}Stake Account${C.reset} ${C.bright}Delegated${C.reset} ${C.bright}Est. Rewards${C.reset} ${C.bright}APY${C.reset} ${C.bright}Status${C.reset} ${C.dim}│${C.reset}`);
329
- console.log(` ${C.dim}├─────────────────────────────────────────────────────────────┤${C.reset}`);
330
-
331
- for (const r of rows) {
332
- const shortAddr = r.stakeAddress ? r.stakeAddress.substring(0, 14) + '...' : 'unknown';
333
- const delegated = r.delegatedStakeFormatted || '—';
334
- const estRew = r.estimatedRewardsFormatted || '—';
335
- const apy = r.apyBps ? `${(r.apyBps / 100).toFixed(2)}%` : '—';
336
- const status = r.isActive
337
- ? `${C.green}● Active${C.reset}`
338
- : r.deactivationEpoch
339
- ? `${C.yellow}○ Deactivated${C.reset}`
340
- : `${C.red}✗ Error${C.reset}`;
341
- const statusColor = r.isActive ? C.green : r.deactivationEpoch ? C.yellow : C.red;
342
-
343
- console.log(
344
- ` ${C.dim}│${C.reset} ${shortAddr.padEnd(20)} ${delegated.padEnd(13)} ${estRew.padEnd(15)} ${apy.padEnd(7)} ${statusColor}${r.isActive ? '● Active' : r.deactivationEpoch ? '○ Deact.' : '✗ Err'}${C.reset} ${C.dim}│${C.reset}`
345
- );
346
- }
347
-
348
- console.log(` ${C.dim}└─────────────────────────────────────────────────────────────┘${C.reset}`);
349
- console.log();
350
- console.log(` ${C.bright}Total Delegated:${C.reset} ${C.cyan}${formatAether(totalDelegatedStake)}${C.reset}`);
351
- console.log(` ${C.bright}Total Est. Rewards:${C.reset} ${C.green}${formatAether(totalEstimatedRewards)}${C.reset}`);
352
- console.log(` ${C.bright}Active Accounts:${C.reset} ${activeCount} of ${rows.length}`);
353
- console.log();
354
- console.log(` ${C.dim}Run "aether rewards claim --address ${address}" to claim unclaimed rewards.${C.reset}\n`);
355
- }
356
-
357
- // ---------------------------------------------------------------------------
358
- // Rewards summary command (one-line)
359
- // ---------------------------------------------------------------------------
360
-
361
- async function rewardsSummary(args) {
362
- const rpc = args.rpc || getDefaultRpc();
363
- let address = args.address || null;
364
-
365
- if (!address) {
366
- const config = loadConfig();
367
- if (!config.defaultWallet) {
368
- console.log(`${C.red}✗ No default wallet and no address provided.${C.reset}`);
369
- return;
370
- }
371
- address = config.defaultWallet;
372
- }
373
-
374
- const stakeAccounts = await fetchWalletStakeAccounts(address);
375
- if (stakeAccounts.length === 0) {
376
- console.log(`${C.yellow}⚠ No stake accounts for ${address.substring(0, 12)}...${C.reset}`);
377
- return;
378
- }
379
-
380
- const results = await Promise.all(stakeAccounts.map(sa => fetchStakeRewards(rpc, sa)));
381
- let totalRewards = BigInt(0);
382
- let totalStake = BigInt(0);
383
- let activeCount = 0;
384
-
385
- for (const r of results) {
386
- if (!r.error) {
387
- totalRewards += BigInt(r.estimatedRewards);
388
- totalStake += BigInt(r.delegatedStake);
389
- if (r.isActive) activeCount++;
390
- }
391
- }
392
-
393
- console.log(`${C.cyan}${address.substring(0, 12)}...${C.reset} │ Stake: ${C.cyan}${formatAether(totalStake)}${C.reset} │ Est.Rewards: ${C.green}${formatAether(totalRewards)}${C.reset} │ Active: ${activeCount}/${results.length}`);
394
- }
395
-
396
- // ---------------------------------------------------------------------------
397
- // Rewards claim command
398
- // ---------------------------------------------------------------------------
399
-
400
- async function rewardsClaim(args) {
401
- const rpc = args.rpc || getDefaultRpc();
402
- const isJson = args.json || false;
403
- let address = args.address || null;
404
- let stakeAccount = args.account || null;
405
-
406
- const config = loadConfig();
407
- const rl = createRl();
408
-
409
- if (!address) {
410
- const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
411
- address = ans.trim();
412
- }
413
-
414
- if (!stakeAccount) {
415
- const stakeAccounts = await fetchWalletStakeAccounts(address);
416
- if (stakeAccounts.length === 0) {
417
- console.log(`\n${C.red}✗ No stake accounts found for this wallet.${C.reset}\n`);
418
- rl.close();
419
- return;
420
- }
421
- if (stakeAccounts.length === 1) {
422
- stakeAccount = stakeAccounts[0];
423
- } else {
424
- console.log(`\n${C.cyan}Select stake account:${C.reset}`);
425
- stakeAccounts.forEach((sa, i) => {
426
- console.log(` ${i + 1}) ${sa.substring(0, 20)}...`);
427
- });
428
- const ans = await question(rl, `${C.cyan}Enter number: ${C.reset}`);
429
- const idx = parseInt(ans.trim()) - 1;
430
- if (idx < 0 || idx >= stakeAccounts.length) {
431
- console.log(`\n${C.red}Invalid selection.${C.reset}\n`);
432
- rl.close();
433
- return;
434
- }
435
- stakeAccount = stakeAccounts[idx];
436
- }
437
- }
438
-
439
- rl.close();
440
-
441
- // Load wallet for signing
442
- const wallet = loadWallet(address);
443
- if (!wallet) {
444
- console.log(`\n${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
445
- console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
446
- return;
447
- }
448
-
449
- console.log(`\n${C.bright}${C.cyan}╔════════════════════════════════════════╗${C.reset}`);
450
- console.log(`${C.bright}${C.cyan}║ Claim Staking Rewards ║${C.reset}`);
451
- console.log(`${C.bright}${C.cyan}╚════════════════════════════════════════╝${C.reset}\n`);
452
- console.log(` ${C.dim}Wallet:${C.reset} ${address.substring(0, 16)}...`);
453
- console.log(` ${C.dim}Stake Account:${C.reset} ${stakeAccount.substring(0, 16)}...`);
454
-
455
- // Fetch current rewards for this stake account
456
- const rewardData = await fetchStakeRewards(rpc, stakeAccount);
457
- if (rewardData.error) {
458
- console.log(`\n${C.red}✗ Failed to fetch stake account: ${rewardData.error}${C.reset}\n`);
459
- return;
460
- }
461
-
462
- console.log(` ${C.dim}Delegated Stake:${C.reset} ${rewardData.delegatedStakeFormatted}`);
463
- console.log(` ${C.dim}Est. Accumulated:${C.reset} ${rewardData.estimatedRewardsFormatted}`);
464
- console.log(` ${C.dim}APY:${C.reset} ${(rewardData.apyBps / 100).toFixed(2)}%`);
465
-
466
- const estimatedRewards = rewardData.estimatedRewards;
467
- if (BigInt(estimatedRewards) === BigInt(0)) {
468
- console.log(`\n${C.yellow}⚠ No rewards accumulated yet.${C.reset}\n`);
469
- return;
470
- }
471
-
472
- const confirm = await question(rl, `\n ${C.yellow}Claim ${rewardData.estimatedRewardsFormatted}? [y/N]${C.reset} > `);
473
- if (confirm.trim().toLowerCase() !== 'y') {
474
- console.log(`${C.dim}Cancelled.${C.reset}\n`);
475
- return;
476
- }
477
-
478
- // Derive keypair from mnemonic for signing
479
- let keypair;
480
- try {
481
- const mnemonic = wallet.mnemonic;
482
- keypair = deriveKeypair(mnemonic);
483
- } catch (err) {
484
- console.log(`\n${C.red}✗ Failed to derive keypair: ${err.message}${C.reset}\n`);
485
- return;
486
- }
487
-
488
- // Build claim transaction
489
- const tx = {
490
- type: 'ClaimRewards',
491
- from: address,
492
- stake_account: stakeAccount,
493
- lamports: estimatedRewards,
494
- timestamp: Math.floor(Date.now() / 1000),
495
- };
496
-
497
- // Sign transaction
498
- const txData = JSON.stringify(tx);
499
- const txHash = crypto.createHash('sha256').update(txData).digest('hex');
500
- const signature = nacl.hash(Buffer.from(txHash, 'hex'));
501
- const signatureB58 = bs58.encode(signature.slice(0, 64));
502
-
503
- tx.signature = signatureB58;
504
-
505
- // Submit transaction
506
- try {
507
- const result = await httpPost(rpc, '/v1/tx', tx);
508
-
509
- if (isJson) {
510
- console.log(JSON.stringify({ success: true, tx: tx, result }, null, 2));
511
- } else {
512
- if (result.success || result.txid) {
513
- console.log(`\n${C.green}✓ Rewards claimed successfully!${C.reset}`);
514
- console.log(` ${C.dim}TX ID: ${result.txid || signatureB58.substring(0, 20)}...${C.reset}`);
515
- console.log(` ${C.dim}Amount: ${rewardData.estimatedRewardsFormatted}${C.reset}`);
516
- console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
517
- } else {
518
- console.log(`\n${C.red}✗ Claim failed: ${result.error || JSON.stringify(result)}${C.reset}\n`);
519
- }
520
- }
521
- } catch (err) {
522
- if (isJson) {
523
- console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
524
- } else {
525
- console.log(`\n${C.red}✗ Failed to submit claim transaction: ${err.message}${C.reset}`);
526
- console.log(` ${C.dim}The rewards are accumulated on-chain and can be claimed later.${C.reset}\n`);
527
- }
528
- }
529
- }
530
-
531
- // ---------------------------------------------------------------------------
532
- // Parse CLI args
533
- // ---------------------------------------------------------------------------
534
-
535
- function parseArgs() {
536
- const args = process.argv.slice(3); // [node, index.js, rewards, <subcmd>, ...]
537
- return args;
538
- }
539
-
540
- function createRl() {
541
- return readline.createInterface({ input: process.stdin, output: process.stdout });
542
- }
543
-
544
- function question(rl, q) {
545
- return new Promise((res) => rl.question(q, res));
546
- }
547
-
548
- // ---------------------------------------------------------------------------
549
- // Main entry point
550
- // ---------------------------------------------------------------------------
551
-
552
- async function main() {
553
- const rawArgs = parseArgs();
554
- const subcmd = rawArgs[0] || 'list';
555
-
556
- // Parse common flags from all args
557
- const allArgs = rawArgs.slice(1);
558
- const rpcIndex = allArgs.findIndex(a => a === '--rpc');
559
- const rpc = rpcIndex !== -1 ? allArgs[rpcIndex + 1] : getDefaultRpc();
560
-
561
- const parsed = {
562
- rpc,
563
- json: allArgs.includes('--json'),
564
- address: null,
565
- account: null,
566
- };
567
-
568
- // Extract --address and --account flags
569
- const addrIdx = allArgs.findIndex(a => a === '--address');
570
- if (addrIdx !== -1 && allArgs[addrIdx + 1]) parsed.address = allArgs[addrIdx + 1];
571
-
572
- const acctIdx = allArgs.findIndex(a => a === '--account');
573
- if (acctIdx !== -1 && allArgs[acctIdx + 1]) parsed.account = allArgs[acctIdx + 1];
574
-
575
- switch (subcmd) {
576
- case 'list':
577
- await rewardsList(parsed);
578
- break;
579
- case 'summary':
580
- await rewardsSummary(parsed);
581
- break;
582
- case 'claim':
583
- await rewardsClaim(parsed);
584
- break;
585
- default:
586
- console.log(`\n${C.cyan}Usage:${C.reset}`);
587
- console.log(` aether rewards list --address <addr> List all staking rewards`);
588
- console.log(` aether rewards summary --address <addr> One-line rewards summary`);
589
- console.log(` aether rewards claim --address <addr> [--account <stakeAcct>] Claim rewards`);
590
- console.log();
591
- console.log(` ${C.dim}--json Output as JSON`);
592
- console.log(` --rpc <url> Use specific RPC endpoint${C.reset}\n`);
593
- }
594
- }
595
-
596
- main().catch(err => {
597
- console.error(`\n${C.red}Error running rewards command:${C.reset}`, err.message, '\n');
598
- });
599
-
600
- module.exports = { rewardsCommand: main };
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
+ * Usage:
9
+ * aether rewards list --address <addr> List all rewards per stake account
10
+ * aether rewards list --address <addr> --json JSON output for scripting
11
+ * aether rewards claim --address <addr> --account <stakeAcct> [--json]
12
+ * aether rewards summary --address <addr> One-line summary of total rewards
13
+ *
14
+ * Requires AETHER_RPC env var or local node running (default: http://127.0.0.1:8899)
15
+ */
16
+
17
+ const http = require('http');
18
+ const https = require('https');
19
+ const readline = require('readline');
20
+ const crypto = require('crypto');
21
+ const bs58 = require('bs58').default;
22
+ const bip39 = require('bip39');
23
+ const nacl = require('tweetnacl');
24
+
25
+ // ANSI colours
26
+ const C = {
27
+ reset: '\x1b[0m',
28
+ bright: '\x1b[1m',
29
+ dim: '\x1b[2m',
30
+ red: '\x1b[31m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ cyan: '\x1b[36m',
34
+ magenta: '\x1b[35m',
35
+ };
36
+
37
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
38
+ const CLI_VERSION = '1.0.5';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Paths & config
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function getAetherDir() {
45
+ return require('path').join(require('os').homedir(), '.aether');
46
+ }
47
+
48
+ function loadConfig() {
49
+ const p = require('path').join(getAetherDir(), 'config.json');
50
+ if (!require('fs').existsSync(p)) return { defaultWallet: null };
51
+ try {
52
+ return JSON.parse(require('fs').readFileSync(p, 'utf8'));
53
+ } catch {
54
+ return { defaultWallet: null };
55
+ }
56
+ }
57
+
58
+ function loadWallet(address) {
59
+ const fp = require('path').join(getAetherDir(), 'wallets', `${address}.json`);
60
+ if (!require('fs').existsSync(fp)) return null;
61
+ return JSON.parse(require('fs').readFileSync(fp, 'utf8'));
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Crypto helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function deriveKeypair(mnemonic) {
69
+ if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic');
70
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
71
+ const seed32 = seedBuffer.slice(0, 32);
72
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
73
+ return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
74
+ }
75
+
76
+ function formatAddress(publicKey) {
77
+ return 'ATH' + bs58.encode(publicKey);
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // HTTP helpers
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function httpRequest(rpcUrl, path) {
85
+ return new Promise((resolve, reject) => {
86
+ const url = new URL(path, rpcUrl);
87
+ const lib = url.protocol === 'https:' ? https : http;
88
+ const req = lib.request({
89
+ hostname: url.hostname,
90
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
91
+ path: url.pathname + url.search,
92
+ method: 'GET',
93
+ timeout: 8000,
94
+ headers: { 'Content-Type': 'application/json' },
95
+ }, (res) => {
96
+ let data = '';
97
+ res.on('data', (chunk) => data += chunk);
98
+ res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
99
+ });
100
+ req.on('error', reject);
101
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
102
+ req.end();
103
+ });
104
+ }
105
+
106
+ function httpPost(rpcUrl, path, body) {
107
+ return new Promise((resolve, reject) => {
108
+ const url = new URL(path, rpcUrl);
109
+ const lib = url.protocol === 'https:' ? https : http;
110
+ const bodyStr = JSON.stringify(body);
111
+ const req = lib.request({
112
+ hostname: url.hostname,
113
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
114
+ path: url.pathname + url.search,
115
+ method: 'POST',
116
+ timeout: 15000,
117
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
118
+ }, (res) => {
119
+ let data = '';
120
+ res.on('data', (chunk) => data += chunk);
121
+ res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
122
+ });
123
+ req.on('error', reject);
124
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
125
+ req.write(bodyStr);
126
+ req.end();
127
+ });
128
+ }
129
+
130
+ function getDefaultRpc() {
131
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
132
+ }
133
+
134
+ function formatAether(lamports) {
135
+ const aeth = lamports / 1e9;
136
+ if (aeth === 0) return '0 AETH';
137
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
138
+ }
139
+
140
+ function formatAethFull(lamports) {
141
+ return (lamports / 1e9).toFixed(6) + ' AETH';
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Rewards calculation helpers
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Fetch stake account info and compute rewards from epoch history.
150
+ * Uses the stake account's delegated stake + activation/deactivation epochs
151
+ * to estimate rewards accrued.
152
+ */
153
+ async function fetchStakeRewards(rpc, stakeAddress) {
154
+ try {
155
+ // Fetch stake account data
156
+ const stakeData = await httpRequest(rpc, `/v1/stake-account/${stakeAddress}`);
157
+ if (!stakeData || stakeData.error) {
158
+ return { stakeAddress, error: stakeData.error || 'Failed to fetch stake account' };
159
+ }
160
+
161
+ const delegatedStake = BigInt(stakeData.delegated_stake || 0);
162
+ const activationEpoch = stakeData.activation_epoch || 0;
163
+ const deactivationEpoch = stakeData.deactivation_epoch || null;
164
+ const stakeType = stakeData.stake_type || 'unknown';
165
+
166
+ // Fetch epoch info for current epoch
167
+ const epochInfo = await httpRequest(rpc, '/v1/epoch-info');
168
+ const currentEpoch = epochInfo.epoch || 0;
169
+ const rewardsPerEpoch = BigInt(epochInfo.rewards_per_epoch || '2000000000'); // default ~2 AETH
170
+
171
+ // Calculate active epochs
172
+ const activeFromEpoch = activationEpoch;
173
+ const activeToEpoch = deactivationEpoch || currentEpoch;
174
+ const activeEpochs = Math.max(0, activeToEpoch - activeFromEpoch);
175
+
176
+ // Rewards accrue proportional to stake share (simplified — assumes network-wide pool)
177
+ // APY is estimated from rewards_per_epoch vs total staked
178
+ const totalNetworkStake = BigInt(epochInfo.total_staked || '1000000000000'); // fallback
179
+ const rewardsRate = Number(rewardsPerEpoch * BigInt(365)) / Number(totalNetworkStake);
180
+ const apyBps = Math.round(rewardsRate * 10000); // basis points
181
+
182
+ // Compute estimated rewards accumulated
183
+ const stakeAeth = Number(delegatedStake) / 1e9;
184
+ const epochDuration = 432000; // seconds per epoch (approx 3.5 days on Aether)
185
+ const yearEpochs = Math.round(31557600 / epochDuration); // ~73 epochs/year
186
+ const estimatedAnnualRewards = stakeAeth * (apyBps / 10000);
187
+ const estimatedRewards = (estimatedAnnualRewards / yearEpochs) * activeEpochs;
188
+
189
+ return {
190
+ stakeAddress,
191
+ delegatedStake: delegatedStake.toString(),
192
+ delegatedStakeFormatted: formatAether(delegatedStake),
193
+ activationEpoch: activeFromEpoch,
194
+ deactivationEpoch,
195
+ isActive: deactivationEpoch === null,
196
+ activeEpochs,
197
+ estimatedRewards: Math.round(estimatedRewards * 1e9),
198
+ estimatedRewardsFormatted: formatAether(Math.round(estimatedRewards * 1e9)),
199
+ apyBps,
200
+ stakeType,
201
+ };
202
+ } catch (err) {
203
+ return { stakeAddress, error: err.message };
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Fetch all stake accounts for a wallet address.
209
+ * Returns array of stake account pubkeys from wallet's session data.
210
+ */
211
+ async function fetchWalletStakeAccounts(walletAddress) {
212
+ const sessionsDir = require('path').join(getAetherDir(), 'sessions');
213
+ if (!require('fs').existsSync(sessionsDir)) return [];
214
+
215
+ const files = require('fs').readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
216
+ const stakeAccounts = [];
217
+
218
+ for (const file of files) {
219
+ try {
220
+ const session = JSON.parse(require('fs').readFileSync(require('path').join(sessionsDir, file), 'utf8'));
221
+ if (session.wallet_address === walletAddress && session.stake_account) {
222
+ stakeAccounts.push(session.stake_account);
223
+ }
224
+ } catch {}
225
+ }
226
+
227
+ return stakeAccounts;
228
+ }
229
+
230
+ /**
231
+ * Fetch wallet info from chain (account data)
232
+ */
233
+ async function fetchAccountInfo(rpc, address) {
234
+ try {
235
+ return await httpRequest(rpc, `/v1/account/${address}`);
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Rewards list command
243
+ // ---------------------------------------------------------------------------
244
+
245
+ async function rewardsList(args) {
246
+ const rpc = args.rpc || getDefaultRpc();
247
+ const isJson = args.json || false;
248
+ let address = args.address || null;
249
+
250
+ // Interactive address prompt if not provided
251
+ if (!address) {
252
+ const config = loadConfig();
253
+ const rl = createRl();
254
+ const answer = await question(rl, `\n${C.cyan}Enter wallet address (or press Enter for default): ${C.reset}`);
255
+ rl.close();
256
+
257
+ if (!answer.trim()) {
258
+ if (!config.defaultWallet) {
259
+ console.log(`\n${C.red}✗ No default wallet and no address provided.${C.reset}`);
260
+ console.log(` ${C.dim}Set a default wallet first: aether wallet default${C.reset}\n`);
261
+ return;
262
+ }
263
+ address = config.defaultWallet;
264
+ } else {
265
+ address = answer.trim();
266
+ }
267
+ }
268
+
269
+ // Validate address format (ATH...)
270
+ if (!address.startsWith('ATH') || address.length < 30) {
271
+ // Try loading from config if it looks like a nickname
272
+ const config = loadConfig();
273
+ if (config.defaultWallet) address = config.defaultWallet;
274
+ }
275
+
276
+ console.log(`\n${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
277
+ console.log(`${C.bright}${C.cyan}║ Staking Rewards — ${address.substring(0, 12)}... ║${C.reset}`);
278
+ console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
279
+
280
+ // Fetch stake accounts for this wallet
281
+ const stakeAccounts = await fetchWalletStakeAccounts(address);
282
+
283
+ if (stakeAccounts.length === 0) {
284
+ console.log(` ${C.yellow}⚠ No stake accounts found for this wallet.${C.reset}`);
285
+ console.log(` ${C.dim}Stake AETH first: aether stake --address ${address} --validator <val> --amount <aeth>${C.reset}\n`);
286
+ return;
287
+ }
288
+
289
+ // Fetch rewards for each stake account
290
+ const rewardsResults = await Promise.all(
291
+ stakeAccounts.map(sa => fetchStakeRewards(rpc, sa))
292
+ );
293
+
294
+ let totalEstimatedRewards = BigInt(0);
295
+ let totalDelegatedStake = BigInt(0);
296
+ let activeCount = 0;
297
+ const rows = [];
298
+
299
+ for (const result of rewardsResults) {
300
+ if (result.error) {
301
+ rows.push({ status: 'error', ...result });
302
+ continue;
303
+ }
304
+
305
+ totalEstimatedRewards += BigInt(result.estimatedRewards);
306
+ totalDelegatedStake += BigInt(result.delegatedStake);
307
+ if (result.isActive) activeCount++;
308
+
309
+ rows.push(result);
310
+ }
311
+
312
+ if (isJson) {
313
+ console.log(JSON.stringify({
314
+ address,
315
+ totalEstimatedRewards: totalEstimatedRewards.toString(),
316
+ totalEstimatedRewardsFormatted: formatAether(totalEstimatedRewards),
317
+ totalDelegatedStake: totalDelegatedStake.toString(),
318
+ totalDelegatedStakeFormatted: formatAether(totalDelegatedStake),
319
+ activeStakeAccounts: activeCount,
320
+ totalStakeAccounts: rows.length,
321
+ stakeAccounts: rows,
322
+ }, null, 2));
323
+ return;
324
+ }
325
+
326
+ // ASCII table header
327
+ console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────┐${C.reset}`);
328
+ console.log(` ${C.dim}│${C.reset} ${C.bright}Stake Account${C.reset} ${C.bright}Delegated${C.reset} ${C.bright}Est. Rewards${C.reset} ${C.bright}APY${C.reset} ${C.bright}Status${C.reset} ${C.dim}│${C.reset}`);
329
+ console.log(` ${C.dim}├─────────────────────────────────────────────────────────────┤${C.reset}`);
330
+
331
+ for (const r of rows) {
332
+ const shortAddr = r.stakeAddress ? r.stakeAddress.substring(0, 14) + '...' : 'unknown';
333
+ const delegated = r.delegatedStakeFormatted || '—';
334
+ const estRew = r.estimatedRewardsFormatted || '—';
335
+ const apy = r.apyBps ? `${(r.apyBps / 100).toFixed(2)}%` : '—';
336
+ const status = r.isActive
337
+ ? `${C.green}● Active${C.reset}`
338
+ : r.deactivationEpoch
339
+ ? `${C.yellow}○ Deactivated${C.reset}`
340
+ : `${C.red}✗ Error${C.reset}`;
341
+ const statusColor = r.isActive ? C.green : r.deactivationEpoch ? C.yellow : C.red;
342
+
343
+ console.log(
344
+ ` ${C.dim}│${C.reset} ${shortAddr.padEnd(20)} ${delegated.padEnd(13)} ${estRew.padEnd(15)} ${apy.padEnd(7)} ${statusColor}${r.isActive ? '● Active' : r.deactivationEpoch ? '○ Deact.' : '✗ Err'}${C.reset} ${C.dim}│${C.reset}`
345
+ );
346
+ }
347
+
348
+ console.log(` ${C.dim}└─────────────────────────────────────────────────────────────┘${C.reset}`);
349
+ console.log();
350
+ console.log(` ${C.bright}Total Delegated:${C.reset} ${C.cyan}${formatAether(totalDelegatedStake)}${C.reset}`);
351
+ console.log(` ${C.bright}Total Est. Rewards:${C.reset} ${C.green}${formatAether(totalEstimatedRewards)}${C.reset}`);
352
+ console.log(` ${C.bright}Active Accounts:${C.reset} ${activeCount} of ${rows.length}`);
353
+ console.log();
354
+ console.log(` ${C.dim}Run "aether rewards claim --address ${address}" to claim unclaimed rewards.${C.reset}\n`);
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Rewards summary command (one-line)
359
+ // ---------------------------------------------------------------------------
360
+
361
+ async function rewardsSummary(args) {
362
+ const rpc = args.rpc || getDefaultRpc();
363
+ let address = args.address || null;
364
+
365
+ if (!address) {
366
+ const config = loadConfig();
367
+ if (!config.defaultWallet) {
368
+ console.log(`${C.red}✗ No default wallet and no address provided.${C.reset}`);
369
+ return;
370
+ }
371
+ address = config.defaultWallet;
372
+ }
373
+
374
+ const stakeAccounts = await fetchWalletStakeAccounts(address);
375
+ if (stakeAccounts.length === 0) {
376
+ console.log(`${C.yellow}⚠ No stake accounts for ${address.substring(0, 12)}...${C.reset}`);
377
+ return;
378
+ }
379
+
380
+ const results = await Promise.all(stakeAccounts.map(sa => fetchStakeRewards(rpc, sa)));
381
+ let totalRewards = BigInt(0);
382
+ let totalStake = BigInt(0);
383
+ let activeCount = 0;
384
+
385
+ for (const r of results) {
386
+ if (!r.error) {
387
+ totalRewards += BigInt(r.estimatedRewards);
388
+ totalStake += BigInt(r.delegatedStake);
389
+ if (r.isActive) activeCount++;
390
+ }
391
+ }
392
+
393
+ console.log(`${C.cyan}${address.substring(0, 12)}...${C.reset} │ Stake: ${C.cyan}${formatAether(totalStake)}${C.reset} │ Est.Rewards: ${C.green}${formatAether(totalRewards)}${C.reset} │ Active: ${activeCount}/${results.length}`);
394
+ }
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // Rewards claim command
398
+ // ---------------------------------------------------------------------------
399
+
400
+ async function rewardsClaim(args) {
401
+ const rpc = args.rpc || getDefaultRpc();
402
+ const isJson = args.json || false;
403
+ let address = args.address || null;
404
+ let stakeAccount = args.account || null;
405
+
406
+ const config = loadConfig();
407
+ const rl = createRl();
408
+
409
+ if (!address) {
410
+ const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
411
+ address = ans.trim();
412
+ }
413
+
414
+ if (!stakeAccount) {
415
+ const stakeAccounts = await fetchWalletStakeAccounts(address);
416
+ if (stakeAccounts.length === 0) {
417
+ console.log(`\n${C.red}✗ No stake accounts found for this wallet.${C.reset}\n`);
418
+ rl.close();
419
+ return;
420
+ }
421
+ if (stakeAccounts.length === 1) {
422
+ stakeAccount = stakeAccounts[0];
423
+ } else {
424
+ console.log(`\n${C.cyan}Select stake account:${C.reset}`);
425
+ stakeAccounts.forEach((sa, i) => {
426
+ console.log(` ${i + 1}) ${sa.substring(0, 20)}...`);
427
+ });
428
+ const ans = await question(rl, `${C.cyan}Enter number: ${C.reset}`);
429
+ const idx = parseInt(ans.trim()) - 1;
430
+ if (idx < 0 || idx >= stakeAccounts.length) {
431
+ console.log(`\n${C.red}Invalid selection.${C.reset}\n`);
432
+ rl.close();
433
+ return;
434
+ }
435
+ stakeAccount = stakeAccounts[idx];
436
+ }
437
+ }
438
+
439
+ rl.close();
440
+
441
+ // Load wallet for signing
442
+ const wallet = loadWallet(address);
443
+ if (!wallet) {
444
+ console.log(`\n${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
445
+ console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
446
+ return;
447
+ }
448
+
449
+ console.log(`\n${C.bright}${C.cyan}╔════════════════════════════════════════╗${C.reset}`);
450
+ console.log(`${C.bright}${C.cyan}║ Claim Staking Rewards ║${C.reset}`);
451
+ console.log(`${C.bright}${C.cyan}╚════════════════════════════════════════╝${C.reset}\n`);
452
+ console.log(` ${C.dim}Wallet:${C.reset} ${address.substring(0, 16)}...`);
453
+ console.log(` ${C.dim}Stake Account:${C.reset} ${stakeAccount.substring(0, 16)}...`);
454
+
455
+ // Fetch current rewards for this stake account
456
+ const rewardData = await fetchStakeRewards(rpc, stakeAccount);
457
+ if (rewardData.error) {
458
+ console.log(`\n${C.red}✗ Failed to fetch stake account: ${rewardData.error}${C.reset}\n`);
459
+ return;
460
+ }
461
+
462
+ console.log(` ${C.dim}Delegated Stake:${C.reset} ${rewardData.delegatedStakeFormatted}`);
463
+ console.log(` ${C.dim}Est. Accumulated:${C.reset} ${rewardData.estimatedRewardsFormatted}`);
464
+ console.log(` ${C.dim}APY:${C.reset} ${(rewardData.apyBps / 100).toFixed(2)}%`);
465
+
466
+ const estimatedRewards = rewardData.estimatedRewards;
467
+ if (BigInt(estimatedRewards) === BigInt(0)) {
468
+ console.log(`\n${C.yellow}⚠ No rewards accumulated yet.${C.reset}\n`);
469
+ return;
470
+ }
471
+
472
+ const confirm = await question(rl, `\n ${C.yellow}Claim ${rewardData.estimatedRewardsFormatted}? [y/N]${C.reset} > `);
473
+ if (confirm.trim().toLowerCase() !== 'y') {
474
+ console.log(`${C.dim}Cancelled.${C.reset}\n`);
475
+ return;
476
+ }
477
+
478
+ // Derive keypair from mnemonic for signing
479
+ let keypair;
480
+ try {
481
+ const mnemonic = wallet.mnemonic;
482
+ keypair = deriveKeypair(mnemonic);
483
+ } catch (err) {
484
+ console.log(`\n${C.red}✗ Failed to derive keypair: ${err.message}${C.reset}\n`);
485
+ return;
486
+ }
487
+
488
+ // Build claim transaction
489
+ const tx = {
490
+ type: 'ClaimRewards',
491
+ from: address,
492
+ stake_account: stakeAccount,
493
+ lamports: estimatedRewards,
494
+ timestamp: Math.floor(Date.now() / 1000),
495
+ };
496
+
497
+ // Sign transaction
498
+ const txData = JSON.stringify(tx);
499
+ const txHash = crypto.createHash('sha256').update(txData).digest('hex');
500
+ const signature = nacl.hash(Buffer.from(txHash, 'hex'));
501
+ const signatureB58 = bs58.encode(signature.slice(0, 64));
502
+
503
+ tx.signature = signatureB58;
504
+
505
+ // Submit transaction
506
+ try {
507
+ const result = await httpPost(rpc, '/v1/tx', tx);
508
+
509
+ if (isJson) {
510
+ console.log(JSON.stringify({ success: true, tx: tx, result }, null, 2));
511
+ } else {
512
+ if (result.success || result.txid) {
513
+ console.log(`\n${C.green}✓ Rewards claimed successfully!${C.reset}`);
514
+ console.log(` ${C.dim}TX ID: ${result.txid || signatureB58.substring(0, 20)}...${C.reset}`);
515
+ console.log(` ${C.dim}Amount: ${rewardData.estimatedRewardsFormatted}${C.reset}`);
516
+ console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
517
+ } else {
518
+ console.log(`\n${C.red}✗ Claim failed: ${result.error || JSON.stringify(result)}${C.reset}\n`);
519
+ }
520
+ }
521
+ } catch (err) {
522
+ if (isJson) {
523
+ console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
524
+ } else {
525
+ console.log(`\n${C.red}✗ Failed to submit claim transaction: ${err.message}${C.reset}`);
526
+ console.log(` ${C.dim}The rewards are accumulated on-chain and can be claimed later.${C.reset}\n`);
527
+ }
528
+ }
529
+ }
530
+
531
+ // ---------------------------------------------------------------------------
532
+ // Parse CLI args
533
+ // ---------------------------------------------------------------------------
534
+
535
+ function parseArgs() {
536
+ const args = process.argv.slice(3); // [node, index.js, rewards, <subcmd>, ...]
537
+ return args;
538
+ }
539
+
540
+ function createRl() {
541
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
542
+ }
543
+
544
+ function question(rl, q) {
545
+ return new Promise((res) => rl.question(q, res));
546
+ }
547
+
548
+ // ---------------------------------------------------------------------------
549
+ // Main entry point
550
+ // ---------------------------------------------------------------------------
551
+
552
+ async function main() {
553
+ const rawArgs = parseArgs();
554
+ const subcmd = rawArgs[0] || 'list';
555
+
556
+ // Parse common flags from all args
557
+ const allArgs = rawArgs.slice(1);
558
+ const rpcIndex = allArgs.findIndex(a => a === '--rpc');
559
+ const rpc = rpcIndex !== -1 ? allArgs[rpcIndex + 1] : getDefaultRpc();
560
+
561
+ const parsed = {
562
+ rpc,
563
+ json: allArgs.includes('--json'),
564
+ address: null,
565
+ account: null,
566
+ };
567
+
568
+ // Extract --address and --account flags
569
+ const addrIdx = allArgs.findIndex(a => a === '--address');
570
+ if (addrIdx !== -1 && allArgs[addrIdx + 1]) parsed.address = allArgs[addrIdx + 1];
571
+
572
+ const acctIdx = allArgs.findIndex(a => a === '--account');
573
+ if (acctIdx !== -1 && allArgs[acctIdx + 1]) parsed.account = allArgs[acctIdx + 1];
574
+
575
+ switch (subcmd) {
576
+ case 'list':
577
+ await rewardsList(parsed);
578
+ break;
579
+ case 'summary':
580
+ await rewardsSummary(parsed);
581
+ break;
582
+ case 'claim':
583
+ await rewardsClaim(parsed);
584
+ break;
585
+ default:
586
+ console.log(`\n${C.cyan}Usage:${C.reset}`);
587
+ console.log(` aether rewards list --address <addr> List all staking rewards`);
588
+ console.log(` aether rewards summary --address <addr> One-line rewards summary`);
589
+ console.log(` aether rewards claim --address <addr> [--account <stakeAcct>] Claim rewards`);
590
+ console.log();
591
+ console.log(` ${C.dim}--json Output as JSON`);
592
+ console.log(` --rpc <url> Use specific RPC endpoint${C.reset}\n`);
593
+ }
594
+ }
595
+
596
+ main().catch(err => {
597
+ console.error(`\n${C.red}Error running rewards command:${C.reset}`, err.message, '\n');
598
+ });
599
+
600
+ module.exports = { rewardsCommand: main };