@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,499 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli apy
4
+ *
5
+ * Estimate APY for a validator or wallet's stake positions.
6
+ * FULLY WIRED TO SDK - Uses @jellylegsai/aether-sdk for all blockchain calls.
7
+ * No manual HTTP - all calls go through AetherClient with real RPC.
8
+ *
9
+ * Usage:
10
+ * aether apy Show network-wide average APY
11
+ * aether apy --validator <addr> APY for a specific validator
12
+ * aether apy --address <addr> APY for a wallet's stake delegations
13
+ * aether apy --json JSON output for scripting/monitoring
14
+ * aether apy --rpc <url> Override default RPC
15
+ *
16
+ * Examples:
17
+ * aether apy --validator ATH3xyz... # Check validator APY
18
+ * aether apy --address ATHabc... # Check your wallet's weighted APY
19
+ * aether apy --json # Machine-readable output
20
+ *
21
+ * SDK Methods Used:
22
+ * - client.getValidatorAPY(address) → GET /v1/validator/<address>/apy
23
+ * - client.getStakePositions(address) → GET /v1/stake/<address>
24
+ * - client.getRewards(address) → GET /v1/rewards/<address>
25
+ * - client.getEpochInfo() → GET /v1/epoch
26
+ * - client.getSupply() → GET /v1/supply
27
+ */
28
+
29
+ const path = require('path');
30
+
31
+ // Import SDK — makes REAL HTTP RPC calls to the blockchain
32
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
33
+ const aether = require(sdkPath);
34
+
35
+ // ANSI colours
36
+ const C = {
37
+ reset: '\x1b[0m',
38
+ bright: '\x1b[1m',
39
+ dim: '\x1b[2m',
40
+ red: '\x1b[31m',
41
+ green: '\x1b[32m',
42
+ yellow: '\x1b[33m',
43
+ cyan: '\x1b[36m',
44
+ magenta: '\x1b[35m',
45
+ };
46
+
47
+ const CLI_VERSION = '1.1.0';
48
+ const EPOCHS_PER_YEAR = 2190; // Aether epochs are ~4 hours
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // SDK Client Setup
52
+ // ---------------------------------------------------------------------------
53
+
54
+ function getDefaultRpc() {
55
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
56
+ }
57
+
58
+ function createClient(rpcUrl) {
59
+ return new aether.AetherClient({ rpcUrl });
60
+ }
61
+
62
+ function getDefaultConfig() {
63
+ const fs = require('fs');
64
+ const cfgPath = path.join(require('os').homedir(), '.aether', 'config.json');
65
+ if (!fs.existsSync(cfgPath)) return { defaultWallet: null };
66
+ try {
67
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
68
+ } catch {
69
+ return { defaultWallet: null };
70
+ }
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Argument parsing
75
+ // ---------------------------------------------------------------------------
76
+
77
+ function parseArgs() {
78
+ const args = process.argv.slice(2);
79
+ const opts = {
80
+ rpc: getDefaultRpc(),
81
+ validator: null,
82
+ address: null,
83
+ asJson: false,
84
+ epochs: 14, // default lookback for APY calc
85
+ };
86
+
87
+ for (let i = 0; i < args.length; i++) {
88
+ const arg = args[i];
89
+ if (arg === '--rpc' || arg === '-r') {
90
+ opts.rpc = args[++i];
91
+ } else if (arg === '--validator' || arg === '-v') {
92
+ opts.validator = args[++i];
93
+ } else if (arg === '--address' || arg === '-a') {
94
+ opts.address = args[++i];
95
+ } else if (arg === '--json' || arg === '-j') {
96
+ opts.asJson = true;
97
+ } else if (arg === '--epochs' || arg === '-e') {
98
+ const v = parseInt(args[++i], 10);
99
+ if (!isNaN(v) && v > 0 && v <= 100) opts.epochs = v;
100
+ } else if (arg === '--help' || arg === '-h') {
101
+ showHelp();
102
+ process.exit(0);
103
+ }
104
+ }
105
+
106
+ // Fall back to default wallet if no address provided
107
+ if (!opts.address && !opts.validator) {
108
+ const cfg = getDefaultConfig();
109
+ opts.address = cfg.defaultWallet;
110
+ }
111
+
112
+ return opts;
113
+ }
114
+
115
+ function showHelp() {
116
+ console.log(`
117
+ ${C.bright}${C.cyan}aether-cli apy${C.reset} - Validator APY Estimator
118
+
119
+ ${C.bright}Usage:${C.reset}
120
+ aether apy Network-wide average APY
121
+ aether apy --validator <addr> APY for a specific validator
122
+ aether apy --address <addr> APY for wallet's stake positions
123
+ aether apy --rpc <url> Override default RPC
124
+ aether apy --json JSON output for scripting
125
+ aether apy --epochs <n> Lookback epochs (default: 14, max: 100)
126
+
127
+ ${C.bright}SDK Methods Used:${C.reset}
128
+ client.getValidatorAPY(address) → GET /v1/validator/<address>/apy
129
+ client.getStakePositions(address) → GET /v1/stake/<address>
130
+ client.getRewards(address) → GET /v1/rewards/<address>
131
+ client.getEpochInfo() → GET /v1/epoch
132
+ client.getSupply() → GET /v1/supply
133
+
134
+ ${C.bright}Examples:${C.reset}
135
+ aether apy --validator ATH3J8... Check a validator's APY
136
+ aether apy --address ATHabc... Check your wallet's weighted APY
137
+ aether apy --json Machine-readable output
138
+ `.trim());
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // APY calculation via SDK (REAL RPC CALLS)
143
+ // ---------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Fetch validator APY via SDK (GET /v1/validator/<address>/apy)
147
+ */
148
+ async function fetchValidatorApy(client, validatorAddr) {
149
+ try {
150
+ const result = await client.getValidatorAPY(validatorAddr);
151
+ return {
152
+ apy: result.apy ?? result.current_apy ?? result.estimated_apy ?? null,
153
+ commission: result.commission ?? null,
154
+ total_stake: result.total_stake ?? result.stake_lamports ?? null,
155
+ source: 'validator_api',
156
+ raw: result,
157
+ };
158
+ } catch (err) {
159
+ return { error: err.message, source: 'validator_api' };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Calculate wallet APY from stake positions and rewards via SDK
165
+ */
166
+ async function calculateWalletApy(client, address, epochs) {
167
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
168
+
169
+ // Parallel SDK calls
170
+ const [stakePositions, rewards, epochInfo, supply] = await Promise.all([
171
+ client.getStakePositions(rawAddr).catch(() => []),
172
+ client.getRewards(rawAddr).catch(() => ({ total: 0, pending: 0, history: [] })),
173
+ client.getEpochInfo().catch(() => ({ epoch: 0, slotsInEpoch: 432000, slotIndex: 0 })),
174
+ client.getSupply().catch(() => null),
175
+ ]);
176
+
177
+ const currentEpoch = epochInfo?.epoch ?? 0;
178
+
179
+ // No stake positions
180
+ if (!Array.isArray(stakePositions) || stakePositions.length === 0) {
181
+ return {
182
+ apy: null,
183
+ apy_pct: null,
184
+ method: 'no_stake',
185
+ epoch: currentEpoch,
186
+ address,
187
+ note: 'No active stake positions found for this address.',
188
+ };
189
+ }
190
+
191
+ // Calculate total staked
192
+ const totalStakedLamports = stakePositions.reduce((sum, s) => {
193
+ return sum + BigInt(s.lamports || s.stake_lamports || 0);
194
+ }, 0n);
195
+
196
+ if (totalStakedLamports === 0n) {
197
+ return {
198
+ apy: null,
199
+ apy_pct: null,
200
+ method: 'no_active_stake',
201
+ epoch: currentEpoch,
202
+ address,
203
+ note: 'Stake positions found but no active stake amount.',
204
+ };
205
+ }
206
+
207
+ // Get reward history
208
+ const rewardHistory = rewards?.history || rewards?.epochs || [];
209
+ const totalRewardsLamports = BigInt(rewards?.total || rewards?.total_rewards || 0);
210
+
211
+ // If we have reward history, calculate from it
212
+ if (rewardHistory.length > 0 && totalRewardsLamports > 0n) {
213
+ const epochsWithData = Math.min(rewardHistory.length, epochs);
214
+ const avgYieldPerEpoch = Number(totalRewardsLamports) / (Number(totalStakedLamports) * epochsWithData);
215
+ const apy = Math.pow(1 + avgYieldPerEpoch, EPOCHS_PER_YEAR) - 1;
216
+
217
+ return {
218
+ apy,
219
+ apy_pct: parseFloat((apy * 100).toFixed(2)),
220
+ method: 'reward_history',
221
+ epoch: currentEpoch,
222
+ epochs_used: epochsWithData,
223
+ epochs_with_rewards: rewardHistory.filter(r => (r.rewards || 0) > 0).length,
224
+ total_rewards_lamports: totalRewardsLamports.toString(),
225
+ total_staked_lamports: totalStakedLamports.toString(),
226
+ avg_yield_per_epoch_pct: parseFloat((avgYieldPerEpoch * 100).toFixed(4)),
227
+ address,
228
+ stake_count: stakePositions.length,
229
+ };
230
+ }
231
+
232
+ // No reward history yet — use network inflation model
233
+ if (supply && !supply.error) {
234
+ // Aether uses ~7% inflation Year 1, declining
235
+ const inflationRate = 0.07;
236
+ return {
237
+ apy: inflationRate,
238
+ apy_pct: 7.0,
239
+ method: 'inflation_model',
240
+ epoch: currentEpoch,
241
+ epochs_used: 0,
242
+ total_staked_lamports: totalStakedLamports.toString(),
243
+ address,
244
+ stake_count: stakePositions.length,
245
+ note: 'No reward history available yet. APY estimated from network inflation model.',
246
+ };
247
+ }
248
+
249
+ // No data available
250
+ return {
251
+ apy: null,
252
+ apy_pct: null,
253
+ method: 'insufficient_data',
254
+ epoch: currentEpoch,
255
+ address,
256
+ stake_count: stakePositions.length,
257
+ note: 'Stake positions exist but no reward data yet. Check back after epoch ends.',
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Estimate APY using SDK
263
+ */
264
+ async function estimateApy({ rpc, validator, address, epochs }) {
265
+ const client = createClient(rpc);
266
+
267
+ // Validator-specific APY
268
+ if (validator) {
269
+ const rawAddr = validator.startsWith('ATH') ? validator.slice(3) : validator;
270
+ const validatorApy = await fetchValidatorApy(client, rawAddr);
271
+
272
+ if (validatorApy.error) {
273
+ // Fall back to wallet calculation method
274
+ return await calculateWalletApy(client, validator, epochs);
275
+ }
276
+
277
+ const apy = validatorApy.apy ?? 0;
278
+ return {
279
+ apy,
280
+ apy_pct: apy !== null ? parseFloat((apy * 100).toFixed(2)) : null,
281
+ method: 'validator_api',
282
+ commission: validatorApy.commission,
283
+ total_stake: validatorApy.total_stake,
284
+ validator,
285
+ source: validatorApy.source,
286
+ };
287
+ }
288
+
289
+ // Wallet APY (weighted across all stake positions)
290
+ if (address) {
291
+ return await calculateWalletApy(client, address, epochs);
292
+ }
293
+
294
+ // Network-wide APY
295
+ try {
296
+ const [epochInfo, supply] = await Promise.all([
297
+ client.getEpochInfo().catch(() => null),
298
+ client.getSupply().catch(() => null),
299
+ ]);
300
+
301
+ if (supply && !supply.error) {
302
+ const inflationRate = 0.07; // 7% base inflation
303
+ return {
304
+ apy: inflationRate,
305
+ apy_pct: 7.0,
306
+ method: 'network_inflation_model',
307
+ epoch: epochInfo?.epoch ?? null,
308
+ note: 'Network-wide APY estimate based on inflation schedule.',
309
+ };
310
+ }
311
+ } catch (err) {
312
+ // Fall through
313
+ }
314
+
315
+ return {
316
+ apy: null,
317
+ apy_pct: null,
318
+ method: 'none',
319
+ error: 'Unable to fetch network data. Is your validator running?',
320
+ };
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Visual formatters
325
+ // ---------------------------------------------------------------------------
326
+
327
+ function formatApyBar(apyPct, maxPct = 20) {
328
+ const totalBars = 20;
329
+ const filled = Math.min(totalBars, Math.round((apyPct / maxPct) * totalBars));
330
+ return C.green + '█'.repeat(filled) + C.dim + '░'.repeat(totalBars - filled) + C.reset;
331
+ }
332
+
333
+ function getApyColor(apyPct) {
334
+ if (apyPct === null || apyPct === undefined) return C.dim;
335
+ if (apyPct >= 8) return C.green;
336
+ if (apyPct >= 4) return C.cyan;
337
+ if (apyPct >= 2) return C.yellow;
338
+ return C.red;
339
+ }
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Output formatters
343
+ // ---------------------------------------------------------------------------
344
+
345
+ function outputHuman(apyData, opts) {
346
+ const { rpc, validator, address } = opts;
347
+
348
+ console.log(`\n${C.bright}${C.cyan}── Validator APY Estimate ──────────────────────────────${C.reset}\n`);
349
+
350
+ const targetLabel = validator
351
+ ? `Validator: ${C.bright}${validator}${C.reset}`
352
+ : address
353
+ ? `Address: ${C.bright}${address}${C.reset}`
354
+ : `Network: ${C.bright}Aether Chain${C.reset}`;
355
+ console.log(` ${C.green}★${C.reset} ${targetLabel}`);
356
+ console.log(` ${C.dim} RPC: ${rpc}${C.reset}`);
357
+ console.log();
358
+
359
+ if (apyData.error) {
360
+ console.log(` ${C.yellow}⚠ ${apyData.error}${C.reset}\n`);
361
+ return;
362
+ }
363
+
364
+ if (apyData.note) {
365
+ console.log(` ${C.yellow}⚠ ${apyData.note}${C.reset}`);
366
+ console.log();
367
+ }
368
+
369
+ if (apyData.apy_pct === null) {
370
+ console.log(` ${C.yellow}⚠ APY data unavailable.${C.reset}`);
371
+ if (apyData.method === 'inflation_model' || apyData.method === 'network_inflation_model') {
372
+ console.log(` ${C.dim} Using network inflation model as estimate.${C.reset}`);
373
+ }
374
+ console.log();
375
+ return;
376
+ }
377
+
378
+ // Main APY display
379
+ const apyColor = getApyColor(apyData.apy_pct);
380
+ console.log(` ${C.bright}${apyColor}${apyData.apy_pct.toFixed(2)}%${C.reset} ${C.dim}APY${C.reset}`);
381
+ console.log();
382
+
383
+ // Visual bar
384
+ console.log(` ${C.dim}Yield:${C.reset} ${formatApyBar(apyData.apy_pct)}`);
385
+ console.log();
386
+
387
+ // Stats
388
+ const methodLabel = apyData.method === 'reward_history'
389
+ ? 'Reward history annualised'
390
+ : apyData.method === 'validator_api'
391
+ ? 'Validator API'
392
+ : 'Inflation model';
393
+ console.log(` ${C.dim}Method:${C.reset} ${C.bright}${methodLabel}${C.reset}`);
394
+
395
+ if (apyData.epoch !== undefined && apyData.epoch !== null) {
396
+ console.log(` ${C.dim}Epoch:${C.reset} ${C.bright}#${apyData.epoch}${C.reset}`);
397
+ }
398
+
399
+ if (apyData.epochs_used !== undefined) {
400
+ console.log(` ${C.dim}Epochs used:${C.reset} ${apyData.epochs_used}`);
401
+ }
402
+
403
+ if (apyData.avg_yield_per_epoch_pct !== undefined) {
404
+ console.log(` ${C.dim}Avg/epoch:${C.reset} ${C.bright}${apyData.avg_yield_per_epoch_pct.toFixed(4)}%${C.reset}`);
405
+ }
406
+
407
+ if (apyData.total_rewards_lamports !== undefined && apyData.total_staked_lamports !== undefined) {
408
+ const totalAeth = (Number(apyData.total_rewards_lamports) / 1e9).toFixed(4);
409
+ const stakedAeth = (Number(apyData.total_staked_lamports) / 1e9).toFixed(2);
410
+ console.log(` ${C.dim}Total rewards:${C.reset} ${C.green}${totalAeth} AETH${C.reset}`);
411
+ console.log(` ${C.dim}Total staked:${C.reset} ${stakedAeth} AETH`);
412
+ }
413
+
414
+ if (apyData.commission !== undefined && apyData.commission !== null) {
415
+ console.log(` ${C.dim}Commission:${C.reset} ${apyData.commission}%`);
416
+ }
417
+
418
+ if (apyData.stake_count !== undefined) {
419
+ console.log(` ${C.dim}Stake positions:${C.reset} ${apyData.stake_count}`);
420
+ }
421
+
422
+ console.log();
423
+
424
+ // Disclaimer
425
+ console.log(` ${C.dim}Note: APY is an estimate based on ${apyData.epochs_used || 0} epoch(s) of reward data.${C.reset}`);
426
+ console.log(` ${C.dim}Actual returns may vary. Check 'aether rewards' for precise figures.${C.reset}`);
427
+ console.log();
428
+ }
429
+
430
+ function outputJson(apyData, opts) {
431
+ console.log(JSON.stringify({
432
+ apy_pct: apyData.apy_pct,
433
+ apy: apyData.apy,
434
+ method: apyData.method,
435
+ epoch: apyData.epoch,
436
+ epochs_used: apyData.epochs_used,
437
+ epochs_with_rewards: apyData.epochs_with_rewards,
438
+ avg_yield_per_epoch_pct: apyData.avg_yield_per_epoch_pct,
439
+ total_rewards_lamports: apyData.total_rewards_lamports,
440
+ total_staked_lamports: apyData.total_staked_lamports,
441
+ commission: apyData.commission,
442
+ validator: apyData.validator,
443
+ address: apyData.address,
444
+ stake_count: apyData.stake_count,
445
+ note: apyData.note || null,
446
+ error: apyData.error || null,
447
+ rpc: opts.rpc,
448
+ cli_version: CLI_VERSION,
449
+ sdk_version: 'SDK via AetherClient',
450
+ timestamp: new Date().toISOString(),
451
+ }, null, 2));
452
+ }
453
+
454
+ // ---------------------------------------------------------------------------
455
+ // Main
456
+ // ---------------------------------------------------------------------------
457
+
458
+ async function apyCommand() {
459
+ const opts = parseArgs();
460
+
461
+ try {
462
+ const apyData = await estimateApy(opts);
463
+
464
+ if (opts.asJson) {
465
+ outputJson(apyData, opts);
466
+ } else {
467
+ outputHuman(apyData, opts);
468
+ }
469
+ } catch (err) {
470
+ if (opts.asJson) {
471
+ console.log(JSON.stringify({
472
+ apy_pct: null,
473
+ apy: null,
474
+ error: err.message,
475
+ validator: opts.validator,
476
+ address: opts.address,
477
+ rpc: opts.rpc,
478
+ cli_version: CLI_VERSION,
479
+ timestamp: new Date().toISOString(),
480
+ }, null, 2));
481
+ } else {
482
+ console.log(`\n ${C.red}✗ APY calculation failed:${C.reset} ${err.message}`);
483
+ console.log(` ${C.dim} RPC: ${opts.rpc}${C.reset}`);
484
+ console.log(` ${C.dim} Is your validator running?${C.reset}\n`);
485
+ }
486
+ process.exit(1);
487
+ }
488
+ }
489
+
490
+ // Export for use in index.js
491
+ module.exports = { apyCommand };
492
+
493
+ // Run if called directly
494
+ if (require.main === module) {
495
+ apyCommand().catch(err => {
496
+ console.error(`\n${C.red}APY command failed:${C.reset} ${err.message}\n`);
497
+ process.exit(1);
498
+ });
499
+ }