aether-hub 1.2.6 → 1.2.8
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/account.js +280 -304
- package/commands/apy.js +480 -480
- package/commands/balance.js +276 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +323 -323
- package/commands/claim.js +292 -0
- package/commands/delegations.js +45 -95
- package/commands/emergency.js +667 -657
- package/commands/epoch.js +275 -357
- package/commands/fees.js +276 -0
- package/commands/info.js +495 -536
- package/commands/monitor.js +431 -431
- package/commands/multisig.js +726 -726
- package/commands/network.js +429 -503
- package/commands/ping.js +266 -320
- package/commands/price.js +253 -253
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +537 -537
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +509 -509
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +31 -46
- package/commands/stats.js +418 -418
- package/commands/status.js +326 -370
- package/commands/supply.js +37 -83
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +65 -227
- package/commands/validator-info.js +10 -4
- package/commands/validator-start.js +1 -1
- package/commands/validator-status.js +32 -73
- package/commands/validators.js +36 -75
- package/commands/wallet.js +8 -29
- package/index.js +65 -8
- package/package.json +1 -3
package/commands/snapshot.js
CHANGED
|
@@ -1,509 +1,509 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* aether-cli snapshot - Aether Node Sync & Snapshot Status
|
|
3
|
-
*
|
|
4
|
-
* Shows how far your node has synced vs the network, snapshot availability,
|
|
5
|
-
* and whether your node is catching up or is fully current.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* aether-cli snapshot # Interactive sync status view
|
|
9
|
-
* aether-cli snapshot --json # JSON output for scripting
|
|
10
|
-
* aether-cli snapshot --rpc <url> # Query a specific RPC endpoint
|
|
11
|
-
* aether-cli snapshot --watch # Refresh every 5 seconds
|
|
12
|
-
*
|
|
13
|
-
* @see docs/MINING_VALIDATOR_TOOLS.md for spec
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const http = require('http');
|
|
17
|
-
const https = require('https');
|
|
18
|
-
|
|
19
|
-
// ANSI colours
|
|
20
|
-
const C = {
|
|
21
|
-
reset: '\x1b[0m',
|
|
22
|
-
bright: '\x1b[1m',
|
|
23
|
-
dim: '\x1b[2m',
|
|
24
|
-
red: '\x1b[31m',
|
|
25
|
-
green: '\x1b[32m',
|
|
26
|
-
yellow: '\x1b[33m',
|
|
27
|
-
blue: '\x1b[34m',
|
|
28
|
-
cyan: '\x1b[36m',
|
|
29
|
-
magenta: '\x1b[35m',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const DEFAULT_RPC = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
33
|
-
const REFRESH_INTERVAL_MS = 5000;
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// HTTP helpers
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
function httpRequest(rpcUrl, path, options = {}) {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const url = new URL(path, rpcUrl);
|
|
42
|
-
const isHttps = url.protocol === 'https:';
|
|
43
|
-
const lib = isHttps ? https : http;
|
|
44
|
-
|
|
45
|
-
const reqOptions = {
|
|
46
|
-
hostname: url.hostname,
|
|
47
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
48
|
-
path: url.pathname + url.search,
|
|
49
|
-
method: 'GET',
|
|
50
|
-
timeout: 8000,
|
|
51
|
-
headers: { 'Content-Type': 'application/json' },
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const req = lib.request(reqOptions, (res) => {
|
|
55
|
-
let data = '';
|
|
56
|
-
res.on('data', (chunk) => (data += chunk));
|
|
57
|
-
res.on('end', () => {
|
|
58
|
-
try { resolve(JSON.parse(data)); }
|
|
59
|
-
catch { resolve({ raw: data }); }
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
req.on('error', reject);
|
|
64
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
65
|
-
req.end();
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function httpPost(rpcUrl, path, body) {
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
const url = new URL(path, rpcUrl);
|
|
72
|
-
const isHttps = url.protocol === 'https:';
|
|
73
|
-
const lib = isHttps ? https : http;
|
|
74
|
-
const bodyStr = JSON.stringify(body);
|
|
75
|
-
|
|
76
|
-
const req = lib.request({
|
|
77
|
-
hostname: url.hostname,
|
|
78
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
79
|
-
path: url.pathname + url.search,
|
|
80
|
-
method: 'POST',
|
|
81
|
-
timeout: 8000,
|
|
82
|
-
headers: {
|
|
83
|
-
'Content-Type': 'application/json',
|
|
84
|
-
'Content-Length': Buffer.byteLength(bodyStr),
|
|
85
|
-
},
|
|
86
|
-
}, (res) => {
|
|
87
|
-
let data = '';
|
|
88
|
-
res.on('data', (chunk) => (data += chunk));
|
|
89
|
-
res.on('end', () => {
|
|
90
|
-
try { resolve(JSON.parse(data)); }
|
|
91
|
-
catch { resolve(data); }
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
req.on('error', reject);
|
|
96
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
97
|
-
req.write(bodyStr);
|
|
98
|
-
req.end();
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// Argument parsing
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
function parseArgs() {
|
|
107
|
-
const args = process.argv.slice(2);
|
|
108
|
-
const options = {
|
|
109
|
-
rpc: DEFAULT_RPC,
|
|
110
|
-
asJson: false,
|
|
111
|
-
watch: false,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
for (let i = 0; i < args.length; i++) {
|
|
115
|
-
if (args[i] === '--rpc' || args[i] === '-r') {
|
|
116
|
-
options.rpc = args[++i];
|
|
117
|
-
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
118
|
-
options.asJson = true;
|
|
119
|
-
} else if (args[i] === '--watch' || args[i] === '-w') {
|
|
120
|
-
options.watch = true;
|
|
121
|
-
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
122
|
-
showHelp();
|
|
123
|
-
process.exit(0);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return options;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function showHelp() {
|
|
131
|
-
console.log(`
|
|
132
|
-
${C.bright}${C.cyan}aether-cli snapshot${C.reset} - Aether Node Sync & Snapshot Status
|
|
133
|
-
|
|
134
|
-
${C.bright}Usage:${C.reset}
|
|
135
|
-
aether-cli snapshot [options]
|
|
136
|
-
|
|
137
|
-
${C.bright}Options:${C.reset}
|
|
138
|
-
-r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC} or $AETHER_RPC)
|
|
139
|
-
-j, --json Output raw JSON (good for scripting)
|
|
140
|
-
-w, --watch Refresh every 5 seconds (live view)
|
|
141
|
-
-h, --help Show this help message
|
|
142
|
-
|
|
143
|
-
${C.bright}Examples:${C.reset}
|
|
144
|
-
aether-cli snapshot # Interactive sync status
|
|
145
|
-
aether-cli snapshot --json # JSON output
|
|
146
|
-
aether-cli snapshot --watch # Live refreshing view
|
|
147
|
-
aether-cli snapshot --rpc https://api.testnet.aether.network
|
|
148
|
-
`.trim());
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
// Data fetchers
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
|
|
155
|
-
/** GET /v1/slot — current network slot */
|
|
156
|
-
async function getSlot(rpc) {
|
|
157
|
-
try {
|
|
158
|
-
const res = await httpRequest(rpc, '/v1/slot');
|
|
159
|
-
return res.slot ?? res.root_slot ?? null;
|
|
160
|
-
} catch {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** GET /v1/block_height — node's synced block height */
|
|
166
|
-
async function getBlockHeight(rpc) {
|
|
167
|
-
try {
|
|
168
|
-
const res = await httpRequest(rpc, '/v1/block_height');
|
|
169
|
-
return res.block_height ?? null;
|
|
170
|
-
} catch {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** GET /v1/epoch — current epoch info */
|
|
176
|
-
async function getEpoch(rpc) {
|
|
177
|
-
try {
|
|
178
|
-
const res = await httpRequest(rpc, '/v1/epoch');
|
|
179
|
-
return res;
|
|
180
|
-
} catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** GET /v1/snapshot — snapshot slot info */
|
|
186
|
-
async function getSnapshot(rpc) {
|
|
187
|
-
try {
|
|
188
|
-
const res = await httpRequest(rpc, '/v1/snapshot');
|
|
189
|
-
return res;
|
|
190
|
-
} catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** POST /v1/version — node version info */
|
|
196
|
-
async function getVersion(rpc) {
|
|
197
|
-
try {
|
|
198
|
-
const res = await httpPost(rpc, '/v1/version', {});
|
|
199
|
-
return res;
|
|
200
|
-
} catch {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** GET /v1/health — node health (if supported) */
|
|
206
|
-
async function getHealth(rpc) {
|
|
207
|
-
try {
|
|
208
|
-
const res = await httpRequest(rpc, '/v1/health');
|
|
209
|
-
return res;
|
|
210
|
-
} catch {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
// Formatting helpers
|
|
217
|
-
// ---------------------------------------------------------------------------
|
|
218
|
-
|
|
219
|
-
function formatNumber(n) {
|
|
220
|
-
if (n === null || n === undefined) return 'N/A';
|
|
221
|
-
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function syncStatus(nodeSlot, networkSlot) {
|
|
225
|
-
if (nodeSlot === null || networkSlot === null) {
|
|
226
|
-
return { label: `${C.yellow}UNKNOWN${C.reset}`, icon: '?', color: C.yellow };
|
|
227
|
-
}
|
|
228
|
-
const diff = networkSlot - nodeSlot;
|
|
229
|
-
const pct = networkSlot > 0 ? ((nodeSlot / networkSlot) * 100).toFixed(1) : '0.0';
|
|
230
|
-
|
|
231
|
-
if (diff <= 0) {
|
|
232
|
-
return { label: `${C.green}SYNCED${C.reset}`, icon: '✓', color: C.green, diff };
|
|
233
|
-
}
|
|
234
|
-
if (diff <= 5) {
|
|
235
|
-
return { label: `${C.green}CATCHING UP${C.reset}`, icon: '◐', color: C.green, diff };
|
|
236
|
-
}
|
|
237
|
-
if (diff <= 50) {
|
|
238
|
-
return { label: `${C.yellow}BEHIND${C.reset}`, icon: '◑', color: C.yellow, diff };
|
|
239
|
-
}
|
|
240
|
-
return { label: `${C.red}FAR BEHIND${C.reset}`, icon: '✗', color: C.red, diff };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function progressBar(nodeSlot, networkSlot, width = 30) {
|
|
244
|
-
if (nodeSlot === null || networkSlot === null || networkSlot === 0) {
|
|
245
|
-
return `${C.dim}[${'─'.repeat(width)}]${C.reset} N/A`;
|
|
246
|
-
}
|
|
247
|
-
const ratio = Math.min(nodeSlot / networkSlot, 1);
|
|
248
|
-
const filled = Math.round(ratio * width);
|
|
249
|
-
const empty = width - filled;
|
|
250
|
-
return (
|
|
251
|
-
`${C.green}[${'█'.repeat(filled)}${C.dim}${'─'.repeat(empty)}${C.reset}]` +
|
|
252
|
-
` ${(ratio * 100).toFixed(1)}%`
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function catchupEstimate(nodeSlot, networkSlot) {
|
|
257
|
-
if (nodeSlot === null || networkSlot === null || networkSlot <= nodeSlot) return null;
|
|
258
|
-
const diff = networkSlot - nodeSlot;
|
|
259
|
-
// Rough estimate: ~2 slots/second typical throughput
|
|
260
|
-
const seconds = Math.floor(diff / 2);
|
|
261
|
-
if (seconds < 60) return `~${seconds}s`;
|
|
262
|
-
if (seconds < 3600) return `~${Math.floor(seconds / 60)}m`;
|
|
263
|
-
return `~${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
// Renderers
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
|
|
270
|
-
function renderSync(data, rpc) {
|
|
271
|
-
const { nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData, asJson } = data;
|
|
272
|
-
|
|
273
|
-
if (asJson) {
|
|
274
|
-
console.log(JSON.stringify({
|
|
275
|
-
rpc,
|
|
276
|
-
fetchedAt: new Date().toISOString(),
|
|
277
|
-
node: {
|
|
278
|
-
slot: nodeSlot,
|
|
279
|
-
blockHeight,
|
|
280
|
-
},
|
|
281
|
-
network: {
|
|
282
|
-
slot: networkSlot,
|
|
283
|
-
},
|
|
284
|
-
sync: {
|
|
285
|
-
status: syncStatus(nodeSlot, networkSlot).label.replace(/\x1b\[\d+m/g, ''),
|
|
286
|
-
slotsBehind: networkSlot !== null && nodeSlot !== null ? Math.max(0, networkSlot - nodeSlot) : null,
|
|
287
|
-
percentSynced: nodeSlot !== null && networkSlot !== null && networkSlot > 0
|
|
288
|
-
? parseFloat((nodeSlot / networkSlot * 100).toFixed(2))
|
|
289
|
-
: null,
|
|
290
|
-
},
|
|
291
|
-
epoch: epochData ? {
|
|
292
|
-
epoch: epochData.epoch,
|
|
293
|
-
slotIndex: epochData.slot_index,
|
|
294
|
-
slotsInEpoch: epochData.slots_in_epoch,
|
|
295
|
-
absoluteSlot: epochData.absolute_slot,
|
|
296
|
-
} : null,
|
|
297
|
-
snapshot: snapshotData && !snapshotData.error ? {
|
|
298
|
-
fullSnapshotSlot: snapshotData.full_snapshot_slot ?? snapshotData.snapshot_slot ?? null,
|
|
299
|
-
incrementalSnapshotSlot: snapshotData.incremental_snapshot_slot ?? null,
|
|
300
|
-
} : null,
|
|
301
|
-
version: versionData?.version ?? versionData?.solana_core ?? versionData?.['software-version'] ?? null,
|
|
302
|
-
healthy: healthData && !healthData.error ? (healthData.ok ?? true) : null,
|
|
303
|
-
}, null, 2));
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const status = syncStatus(nodeSlot, networkSlot);
|
|
308
|
-
const now = new Date().toLocaleTimeString();
|
|
309
|
-
const catchup = catchupEstimate(nodeSlot, networkSlot);
|
|
310
|
-
|
|
311
|
-
console.log();
|
|
312
|
-
console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
313
|
-
console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER NODE SNAPSHOT / SYNC STATUS${C.reset}${C.cyan} ║${C.reset}`);
|
|
314
|
-
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
315
|
-
console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
|
|
316
|
-
console.log(` ${C.dim}Updated:${C.reset} ${now}`);
|
|
317
|
-
console.log();
|
|
318
|
-
|
|
319
|
-
// Health indicator
|
|
320
|
-
if (healthData && !healthData.error) {
|
|
321
|
-
const ok = healthData.ok ?? true;
|
|
322
|
-
console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
323
|
-
console.log(` ${C.bright}│${C.reset} Node Health: ${ok ? `${C.green}● HEALTHY${C.reset}` : `${C.red}● UNHEALTHY${C.reset}`}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
324
|
-
console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
325
|
-
console.log();
|
|
326
|
-
} else if (healthData && healthData.error) {
|
|
327
|
-
console.log(` ${C.red}⚠ Health check failed:${C.reset} ${healthData.error}\n`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Sync status — large prominent display
|
|
331
|
-
console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
332
|
-
console.log(` ${C.bright}│${C.reset} Sync Status: ${status.color}${C.bright}${status.label}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
333
|
-
if (status.diff !== undefined && status.diff > 0) {
|
|
334
|
-
console.log(` ${C.bright}│${C.reset} Slots behind: ${C.yellow}${formatNumber(status.diff)}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
335
|
-
if (catchup) {
|
|
336
|
-
console.log(` ${C.bright}│${C.reset} Est. time to sync: ${C.cyan}${catchup}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
340
|
-
console.log();
|
|
341
|
-
|
|
342
|
-
// Progress bar
|
|
343
|
-
console.log(` ${C.bright}── Slot Progress ───────────────────────────────────────────${C.reset}`);
|
|
344
|
-
console.log(` ${progressBar(nodeSlot, networkSlot)}`);
|
|
345
|
-
console.log();
|
|
346
|
-
|
|
347
|
-
// Slot details
|
|
348
|
-
console.log(` ${C.bright}┌────────────────────┬────────────────────┐${C.reset}`);
|
|
349
|
-
console.log(` ${C.bright}│${C.reset} ${C.cyan}Your Node${C.reset} ${C.bright}│${C.reset} ${C.cyan}Network${C.reset} ${C.bright}│${C.reset}`);
|
|
350
|
-
console.log(` ${C.bright}├────────────────────┼────────────────────┤${C.reset}`);
|
|
351
|
-
const nodeStr = nodeSlot !== null ? formatNumber(nodeSlot) : 'N/A';
|
|
352
|
-
const netStr = networkSlot !== null ? formatNumber(networkSlot) : 'N/A';
|
|
353
|
-
console.log(` ${C.bright}│${C.reset} Slot: ${C.green}${nodeStr.padEnd(22)}${C.reset} ${C.bright}│${C.reset} Slot: ${C.cyan}${netStr.padEnd(22)}${C.reset} ${C.bright}│${C.reset}`);
|
|
354
|
-
const bhStr = blockHeight !== null ? formatNumber(blockHeight) : 'N/A';
|
|
355
|
-
console.log(` ${C.bright}│${C.reset} Block: ${C.blue}${bhStr.padEnd(21)}${C.reset} ${C.bright}│${C.reset} ${C.dim}Block: same as slot${C.reset} ${C.bright}│${C.reset}`);
|
|
356
|
-
console.log(` ${C.bright}└────────────────────┴────────────────────┘${C.reset}`);
|
|
357
|
-
console.log();
|
|
358
|
-
|
|
359
|
-
// Epoch info
|
|
360
|
-
if (epochData && epochData.epoch !== undefined) {
|
|
361
|
-
console.log(` ${C.bright}── Epoch Info ──────────────────────────────────────────────${C.reset}`);
|
|
362
|
-
const ep = epochData.epoch;
|
|
363
|
-
const slotIdx = epochData.slot_index !== undefined ? epochData.slot_index : '?';
|
|
364
|
-
const slotsInEp = epochData.slots_in_epoch !== undefined ? epochData.slots_in_epoch : '?';
|
|
365
|
-
const progress = slotsInEp !== '?' && slotsInEp > 0
|
|
366
|
-
? ((slotIdx / slotsInEp) * 100).toFixed(1) + '%'
|
|
367
|
-
: '?';
|
|
368
|
-
console.log(` ${C.dim}Epoch:${C.reset} ${C.bright}${ep}${C.reset} ${C.dim}Slot in epoch:${C.reset} ${C.bright}${slotIdx} / ${slotsInEp}${C.reset} ${C.dim}(${progress})${C.reset}`);
|
|
369
|
-
if (epochData.absolute_slot !== undefined) {
|
|
370
|
-
console.log(` ${C.dim}Absolute slot:${C.reset} ${C.bright}${formatNumber(epochData.absolute_slot)}${C.reset}`);
|
|
371
|
-
}
|
|
372
|
-
console.log();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Snapshot info
|
|
376
|
-
if (snapshotData && !snapshotData.error) {
|
|
377
|
-
console.log(` ${C.bright}── Snapshot Info ───────────────────────────────────────────${C.reset}`);
|
|
378
|
-
const fullSnap = snapshotData.full_snapshot_slot ?? snapshotData.snapshot_slot ?? null;
|
|
379
|
-
const incSnap = snapshotData.incremental_snapshot_slot ?? null;
|
|
380
|
-
|
|
381
|
-
if (fullSnap !== null) {
|
|
382
|
-
const age = networkSlot !== null ? formatNumber(networkSlot - fullSnap) : null;
|
|
383
|
-
console.log(` ${C.dim}Full snapshot slot:${C.reset} ${C.green}${formatNumber(fullSnap)}${C.reset}`);
|
|
384
|
-
if (age !== null) {
|
|
385
|
-
console.log(` ${C.dim} (~${age} slots ago)${C.reset}`);
|
|
386
|
-
}
|
|
387
|
-
if (nodeSlot !== null && networkSlot !== null) {
|
|
388
|
-
const snapAge = networkSlot - fullSnap;
|
|
389
|
-
const freshness = snapAge < 100 ? `${C.green}fresh${C.reset}` : snapAge < 1000 ? `${C.yellow}aging${C.reset}` : `${C.red}old${C.reset}`;
|
|
390
|
-
console.log(` ${C.dim} Snapshot freshness:${C.reset} ${freshness}`);
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
console.log(` ${C.dim}No full snapshot available.${C.reset}`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (incSnap !== null) {
|
|
397
|
-
console.log(` ${C.dim}Incremental snapshot slot:${C.reset} ${C.cyan}${formatNumber(incSnap)}${C.reset}`);
|
|
398
|
-
}
|
|
399
|
-
console.log();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Version info
|
|
403
|
-
if (versionData && !versionData.error) {
|
|
404
|
-
const ver = versionData.version || versionData.solana_core || versionData['software-version'];
|
|
405
|
-
if (ver) {
|
|
406
|
-
console.log(` ${C.bright}── Node Version ───────────────────────────────────────────${C.reset}`);
|
|
407
|
-
console.log(` ${C.dim}Version:${C.reset} ${C.green}${ver}${C.reset}`);
|
|
408
|
-
console.log();
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Tips
|
|
413
|
-
if (status.diff === 0 || status.diff === undefined) {
|
|
414
|
-
console.log(` ${C.green}✓ Node is fully synced with the network.${C.reset}`);
|
|
415
|
-
} else if (status.diff > 0) {
|
|
416
|
-
console.log(` ${C.yellow}⏳ Node is catching up — this is normal on first start or after a restart.${C.reset}`);
|
|
417
|
-
console.log(` ${C.dim} For faster sync, try downloading a recent snapshot from a peer.${C.reset}`);
|
|
418
|
-
console.log(` ${C.dim} Check: aether-cli network --peers${C.reset}`);
|
|
419
|
-
}
|
|
420
|
-
console.log();
|
|
421
|
-
console.log(` ${C.dim}Tip: --watch for live view | --json for scripting${C.reset}`);
|
|
422
|
-
console.log();
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// ---------------------------------------------------------------------------
|
|
426
|
-
// Watch mode
|
|
427
|
-
// ---------------------------------------------------------------------------
|
|
428
|
-
|
|
429
|
-
async function watchMode(rpc) {
|
|
430
|
-
const readline = require('readline');
|
|
431
|
-
let running = true;
|
|
432
|
-
|
|
433
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
434
|
-
const drainStdin = () => {
|
|
435
|
-
rl.close();
|
|
436
|
-
running = false;
|
|
437
|
-
};
|
|
438
|
-
process.stdin.on('data', drainStdin);
|
|
439
|
-
process.stdin.resume();
|
|
440
|
-
|
|
441
|
-
console.log(` ${C.cyan}Live sync monitoring started.${C.reset} ${C.dim}Press Ctrl+C to stop.${C.reset}\n`);
|
|
442
|
-
|
|
443
|
-
while (running) {
|
|
444
|
-
// Move cursor up and clear lines for clean refresh
|
|
445
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
const [nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData] =
|
|
449
|
-
await Promise.all([
|
|
450
|
-
getSlot(rpc),
|
|
451
|
-
getSlot(rpc), // network slot uses same endpoint
|
|
452
|
-
getBlockHeight(rpc),
|
|
453
|
-
getEpoch(rpc),
|
|
454
|
-
getSnapshot(rpc),
|
|
455
|
-
getVersion(rpc),
|
|
456
|
-
getHealth(rpc),
|
|
457
|
-
]);
|
|
458
|
-
|
|
459
|
-
renderSync({ nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData, asJson: false }, rpc);
|
|
460
|
-
} catch (err) {
|
|
461
|
-
console.log(` ${C.red}✗ Error fetching data:${C.reset} ${err.message}`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (!running) break;
|
|
465
|
-
await new Promise((res) => setTimeout(res, REFRESH_INTERVAL_MS));
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
process.stdin.pause();
|
|
469
|
-
process.stdin.off('data', drainStdin);
|
|
470
|
-
console.log(`\n ${C.dim}Stopped.${C.reset}\n`);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// ---------------------------------------------------------------------------
|
|
474
|
-
// Main
|
|
475
|
-
// ---------------------------------------------------------------------------
|
|
476
|
-
|
|
477
|
-
async function snapshotCommand() {
|
|
478
|
-
const opts = parseArgs();
|
|
479
|
-
const rpc = opts.rpc;
|
|
480
|
-
|
|
481
|
-
if (opts.watch) {
|
|
482
|
-
await watchMode(rpc);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const [nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData] =
|
|
487
|
-
await Promise.all([
|
|
488
|
-
getSlot(rpc),
|
|
489
|
-
getSlot(rpc), // same endpoint for network slot
|
|
490
|
-
getBlockHeight(rpc),
|
|
491
|
-
getEpoch(rpc),
|
|
492
|
-
getSnapshot(rpc),
|
|
493
|
-
getVersion(rpc),
|
|
494
|
-
getHealth(rpc),
|
|
495
|
-
]);
|
|
496
|
-
|
|
497
|
-
renderSync({ nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData, asJson: opts.asJson }, rpc);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
module.exports = { snapshotCommand };
|
|
501
|
-
|
|
502
|
-
if (require.main === module) {
|
|
503
|
-
snapshotCommand().catch((err) => {
|
|
504
|
-
console.error(`\n${C.red}✗ Snapshot command failed:${C.reset} ${err.message}`);
|
|
505
|
-
console.error(` ${C.dim}Check that your validator is running and RPC is accessible.${C.reset}`);
|
|
506
|
-
console.error(` ${C.dim}Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
|
|
507
|
-
process.exit(1);
|
|
508
|
-
});
|
|
509
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* aether-cli snapshot - Aether Node Sync & Snapshot Status
|
|
3
|
+
*
|
|
4
|
+
* Shows how far your node has synced vs the network, snapshot availability,
|
|
5
|
+
* and whether your node is catching up or is fully current.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* aether-cli snapshot # Interactive sync status view
|
|
9
|
+
* aether-cli snapshot --json # JSON output for scripting
|
|
10
|
+
* aether-cli snapshot --rpc <url> # Query a specific RPC endpoint
|
|
11
|
+
* aether-cli snapshot --watch # Refresh every 5 seconds
|
|
12
|
+
*
|
|
13
|
+
* @see docs/MINING_VALIDATOR_TOOLS.md for spec
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const https = require('https');
|
|
18
|
+
|
|
19
|
+
// ANSI colours
|
|
20
|
+
const C = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
bright: '\x1b[1m',
|
|
23
|
+
dim: '\x1b[2m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
green: '\x1b[32m',
|
|
26
|
+
yellow: '\x1b[33m',
|
|
27
|
+
blue: '\x1b[34m',
|
|
28
|
+
cyan: '\x1b[36m',
|
|
29
|
+
magenta: '\x1b[35m',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const DEFAULT_RPC = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
33
|
+
const REFRESH_INTERVAL_MS = 5000;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// HTTP helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function httpRequest(rpcUrl, path, options = {}) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const url = new URL(path, rpcUrl);
|
|
42
|
+
const isHttps = url.protocol === 'https:';
|
|
43
|
+
const lib = isHttps ? https : http;
|
|
44
|
+
|
|
45
|
+
const reqOptions = {
|
|
46
|
+
hostname: url.hostname,
|
|
47
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
48
|
+
path: url.pathname + url.search,
|
|
49
|
+
method: 'GET',
|
|
50
|
+
timeout: 8000,
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const req = lib.request(reqOptions, (res) => {
|
|
55
|
+
let data = '';
|
|
56
|
+
res.on('data', (chunk) => (data += chunk));
|
|
57
|
+
res.on('end', () => {
|
|
58
|
+
try { resolve(JSON.parse(data)); }
|
|
59
|
+
catch { resolve({ raw: data }); }
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
req.on('error', reject);
|
|
64
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function httpPost(rpcUrl, path, body) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const url = new URL(path, rpcUrl);
|
|
72
|
+
const isHttps = url.protocol === 'https:';
|
|
73
|
+
const lib = isHttps ? https : http;
|
|
74
|
+
const bodyStr = JSON.stringify(body);
|
|
75
|
+
|
|
76
|
+
const req = lib.request({
|
|
77
|
+
hostname: url.hostname,
|
|
78
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
79
|
+
path: url.pathname + url.search,
|
|
80
|
+
method: 'POST',
|
|
81
|
+
timeout: 8000,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
85
|
+
},
|
|
86
|
+
}, (res) => {
|
|
87
|
+
let data = '';
|
|
88
|
+
res.on('data', (chunk) => (data += chunk));
|
|
89
|
+
res.on('end', () => {
|
|
90
|
+
try { resolve(JSON.parse(data)); }
|
|
91
|
+
catch { resolve(data); }
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
req.on('error', reject);
|
|
96
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
97
|
+
req.write(bodyStr);
|
|
98
|
+
req.end();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Argument parsing
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function parseArgs() {
|
|
107
|
+
const args = process.argv.slice(2);
|
|
108
|
+
const options = {
|
|
109
|
+
rpc: DEFAULT_RPC,
|
|
110
|
+
asJson: false,
|
|
111
|
+
watch: false,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < args.length; i++) {
|
|
115
|
+
if (args[i] === '--rpc' || args[i] === '-r') {
|
|
116
|
+
options.rpc = args[++i];
|
|
117
|
+
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
118
|
+
options.asJson = true;
|
|
119
|
+
} else if (args[i] === '--watch' || args[i] === '-w') {
|
|
120
|
+
options.watch = true;
|
|
121
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
122
|
+
showHelp();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return options;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function showHelp() {
|
|
131
|
+
console.log(`
|
|
132
|
+
${C.bright}${C.cyan}aether-cli snapshot${C.reset} - Aether Node Sync & Snapshot Status
|
|
133
|
+
|
|
134
|
+
${C.bright}Usage:${C.reset}
|
|
135
|
+
aether-cli snapshot [options]
|
|
136
|
+
|
|
137
|
+
${C.bright}Options:${C.reset}
|
|
138
|
+
-r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC} or $AETHER_RPC)
|
|
139
|
+
-j, --json Output raw JSON (good for scripting)
|
|
140
|
+
-w, --watch Refresh every 5 seconds (live view)
|
|
141
|
+
-h, --help Show this help message
|
|
142
|
+
|
|
143
|
+
${C.bright}Examples:${C.reset}
|
|
144
|
+
aether-cli snapshot # Interactive sync status
|
|
145
|
+
aether-cli snapshot --json # JSON output
|
|
146
|
+
aether-cli snapshot --watch # Live refreshing view
|
|
147
|
+
aether-cli snapshot --rpc https://api.testnet.aether.network
|
|
148
|
+
`.trim());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Data fetchers
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/** GET /v1/slot — current network slot */
|
|
156
|
+
async function getSlot(rpc) {
|
|
157
|
+
try {
|
|
158
|
+
const res = await httpRequest(rpc, '/v1/slot');
|
|
159
|
+
return res.slot ?? res.root_slot ?? null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** GET /v1/block_height — node's synced block height */
|
|
166
|
+
async function getBlockHeight(rpc) {
|
|
167
|
+
try {
|
|
168
|
+
const res = await httpRequest(rpc, '/v1/block_height');
|
|
169
|
+
return res.block_height ?? null;
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** GET /v1/epoch — current epoch info */
|
|
176
|
+
async function getEpoch(rpc) {
|
|
177
|
+
try {
|
|
178
|
+
const res = await httpRequest(rpc, '/v1/epoch');
|
|
179
|
+
return res;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** GET /v1/snapshot — snapshot slot info */
|
|
186
|
+
async function getSnapshot(rpc) {
|
|
187
|
+
try {
|
|
188
|
+
const res = await httpRequest(rpc, '/v1/snapshot');
|
|
189
|
+
return res;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** POST /v1/version — node version info */
|
|
196
|
+
async function getVersion(rpc) {
|
|
197
|
+
try {
|
|
198
|
+
const res = await httpPost(rpc, '/v1/version', {});
|
|
199
|
+
return res;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** GET /v1/health — node health (if supported) */
|
|
206
|
+
async function getHealth(rpc) {
|
|
207
|
+
try {
|
|
208
|
+
const res = await httpRequest(rpc, '/v1/health');
|
|
209
|
+
return res;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Formatting helpers
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
function formatNumber(n) {
|
|
220
|
+
if (n === null || n === undefined) return 'N/A';
|
|
221
|
+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function syncStatus(nodeSlot, networkSlot) {
|
|
225
|
+
if (nodeSlot === null || networkSlot === null) {
|
|
226
|
+
return { label: `${C.yellow}UNKNOWN${C.reset}`, icon: '?', color: C.yellow };
|
|
227
|
+
}
|
|
228
|
+
const diff = networkSlot - nodeSlot;
|
|
229
|
+
const pct = networkSlot > 0 ? ((nodeSlot / networkSlot) * 100).toFixed(1) : '0.0';
|
|
230
|
+
|
|
231
|
+
if (diff <= 0) {
|
|
232
|
+
return { label: `${C.green}SYNCED${C.reset}`, icon: '✓', color: C.green, diff };
|
|
233
|
+
}
|
|
234
|
+
if (diff <= 5) {
|
|
235
|
+
return { label: `${C.green}CATCHING UP${C.reset}`, icon: '◐', color: C.green, diff };
|
|
236
|
+
}
|
|
237
|
+
if (diff <= 50) {
|
|
238
|
+
return { label: `${C.yellow}BEHIND${C.reset}`, icon: '◑', color: C.yellow, diff };
|
|
239
|
+
}
|
|
240
|
+
return { label: `${C.red}FAR BEHIND${C.reset}`, icon: '✗', color: C.red, diff };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function progressBar(nodeSlot, networkSlot, width = 30) {
|
|
244
|
+
if (nodeSlot === null || networkSlot === null || networkSlot === 0) {
|
|
245
|
+
return `${C.dim}[${'─'.repeat(width)}]${C.reset} N/A`;
|
|
246
|
+
}
|
|
247
|
+
const ratio = Math.min(nodeSlot / networkSlot, 1);
|
|
248
|
+
const filled = Math.round(ratio * width);
|
|
249
|
+
const empty = width - filled;
|
|
250
|
+
return (
|
|
251
|
+
`${C.green}[${'█'.repeat(filled)}${C.dim}${'─'.repeat(empty)}${C.reset}]` +
|
|
252
|
+
` ${(ratio * 100).toFixed(1)}%`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function catchupEstimate(nodeSlot, networkSlot) {
|
|
257
|
+
if (nodeSlot === null || networkSlot === null || networkSlot <= nodeSlot) return null;
|
|
258
|
+
const diff = networkSlot - nodeSlot;
|
|
259
|
+
// Rough estimate: ~2 slots/second typical throughput
|
|
260
|
+
const seconds = Math.floor(diff / 2);
|
|
261
|
+
if (seconds < 60) return `~${seconds}s`;
|
|
262
|
+
if (seconds < 3600) return `~${Math.floor(seconds / 60)}m`;
|
|
263
|
+
return `~${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Renderers
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
function renderSync(data, rpc) {
|
|
271
|
+
const { nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData, asJson } = data;
|
|
272
|
+
|
|
273
|
+
if (asJson) {
|
|
274
|
+
console.log(JSON.stringify({
|
|
275
|
+
rpc,
|
|
276
|
+
fetchedAt: new Date().toISOString(),
|
|
277
|
+
node: {
|
|
278
|
+
slot: nodeSlot,
|
|
279
|
+
blockHeight,
|
|
280
|
+
},
|
|
281
|
+
network: {
|
|
282
|
+
slot: networkSlot,
|
|
283
|
+
},
|
|
284
|
+
sync: {
|
|
285
|
+
status: syncStatus(nodeSlot, networkSlot).label.replace(/\x1b\[\d+m/g, ''),
|
|
286
|
+
slotsBehind: networkSlot !== null && nodeSlot !== null ? Math.max(0, networkSlot - nodeSlot) : null,
|
|
287
|
+
percentSynced: nodeSlot !== null && networkSlot !== null && networkSlot > 0
|
|
288
|
+
? parseFloat((nodeSlot / networkSlot * 100).toFixed(2))
|
|
289
|
+
: null,
|
|
290
|
+
},
|
|
291
|
+
epoch: epochData ? {
|
|
292
|
+
epoch: epochData.epoch,
|
|
293
|
+
slotIndex: epochData.slot_index,
|
|
294
|
+
slotsInEpoch: epochData.slots_in_epoch,
|
|
295
|
+
absoluteSlot: epochData.absolute_slot,
|
|
296
|
+
} : null,
|
|
297
|
+
snapshot: snapshotData && !snapshotData.error ? {
|
|
298
|
+
fullSnapshotSlot: snapshotData.full_snapshot_slot ?? snapshotData.snapshot_slot ?? null,
|
|
299
|
+
incrementalSnapshotSlot: snapshotData.incremental_snapshot_slot ?? null,
|
|
300
|
+
} : null,
|
|
301
|
+
version: versionData?.version ?? versionData?.solana_core ?? versionData?.['software-version'] ?? null,
|
|
302
|
+
healthy: healthData && !healthData.error ? (healthData.ok ?? true) : null,
|
|
303
|
+
}, null, 2));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const status = syncStatus(nodeSlot, networkSlot);
|
|
308
|
+
const now = new Date().toLocaleTimeString();
|
|
309
|
+
const catchup = catchupEstimate(nodeSlot, networkSlot);
|
|
310
|
+
|
|
311
|
+
console.log();
|
|
312
|
+
console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
313
|
+
console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER NODE SNAPSHOT / SYNC STATUS${C.reset}${C.cyan} ║${C.reset}`);
|
|
314
|
+
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
315
|
+
console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
|
|
316
|
+
console.log(` ${C.dim}Updated:${C.reset} ${now}`);
|
|
317
|
+
console.log();
|
|
318
|
+
|
|
319
|
+
// Health indicator
|
|
320
|
+
if (healthData && !healthData.error) {
|
|
321
|
+
const ok = healthData.ok ?? true;
|
|
322
|
+
console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
323
|
+
console.log(` ${C.bright}│${C.reset} Node Health: ${ok ? `${C.green}● HEALTHY${C.reset}` : `${C.red}● UNHEALTHY${C.reset}`}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
324
|
+
console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
325
|
+
console.log();
|
|
326
|
+
} else if (healthData && healthData.error) {
|
|
327
|
+
console.log(` ${C.red}⚠ Health check failed:${C.reset} ${healthData.error}\n`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Sync status — large prominent display
|
|
331
|
+
console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
332
|
+
console.log(` ${C.bright}│${C.reset} Sync Status: ${status.color}${C.bright}${status.label}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
333
|
+
if (status.diff !== undefined && status.diff > 0) {
|
|
334
|
+
console.log(` ${C.bright}│${C.reset} Slots behind: ${C.yellow}${formatNumber(status.diff)}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
335
|
+
if (catchup) {
|
|
336
|
+
console.log(` ${C.bright}│${C.reset} Est. time to sync: ${C.cyan}${catchup}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
340
|
+
console.log();
|
|
341
|
+
|
|
342
|
+
// Progress bar
|
|
343
|
+
console.log(` ${C.bright}── Slot Progress ───────────────────────────────────────────${C.reset}`);
|
|
344
|
+
console.log(` ${progressBar(nodeSlot, networkSlot)}`);
|
|
345
|
+
console.log();
|
|
346
|
+
|
|
347
|
+
// Slot details
|
|
348
|
+
console.log(` ${C.bright}┌────────────────────┬────────────────────┐${C.reset}`);
|
|
349
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Your Node${C.reset} ${C.bright}│${C.reset} ${C.cyan}Network${C.reset} ${C.bright}│${C.reset}`);
|
|
350
|
+
console.log(` ${C.bright}├────────────────────┼────────────────────┤${C.reset}`);
|
|
351
|
+
const nodeStr = nodeSlot !== null ? formatNumber(nodeSlot) : 'N/A';
|
|
352
|
+
const netStr = networkSlot !== null ? formatNumber(networkSlot) : 'N/A';
|
|
353
|
+
console.log(` ${C.bright}│${C.reset} Slot: ${C.green}${nodeStr.padEnd(22)}${C.reset} ${C.bright}│${C.reset} Slot: ${C.cyan}${netStr.padEnd(22)}${C.reset} ${C.bright}│${C.reset}`);
|
|
354
|
+
const bhStr = blockHeight !== null ? formatNumber(blockHeight) : 'N/A';
|
|
355
|
+
console.log(` ${C.bright}│${C.reset} Block: ${C.blue}${bhStr.padEnd(21)}${C.reset} ${C.bright}│${C.reset} ${C.dim}Block: same as slot${C.reset} ${C.bright}│${C.reset}`);
|
|
356
|
+
console.log(` ${C.bright}└────────────────────┴────────────────────┘${C.reset}`);
|
|
357
|
+
console.log();
|
|
358
|
+
|
|
359
|
+
// Epoch info
|
|
360
|
+
if (epochData && epochData.epoch !== undefined) {
|
|
361
|
+
console.log(` ${C.bright}── Epoch Info ──────────────────────────────────────────────${C.reset}`);
|
|
362
|
+
const ep = epochData.epoch;
|
|
363
|
+
const slotIdx = epochData.slot_index !== undefined ? epochData.slot_index : '?';
|
|
364
|
+
const slotsInEp = epochData.slots_in_epoch !== undefined ? epochData.slots_in_epoch : '?';
|
|
365
|
+
const progress = slotsInEp !== '?' && slotsInEp > 0
|
|
366
|
+
? ((slotIdx / slotsInEp) * 100).toFixed(1) + '%'
|
|
367
|
+
: '?';
|
|
368
|
+
console.log(` ${C.dim}Epoch:${C.reset} ${C.bright}${ep}${C.reset} ${C.dim}Slot in epoch:${C.reset} ${C.bright}${slotIdx} / ${slotsInEp}${C.reset} ${C.dim}(${progress})${C.reset}`);
|
|
369
|
+
if (epochData.absolute_slot !== undefined) {
|
|
370
|
+
console.log(` ${C.dim}Absolute slot:${C.reset} ${C.bright}${formatNumber(epochData.absolute_slot)}${C.reset}`);
|
|
371
|
+
}
|
|
372
|
+
console.log();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Snapshot info
|
|
376
|
+
if (snapshotData && !snapshotData.error) {
|
|
377
|
+
console.log(` ${C.bright}── Snapshot Info ───────────────────────────────────────────${C.reset}`);
|
|
378
|
+
const fullSnap = snapshotData.full_snapshot_slot ?? snapshotData.snapshot_slot ?? null;
|
|
379
|
+
const incSnap = snapshotData.incremental_snapshot_slot ?? null;
|
|
380
|
+
|
|
381
|
+
if (fullSnap !== null) {
|
|
382
|
+
const age = networkSlot !== null ? formatNumber(networkSlot - fullSnap) : null;
|
|
383
|
+
console.log(` ${C.dim}Full snapshot slot:${C.reset} ${C.green}${formatNumber(fullSnap)}${C.reset}`);
|
|
384
|
+
if (age !== null) {
|
|
385
|
+
console.log(` ${C.dim} (~${age} slots ago)${C.reset}`);
|
|
386
|
+
}
|
|
387
|
+
if (nodeSlot !== null && networkSlot !== null) {
|
|
388
|
+
const snapAge = networkSlot - fullSnap;
|
|
389
|
+
const freshness = snapAge < 100 ? `${C.green}fresh${C.reset}` : snapAge < 1000 ? `${C.yellow}aging${C.reset}` : `${C.red}old${C.reset}`;
|
|
390
|
+
console.log(` ${C.dim} Snapshot freshness:${C.reset} ${freshness}`);
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
console.log(` ${C.dim}No full snapshot available.${C.reset}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (incSnap !== null) {
|
|
397
|
+
console.log(` ${C.dim}Incremental snapshot slot:${C.reset} ${C.cyan}${formatNumber(incSnap)}${C.reset}`);
|
|
398
|
+
}
|
|
399
|
+
console.log();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Version info
|
|
403
|
+
if (versionData && !versionData.error) {
|
|
404
|
+
const ver = versionData.version || versionData.solana_core || versionData['software-version'];
|
|
405
|
+
if (ver) {
|
|
406
|
+
console.log(` ${C.bright}── Node Version ───────────────────────────────────────────${C.reset}`);
|
|
407
|
+
console.log(` ${C.dim}Version:${C.reset} ${C.green}${ver}${C.reset}`);
|
|
408
|
+
console.log();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Tips
|
|
413
|
+
if (status.diff === 0 || status.diff === undefined) {
|
|
414
|
+
console.log(` ${C.green}✓ Node is fully synced with the network.${C.reset}`);
|
|
415
|
+
} else if (status.diff > 0) {
|
|
416
|
+
console.log(` ${C.yellow}⏳ Node is catching up — this is normal on first start or after a restart.${C.reset}`);
|
|
417
|
+
console.log(` ${C.dim} For faster sync, try downloading a recent snapshot from a peer.${C.reset}`);
|
|
418
|
+
console.log(` ${C.dim} Check: aether-cli network --peers${C.reset}`);
|
|
419
|
+
}
|
|
420
|
+
console.log();
|
|
421
|
+
console.log(` ${C.dim}Tip: --watch for live view | --json for scripting${C.reset}`);
|
|
422
|
+
console.log();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Watch mode
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
async function watchMode(rpc) {
|
|
430
|
+
const readline = require('readline');
|
|
431
|
+
let running = true;
|
|
432
|
+
|
|
433
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
434
|
+
const drainStdin = () => {
|
|
435
|
+
rl.close();
|
|
436
|
+
running = false;
|
|
437
|
+
};
|
|
438
|
+
process.stdin.on('data', drainStdin);
|
|
439
|
+
process.stdin.resume();
|
|
440
|
+
|
|
441
|
+
console.log(` ${C.cyan}Live sync monitoring started.${C.reset} ${C.dim}Press Ctrl+C to stop.${C.reset}\n`);
|
|
442
|
+
|
|
443
|
+
while (running) {
|
|
444
|
+
// Move cursor up and clear lines for clean refresh
|
|
445
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const [nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData] =
|
|
449
|
+
await Promise.all([
|
|
450
|
+
getSlot(rpc),
|
|
451
|
+
getSlot(rpc), // network slot uses same endpoint
|
|
452
|
+
getBlockHeight(rpc),
|
|
453
|
+
getEpoch(rpc),
|
|
454
|
+
getSnapshot(rpc),
|
|
455
|
+
getVersion(rpc),
|
|
456
|
+
getHealth(rpc),
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
renderSync({ nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData, asJson: false }, rpc);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.log(` ${C.red}✗ Error fetching data:${C.reset} ${err.message}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (!running) break;
|
|
465
|
+
await new Promise((res) => setTimeout(res, REFRESH_INTERVAL_MS));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
process.stdin.pause();
|
|
469
|
+
process.stdin.off('data', drainStdin);
|
|
470
|
+
console.log(`\n ${C.dim}Stopped.${C.reset}\n`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Main
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
async function snapshotCommand() {
|
|
478
|
+
const opts = parseArgs();
|
|
479
|
+
const rpc = opts.rpc;
|
|
480
|
+
|
|
481
|
+
if (opts.watch) {
|
|
482
|
+
await watchMode(rpc);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const [nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData] =
|
|
487
|
+
await Promise.all([
|
|
488
|
+
getSlot(rpc),
|
|
489
|
+
getSlot(rpc), // same endpoint for network slot
|
|
490
|
+
getBlockHeight(rpc),
|
|
491
|
+
getEpoch(rpc),
|
|
492
|
+
getSnapshot(rpc),
|
|
493
|
+
getVersion(rpc),
|
|
494
|
+
getHealth(rpc),
|
|
495
|
+
]);
|
|
496
|
+
|
|
497
|
+
renderSync({ nodeSlot, networkSlot, blockHeight, epochData, snapshotData, versionData, healthData, asJson: opts.asJson }, rpc);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
module.exports = { snapshotCommand };
|
|
501
|
+
|
|
502
|
+
if (require.main === module) {
|
|
503
|
+
snapshotCommand().catch((err) => {
|
|
504
|
+
console.error(`\n${C.red}✗ Snapshot command failed:${C.reset} ${err.message}`);
|
|
505
|
+
console.error(` ${C.dim}Check that your validator is running and RPC is accessible.${C.reset}`);
|
|
506
|
+
console.error(` ${C.dim}Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
});
|
|
509
|
+
}
|