@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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/aether-cli-1.0.0.tgz +0 -0
- package/aether-cli-1.8.0.tgz +0 -0
- package/aether-hub-1.0.5.tgz +0 -0
- package/aether-hub-1.1.8.tgz +0 -0
- package/aether-hub-1.2.1.tgz +0 -0
- package/commands/account.js +280 -0
- package/commands/apy.js +499 -0
- package/commands/balance.js +241 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +387 -0
- package/commands/claim.js +490 -0
- package/commands/config.js +851 -0
- package/commands/delegations.js +582 -0
- package/commands/doctor.js +769 -0
- package/commands/emergency.js +667 -0
- package/commands/epoch.js +275 -0
- package/commands/fees.js +276 -0
- package/commands/index.js +78 -0
- package/commands/info.js +495 -0
- package/commands/init.js +816 -0
- package/commands/install.js +666 -0
- package/commands/kyc.js +272 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/multisig.js +701 -0
- package/commands/network.js +429 -0
- package/commands/nft.js +857 -0
- package/commands/ping.js +266 -0
- package/commands/price.js +253 -0
- package/commands/rewards.js +931 -0
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +656 -0
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +470 -0
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +205 -0
- package/commands/stake.js +516 -0
- package/commands/stats.js +396 -0
- package/commands/status.js +327 -0
- package/commands/supply.js +391 -0
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +346 -0
- package/commands/unstake.js +597 -0
- package/commands/validator-info.js +657 -0
- package/commands/validator-register.js +593 -0
- package/commands/validator-start.js +323 -0
- package/commands/validator-status.js +227 -0
- package/commands/validators.js +626 -0
- package/commands/wallet.js +1570 -0
- package/index.js +593 -0
- package/lib/errors.js +398 -0
- package/package.json +76 -0
- package/sdk/README.md +210 -0
- package/sdk/index.js +1639 -0
- package/sdk/package.json +34 -0
- package/sdk/rpc.js +254 -0
- package/sdk/test.js +85 -0
- package/test/doctor.test.js +76 -0
- 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
|
+
}
|