@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,657 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli validator-info
4
+ *
5
+ * Display detailed information about a specific validator including:
6
+ * - APY (Annual Percentage Yield)
7
+ * - Commission rate
8
+ * - Total stake
9
+ * - Stake positions and delegations
10
+ * - Rewards earned
11
+ * - Uptime and performance metrics
12
+ * - Validator metadata
13
+ * - Current epoch participation
14
+ *
15
+ * Usage:
16
+ * aether validator-info --address <validator_addr> [--json] [--rpc <url>]
17
+ * aether validator-info <validator_addr>
18
+ * aether validator-info --address <addr> --delegations
19
+ * aether validator-info --address <addr> --rewards
20
+ *
21
+ * Examples:
22
+ * aether validator-info ATHabc...
23
+ * aether validator-info ATHabc... --json
24
+ * aether validator-info ATHabc... --rpc http://localhost:8899
25
+ * aether validator-info ATHabc... --delegations --limit 50
26
+ *
27
+ * SDK wired to:
28
+ * - GET /v1/validator/<address>/apy (via client.getValidatorAPY())
29
+ * - GET /v1/validators (via client.getValidators())
30
+ * - GET /v1/stake/<address> (via client.getStakePositions())
31
+ * - GET /v1/rewards/<address> (via client.getRewards())
32
+ * - GET /v1/epoch (via client.getEpochInfo())
33
+ * - GET /v1/slot (via client.getSlot())
34
+ */
35
+
36
+ const path = require('path');
37
+
38
+ // Import SDK for blockchain RPC calls
39
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
40
+ const aether = require(sdkPath);
41
+
42
+ // ANSI colours
43
+ const C = {
44
+ reset: '\x1b[0m',
45
+ bright: '\x1b[1m',
46
+ dim: '\x1b[2m',
47
+ red: '\x1b[31m',
48
+ green: '\x1b[32m',
49
+ yellow: '\x1b[33m',
50
+ cyan: '\x1b[36m',
51
+ magenta: '\x1b[35m',
52
+ blue: '\x1b[34m',
53
+ };
54
+
55
+ const CLI_VERSION = '1.2.0';
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Config
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function getDefaultRpc() {
62
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
63
+ }
64
+
65
+ function createClient(rpcUrl) {
66
+ return new aether.AetherClient({ rpcUrl });
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Argument parsing
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function parseArgs() {
74
+ const args = process.argv.slice(2);
75
+ const result = {
76
+ address: null,
77
+ json: false,
78
+ rpc: null,
79
+ showDelegations: false,
80
+ showRewards: false,
81
+ limit: 20,
82
+ };
83
+
84
+ for (let i = 0; i < args.length; i++) {
85
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
86
+ result.address = args[++i];
87
+ } else if (args[i] === '--json' || args[i] === '-j') {
88
+ result.json = true;
89
+ } else if (args[i] === '--rpc' && args[i + 1]) {
90
+ result.rpc = args[++i];
91
+ } else if (args[i] === '--delegations' || args[i] === '-d') {
92
+ result.showDelegations = true;
93
+ } else if (args[i] === '--rewards' || args[i] === '-r') {
94
+ result.showRewards = true;
95
+ } else if (args[i] === '--limit' || args[i] === '-l') {
96
+ const val = parseInt(args[++i], 10);
97
+ if (!isNaN(val) && val > 0) result.limit = val;
98
+ } else if (args[i] === '--help' || args[i] === '-h') {
99
+ result.help = true;
100
+ } else if (!result.address && !args[i].startsWith('-')) {
101
+ // Positional argument for address
102
+ result.address = args[i];
103
+ }
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Format helpers
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function formatAether(lamports) {
114
+ const aeth = (lamports || 0) / 1e9;
115
+ return aeth.toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 }) + ' AETH';
116
+ }
117
+
118
+ function formatPercent(value) {
119
+ if (value === null || value === undefined) return 'N/A';
120
+ if (typeof value === 'string') value = parseFloat(value);
121
+ if (isNaN(value)) return 'N/A';
122
+ return value.toFixed(2) + '%';
123
+ }
124
+
125
+ function formatUptime(seconds) {
126
+ if (!seconds) return 'N/A';
127
+ const days = Math.floor(seconds / 86400);
128
+ const hours = Math.floor((seconds % 86400) / 3600);
129
+ const mins = Math.floor((seconds % 3600) / 60);
130
+ if (days > 0) return `${days}d ${hours}h`;
131
+ if (hours > 0) return `${hours}h ${mins}m`;
132
+ return `${mins}m`;
133
+ }
134
+
135
+ function formatUptimePercent(value) {
136
+ if (value === null || value === undefined) return 'N/A';
137
+ const pct = typeof value === 'number' ? value : parseFloat(value);
138
+ if (isNaN(pct)) return 'N/A';
139
+ if (pct >= 99) return C.green + pct.toFixed(2) + '%' + C.reset;
140
+ if (pct >= 95) return C.yellow + pct.toFixed(2) + '%' + C.reset;
141
+ return C.red + pct.toFixed(2) + '%' + C.reset;
142
+ }
143
+
144
+ function shortPubkey(pubkey, len = 8) {
145
+ if (!pubkey || pubkey.length < 16) return pubkey || 'unknown';
146
+ return pubkey.slice(0, len) + '...' + pubkey.slice(-len);
147
+ }
148
+
149
+ function getStatusColor(status) {
150
+ const s = (status || '').toLowerCase();
151
+ if (s === 'active') return C.green;
152
+ if (s === 'delinquent') return C.red;
153
+ if (s === 'inactive') return C.dim;
154
+ if (s === 'jailed') return C.red;
155
+ return C.yellow;
156
+ }
157
+
158
+ function getStakeStatusColor(status) {
159
+ const s = (status || '').toLowerCase();
160
+ if (s === 'active') return C.green;
161
+ if (s === 'activating') return C.cyan;
162
+ if (s === 'deactivating') return C.yellow;
163
+ if (s === 'inactive') return C.dim;
164
+ return C.reset;
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // RPC fetchers - Real blockchain calls via SDK
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * Fetch validator APY via SDK (GET /v1/validator/<address>/apy)
173
+ */
174
+ async function fetchValidatorAPY(client, address) {
175
+ try {
176
+ const result = await client.getValidatorAPY(address);
177
+ return result;
178
+ } catch (err) {
179
+ return { error: err.message };
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Fetch all validators and find the specific one
185
+ * Uses SDK's getValidators() which calls GET /v1/validators
186
+ */
187
+ async function fetchValidatorInfo(client, address) {
188
+ try {
189
+ const validators = await client.getValidators();
190
+ const validator = validators.find(v =>
191
+ v.address === address ||
192
+ v.pubkey === address ||
193
+ v.id === address ||
194
+ v.vote_account === address
195
+ );
196
+ return validator || null;
197
+ } catch (err) {
198
+ return { error: err.message };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Fetch stake positions for the validator
204
+ * Uses SDK's getStakePositions() which calls GET /v1/stake/<address>
205
+ */
206
+ async function fetchStakePositions(client, address) {
207
+ try {
208
+ const result = await client.getStakePositions(address);
209
+ return Array.isArray(result) ? result : [];
210
+ } catch (err) {
211
+ return { error: err.message };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Fetch rewards for the validator
217
+ * Uses SDK's getRewards() which calls GET /v1/rewards/<address>
218
+ */
219
+ async function fetchRewards(client, address) {
220
+ try {
221
+ const result = await client.getRewards(address);
222
+ return result;
223
+ } catch (err) {
224
+ return { error: err.message };
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Fetch current slot and epoch info
230
+ */
231
+ async function fetchNetworkInfo(client) {
232
+ try {
233
+ const [slot, epochInfo] = await Promise.all([
234
+ client.getSlot(),
235
+ client.getEpochInfo().catch(() => null),
236
+ ]);
237
+ return { slot, epoch: epochInfo };
238
+ } catch (err) {
239
+ return { slot: null, epoch: null };
240
+ }
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Display helpers
245
+ // ---------------------------------------------------------------------------
246
+
247
+ function printValidatorHeader(address, rawAddr) {
248
+ console.log(`\n${C.bright}${C.cyan}═══ Validator Information ════════════════════════════════${C.reset}\n`);
249
+ console.log(` ${C.dim}Address:${C.reset} ${C.bright}${address}${C.reset}`);
250
+ console.log(` ${C.dim}Raw:${C.reset} ${C.dim}${rawAddr}${C.reset}\n`);
251
+ }
252
+
253
+ function printValidatorCard(info, apyData, networkInfo) {
254
+ const statusColor = getStatusColor(info.status);
255
+ const statusText = (info.status || 'UNKNOWN').toUpperCase();
256
+
257
+ // Calculate stake percentage
258
+ const totalNetworkStake = apyData?.total_network_stake || info?.total_network_stake;
259
+ const stakePct = totalNetworkStake && info.stake_lamports
260
+ ? ((info.stake_lamports / totalNetworkStake) * 100).toFixed(4)
261
+ : null;
262
+
263
+ console.log(` ${C.bright}┌─────────────────────────────────────────────────────────┐${C.reset}`);
264
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}★ Validator Overview${C.reset}${' '.repeat(40)}${C.bright}│${C.reset}`);
265
+ console.log(` ${C.bright}├─────────────────────────────────────────────────────────┤${C.reset}`);
266
+
267
+ // Status row
268
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Status:${C.reset} ${statusColor}${statusText.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
269
+
270
+ // APY row
271
+ const apy = apyData?.apy ?? apyData?.current_apy ?? info?.apy;
272
+ if (apy !== null && apy !== undefined) {
273
+ const apyStr = formatPercent(apy);
274
+ const apyColor = apy >= 7 ? C.green : apy >= 4 ? C.yellow : C.dim;
275
+ console.log(` ${C.bright}│${C.reset} ${C.dim}APY:${C.reset} ${apyColor}${apyStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
276
+ }
277
+
278
+ // Commission row
279
+ const commission = apyData?.commission ?? info?.commission ?? info?.commission_bps;
280
+ if (commission !== null && commission !== undefined) {
281
+ const commValue = typeof commission === 'number' && commission > 100
282
+ ? commission / 100 // Handle basis points
283
+ : commission;
284
+ const commStr = formatPercent(commValue);
285
+ const commColor = commValue <= 5 ? C.green : commValue <= 10 ? C.yellow : C.red;
286
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Commission:${C.reset} ${commColor}${commStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
287
+ }
288
+
289
+ // Stake row
290
+ const stakeLamports = info?.stake_lamports ?? info?.stake ?? info?.activated_stake;
291
+ if (stakeLamports !== null && stakeLamports !== undefined) {
292
+ const stakeStr = formatAether(stakeLamports);
293
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Total Stake:${C.reset} ${C.green}${stakeStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
294
+ if (stakePct) {
295
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Network %:${C.reset} ${C.cyan}${(stakePct + '%').padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
296
+ }
297
+ }
298
+
299
+ // Uptime row
300
+ const uptime = info?.uptime_seconds ?? info?.uptime;
301
+ if (uptime !== null && uptime !== undefined) {
302
+ const uptimeStr = formatUptime(uptime);
303
+ const uptimeVal = typeof uptime === 'number' ? uptime : parseFloat(uptime);
304
+ const uptimeColor = uptimeVal > 95 ? C.green : uptimeVal > 80 ? C.yellow : C.red;
305
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Uptime:${C.reset} ${uptimeColor}${uptimeStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
306
+ }
307
+
308
+ // Votes row
309
+ const votes = info?.votes ?? info?.vote_count;
310
+ if (votes !== null && votes !== undefined) {
311
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Votes:${C.reset} ${C.cyan}${votes.toLocaleString().padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
312
+ }
313
+
314
+ // Credits row
315
+ const credits = info?.credits ?? info?.credit_count;
316
+ if (credits !== null && credits !== undefined) {
317
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Credits:${C.reset} ${C.magenta}${credits.toLocaleString().padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
318
+ }
319
+
320
+ // Skip rate
321
+ const skipRate = info?.skip_rate;
322
+ if (skipRate !== null && skipRate !== undefined) {
323
+ const skipStr = formatPercent(skipRate);
324
+ const skipColor = skipRate < 2 ? C.green : skipRate < 10 ? C.yellow : C.red;
325
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Skip Rate:${C.reset} ${skipColor}${skipStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
326
+ }
327
+
328
+ // Last vote
329
+ const lastVote = info?.last_vote ?? info?.last_vote_slot;
330
+ if (lastVote !== null && lastVote !== undefined && networkInfo?.slot) {
331
+ const slotDiff = networkInfo.slot - lastVote;
332
+ const lastVoteStr = `${lastVote.toLocaleString()} (${slotDiff} slots ago)`;
333
+ const lvColor = slotDiff < 10 ? C.green : slotDiff < 100 ? C.yellow : C.red;
334
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Last Vote:${C.reset} ${lvColor}${lastVoteStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
335
+ }
336
+
337
+ // Epoch info
338
+ if (networkInfo?.epoch) {
339
+ const epochStr = `Epoch ${networkInfo.epoch.epoch} (${networkInfo.epoch.slotIndex}/${networkInfo.epoch.slotsInEpoch} slots)`;
340
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Epoch:${C.reset} ${C.dim}${epochStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
341
+ }
342
+
343
+ // Identity
344
+ const identity = info?.identity ?? info?.node_id;
345
+ if (identity) {
346
+ const idStr = shortPubkey(identity, 12);
347
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Identity:${C.reset} ${C.dim}${idStr.padEnd(43)}${C.reset}${C.bright}│${C.reset}`);
348
+ }
349
+
350
+ console.log(` ${C.bright}└─────────────────────────────────────────────────────────┘${C.reset}`);
351
+
352
+ // Name and description
353
+ const name = info?.name ?? info?.moniker;
354
+ if (name) {
355
+ console.log(`\n ${C.bright}Name:${C.reset} ${name}`);
356
+ }
357
+
358
+ const details = info?.details ?? info?.description;
359
+ if (details) {
360
+ console.log(` ${C.dim}${details.slice(0, 80)}${details.length > 80 ? '...' : ''}${C.reset}`);
361
+ }
362
+
363
+ // Website
364
+ if (info?.website) {
365
+ console.log(` ${C.dim}Website:${C.reset} ${C.cyan}${info.website}${C.reset}`);
366
+ }
367
+ }
368
+
369
+ function printStakePositions(stakePositions, limit) {
370
+ if (!stakePositions || stakePositions.length === 0) return;
371
+
372
+ const positions = stakePositions.slice(0, limit);
373
+
374
+ console.log(`\n ${C.bright}${C.cyan}── Stake Positions (${positions.length}/${stakePositions.length}) ─────────────────────${C.reset}\n`);
375
+
376
+ let totalStaked = 0;
377
+
378
+ positions.forEach((pos, i) => {
379
+ const stakeAcct = pos.pubkey || pos.publicKey || pos.account || 'unknown';
380
+ const validator = pos.validator || pos.delegate || pos.vote_account || 'unknown';
381
+ const lamports = pos.lamports || pos.stake_lamports || 0;
382
+ const status = pos.status || pos.state || 'unknown';
383
+ const activationEpoch = pos.activation_epoch || pos.activationEpoch;
384
+ const deactivationEpoch = pos.deactivation_epoch || pos.deactivationEpoch;
385
+ const rewards = pos.rewards_earned || 0;
386
+
387
+ totalStaked += lamports;
388
+
389
+ const statusColor = getStakeStatusColor(status);
390
+ const statusText = status.toUpperCase();
391
+
392
+ console.log(` ${C.dim}┌─${'─'.repeat(58)}┐${C.reset}`);
393
+ console.log(` ${C.dim}│${C.reset} ${C.bright}#${i + 1}${C.reset} ${statusColor}[${statusText}]${C.reset}${' '.repeat(50 - statusText.length)}${C.dim}│${C.reset}`);
394
+ console.log(` ${C.dim}│${C.reset} ${C.dim}Account:${C.reset} ${shortPubkey(stakeAcct, 14).padEnd(40)}${C.dim}│${C.reset}`);
395
+ console.log(` ${C.dim}│${C.reset} ${C.dim}Amount:${C.reset} ${C.green}${formatAether(lamports).padEnd(40)}${C.reset}${C.dim}│${C.reset}`);
396
+
397
+ if (validator !== 'unknown') {
398
+ console.log(` ${C.dim}│${C.reset} ${C.dim}Delegate:${C.reset} ${shortPubkey(validator, 14).padEnd(40)}${C.dim}│${C.reset}`);
399
+ }
400
+
401
+ if (activationEpoch !== undefined) {
402
+ console.log(` ${C.dim}│${C.reset} ${C.dim}Activated:${C.reset} epoch ${activationEpoch.toString().padEnd(33)}${C.dim}│${C.reset}`);
403
+ }
404
+
405
+ if (deactivationEpoch !== undefined) {
406
+ console.log(` ${C.dim}│${C.reset} ${C.dim}Deactivates:${C.reset} epoch ${deactivationEpoch.toString().padEnd(31)}${C.dim}│${C.reset}`);
407
+ }
408
+
409
+ if (rewards > 0) {
410
+ console.log(` ${C.dim}│${C.reset} ${C.dim}Rewards:${C.reset} ${C.magenta}+${formatAether(rewards).padEnd(39)}${C.reset}${C.dim}│${C.reset}`);
411
+ }
412
+
413
+ console.log(` ${C.dim}└${'─'.repeat(59)}┘${C.reset}`);
414
+ console.log();
415
+ });
416
+
417
+ console.log(` ${C.bright}Total Staked:${C.reset} ${C.green}${formatAether(totalStaked)}${C.reset}\n`);
418
+ }
419
+
420
+ function printRewards(rewardsData) {
421
+ if (!rewardsData || rewardsData.error) return;
422
+
423
+ console.log(`\n ${C.bright}${C.cyan}── Rewards Summary ────────────────────────────────────────${C.reset}\n`);
424
+
425
+ const total = rewardsData.total ?? rewardsData.total_rewards ?? rewardsData.lifetime_rewards ?? 0;
426
+ const pending = rewardsData.pending ?? rewardsData.pending_rewards ?? rewardsData.claimable ?? 0;
427
+ const claimed = rewardsData.claimed ?? rewardsData.claimed_rewards ?? (total - pending);
428
+ const rewardsPerEpoch = rewardsData.rewards_per_epoch ?? rewardsData.epoch_rate ?? 0;
429
+
430
+ console.log(` ${C.bright}┌─────────────────────────────────────────────────────────┐${C.reset}`);
431
+ console.log(` ${C.bright}│${C.reset} ${C.cyan}★ Rewards${C.reset}${' '.repeat(46)}${C.bright}│${C.reset}`);
432
+ console.log(` ${C.bright}├─────────────────────────────────────────────────────────┤${C.reset}`);
433
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Total Earned:${C.reset} ${C.green}${formatAether(total).padEnd(37)}${C.reset}${C.bright}│${C.reset}`);
434
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Claimed:${C.reset} ${C.cyan}${formatAether(claimed).padEnd(37)}${C.reset}${C.bright}│${C.reset}`);
435
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Pending Claim:${C.reset} ${C.yellow}${formatAether(pending).padEnd(37)}${C.reset}${C.bright}│${C.reset}`);
436
+
437
+ if (rewardsPerEpoch > 0) {
438
+ console.log(` ${C.bright}│${C.reset} ${C.dim}Per Epoch:${C.reset} ${C.magenta}${formatAether(rewardsPerEpoch).padEnd(37)}${C.reset}${C.bright}│${C.reset}`);
439
+ }
440
+ console.log(` ${C.bright}└─────────────────────────────────────────────────────────┘${C.reset}`);
441
+
442
+ if (pending > 0) {
443
+ console.log(`\n ${C.dim}💡 Tip: Claim pending rewards with:${C.reset}`);
444
+ console.log(` ${C.cyan}aether claim --address <your_wallet>${C.reset}\n`);
445
+ }
446
+ }
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Main command
450
+ // ---------------------------------------------------------------------------
451
+
452
+ async function validatorInfoCommand() {
453
+ const opts = parseArgs();
454
+
455
+ if (opts.help) {
456
+ console.log(`
457
+ ${C.bright}${C.cyan}validator-info${C.reset} — Display detailed information about a validator
458
+
459
+ ${C.bright}USAGE${C.reset}
460
+ aether validator-info <address> [options]
461
+ aether validator-info --address <addr> [options]
462
+
463
+ ${C.bright}OPTIONS${C.reset}
464
+ --address <addr> Validator address (ATH...)
465
+ -a <addr>
466
+ --delegations, -d Show stake delegations/positions
467
+ --rewards, -r Show rewards summary
468
+ --limit <n> Max delegations to show (default: 20)
469
+ --json Output raw JSON
470
+ --rpc <url> RPC endpoint (default: AETHER_RPC or localhost:8899)
471
+ --help Show this help
472
+
473
+ ${C.bright}SDK METHODS USED${C.reset}
474
+ client.getValidatorAPY() → GET /v1/validator/<address>/apy
475
+ client.getValidators() → GET /v1/validators
476
+ client.getStakePositions() → GET /v1/stake/<address>
477
+ client.getRewards() → GET /v1/rewards/<address>
478
+ client.getEpochInfo() → GET /v1/epoch
479
+ client.getSlot() → GET /v1/slot
480
+
481
+ ${C.bright}EXAMPLES${C.reset}
482
+ aether validator-info ATH3abc...
483
+ aether validator-info ATH3abc... --json
484
+ aether validator-info ATH3abc... --delegations --limit 50
485
+ aether validator-info --address ATH3abc... --rewards --rpc http://localhost:8899
486
+ `);
487
+ return;
488
+ }
489
+
490
+ if (!opts.address) {
491
+ console.log(` ${C.red}✗ Missing validator address${C.reset}\n`);
492
+ console.log(` Usage: aether validator-info <address> [--json] [--rpc <url>]\n`);
493
+ process.exit(1);
494
+ }
495
+
496
+ const rpcUrl = opts.rpc || getDefaultRpc();
497
+ const client = createClient(rpcUrl);
498
+ const address = opts.address;
499
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
500
+
501
+ if (!opts.json) {
502
+ printValidatorHeader(address, rawAddr);
503
+ }
504
+
505
+ try {
506
+ // Fetch data in parallel for efficiency
507
+ const fetchPromises = [
508
+ fetchValidatorAPY(client, rawAddr),
509
+ fetchValidatorInfo(client, rawAddr),
510
+ fetchNetworkInfo(client),
511
+ ];
512
+
513
+ // Conditionally fetch additional data
514
+ if (opts.showDelegations) {
515
+ fetchPromises.push(fetchStakePositions(client, rawAddr));
516
+ }
517
+ if (opts.showRewards) {
518
+ fetchPromises.push(fetchRewards(client, rawAddr));
519
+ }
520
+
521
+ const results = await Promise.all(fetchPromises);
522
+ const [apyData, validatorInfo, networkInfo] = results;
523
+
524
+ // Extract optional results
525
+ let stakePositions = null;
526
+ let rewardsData = null;
527
+ let resultIdx = 3;
528
+ if (opts.showDelegations) {
529
+ stakePositions = results[resultIdx++];
530
+ }
531
+ if (opts.showRewards) {
532
+ rewardsData = results[resultIdx++];
533
+ }
534
+
535
+ // Check for critical errors
536
+ if (apyData.error && !validatorInfo) {
537
+ throw new Error(`Validator not found or RPC error: ${apyData.error}`);
538
+ }
539
+
540
+ // Build comprehensive response object for JSON output
541
+ const response = {
542
+ address: address,
543
+ raw_address: rawAddr,
544
+ rpc: rpcUrl,
545
+ slot: networkInfo?.slot,
546
+ epoch: networkInfo?.epoch,
547
+ apy: null,
548
+ commission: null,
549
+ stake: null,
550
+ status: null,
551
+ uptime: null,
552
+ votes: null,
553
+ credits: null,
554
+ skip_rate: null,
555
+ last_vote: null,
556
+ identity: null,
557
+ name: null,
558
+ website: null,
559
+ details: null,
560
+ stake_positions: stakePositions && !stakePositions.error ? stakePositions : null,
561
+ rewards: rewardsData && !rewardsData.error ? {
562
+ total: rewardsData.total ?? rewardsData.total_rewards ?? 0,
563
+ pending: rewardsData.pending ?? rewardsData.pending_rewards ?? 0,
564
+ claimed: rewardsData.claimed ?? rewardsData.claimed_rewards ?? 0,
565
+ per_epoch: rewardsData.rewards_per_epoch ?? 0,
566
+ } : null,
567
+ fetched_at: new Date().toISOString(),
568
+ cli_version: CLI_VERSION,
569
+ };
570
+
571
+ // Extract APY data
572
+ if (!apyData.error) {
573
+ response.apy = apyData.apy ?? apyData.current_apy ?? apyData.estimated_apy ?? null;
574
+ response.commission = apyData.commission ?? null;
575
+ }
576
+
577
+ // Extract validator info
578
+ if (validatorInfo && !validatorInfo.error) {
579
+ response.stake = validatorInfo.stake_lamports ?? validatorInfo.stake ?? validatorInfo.activated_stake ?? null;
580
+ response.status = validatorInfo.status ?? validatorInfo.state ?? 'unknown';
581
+ response.uptime = validatorInfo.uptime_seconds ?? validatorInfo.uptime ?? null;
582
+ response.votes = validatorInfo.votes ?? validatorInfo.vote_count ?? null;
583
+ response.credits = validatorInfo.credits ?? validatorInfo.credit_count ?? null;
584
+ response.skip_rate = validatorInfo.skip_rate ?? null;
585
+ response.last_vote = validatorInfo.last_vote ?? validatorInfo.last_vote_slot ?? null;
586
+ response.identity = validatorInfo.identity ?? validatorInfo.node_id ?? null;
587
+ response.name = validatorInfo.name ?? validatorInfo.moniker ?? null;
588
+ response.website = validatorInfo.website ?? null;
589
+ response.details = validatorInfo.details ?? validatorInfo.description ?? null;
590
+
591
+ // Use commission from validator info if not in APY data
592
+ if (response.commission === null) {
593
+ response.commission = validatorInfo.commission ?? validatorInfo.commission_bps ?? null;
594
+ }
595
+ }
596
+
597
+ // JSON output
598
+ if (opts.json) {
599
+ console.log(JSON.stringify(response, (key, value) => {
600
+ // Convert BigInt to string for JSON serialization
601
+ if (typeof value === 'bigint') return value.toString();
602
+ return value;
603
+ }, 2));
604
+ return;
605
+ }
606
+
607
+ // Pretty output
608
+ printValidatorCard(validatorInfo, apyData, networkInfo);
609
+
610
+ // Show delegations if requested or if showing all
611
+ if (opts.showDelegations && stakePositions && !stakePositions.error) {
612
+ printStakePositions(stakePositions, opts.limit);
613
+ }
614
+
615
+ // Show rewards if requested
616
+ if (opts.showRewards && rewardsData && !rewardsData.error) {
617
+ printRewards(rewardsData);
618
+ }
619
+
620
+ // Quick actions
621
+ console.log(` ${C.dim}── Quick Actions ─────────────────────────────────────────${C.reset}\n`);
622
+ console.log(` ${C.dim}• Stake with this validator:${C.reset}`);
623
+ console.log(` ${C.cyan}aether stake --validator ${shortPubkey(address, 8)} --amount <AETH>${C.reset}`);
624
+ console.log(` ${C.dim}• View all validators:${C.reset}`);
625
+ console.log(` ${C.cyan}aether validators list${C.reset}`);
626
+ console.log(` ${C.dim}• Check your delegations:${C.reset}`);
627
+ console.log(` ${C.cyan}aether stake-positions --address <your_wallet>${C.reset}`);
628
+ console.log();
629
+
630
+ } catch (err) {
631
+ if (opts.json) {
632
+ console.log(JSON.stringify({
633
+ address: address,
634
+ error: err.message,
635
+ rpc: rpcUrl,
636
+ fetched_at: new Date().toISOString(),
637
+ }, null, 2));
638
+ } else {
639
+ console.log(` ${C.red}✗ Failed to fetch validator info:${C.reset} ${err.message}\n`);
640
+ console.log(` ${C.dim} Troubleshooting:${C.reset}`);
641
+ console.log(` • Is your validator running? Check with: ${C.cyan}aether ping${C.reset}`);
642
+ console.log(` • Verify RPC endpoint: ${C.dim}${rpcUrl}${C.reset}`);
643
+ console.log(` • Set custom RPC: ${C.dim}AETHER_RPC=https://your-rpc-url${C.reset}`);
644
+ console.log(` • Check network status: ${C.cyan}aether network${C.reset}\n`);
645
+ }
646
+ process.exit(1);
647
+ }
648
+ }
649
+
650
+ module.exports = { validatorInfoCommand };
651
+
652
+ if (require.main === module) {
653
+ validatorInfoCommand().catch(err => {
654
+ console.error(`\n ${C.red}✗ Error:${C.reset} ${err.message}\n`);
655
+ process.exit(1);
656
+ });
657
+ }