aether-hub 1.2.0 → 1.2.2
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/commands/epoch.js +357 -0
- package/commands/supply.js +437 -0
- package/commands/validator-info.js +640 -0
- package/index.js +22 -1
- package/package.json +1 -1
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli validator-info
|
|
4
|
+
*
|
|
5
|
+
* Inspect a specific validator by identity address or name.
|
|
6
|
+
* Shows stake, APY, score, commission, tier, uptime, epoch performance,
|
|
7
|
+
* and a breakdown of delegators.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aether validator info <addressOrName> Inspect a validator
|
|
11
|
+
* aether validator info <addressOrName> --json JSON output for scripting
|
|
12
|
+
* aether validator info <addressOrName> --rpc <url> Use specific RPC
|
|
13
|
+
*
|
|
14
|
+
* Examples:
|
|
15
|
+
* aether validator info ATH3mGH...
|
|
16
|
+
* aether validator info jellylegs --json
|
|
17
|
+
* aether validator info --address ATH3mGH... --rpc http://custom:8899
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const http = require('http');
|
|
21
|
+
const https = require('https');
|
|
22
|
+
|
|
23
|
+
// ANSI colours
|
|
24
|
+
const C = {
|
|
25
|
+
reset: '\x1b[0m',
|
|
26
|
+
bright: '\x1b[1m',
|
|
27
|
+
dim: '\x1b[2m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
green: '\x1b[32m',
|
|
30
|
+
yellow: '\x1b[33m',
|
|
31
|
+
blue: '\x1b[34m',
|
|
32
|
+
cyan: '\x1b[36m',
|
|
33
|
+
magenta: '\x1b[35m',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_RPC = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// HTTP helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function httpRequest(rpcUrl, path) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const url = new URL(path, rpcUrl);
|
|
45
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
46
|
+
const req = lib.request({
|
|
47
|
+
hostname: url.hostname,
|
|
48
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
49
|
+
path: url.pathname + url.search,
|
|
50
|
+
method: 'GET',
|
|
51
|
+
timeout: 8000,
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
}, (res) => {
|
|
54
|
+
let data = '';
|
|
55
|
+
res.on('data', (chunk) => data += chunk);
|
|
56
|
+
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
|
|
57
|
+
});
|
|
58
|
+
req.on('error', reject);
|
|
59
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
60
|
+
req.end();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function httpPost(rpcUrl, path, body) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const url = new URL(path, rpcUrl);
|
|
67
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
68
|
+
const bodyStr = JSON.stringify(body);
|
|
69
|
+
const req = lib.request({
|
|
70
|
+
hostname: url.hostname,
|
|
71
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
72
|
+
path: url.pathname + url.search,
|
|
73
|
+
method: 'POST',
|
|
74
|
+
timeout: 8000,
|
|
75
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
|
|
76
|
+
}, (res) => {
|
|
77
|
+
let data = '';
|
|
78
|
+
res.on('data', (chunk) => data += chunk);
|
|
79
|
+
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(data); } });
|
|
80
|
+
});
|
|
81
|
+
req.on('error', reject);
|
|
82
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
83
|
+
req.write(bodyStr);
|
|
84
|
+
req.end();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Argument parsing
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
function parseArgs() {
|
|
93
|
+
const raw = process.argv.slice(3); // [node, index.js, validator, info, ...]
|
|
94
|
+
const opts = {
|
|
95
|
+
rpc: DEFAULT_RPC,
|
|
96
|
+
target: null,
|
|
97
|
+
asJson: false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < raw.length; i++) {
|
|
101
|
+
const arg = raw[i];
|
|
102
|
+
if ((arg === '--rpc' || arg === '-r') && raw[i + 1] && !raw[i + 1].startsWith('-')) {
|
|
103
|
+
opts.rpc = raw[++i];
|
|
104
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
105
|
+
opts.asJson = true;
|
|
106
|
+
} else if ((arg === '--address' || arg === '-a') && raw[i + 1] && !raw[i + 1].startsWith('-')) {
|
|
107
|
+
opts.target = raw[++i];
|
|
108
|
+
} else if (!arg.startsWith('-') && !opts.target) {
|
|
109
|
+
opts.target = arg;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return opts;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Data fetchers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/** Fetch all validators and find the matching one */
|
|
121
|
+
async function fetchValidatorByIdentity(rpc, identity) {
|
|
122
|
+
// identity can be a full address, partial address, or name/moniker
|
|
123
|
+
const validators = await fetchAllValidators(rpc);
|
|
124
|
+
if (!validators || validators.length === 0) return null;
|
|
125
|
+
|
|
126
|
+
const isAddress = identity.startsWith('ATH');
|
|
127
|
+
|
|
128
|
+
let match = null;
|
|
129
|
+
|
|
130
|
+
if (isAddress) {
|
|
131
|
+
// Exact or prefix match on pubkey/identity
|
|
132
|
+
match = validators.find(v =>
|
|
133
|
+
(v.pubkey && (v.pubkey === identity || v.pubkey.startsWith(identity))) ||
|
|
134
|
+
(v.address && (v.address === identity || v.address.startsWith(identity))) ||
|
|
135
|
+
(v.identity && (v.identity === identity || v.identity.startsWith(identity)))
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!match) {
|
|
140
|
+
// Try name/moniker match (case-insensitive partial)
|
|
141
|
+
const lower = identity.toLowerCase();
|
|
142
|
+
match = validators.find(v =>
|
|
143
|
+
(v.name && v.name.toLowerCase().includes(lower)) ||
|
|
144
|
+
(v.moniker && v.moniker.toLowerCase().includes(lower))
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!match && identity.length >= 8) {
|
|
149
|
+
// Try prefix match on any field
|
|
150
|
+
match = validators.find(v => {
|
|
151
|
+
const pk = v.pubkey || v.address || v.identity || '';
|
|
152
|
+
return pk.startsWith(identity) || pk.endsWith(identity);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return match;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Fetch all validators from the network */
|
|
160
|
+
async function fetchAllValidators(rpc) {
|
|
161
|
+
try {
|
|
162
|
+
const res = await httpRequest(rpc, '/v1/validators');
|
|
163
|
+
if (res && !res.error) {
|
|
164
|
+
if (Array.isArray(res)) return res;
|
|
165
|
+
if (res.validators && Array.isArray(res.validators)) return res.validators;
|
|
166
|
+
if (res.accounts && Array.isArray(res.accounts)) return res.accounts;
|
|
167
|
+
}
|
|
168
|
+
const res2 = await httpPost(rpc, '/v1/validators', {});
|
|
169
|
+
if (res2 && !res2.error) {
|
|
170
|
+
if (Array.isArray(res2)) return res2;
|
|
171
|
+
if (res2.validators && Array.isArray(res2.validators)) return res2.validators;
|
|
172
|
+
}
|
|
173
|
+
return [];
|
|
174
|
+
} catch {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Fetch epoch info for APY and epoch calculations */
|
|
180
|
+
async function fetchEpochInfo(rpc) {
|
|
181
|
+
try {
|
|
182
|
+
return await httpRequest(rpc, '/v1/epoch-info');
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Fetch supply for APY estimation */
|
|
189
|
+
async function fetchSupply(rpc) {
|
|
190
|
+
try {
|
|
191
|
+
return await httpRequest(rpc, '/v1/supply');
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Fetch delegators for a specific validator */
|
|
198
|
+
async function fetchDelegators(rpc, validatorPubkey) {
|
|
199
|
+
try {
|
|
200
|
+
const res = await httpRequest(rpc, `/v1/validator/${encodeURIComponent(validatorPubkey)}/delegators`);
|
|
201
|
+
if (res && !res.error) {
|
|
202
|
+
return Array.isArray(res) ? res : (res.delegators || []);
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
} catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Fetch recent performance history for a validator */
|
|
211
|
+
async function fetchValidatorPerformance(rpc, validatorPubkey) {
|
|
212
|
+
try {
|
|
213
|
+
const res = await httpRequest(rpc, `/v1/validator/${encodeURIComponent(validatorPubkey)}/performance`);
|
|
214
|
+
if (res && !res.error) {
|
|
215
|
+
return res;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Normalise a validator record
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
function normalise(v) {
|
|
228
|
+
const pubkey = v.pubkey || v.address || v.identity || v.id || null;
|
|
229
|
+
const name = v.name || v.moniker || v.label || null;
|
|
230
|
+
const tier = (v.tier || v.node_type || v.type || 'full').toLowerCase();
|
|
231
|
+
const stake = typeof v.stake === 'bigint' ? v.stake
|
|
232
|
+
: typeof v.stake === 'object' ? BigInt(v.stake.toString())
|
|
233
|
+
: BigInt(v.stake || v.delegatedStake || v.stake_lamports || v.lamports || 0);
|
|
234
|
+
const score = v.score !== undefined ? v.score
|
|
235
|
+
: v.uptime !== undefined ? Math.round(v.uptime * 100)
|
|
236
|
+
: null;
|
|
237
|
+
const apy = v.apy !== undefined ? v.apy
|
|
238
|
+
: v.apy_bps !== undefined ? v.apy_bps / 100
|
|
239
|
+
: null;
|
|
240
|
+
const commission = v.commission !== undefined ? v.commission
|
|
241
|
+
: v.commission_bps !== undefined ? v.commission_bps / 100
|
|
242
|
+
: null;
|
|
243
|
+
const version = v.version || v.agent || null;
|
|
244
|
+
const ip = v.ip || v.remote || null;
|
|
245
|
+
const lastVote = v.last_vote || v.lastVote || null;
|
|
246
|
+
const stakeAccounts = v.stake_accounts || v.stakeAccounts || v.delegators || null;
|
|
247
|
+
const activatedStake = v.activated_stake || null;
|
|
248
|
+
const lastEpochStake = v.last_epoch_stake || v.stake_last_epoch || null;
|
|
249
|
+
const credits = v.credits || v.epoch_credits || null;
|
|
250
|
+
const rootSlot = v.root_slot || v.rootSlot || null;
|
|
251
|
+
const delinquent = v.delinquent || false;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
pubkey, name, tier, stake, score, apy, commission,
|
|
255
|
+
version, ip, lastVote, stakeAccounts, activatedStake,
|
|
256
|
+
lastEpochStake, credits, rootSlot, delinquent,
|
|
257
|
+
_raw: v,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatAether(lamports) {
|
|
262
|
+
const aeth = Number(lamports) / 1e9;
|
|
263
|
+
if (aeth === 0) return '0 AETH';
|
|
264
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatAethFull(lamports) {
|
|
268
|
+
return (Number(lamports) / 1e9).toFixed(6) + ' AETH';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function formatNumber(n) {
|
|
272
|
+
if (n === null || n === undefined) return '—';
|
|
273
|
+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function tierBadge(tier) {
|
|
277
|
+
if (tier === 'full') return `${C.cyan}◆ FULL${C.reset}`;
|
|
278
|
+
if (tier === 'lite') return `${C.yellow}◇ LITE${C.reset}`;
|
|
279
|
+
if (tier === 'observer') return `${C.green}○ OBSERVER${C.reset}`;
|
|
280
|
+
return `${C.dim}[${tier}]${C.reset}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function tierColor(tier) {
|
|
284
|
+
if (tier === 'full') return C.cyan;
|
|
285
|
+
if (tier === 'lite') return C.yellow;
|
|
286
|
+
if (tier === 'observer') return C.green;
|
|
287
|
+
return C.reset;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function scoreColor(score) {
|
|
291
|
+
if (score === null || score === undefined) return C.dim;
|
|
292
|
+
if (score >= 80) return C.green;
|
|
293
|
+
if (score >= 50) return C.yellow;
|
|
294
|
+
return C.red;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function apyColor(apy) {
|
|
298
|
+
if (apy === null || apy === undefined) return C.dim;
|
|
299
|
+
if (apy >= 8) return C.green;
|
|
300
|
+
if (apy >= 5) return C.cyan;
|
|
301
|
+
if (apy >= 2) return C.yellow;
|
|
302
|
+
return C.red;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Render output
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
function renderHeader(v) {
|
|
310
|
+
const shortAddr = v.pubkey
|
|
311
|
+
? v.pubkey.slice(0, 12) + '...' + v.pubkey.slice(-8)
|
|
312
|
+
: 'unknown';
|
|
313
|
+
|
|
314
|
+
console.log();
|
|
315
|
+
console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
316
|
+
console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}VALIDATOR DETAILS${C.reset} ${C.dim}${shortAddr}${C.reset} ${C.bright}║${C.reset}`);
|
|
317
|
+
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
318
|
+
console.log();
|
|
319
|
+
|
|
320
|
+
if (v.name) {
|
|
321
|
+
console.log(` ${C.cyan}${C.bright}${v.name}${C.reset}`);
|
|
322
|
+
}
|
|
323
|
+
if (v.pubkey) {
|
|
324
|
+
console.log(` ${C.dim}${v.pubkey}${C.reset}`);
|
|
325
|
+
}
|
|
326
|
+
console.log();
|
|
327
|
+
|
|
328
|
+
// Tier + delinquent banner
|
|
329
|
+
const tierStr = tierBadge(v.tier);
|
|
330
|
+
const deliqStr = v.delinquent ? ` ${C.red}⚠ DELINQUENT${C.reset}` : '';
|
|
331
|
+
console.log(` ${tierStr}${deliqStr}`);
|
|
332
|
+
console.log();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function renderStats(v, epochInfo) {
|
|
336
|
+
const stakeFormatted = formatAether(v.stake);
|
|
337
|
+
const stakeAeth = Number(v.stake) / 1e9;
|
|
338
|
+
const score = v.score;
|
|
339
|
+
const apy = v.apy;
|
|
340
|
+
const commission = v.commission;
|
|
341
|
+
|
|
342
|
+
// APY bar (0-15% range for display)
|
|
343
|
+
const apyBarLen = 12;
|
|
344
|
+
const apyDisplay = apy !== null && apy !== undefined ? apy : 0;
|
|
345
|
+
const apyFillLen = Math.min(apyBarLen, Math.round((apyDisplay / 15) * apyBarLen));
|
|
346
|
+
|
|
347
|
+
// Commission bar (0-100%)
|
|
348
|
+
const commBarLen = 10;
|
|
349
|
+
const commDisplay = commission !== null && commission !== undefined ? commission : 0;
|
|
350
|
+
const commFillLen = Math.round((commDisplay / 100) * commBarLen);
|
|
351
|
+
|
|
352
|
+
console.log(` ${C.bright}┌───────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
353
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Stake${C.reset} ${formatAether(v.stake).padEnd(15)} ${C.cyan}${formatAethFull(v.stake).padEnd(14)} ${C.bright}│${C.reset}`);
|
|
354
|
+
|
|
355
|
+
if (v.lastEpochStake !== null && v.lastEpochStake !== undefined) {
|
|
356
|
+
const lastAeth = Number(v.lastEpochStake) / 1e9;
|
|
357
|
+
const change = stakeAeth - lastAeth;
|
|
358
|
+
const changeStr = change >= 0 ? `+${change.toFixed(2)}` : change.toFixed(2);
|
|
359
|
+
const changeColor = change >= 0 ? C.green : C.red;
|
|
360
|
+
console.log(` ${C.bright}│${C.reset} ${C.dim}Last epoch${C.reset} ${formatAethFull(v.lastEpochStake).padEnd(15)} ${changeColor}${changeStr} AETH${C.reset}`.padEnd(75) + ` ${C.bright}│${C.reset}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (score !== null && score !== undefined) {
|
|
364
|
+
const sc = scoreColor(score);
|
|
365
|
+
const scoreLabel = score >= 80 ? 'excellent' : score >= 50 ? 'good' : score >= 20 ? 'fair' : 'poor';
|
|
366
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Score${C.reset} ${sc}${score}${C.reset}% (${scoreLabel})`.padEnd(75) + ` ${C.bright}│${C.reset}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (apy !== null && apy !== undefined) {
|
|
370
|
+
const ac = apyColor(apy);
|
|
371
|
+
const apyBar = `${ac}${'█'.repeat(apyFillLen)}${C.dim}${'░'.repeat(apyBarLen - apyFillLen)}${C.reset}`;
|
|
372
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Est. APY${C.reset} ${ac}${apy.toFixed(2)}%${C.reset} ${apyBar}`.padEnd(75) + ` ${C.bright}│${C.reset}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (commission !== null && commission !== undefined) {
|
|
376
|
+
const commBar = `${C.yellow}${'█'.repeat(commFillLen)}${C.dim}${'░'.repeat(commBarLen - commFillLen)}${C.reset}`;
|
|
377
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Commission${C.reset} ${commission.toFixed(1)}% ${commBar}`.padEnd(75) + ` ${C.bright}│${C.reset}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(` ${C.bright}└───────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
381
|
+
console.log();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function renderNodeInfo(v) {
|
|
385
|
+
const items = [];
|
|
386
|
+
if (v.version) items.push({ label: 'Version', value: v.version, color: C.cyan });
|
|
387
|
+
if (v.ip) items.push({ label: 'IP', value: v.ip, color: C.dim });
|
|
388
|
+
if (v.lastVote !== null && v.lastVote !== undefined) items.push({ label: 'Last vote', value: `slot ${formatNumber(v.lastVote)}`, color: C.cyan });
|
|
389
|
+
if (v.rootSlot !== null && v.rootSlot !== undefined) items.push({ label: 'Root slot', value: formatNumber(v.rootSlot), color: C.cyan });
|
|
390
|
+
|
|
391
|
+
if (items.length === 0) return;
|
|
392
|
+
|
|
393
|
+
console.log(` ${C.bright}── Node Info ──────────────────────────────────────────────${C.reset}`);
|
|
394
|
+
for (const item of items) {
|
|
395
|
+
console.log(` ${C.dim}${item.label}:${C.reset} ${item.color}${item.value}${C.reset}`);
|
|
396
|
+
}
|
|
397
|
+
console.log();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderEpochPerformance(v, epochInfo, performance) {
|
|
401
|
+
console.log(` ${C.bright}── Epoch Performance ─────────────────────────────────────${C.reset}`);
|
|
402
|
+
|
|
403
|
+
if (epochInfo) {
|
|
404
|
+
const ep = epochInfo.epoch;
|
|
405
|
+
const slotIdx = epochInfo.slot_index;
|
|
406
|
+
const slotsInEp = epochInfo.slots_in_epoch;
|
|
407
|
+
const progress = slotsInEp > 0 ? ((slotIdx / slotsInEp) * 100).toFixed(1) : '?';
|
|
408
|
+
console.log(` ${C.dim}Current epoch:${C.reset} ${C.bright}${ep}${C.reset} ${C.dim}progress: ${C.reset}${progress}%`);
|
|
409
|
+
if (slotsInEp) console.log(` ${C.dim}Slots in epoch:${C.reset} ${formatNumber(slotsInEp)}`);
|
|
410
|
+
if (slotIdx !== undefined) console.log(` ${C.dim}Current slot:${C.reset} ${formatNumber(slotIdx)}`);
|
|
411
|
+
console.log();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (v.credits !== null && v.credits !== undefined) {
|
|
415
|
+
if (Array.isArray(v.credits) && v.credits.length > 0) {
|
|
416
|
+
const [ep, cr, pr] = v.credits.length >= 3
|
|
417
|
+
? [v.credits[v.credits.length - 1], v.credits[v.credits.length - 2], v.credits[v.credits.length - 3]]
|
|
418
|
+
: [null, null, null];
|
|
419
|
+
if (ep !== null) console.log(` ${C.dim}Epoch credits:${C.reset} ${formatNumber(ep)} ${C.dim}(prev: ${formatNumber(pr)})${C.reset}`);
|
|
420
|
+
} else if (typeof v.credits === 'number') {
|
|
421
|
+
console.log(` ${C.dim}Epoch credits:${C.reset} ${formatNumber(v.credits)}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (performance) {
|
|
426
|
+
if (performance.slots_in_epoch !== undefined) {
|
|
427
|
+
console.log(` ${C.dim}Epoch slots:${C.reset} ${formatNumber(performance.slots_in_epoch)}`);
|
|
428
|
+
}
|
|
429
|
+
if (performance.slots_produced !== undefined) {
|
|
430
|
+
const pct = performance.slots_in_epoch > 0
|
|
431
|
+
? ((performance.slots_produced / performance.slots_in_epoch) * 100).toFixed(1)
|
|
432
|
+
: '?';
|
|
433
|
+
console.log(` ${C.dim}Slots produced:${C.reset} ${performance.slots_produced} / ${formatNumber(performance.slots_in_epoch)} ${C.dim}(${pct}%)${C.reset}`);
|
|
434
|
+
}
|
|
435
|
+
if (performance.credits !== undefined) {
|
|
436
|
+
console.log(` ${C.dim}Credits earned:${C.reset} ${formatNumber(performance.credits)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
console.log();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderDelegators(delegators) {
|
|
444
|
+
if (!delegators || delegators.length === 0) {
|
|
445
|
+
console.log(` ${C.bright}── Delegators ────────────────────────────────────────────${C.reset}`);
|
|
446
|
+
console.log(` ${C.dim}No delegator data available for this validator.${C.reset}`);
|
|
447
|
+
console.log();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log(` ${C.bright}── Delegators (${delegators.length}) ──────────────────────────────────${C.reset}`);
|
|
452
|
+
console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
453
|
+
console.log(` ${C.dim}│${C.reset} ${C.cyan}Delegator${C.reset.padEnd(44)} ${C.cyan}Stake${C.reset.padEnd(18)} ${C.cyan}Status${C.reset.padEnd(12)} ${C.dim}│${C.reset}`);
|
|
454
|
+
console.log(` ${C.dim}├─────────────────────────────────────────────────────────────────────────┤${C.reset}`);
|
|
455
|
+
|
|
456
|
+
const sorted = [...delegators].sort((a, b) => {
|
|
457
|
+
const aLamports = Number(a.lamports || a.stake || 0);
|
|
458
|
+
const bLamports = Number(b.lamports || b.stake || 0);
|
|
459
|
+
return bLamports - aLamports;
|
|
460
|
+
}).slice(0, 20);
|
|
461
|
+
|
|
462
|
+
let totalDelegated = BigInt(0);
|
|
463
|
+
|
|
464
|
+
for (const d of sorted) {
|
|
465
|
+
const addr = d.address || d.pubkey || d.delegator || 'unknown';
|
|
466
|
+
const shortAddr = addr.slice(0, 20) + '...' + addr.slice(-12);
|
|
467
|
+
const lamports = BigInt(d.lamports || d.stake || 0);
|
|
468
|
+
totalDelegated += lamports;
|
|
469
|
+
const stakeFormatted = formatAether(lamports);
|
|
470
|
+
const status = d.status || d.activation_status || 'active';
|
|
471
|
+
const statusColor = status === 'active' ? C.green : status === 'pending' ? C.yellow : C.dim;
|
|
472
|
+
|
|
473
|
+
console.log(
|
|
474
|
+
` ${C.dim}│${C.reset} ${shortAddr.padEnd(46)} ${stakeFormatted.padEnd(18)} ${statusColor}${(status + '').padEnd(12)}${C.reset} ${C.dim}│${C.reset}`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (delegators.length > 20) {
|
|
479
|
+
console.log(` ${C.dim}│${C.reset} ${C.dim}... and ${delegators.length - 20} more delegators (use --json for full list)${C.reset}`.padEnd(77) + `${C.dim}│${C.reset}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
console.log(` ${C.dim}└─────────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
483
|
+
console.log(` ${C.dim}Total delegated (shown): ${formatAether(totalDelegated)}${C.reset}`);
|
|
484
|
+
console.log();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function renderRank(validators, v) {
|
|
488
|
+
if (!validators || !v) return;
|
|
489
|
+
|
|
490
|
+
const sorted = [...validators].sort((a, b) => {
|
|
491
|
+
const aStake = typeof a.stake === 'bigint' ? Number(a.stake) : Number(a.stake || 0);
|
|
492
|
+
const bStake = typeof b.stake === 'bigint' ? Number(b.stake) : Number(b.stake || 0);
|
|
493
|
+
return bStake - aStake;
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const rank = sorted.findIndex(m => {
|
|
497
|
+
const mPub = m.pubkey || m.address || '';
|
|
498
|
+
const vPub = v.pubkey || '';
|
|
499
|
+
return mPub === vPub;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
if (rank >= 0) {
|
|
503
|
+
const pct = ((rank + 1) / sorted.length * 100).toFixed(1);
|
|
504
|
+
console.log(` ${C.bright}── Network Rank ───────────────────────────────────────────${C.reset}`);
|
|
505
|
+
console.log(` ${C.dim}Stake rank:${C.reset} ${C.bright}#${rank + 1}${C.reset} of ${sorted.length} validators ${C.dim}(top ${pct}%)${C.reset}`);
|
|
506
|
+
console.log();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function renderJson(v, delegators, performance, epochInfo, validators) {
|
|
511
|
+
const out = {
|
|
512
|
+
rpc: opts.rpc,
|
|
513
|
+
pubkey: v.pubkey,
|
|
514
|
+
name: v.name,
|
|
515
|
+
tier: v.tier,
|
|
516
|
+
stake: v.stake.toString(),
|
|
517
|
+
stake_aeth: Number(v.stake) / 1e9,
|
|
518
|
+
stake_formatted: formatAether(v.stake),
|
|
519
|
+
score: v.score,
|
|
520
|
+
apy: v.apy,
|
|
521
|
+
commission: v.commission,
|
|
522
|
+
version: v.version,
|
|
523
|
+
ip: v.ip,
|
|
524
|
+
last_vote: v.lastVote,
|
|
525
|
+
root_slot: v.rootSlot,
|
|
526
|
+
delinquent: v.delinquent,
|
|
527
|
+
activated_stake: v.activatedStake?.toString(),
|
|
528
|
+
last_epoch_stake: v.lastEpochStake?.toString(),
|
|
529
|
+
credits: v.credits,
|
|
530
|
+
delegators: delegators ? delegators.map(d => ({
|
|
531
|
+
address: d.address || d.pubkey || d.delegator,
|
|
532
|
+
lamports: String(d.lamports || d.stake || 0),
|
|
533
|
+
stake_aeth: Number(d.lamports || d.stake || 0) / 1e9,
|
|
534
|
+
status: d.status || d.activation_status || 'unknown',
|
|
535
|
+
})) : [],
|
|
536
|
+
performance,
|
|
537
|
+
epoch_info: epochInfo,
|
|
538
|
+
fetched_at: new Date().toISOString(),
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// Add rank
|
|
542
|
+
if (validators && v.pubkey) {
|
|
543
|
+
const sorted = [...validators].sort((a, b) => {
|
|
544
|
+
const aS = typeof a.stake === 'bigint' ? Number(a.stake) : Number(a.stake || 0);
|
|
545
|
+
const bS = typeof b.stake === 'bigint' ? Number(b.stake) : Number(b.stake || 0);
|
|
546
|
+
return bS - aS;
|
|
547
|
+
});
|
|
548
|
+
const rank = sorted.findIndex(m => (m.pubkey || m.address || '') === v.pubkey);
|
|
549
|
+
if (rank >= 0) out.rank = { position: rank + 1, total: sorted.length };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log(JSON.stringify(out, null, 2));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
// Main
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
async function main() {
|
|
560
|
+
const opts = parseArgs();
|
|
561
|
+
|
|
562
|
+
if (!opts.target) {
|
|
563
|
+
console.log(`\n ${C.red}✗ Missing validator address or name.${C.reset}`);
|
|
564
|
+
console.log(`\n ${C.cyan}Usage:${C.reset}`);
|
|
565
|
+
console.log(` ${C.cyan}aether validator info <addressOrName>${C.reset} Inspect a validator`);
|
|
566
|
+
console.log(` ${C.cyan}aether validator info <addressOrName> --json${C.reset} JSON output`);
|
|
567
|
+
console.log(` ${C.cyan}aether validator info --address <addr>${C.reset} Use --address flag`);
|
|
568
|
+
console.log();
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const rpc = opts.rpc;
|
|
573
|
+
|
|
574
|
+
if (!opts.asJson) {
|
|
575
|
+
console.log(`\n${C.dim}Looking up "${opts.target}" on ${rpc}...${C.reset}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Fetch everything in parallel
|
|
579
|
+
const [rawValidator, allValidators, epochInfo, supply] = await Promise.all([
|
|
580
|
+
fetchValidatorByIdentity(rpc, opts.target),
|
|
581
|
+
fetchAllValidators(rpc),
|
|
582
|
+
fetchEpochInfo(rpc),
|
|
583
|
+
fetchSupply(rpc),
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
if (!rawValidator) {
|
|
587
|
+
if (opts.asJson) {
|
|
588
|
+
console.log(JSON.stringify({ error: 'Validator not found', query: opts.target, rpc }, null, 2));
|
|
589
|
+
} else {
|
|
590
|
+
console.log(`\n ${C.red}✗ Validator not found:${C.reset} "${opts.target}"`);
|
|
591
|
+
console.log(` ${C.dim}Try a full address (ATH...), partial address (first 8+ chars), or name.${C.reset}`);
|
|
592
|
+
console.log(` ${C.dim}List all validators: aether validators list${C.reset}\n`);
|
|
593
|
+
}
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const v = normalise(rawValidator);
|
|
598
|
+
|
|
599
|
+
// Fetch delegators and performance in parallel
|
|
600
|
+
const [delegators, performance] = await Promise.all([
|
|
601
|
+
v.pubkey ? fetchDelegators(rpc, v.pubkey) : Promise.resolve([]),
|
|
602
|
+
v.pubkey ? fetchValidatorPerformance(rpc, v.pubkey) : Promise.resolve(null),
|
|
603
|
+
]);
|
|
604
|
+
|
|
605
|
+
// Estimate APY if not available
|
|
606
|
+
let apyEstimated = v.apy;
|
|
607
|
+
if ((apyEstimated === null || apyEstimated === undefined) && supply && !supply.error && epochInfo) {
|
|
608
|
+
const totalStake = Number(supply.total_staked || supply.total || 0);
|
|
609
|
+
const rewardsPerEpoch = Number(epochInfo.rewards_per_epoch || '2000000000');
|
|
610
|
+
if (totalStake > 0 && rewardsPerEpoch > 0) {
|
|
611
|
+
const validatorStake = Number(v.stake);
|
|
612
|
+
const networkApy = (rewardsPerEpoch / totalStake) * 73;
|
|
613
|
+
if (validatorStake > 0) {
|
|
614
|
+
apyEstimated = networkApy;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const finalV = { ...v, apy: apyEstimated !== undefined ? apyEstimated : v.apy };
|
|
620
|
+
|
|
621
|
+
if (opts.asJson) {
|
|
622
|
+
renderJson(finalV, delegators, performance, epochInfo, allValidators);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
renderHeader(finalV);
|
|
627
|
+
renderStats(finalV, epochInfo);
|
|
628
|
+
renderNodeInfo(finalV);
|
|
629
|
+
renderEpochPerformance(finalV, epochInfo, performance);
|
|
630
|
+
renderDelegators(delegators);
|
|
631
|
+
renderRank(allValidators, finalV);
|
|
632
|
+
|
|
633
|
+
console.log(` ${C.dim}Fetched from: ${rpc}${C.reset}`);
|
|
634
|
+
console.log();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
main().catch(err => {
|
|
638
|
+
console.error(`\n${C.red}✗ Validator info failed:${C.reset} ${err.message}\n`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
});
|
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
const { doctorCommand } = require('./commands/doctor');
|
|
10
10
|
const { validatorStart } = require('./commands/validator-start');
|
|
11
11
|
const { validatorStatus } = require('./commands/validator-status');
|
|
12
|
+
const { validatorInfo } = require('./commands/validator-info');
|
|
12
13
|
const { init } = require('./commands/init');
|
|
13
14
|
const { monitorLoop } = require('./commands/monitor');
|
|
14
15
|
const { logsCommand } = require('./commands/logs');
|
|
@@ -22,6 +23,8 @@ const { rewardsCommand } = require('./commands/rewards');
|
|
|
22
23
|
const { accountCommand } = require('./commands/account');
|
|
23
24
|
const { emergencyCommand } = require('./commands/emergency');
|
|
24
25
|
const { priceCommand } = require('./commands/price');
|
|
26
|
+
const { epochCommand } = require('./commands/epoch');
|
|
27
|
+
const { supplyCommand } = require('./commands/supply');
|
|
25
28
|
const readline = require('readline');
|
|
26
29
|
|
|
27
30
|
// CLI version
|
|
@@ -279,9 +282,12 @@ const COMMANDS = {
|
|
|
279
282
|
case 'status':
|
|
280
283
|
validatorStatus();
|
|
281
284
|
break;
|
|
285
|
+
case 'info':
|
|
286
|
+
validatorInfo();
|
|
287
|
+
break;
|
|
282
288
|
default:
|
|
283
289
|
console.error(`Unknown validator command: ${subcmd}`);
|
|
284
|
-
console.error('Valid commands: start, status');
|
|
290
|
+
console.error('Valid commands: start, status, info');
|
|
285
291
|
process.exit(1);
|
|
286
292
|
}
|
|
287
293
|
},
|
|
@@ -319,6 +325,21 @@ const COMMANDS = {
|
|
|
319
325
|
accountCommand();
|
|
320
326
|
},
|
|
321
327
|
},
|
|
328
|
+
epoch: {
|
|
329
|
+
description: 'Aether epoch info — current epoch, slot, time remaining, APY estimate — aether epoch [--json] [--schedule]',
|
|
330
|
+
handler: () => {
|
|
331
|
+
const { epochCommand } = require('./commands/epoch');
|
|
332
|
+
epochCommand();
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
supply: {
|
|
336
|
+
description: 'Aether token supply — total, circulating, staked, burned — aether supply [--json] [--verbose]',
|
|
337
|
+
handler: () => {
|
|
338
|
+
const { supplyCommand } = require('./commands/supply');
|
|
339
|
+
// Pass full argv so supply.js can parse its own --help etc.
|
|
340
|
+
supplyCommand();
|
|
341
|
+
},
|
|
342
|
+
},
|
|
322
343
|
validators: {
|
|
323
344
|
description: 'List active validators — aether validators list [--tier full|lite|observer] [--json]',
|
|
324
345
|
handler: () => {
|