aether-hub 1.2.3 → 1.2.4

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.
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli apy
4
+ *
5
+ * Estimate APY for a validator or wallet's stake positions.
6
+ * Fetches recent reward history, computes average yield over the current
7
+ * epoch, and annualises it to an APY figure.
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
+
22
+ const http = require('http');
23
+ const https = require('https');
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 CLI_VERSION = '1.0.0';
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Config
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function getDefaultRpc() {
44
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
45
+ }
46
+
47
+ function getDefaultConfig() {
48
+ const fs = require('fs');
49
+ const path = require('path');
50
+ const os = require('os');
51
+ const cfgPath = path.join(os.homedir(), '.aether', 'config.json');
52
+ if (!fs.existsSync(cfgPath)) return { defaultWallet: null };
53
+ try {
54
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
55
+ } catch {
56
+ return { defaultWallet: null };
57
+ }
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // HTTP helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function httpRequest(rpcUrl, pathStr, timeoutMs = 10000) {
65
+ return new Promise((resolve, reject) => {
66
+ const url = new URL(pathStr, rpcUrl);
67
+ const lib = url.protocol === 'https:' ? https : http;
68
+ const req = lib.request({
69
+ hostname: url.hostname,
70
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
71
+ path: url.pathname + url.search,
72
+ method: 'GET',
73
+ timeout: timeoutMs,
74
+ headers: { 'Content-Type': 'application/json' },
75
+ }, (res) => {
76
+ let data = '';
77
+ res.on('data', (chunk) => data += chunk);
78
+ res.on('end', () => {
79
+ try { resolve(JSON.parse(data)); }
80
+ catch { resolve({ _raw: data }); }
81
+ });
82
+ });
83
+ req.on('error', reject);
84
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
85
+ req.end();
86
+ });
87
+ }
88
+
89
+ function httpPost(rpcUrl, pathStr, body, timeoutMs = 10000) {
90
+ return new Promise((resolve, reject) => {
91
+ const url = new URL(pathStr, rpcUrl);
92
+ const lib = url.protocol === 'https:' ? https : http;
93
+ const bodyStr = JSON.stringify(body);
94
+ const req = lib.request({
95
+ hostname: url.hostname,
96
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
97
+ path: url.pathname + url.search,
98
+ method: 'POST',
99
+ timeout: timeoutMs,
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ 'Content-Length': Buffer.byteLength(bodyStr),
103
+ },
104
+ }, (res) => {
105
+ let data = '';
106
+ res.on('data', (chunk) => data += chunk);
107
+ res.on('end', () => {
108
+ try { resolve(JSON.parse(data)); }
109
+ catch { resolve({ _raw: data }); }
110
+ });
111
+ });
112
+ req.on('error', reject);
113
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
114
+ req.write(bodyStr);
115
+ req.end();
116
+ });
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Argument parsing
121
+ // ---------------------------------------------------------------------------
122
+
123
+ function parseArgs() {
124
+ const args = process.argv.slice(2);
125
+ const opts = {
126
+ rpc: getDefaultRpc(),
127
+ validator: null,
128
+ address: null,
129
+ asJson: false,
130
+ epochs: 14, // default lookback for APY calc
131
+ };
132
+
133
+ for (let i = 0; i < args.length; i++) {
134
+ const arg = args[i];
135
+ if (arg === '--rpc' || arg === '-r') {
136
+ opts.rpc = args[++i];
137
+ } else if (arg === '--validator' || arg === '-v') {
138
+ opts.validator = args[++i];
139
+ } else if (arg === '--address' || arg === '-a') {
140
+ opts.address = args[++i];
141
+ } else if (arg === '--json' || arg === '-j') {
142
+ opts.asJson = true;
143
+ } else if (arg === '--epochs' || arg === '-e') {
144
+ const v = parseInt(args[++i], 10);
145
+ if (!isNaN(v) && v > 0 && v <= 100) opts.epochs = v;
146
+ } else if (arg === '--help' || arg === '-h') {
147
+ showHelp();
148
+ process.exit(0);
149
+ }
150
+ }
151
+
152
+ // Fall back to default wallet if no address provided
153
+ if (!opts.address && !opts.validator) {
154
+ const cfg = getDefaultConfig();
155
+ opts.address = cfg.defaultWallet;
156
+ }
157
+
158
+ return opts;
159
+ }
160
+
161
+ function showHelp() {
162
+ console.log(`
163
+ ${C.bright}${C.cyan}aether-cli apy${C.reset} - Validator APY Estimator
164
+
165
+ ${C.bright}Usage:${C.reset}
166
+ aether apy Network-wide average APY
167
+ aether apy --validator <addr> APY for a specific validator
168
+ aether apy --address <addr> APY for wallet's stake positions
169
+ aether apy --rpc <url> Override default RPC
170
+ aether apy --json JSON output for scripting
171
+ aether apy --epochs <n> Lookback epochs (default: 14, max: 100)
172
+
173
+ ${C.bright}Examples:${C.reset}
174
+ aether apy --validator ATH3J8... Check a validator's APY
175
+ aether apy --address ATHabc... Check your wallet's weighted APY
176
+ aether apy --json Machine-readable output
177
+ `.trim());
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // APY calculation
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /**
185
+ * Estimate APY from reward history.
186
+ *
187
+ * Strategy:
188
+ * 1. Fetch last N epochs of reward history for the target
189
+ * 2. Compute per-epoch yield: rewards_staked
190
+ * 3. Average the yield, then annualise: APY = ((1 + avg_yield) ^ epochs_per_year) - 1
191
+ *
192
+ * Aether epochs are ~4 hours → ~2190 epochs/year
193
+ */
194
+ async function estimateApy({ rpc, validator, address, epochs }) {
195
+ const EPOCHS_PER_YEAR = 2190;
196
+
197
+ // Normalise address: strip ATH prefix for API calls
198
+ const rawAddr = (validator || address || '').startsWith('ATH')
199
+ ? (validator || address || '').slice(3)
200
+ : (validator || address || '');
201
+
202
+ // Step 1: fetch epoch info to get current epoch
203
+ let currentEpoch = null;
204
+ let epochLength = null;
205
+ try {
206
+ const epochInfo = await httpRequest(rpc, '/v1/epoch/info');
207
+ if (!epochInfo.error) {
208
+ currentEpoch = epochInfo.epoch;
209
+ epochLength = epochInfo.slots_in_epoch || epochInfo.epoch_length;
210
+ }
211
+ } catch { /* use defaults */ }
212
+
213
+ // Step 2: fetch reward history
214
+ // Try /v1/rewards?address=<addr>&epochs=<N> first
215
+ let rewardHistory = [];
216
+ try {
217
+ const fromEpoch = Math.max(0, (currentEpoch || epochs) - epochs);
218
+ const rewardsRes = await httpRequest(
219
+ rpc,
220
+ `/v1/rewards?address=${encodeURIComponent(rawAddr)}&from_epoch=${fromEpoch}&limit=${epochs}`
221
+ );
222
+ if (!rewardsRes.error) {
223
+ rewardHistory = Array.isArray(rewardsRes)
224
+ ? rewardsRes
225
+ : (rewardsRes.rewards || []);
226
+ }
227
+ } catch { /* try alternate endpoint */ }
228
+
229
+ // Fallback: /v1/stake?address=<addr> (contains accumulated rewards)
230
+ if (rewardHistory.length === 0) {
231
+ try {
232
+ const stakeRes = await httpRequest(
233
+ rpc,
234
+ `/v1/stake?address=${encodeURIComponent(rawAddr)}`
235
+ );
236
+ if (!stakeRes.error) {
237
+ const accounts = Array.isArray(stakeRes) ? stakeRes : (stakeRes.accounts || []);
238
+ for (const acc of accounts) {
239
+ if (acc.rewards !== undefined) {
240
+ rewardHistory.push({
241
+ epoch: acc.epoch || currentEpoch,
242
+ rewards: acc.rewards,
243
+ lamports: acc.stake_lamports || acc.lamports || 0,
244
+ });
245
+ }
246
+ }
247
+ }
248
+ } catch { /* no stake data */ }
249
+ }
250
+
251
+ // Step 3: calculate APY
252
+ if (rewardHistory.length === 0) {
253
+ // No reward data — try fetching network-wide stats for a ballpark
254
+ try {
255
+ const supplyRes = await httpRequest(rpc, '/v1/supply');
256
+ const epochRes = await httpRequest(rpc, '/v1/epoch/info');
257
+ if (!supplyRes.error && !epochRes.error) {
258
+ // Rough estimate: total_reward_rate from inflation schedule
259
+ // Aether uses ~7% inflation Year 1, declining. Use 7% as starting point.
260
+ const inflationRate = 0.07; // 7% base, would need real data for precision
261
+ return {
262
+ apy: inflationRate,
263
+ apy_pct: inflationRate * 100,
264
+ method: 'inflation_model',
265
+ epoch: currentEpoch,
266
+ epochs_used: 0,
267
+ epochs_available: 0,
268
+ total_staked: supplyRes.total || 0,
269
+ validator,
270
+ address,
271
+ note: 'No reward history available. APY estimated from network inflation model.',
272
+ };
273
+ }
274
+ } catch { /* no network data either */ }
275
+
276
+ return {
277
+ apy: null,
278
+ apy_pct: null,
279
+ method: 'none',
280
+ epoch: currentEpoch,
281
+ epochs_used: 0,
282
+ error: 'No reward history or network data available. Ensure your validator is running and has reward data.',
283
+ };
284
+ }
285
+
286
+ // Compute weighted average yield per epoch
287
+ let totalRewards = 0;
288
+ let totalStaked = 0;
289
+ let epochsWithRewards = 0;
290
+
291
+ for (const entry of rewardHistory) {
292
+ const rewards = entry.rewards || 0;
293
+ const staked = entry.lamports || entry.stake_lamports || 0;
294
+ totalRewards += rewards;
295
+ if (staked > 0) totalStaked += staked;
296
+ if (rewards > 0) epochsWithRewards++;
297
+ }
298
+
299
+ if (totalStaked === 0 || totalRewards === 0) {
300
+ return {
301
+ apy: null,
302
+ apy_pct: null,
303
+ method: 'insufficient_data',
304
+ epoch: currentEpoch,
305
+ epochs_used: rewardHistory.length,
306
+ epochs_with_rewards: epochsWithRewards,
307
+ validator,
308
+ address,
309
+ note: 'Stake positions exist but no reward data yet. Check back after epoch ends.',
310
+ };
311
+ }
312
+
313
+ // Average epoch yield = total rewards / (total staked * num epochs)
314
+ const avgYieldPerEpoch = totalRewards / (totalStaked * rewardHistory.length);
315
+
316
+ // Annualise: APY = (1 + yield_per_epoch) ^ epochs_per_year - 1
317
+ const apy = Math.pow(1 + avgYieldPerEpoch, EPOCHS_PER_YEAR) - 1;
318
+ const apyPct = apy * 100;
319
+
320
+ return {
321
+ apy,
322
+ apy_pct: parseFloat(apyPct.toFixed(2)),
323
+ method: 'reward_history',
324
+ epoch: currentEpoch,
325
+ epochs_used: rewardHistory.length,
326
+ epochs_with_rewards: epochsWithRewards,
327
+ total_rewards_lamports: totalRewards,
328
+ total_staked_lamports: totalStaked,
329
+ avg_yield_per_epoch_pct: parseFloat((avgYieldPerEpoch * 100).toFixed(4)),
330
+ validator,
331
+ address,
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Format APY bar: ████░░░░░░
337
+ */
338
+ function formatApyBar(apyPct, maxPct = 20) {
339
+ const totalBars = 20;
340
+ const filled = Math.min(totalBars, Math.round((apyPct / maxPct) * totalBars));
341
+ return C.green + '█'.repeat(filled) + C.dim + '░'.repeat(totalBars - filled) + C.reset;
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Output formatters
346
+ // ---------------------------------------------------------------------------
347
+
348
+ function outputHuman(apyData, opts) {
349
+ const { rpc, validator, address, asJson } = opts;
350
+
351
+ console.log(`\n${C.bright}${C.cyan}── Validator APY Estimate ──────────────────────────────${C.reset}\n`);
352
+
353
+ const targetLabel = validator
354
+ ? `Validator: ${C.bright}${validator}${C.reset}`
355
+ : `Address: ${C.bright}${address}${C.reset}`;
356
+ console.log(` ${C.green}★${C.reset} ${targetLabel}`);
357
+ console.log(` ${C.dim} RPC: ${rpc}${C.reset}`);
358
+ console.log();
359
+
360
+ if (apyData.error) {
361
+ console.log(` ${C.yellow}⚠ ${apyData.error}${C.reset}\n`);
362
+ return;
363
+ }
364
+
365
+ if (apyData.note) {
366
+ console.log(` ${C.yellow}⚠ ${apyData.note}${C.reset}`);
367
+ console.log();
368
+ }
369
+
370
+ if (apyData.apy_pct === null) {
371
+ console.log(` ${C.yellow}⚠ APY data unavailable.${C.reset}`);
372
+ if (apyData.method === 'inflation_model') {
373
+ console.log(` ${C.dim} Using network inflation model as estimate.${C.reset}`);
374
+ }
375
+ console.log();
376
+ return;
377
+ }
378
+
379
+ // Main APY display
380
+ const apyColor = apyData.apy_pct >= 8
381
+ ? C.green
382
+ : apyData.apy_pct >= 4
383
+ ? C.cyan
384
+ : apyData.apy_pct >= 2
385
+ ? C.yellow
386
+ : C.red;
387
+
388
+ console.log(` ${C.bright}${apyColor}${apyData.apy_pct.toFixed(2)}%${C.reset} ${C.dim}APY${C.reset}`);
389
+ console.log();
390
+
391
+ // Visual bar
392
+ console.log(` ${C.dim}Yield:${C.reset} ${formatApyBar(apyData.apy_pct)}`);
393
+ console.log();
394
+
395
+ // Stats
396
+ console.log(` ${C.dim}Method:${C.reset} ${C.bright}${apyData.method === 'reward_history' ? 'Reward history annualised' : 'Inflation model'}${C.reset}`);
397
+ if (apyData.epoch !== undefined && apyData.epoch !== null) {
398
+ console.log(` ${C.dim}Epoch:${C.reset} ${C.bright}#${apyData.epoch}${C.reset}`);
399
+ }
400
+ console.log(` ${C.dim}Epochs used:${C.reset} ${apyData.epochs_used}`);
401
+
402
+ if (apyData.avg_yield_per_epoch_pct !== undefined) {
403
+ console.log(` ${C.dim}Avg/epoch:${C.reset} ${C.bright}${apyData.avg_yield_per_epoch_pct.toFixed(4)}%${C.reset}`);
404
+ }
405
+
406
+ if (apyData.total_rewards_lamports !== undefined && apyData.total_staked_lamports !== undefined) {
407
+ const totalAeth = (apyData.total_rewards_lamports / 1e9).toFixed(4);
408
+ const stakedAeth = (apyData.total_staked_lamports / 1e9).toFixed(2);
409
+ console.log(` ${C.dim}Total rewards:${C.reset} ${C.green}${totalAeth} AETH${C.reset}`);
410
+ console.log(` ${C.dim}Total staked:${C.reset} ${stakedAeth} AETH`);
411
+ }
412
+
413
+ console.log();
414
+
415
+ // Disclaimer
416
+ console.log(` ${C.dim}Note: APY is an estimate based on ${apyData.epochs_used} epoch(s) of reward data.${C.reset}`);
417
+ console.log(` ${C.dim}Actual returns may vary. Check aether rewards for precise figures.${C.reset}`);
418
+ console.log();
419
+ }
420
+
421
+ function outputJson(apyData, opts) {
422
+ console.log(JSON.stringify({
423
+ apy_pct: apyData.apy_pct,
424
+ apy: apyData.apy,
425
+ method: apyData.method,
426
+ epoch: apyData.epoch,
427
+ epochs_used: apyData.epochs_used,
428
+ epochs_with_rewards: apyData.epochs_with_rewards,
429
+ avg_yield_per_epoch_pct: apyData.avg_yield_per_epoch_pct,
430
+ total_rewards_lamports: apyData.total_rewards_lamports,
431
+ total_staked_lamports: apyData.total_staked_lamports,
432
+ validator: apyData.validator,
433
+ address: apyData.address,
434
+ note: apyData.note || null,
435
+ error: apyData.error || null,
436
+ rpc: opts.rpc,
437
+ cli_version: CLI_VERSION,
438
+ timestamp: new Date().toISOString(),
439
+ }, null, 2));
440
+ }
441
+
442
+ // ---------------------------------------------------------------------------
443
+ // Main
444
+ // ---------------------------------------------------------------------------
445
+
446
+ async function main() {
447
+ const opts = parseArgs();
448
+
449
+ try {
450
+ const apyData = await estimateApy(opts);
451
+
452
+ if (opts.asJson) {
453
+ outputJson(apyData, opts);
454
+ } else {
455
+ outputHuman(apyData, opts);
456
+ }
457
+ } catch (err) {
458
+ if (opts.asJson) {
459
+ console.log(JSON.stringify({
460
+ apy_pct: null,
461
+ apy: null,
462
+ error: err.message,
463
+ validator: opts.validator,
464
+ address: opts.address,
465
+ rpc: opts.rpc,
466
+ cli_version: CLI_VERSION,
467
+ timestamp: new Date().toISOString(),
468
+ }, null, 2));
469
+ } else {
470
+ console.log(`\n ${C.red}✗ APY calculation failed:${C.reset} ${err.message}`);
471
+ console.log(` ${C.dim} RPC: ${opts.rpc}${C.reset}`);
472
+ console.log(` ${C.dim} Is your validator running?${C.reset}\n`);
473
+ }
474
+ process.exit(1);
475
+ }
476
+ }
477
+
478
+ main();
479
+
480
+ module.exports = { apyCommand: main };
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli stake-positions
4
+ *
5
+ * Query and display current stake positions/delegations for a wallet.
6
+ * Shows validator, amount, status, and accumulated rewards.
7
+ *
8
+ * Usage:
9
+ * aether stake-positions --address <addr> [--json]
10
+ * aether wallet stake-positions --address <addr> [--json]
11
+ *
12
+ * Examples:
13
+ * aether stake-positions --address ATHxxx
14
+ * aether wallet stake-positions --address ATHxxx --json
15
+ */
16
+
17
+ const https = require('https');
18
+ const http = require('http');
19
+
20
+ // ANSI colours
21
+ const C = {
22
+ reset: '\x1b[0m',
23
+ bright: '\x1b[1m',
24
+ dim: '\x1b[2m',
25
+ red: '\x1b[31m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ cyan: '\x1b[36m',
29
+ magenta: '\x1b[35m',
30
+ };
31
+
32
+ const CLI_VERSION = '1.0.0';
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Config
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function getDefaultRpc() {
39
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // HTTP helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function httpRequest(rpcUrl, pathStr, timeoutMs = 8000) {
47
+ return new Promise((resolve, reject) => {
48
+ const url = new URL(pathStr, rpcUrl);
49
+ const lib = url.protocol === 'https:' ? https : http;
50
+ const req = lib.request({
51
+ hostname: url.hostname,
52
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
53
+ path: url.pathname + url.search,
54
+ method: 'GET',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ }, (res) => {
57
+ let data = '';
58
+ res.on('data', (chunk) => data += chunk);
59
+ res.on('end', () => {
60
+ try { resolve(JSON.parse(data)); }
61
+ catch { resolve(data); }
62
+ });
63
+ });
64
+ req.on('error', reject);
65
+ req.setTimeout(timeoutMs, () => {
66
+ req.destroy();
67
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
68
+ });
69
+ req.end();
70
+ });
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Argument parsing
75
+ // ---------------------------------------------------------------------------
76
+
77
+ function parseArgs() {
78
+ const args = process.argv.slice(2);
79
+ const result = { address: null, json: false };
80
+
81
+ for (let i = 0; i < args.length; i++) {
82
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
83
+ result.address = args[i + 1];
84
+ i++;
85
+ } else if (args[i] === '--json' || args[i] === '--json-output') {
86
+ result.json = true;
87
+ } else if (args[i] === '--rpc' && args[i + 1]) {
88
+ result.rpc = args[i + 1];
89
+ i++;
90
+ } else if (args[i] === '--help' || args[i] === '-h') {
91
+ result.help = true;
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Balance formatting
100
+ // ---------------------------------------------------------------------------
101
+
102
+ function formatAether(lamports) {
103
+ const aeth = (lamports || 0) / 1e9;
104
+ return aeth.toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 }) + ' AETH';
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Main
109
+ // ---------------------------------------------------------------------------
110
+
111
+ async function stakePositionsCommand() {
112
+ const opts = parseArgs();
113
+
114
+ if (opts.help) {
115
+ console.log(`
116
+ ${C.bright}${C.cyan}stake-positions${C.reset} — Query active stake delegations for a wallet
117
+
118
+ ${C.bright}USAGE${C.reset}
119
+ aether stake-positions --address <addr> [--json] [--rpc <url>]
120
+
121
+ ${C.bright}OPTIONS${C.reset}
122
+ --address <addr> Wallet address (ATH...)
123
+ --json Output raw JSON
124
+ --rpc <url> RPC endpoint (default: AETHER_RPC or localhost:8899)
125
+ --help Show this help
126
+
127
+ ${C.bright}EXAMPLES${C.reset}
128
+ aether stake-positions --address ATH3abc...
129
+ aether stake-positions --address ATH3abc... --json
130
+ `);
131
+ return;
132
+ }
133
+
134
+ if (!opts.address) {
135
+ console.log(` ${C.red}✗ Missing --address${C.reset}\n`);
136
+ console.log(` Usage: aether stake-positions --address <addr> [--json]\n`);
137
+ return;
138
+ }
139
+
140
+ const rpcUrl = opts.rpc || getDefaultRpc();
141
+ const address = opts.address;
142
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
143
+
144
+ if (!opts.json) {
145
+ console.log(`\n${C.bright}${C.cyan}── Stake Positions ──────────────────────────────────────${C.reset}\n`);
146
+ console.log(` ${C.dim}Wallet:${C.reset} ${address}`);
147
+ console.log(` ${C.dim}RPC: ${C.reset} ${rpcUrl}\n`);
148
+ }
149
+
150
+ try {
151
+ // Fetch stake accounts
152
+ const res = await httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`);
153
+
154
+ let stakeAccounts = [];
155
+ if (Array.isArray(res)) {
156
+ stakeAccounts = res;
157
+ } else if (res && typeof res === 'object') {
158
+ stakeAccounts = res.accounts || res.stake_accounts || res.data || [];
159
+ }
160
+
161
+ if (opts.json) {
162
+ const totalLamports = stakeAccounts.reduce((sum, acc) => sum + (acc.stake_lamports || acc.lamports || 0), 0);
163
+ console.log(JSON.stringify({
164
+ wallet_address: address,
165
+ stake_accounts: stakeAccounts.map(acc => ({
166
+ stake_account: acc.pubkey || acc.publicKey || acc.account || 'unknown',
167
+ validator: acc.validator || acc.delegate || acc.validator_address || 'unknown',
168
+ stake_lamports: acc.stake_lamports || acc.lamports || 0,
169
+ stake_aeth: ((acc.stake_lamports || acc.lamports || 0) / 1e9).toFixed(4),
170
+ status: acc.status || acc.state || 'active',
171
+ updated_epoch: acc.epoch || acc.last_update_epoch || null,
172
+ })),
173
+ total_staked_lamports: totalLamports,
174
+ total_staked_aeth: (totalLamports / 1e9).toFixed(4),
175
+ count: stakeAccounts.length,
176
+ }, null, 2));
177
+ return;
178
+ }
179
+
180
+ if (!stakeAccounts || stakeAccounts.length === 0) {
181
+ console.log(` ${C.yellow}? No active stake positions found.${C.reset}`);
182
+ console.log(` ${C.dim} Stake AETH with: ${C.cyan}aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
183
+ return;
184
+ }
185
+
186
+ let totalStaked = 0;
187
+ console.log(` ${C.bright}Stake Positions (${stakeAccounts.length})${C.reset}\n`);
188
+
189
+ for (const acc of stakeAccounts) {
190
+ const stakeAcct = acc.pubkey || acc.publicKey || acc.account || 'unknown';
191
+ const validator = acc.validator || acc.delegate || acc.validator_address || 'unknown';
192
+ const lamports = acc.stake_lamports || acc.lamports || 0;
193
+ const status = (acc.status || acc.state || 'active').toLowerCase();
194
+ const epoch = acc.epoch || acc.last_update_epoch || null;
195
+
196
+ totalStaked += lamports;
197
+
198
+ const statusColor = status === 'active' ? C.green : status === 'deactivating' ? C.yellow : C.dim;
199
+ const shortAcct = stakeAcct.length > 20 ? stakeAcct.slice(0, 8) + '.' + stakeAcct.slice(-8) : stakeAcct;
200
+ const shortVal = validator.length > 20 ? validator.slice(0, 8) + '.' + validator.slice(-8) : validator;
201
+ const aeth = (lamports / 1e9).toFixed(4);
202
+
203
+ console.log(` ${C.dim}┌─${C.bright}${statusColor} ${status.toUpperCase()}${C.reset}`);
204
+ console.log(` │ ${C.dim}Stake acct:${C.reset} ${shortAcct}`);
205
+ console.log(` │ ${C.dim}Validator:${C.reset} ${shortVal}`);
206
+ console.log(` │ ${C.dim}Staked:${C.reset} ${C.bright}${aeth} AETH${C.reset} (${lamports.toLocaleString()} lamports)`);
207
+ if (epoch) console.log(` │ ${C.dim}Epoch:${C.reset} ${C.bright}#${epoch}${C.reset}`);
208
+ console.log(` ${C.dim}└${C.reset}\n`);
209
+ }
210
+
211
+ console.log(` ${C.dim}────────────────────────────────────────${C.reset}`);
212
+ console.log(` ${C.bright}Total Staked:${C.reset} ${C.green}${formatAether(totalStaked)}${C.reset}\n`);
213
+
214
+ } catch (err) {
215
+ console.log(` ${C.red}? Failed to fetch stake positions:${C.reset} ${err.message}\n`);
216
+ process.exit(1);
217
+ }
218
+ }
219
+
220
+ stakePositionsCommand();
package/index.js CHANGED
@@ -27,6 +27,7 @@ const { epochCommand } = require('./commands/epoch');
27
27
  const { supplyCommand } = require('./commands/supply');
28
28
  const { statusCommand } = require('./commands/status');
29
29
  const { broadcastCommand } = require('./commands/broadcast');
30
+ const { apyCommand } = require('./commands/apy');
30
31
  const readline = require('readline');
31
32
 
32
33
  // CLI version
@@ -197,11 +198,8 @@ const COMMANDS = {
197
198
  'stake-positions': {
198
199
  description: 'Show current stake positions/delegations — aether stake-positions --address <addr> [--json]',
199
200
  handler: () => {
200
- const { walletCommand } = require('./commands/wallet');
201
- const originalArgv = process.argv;
202
- process.argv = [...originalArgv.slice(0, 2), 'wallet', 'stake-positions', ...originalArgv.slice(3)];
203
- walletCommand();
204
- process.argv = originalArgv;
201
+ const { stakePositionsCommand } = require('./commands/stake-positions');
202
+ stakePositionsCommand();
205
203
  },
206
204
  },
207
205
  unstake: {
@@ -369,6 +367,13 @@ const COMMANDS = {
369
367
  broadcastCommand();
370
368
  },
371
369
  },
370
+ apy: {
371
+ description: 'Validator APY estimator — aether apy [--validator <addr>] [--address <addr>] [--json] [--epochs <n>]',
372
+ handler: () => {
373
+ const { apyCommand } = require('./commands/apy');
374
+ apyCommand();
375
+ },
376
+ },
372
377
  ping: {
373
378
  description: 'Ping RPC endpoint — measure latency, check node health — aether ping [--rpc <url>] [--count <n>] [--json]',
374
379
  handler: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-hub",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "AeTHer Validator CLI — tiered validators (Full/Lite/Observer), system checks, onboarding, and node management",
5
5
  "main": "index.js",
6
6
  "bin": {