@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
package/commands/slot.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli slot
|
|
4
|
+
*
|
|
5
|
+
* Get current slot number from the Aether blockchain.
|
|
6
|
+
* Uses @jellylegsai/aether-sdk for REAL HTTP RPC calls.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* aether slot Show current slot
|
|
10
|
+
* aether slot --json JSON output for scripting
|
|
11
|
+
* aether slot --rpc <url> Custom RPC endpoint
|
|
12
|
+
*
|
|
13
|
+
* RPC Endpoint: GET /v1/slot
|
|
14
|
+
* SDK Function: sdk.getSlot()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
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
|
+
cyan: '\x1b[36m',
|
|
28
|
+
magenta: '\x1b[35m',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const CLI_VERSION = '1.0.0';
|
|
32
|
+
|
|
33
|
+
// Import SDK — makes REAL HTTP RPC calls to the blockchain
|
|
34
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
35
|
+
const aether = require(sdkPath);
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
function getDefaultRpc() {
|
|
42
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createClient(rpc) {
|
|
46
|
+
return new aether.AetherClient({ rpcUrl: rpc });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Argument parsing
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function parseArgs() {
|
|
54
|
+
const args = process.argv.slice(2);
|
|
55
|
+
return {
|
|
56
|
+
rpc: getDefaultRpc(),
|
|
57
|
+
asJson: args.includes('--json') || args.includes('-j'),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Slot query - REAL RPC call via SDK
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
async function fetchSlot(rpc) {
|
|
66
|
+
const client = createClient(rpc);
|
|
67
|
+
|
|
68
|
+
// Real RPC call: GET /v1/slot
|
|
69
|
+
// SDK function: client.getSlot()
|
|
70
|
+
const slot = await client.getSlot();
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
slot,
|
|
74
|
+
rpc,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Output formatters
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function printSlot(data) {
|
|
84
|
+
const { slot, rpc } = data;
|
|
85
|
+
|
|
86
|
+
console.log(`\n${C.bright}${C.cyan}── Aether Current Slot ──────────────────────────────────${C.reset}\n`);
|
|
87
|
+
console.log(` ${C.bright}Slot:${C.reset} ${C.green}${slot.toLocaleString()}${C.reset}`);
|
|
88
|
+
console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
|
|
89
|
+
console.log(` ${C.dim}Time:${C.reset} ${data.timestamp}${C.reset}\n`);
|
|
90
|
+
|
|
91
|
+
// Context info
|
|
92
|
+
console.log(` ${C.dim}RPC Endpoint: GET /v1/slot${C.reset}`);
|
|
93
|
+
console.log(` ${C.dim}SDK Function: sdk.getSlot()${C.reset}\n`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printJson(data) {
|
|
97
|
+
console.log(JSON.stringify({
|
|
98
|
+
slot: data.slot,
|
|
99
|
+
rpc: data.rpc,
|
|
100
|
+
timestamp: data.timestamp,
|
|
101
|
+
cli_version: CLI_VERSION,
|
|
102
|
+
sdk: '@jellylegsai/aether-sdk',
|
|
103
|
+
rpc_endpoint: 'GET /v1/slot',
|
|
104
|
+
}, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Main command
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
async function slotCommand() {
|
|
112
|
+
const options = parseArgs();
|
|
113
|
+
const { rpc, asJson } = options;
|
|
114
|
+
|
|
115
|
+
if (!asJson) {
|
|
116
|
+
console.log(`${C.dim}Fetching current slot from ${C.cyan}${rpc}${C.dim}...${C.reset}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Real blockchain RPC call via SDK
|
|
121
|
+
const data = await fetchSlot(rpc);
|
|
122
|
+
|
|
123
|
+
if (asJson) {
|
|
124
|
+
printJson(data);
|
|
125
|
+
} else {
|
|
126
|
+
printSlot(data);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (asJson) {
|
|
130
|
+
console.log(JSON.stringify({
|
|
131
|
+
error: err.message,
|
|
132
|
+
rpc,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
}));
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`\n${C.red}✗ Failed to fetch slot: ${err.message}${C.reset}`);
|
|
137
|
+
console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
|
|
138
|
+
console.log(` ${C.dim}Make sure the Aether node is running at ${rpc}${C.reset}\n`);
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Exports
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
module.exports = { slotCommand };
|
|
149
|
+
|
|
150
|
+
if (require.main === module) {
|
|
151
|
+
slotCommand().catch(err => {
|
|
152
|
+
console.error(`${C.red}Slot command failed:${C.reset} ${err.message}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli snapshot - Aether Node Sync & Snapshot Status
|
|
4
|
+
*
|
|
5
|
+
* Shows how far your node has synced vs the network, snapshot availability,
|
|
6
|
+
* and whether your node is catching up or is fully current.
|
|
7
|
+
*
|
|
8
|
+
* FULLY WIRED TO SDK - Uses @jellylegsai/aether-sdk for all blockchain calls.
|
|
9
|
+
* No manual HTTP - all calls go through AetherClient with real RPC.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* aether-cli snapshot # Interactive sync status view
|
|
13
|
+
* aether-cli snapshot --json # JSON output for scripting
|
|
14
|
+
* aether-cli snapshot --rpc <url> # Query a specific RPC endpoint
|
|
15
|
+
* aether-cli snapshot --watch # Refresh every 5 seconds
|
|
16
|
+
*
|
|
17
|
+
* SDK Methods Used:
|
|
18
|
+
* - client.getSlot() → GET /v1/slot
|
|
19
|
+
* - client.getBlockHeight() → GET /v1/blockheight
|
|
20
|
+
* - client.getEpochInfo() → GET /v1/epoch
|
|
21
|
+
* - client.getHealth() → GET /v1/health
|
|
22
|
+
* - client.getVersion() → GET /v1/version
|
|
23
|
+
* - client.getSupply() → GET /v1/supply (for additional context)
|
|
24
|
+
*
|
|
25
|
+
* @see docs/MINING_VALIDATOR_TOOLS.md for spec
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
// Import SDK for ALL blockchain RPC calls
|
|
31
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
32
|
+
const aether = require(sdkPath);
|
|
33
|
+
|
|
34
|
+
// ANSI colours
|
|
35
|
+
const C = {
|
|
36
|
+
reset: '\x1b[0m',
|
|
37
|
+
bright: '\x1b[1m',
|
|
38
|
+
dim: '\x1b[2m',
|
|
39
|
+
red: '\x1b[31m',
|
|
40
|
+
green: '\x1b[32m',
|
|
41
|
+
yellow: '\x1b[33m',
|
|
42
|
+
blue: '\x1b[34m',
|
|
43
|
+
cyan: '\x1b[36m',
|
|
44
|
+
magenta: '\x1b[35m',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CLI_VERSION = '1.1.0';
|
|
48
|
+
const REFRESH_INTERVAL_MS = 5000;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// SDK Client Setup
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function getDefaultRpc() {
|
|
55
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createClient(rpcUrl) {
|
|
59
|
+
return new aether.AetherClient({ rpcUrl });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Argument parsing
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function parseArgs() {
|
|
67
|
+
const args = process.argv.slice(2);
|
|
68
|
+
const options = {
|
|
69
|
+
rpc: getDefaultRpc(),
|
|
70
|
+
asJson: false,
|
|
71
|
+
watch: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < args.length; i++) {
|
|
75
|
+
if (args[i] === '--rpc' || args[i] === '-r') {
|
|
76
|
+
options.rpc = args[++i];
|
|
77
|
+
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
78
|
+
options.asJson = true;
|
|
79
|
+
} else if (args[i] === '--watch' || args[i] === '-w') {
|
|
80
|
+
options.watch = true;
|
|
81
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
82
|
+
showHelp();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return options;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function showHelp() {
|
|
91
|
+
console.log(`
|
|
92
|
+
${C.bright}${C.cyan}aether-cli snapshot${C.reset} - Aether Node Sync & Snapshot Status (SDK-Wired)
|
|
93
|
+
|
|
94
|
+
${C.bright}Usage:${C.reset}
|
|
95
|
+
aether-cli snapshot [options]
|
|
96
|
+
|
|
97
|
+
${C.bright}Options:${C.reset}
|
|
98
|
+
-r, --rpc <url> RPC endpoint (default: ${getDefaultRpc()} or $AETHER_RPC)
|
|
99
|
+
-j, --json Output raw JSON (good for scripting)
|
|
100
|
+
-w, --watch Refresh every 5 seconds (live view)
|
|
101
|
+
-h, --help Show this help message
|
|
102
|
+
|
|
103
|
+
${C.bright}SDK Methods Used:${C.reset}
|
|
104
|
+
client.getSlot() → GET /v1/slot
|
|
105
|
+
client.getBlockHeight() → GET /v1/blockheight
|
|
106
|
+
client.getEpochInfo() → GET /v1/epoch
|
|
107
|
+
client.getHealth() → GET /v1/health
|
|
108
|
+
client.getVersion() → GET /v1/version
|
|
109
|
+
|
|
110
|
+
${C.bright}Examples:${C.reset}
|
|
111
|
+
aether-cli snapshot # Interactive sync status
|
|
112
|
+
aether-cli snapshot --json # JSON output
|
|
113
|
+
aether-cli snapshot --watch # Live refreshing view
|
|
114
|
+
aether-cli snapshot --rpc https://api.testnet.aether.network
|
|
115
|
+
`.trim());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Data fetchers - ALL SDK WIRED (REAL RPC CALLS)
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/** SDK call: GET /v1/slot - current network slot */
|
|
123
|
+
async function getSlot(rpc) {
|
|
124
|
+
const client = createClient(rpc);
|
|
125
|
+
try {
|
|
126
|
+
return await client.getSlot();
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** SDK call: GET /v1/blockheight - node's synced block height */
|
|
133
|
+
async function getBlockHeight(rpc) {
|
|
134
|
+
const client = createClient(rpc);
|
|
135
|
+
try {
|
|
136
|
+
return await client.getBlockHeight();
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** SDK call: GET /v1/epoch - current epoch info */
|
|
143
|
+
async function getEpoch(rpc) {
|
|
144
|
+
const client = createClient(rpc);
|
|
145
|
+
try {
|
|
146
|
+
return await client.getEpochInfo();
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** SDK call: GET /v1/health - node health */
|
|
153
|
+
async function getHealth(rpc) {
|
|
154
|
+
const client = createClient(rpc);
|
|
155
|
+
try {
|
|
156
|
+
const health = await client.getHealth();
|
|
157
|
+
return { ok: health === 'ok' || health === 'healthy', status: health };
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** SDK call: GET /v1/version - node version info */
|
|
164
|
+
async function getVersion(rpc) {
|
|
165
|
+
const client = createClient(rpc);
|
|
166
|
+
try {
|
|
167
|
+
return await client.getVersion();
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** SDK call: GET /v1/supply - token supply for context */
|
|
174
|
+
async function getSupply(rpc) {
|
|
175
|
+
const client = createClient(rpc);
|
|
176
|
+
try {
|
|
177
|
+
return await client.getSupply();
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Formatting helpers
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
function formatNumber(n) {
|
|
188
|
+
if (n === null || n === undefined) return 'N/A';
|
|
189
|
+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function syncStatus(nodeSlot, networkSlot) {
|
|
193
|
+
if (nodeSlot === null || networkSlot === null) {
|
|
194
|
+
return { label: `${C.yellow}UNKNOWN${C.reset}`, icon: '?', color: C.yellow };
|
|
195
|
+
}
|
|
196
|
+
const diff = networkSlot - nodeSlot;
|
|
197
|
+
const pct = networkSlot > 0 ? ((nodeSlot / networkSlot) * 100).toFixed(1) : '0.0';
|
|
198
|
+
|
|
199
|
+
if (diff <= 0) {
|
|
200
|
+
return { label: `${C.green}SYNCED${C.reset}`, icon: '✓', color: C.green, diff };
|
|
201
|
+
}
|
|
202
|
+
if (diff <= 5) {
|
|
203
|
+
return { label: `${C.green}CATCHING UP${C.reset}`, icon: '◐', color: C.green, diff };
|
|
204
|
+
}
|
|
205
|
+
if (diff <= 50) {
|
|
206
|
+
return { label: `${C.yellow}BEHIND${C.reset}`, icon: '◑', color: C.yellow, diff };
|
|
207
|
+
}
|
|
208
|
+
return { label: `${C.red}FAR BEHIND${C.reset}`, icon: '✗', color: C.red, diff };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function progressBar(nodeSlot, networkSlot, width = 30) {
|
|
212
|
+
if (nodeSlot === null || networkSlot === null || networkSlot === 0) {
|
|
213
|
+
return `${C.dim}[${'─'.repeat(width)}]${C.reset} N/A`;
|
|
214
|
+
}
|
|
215
|
+
const ratio = Math.min(nodeSlot / networkSlot, 1);
|
|
216
|
+
const filled = Math.round(ratio * width);
|
|
217
|
+
const empty = width - filled;
|
|
218
|
+
return (
|
|
219
|
+
`${C.green}[${'█'.repeat(filled)}${C.dim}${'─'.repeat(empty)}${C.reset}]` +
|
|
220
|
+
` ${(ratio * 100).toFixed(1)}%`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function catchupEstimate(nodeSlot, networkSlot) {
|
|
225
|
+
if (nodeSlot === null || networkSlot === null || networkSlot <= nodeSlot) return null;
|
|
226
|
+
const diff = networkSlot - nodeSlot;
|
|
227
|
+
// Rough estimate: ~2 slots/second typical throughput
|
|
228
|
+
const seconds = Math.floor(diff / 2);
|
|
229
|
+
if (seconds < 60) return `~${seconds}s`;
|
|
230
|
+
if (seconds < 3600) return `~${Math.floor(seconds / 60)}m`;
|
|
231
|
+
return `~${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Renderers
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
function renderSync(data, rpc) {
|
|
239
|
+
const { nodeSlot, networkSlot, blockHeight, epochData, versionData, healthData, supplyData, asJson } = data;
|
|
240
|
+
|
|
241
|
+
if (asJson) {
|
|
242
|
+
const status = syncStatus(nodeSlot, networkSlot);
|
|
243
|
+
console.log(JSON.stringify({
|
|
244
|
+
rpc,
|
|
245
|
+
fetchedAt: new Date().toISOString(),
|
|
246
|
+
node: {
|
|
247
|
+
slot: nodeSlot,
|
|
248
|
+
blockHeight,
|
|
249
|
+
},
|
|
250
|
+
network: {
|
|
251
|
+
slot: networkSlot,
|
|
252
|
+
},
|
|
253
|
+
sync: {
|
|
254
|
+
status: status.label.replace(/\x1b\[\d+m/g, ''),
|
|
255
|
+
slotsBehind: networkSlot !== null && nodeSlot !== null ? Math.max(0, networkSlot - nodeSlot) : null,
|
|
256
|
+
percentSynced: nodeSlot !== null && networkSlot !== null && networkSlot > 0
|
|
257
|
+
? parseFloat((nodeSlot / networkSlot * 100).toFixed(2))
|
|
258
|
+
: null,
|
|
259
|
+
},
|
|
260
|
+
epoch: epochData ? {
|
|
261
|
+
epoch: epochData.epoch,
|
|
262
|
+
slotIndex: epochData.slotIndex,
|
|
263
|
+
slotsInEpoch: epochData.slotsInEpoch,
|
|
264
|
+
absoluteSlot: epochData.absoluteSlot,
|
|
265
|
+
} : null,
|
|
266
|
+
version: versionData?.aetherCore ?? versionData?.version ?? null,
|
|
267
|
+
health: healthData,
|
|
268
|
+
supply: supplyData ? {
|
|
269
|
+
total: supplyData.total,
|
|
270
|
+
circulating: supplyData.circulating,
|
|
271
|
+
} : null,
|
|
272
|
+
sdk_version: CLI_VERSION,
|
|
273
|
+
}, null, 2));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const status = syncStatus(nodeSlot, networkSlot);
|
|
278
|
+
const now = new Date().toLocaleTimeString();
|
|
279
|
+
const catchup = catchupEstimate(nodeSlot, networkSlot);
|
|
280
|
+
|
|
281
|
+
console.log();
|
|
282
|
+
console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
283
|
+
console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER NODE SNAPSHOT / SYNC STATUS${C.reset}${C.cyan} ║${C.reset}`);
|
|
284
|
+
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
285
|
+
console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
|
|
286
|
+
console.log(` ${C.dim}SDK:${C.reset} v${CLI_VERSION} │ ${C.dim}Updated:${C.reset} ${now}`);
|
|
287
|
+
console.log();
|
|
288
|
+
|
|
289
|
+
// Health indicator via SDK
|
|
290
|
+
if (healthData) {
|
|
291
|
+
const ok = healthData.ok ?? true;
|
|
292
|
+
console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
293
|
+
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}`);
|
|
294
|
+
console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
295
|
+
console.log();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Sync status — large prominent display
|
|
299
|
+
console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
300
|
+
console.log(` ${C.bright}│${C.reset} Sync Status: ${status.color}${C.bright}${status.label}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
301
|
+
if (status.diff !== undefined && status.diff > 0) {
|
|
302
|
+
console.log(` ${C.bright}│${C.reset} Slots behind: ${C.yellow}${formatNumber(status.diff)}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
303
|
+
if (catchup) {
|
|
304
|
+
console.log(` ${C.bright}│${C.reset} Est. time to sync: ${C.cyan}${catchup}${C.reset}`.padEnd(65) + `${C.bright}│${C.reset}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
308
|
+
console.log();
|
|
309
|
+
|
|
310
|
+
// Progress bar
|
|
311
|
+
console.log(` ${C.bright}── Slot Progress ───────────────────────────────────────────${C.reset}`);
|
|
312
|
+
console.log(` ${progressBar(nodeSlot, networkSlot)}`);
|
|
313
|
+
console.log();
|
|
314
|
+
|
|
315
|
+
// Slot details
|
|
316
|
+
console.log(` ${C.bright}┌────────────────────┬────────────────────┐${C.reset}`);
|
|
317
|
+
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}`);
|
|
318
|
+
console.log(` ${C.bright}├────────────────────┼────────────────────┤${C.reset}`);
|
|
319
|
+
const nodeStr = nodeSlot !== null ? formatNumber(nodeSlot) : 'N/A';
|
|
320
|
+
const netStr = networkSlot !== null ? formatNumber(networkSlot) : 'N/A';
|
|
321
|
+
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}`);
|
|
322
|
+
const bhStr = blockHeight !== null ? formatNumber(blockHeight) : 'N/A';
|
|
323
|
+
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}`);
|
|
324
|
+
console.log(` ${C.bright}└────────────────────┴────────────────────┘${C.reset}`);
|
|
325
|
+
console.log();
|
|
326
|
+
|
|
327
|
+
// Epoch info via SDK
|
|
328
|
+
if (epochData && epochData.epoch !== undefined) {
|
|
329
|
+
console.log(` ${C.bright}── Epoch Info ──────────────────────────────────────────────${C.reset}`);
|
|
330
|
+
const ep = epochData.epoch;
|
|
331
|
+
const slotIdx = epochData.slotIndex !== undefined ? epochData.slotIndex : '?';
|
|
332
|
+
const slotsInEp = epochData.slotsInEpoch !== undefined ? epochData.slotsInEpoch : '?';
|
|
333
|
+
const progress = slotsInEp !== '?' && slotsInEp > 0
|
|
334
|
+
? ((slotIdx / slotsInEp) * 100).toFixed(1) + '%'
|
|
335
|
+
: '?';
|
|
336
|
+
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}`);
|
|
337
|
+
if (epochData.absoluteSlot !== undefined) {
|
|
338
|
+
console.log(` ${C.dim}Absolute slot:${C.reset} ${C.bright}${formatNumber(epochData.absoluteSlot)}${C.reset}`);
|
|
339
|
+
}
|
|
340
|
+
console.log(` ${C.dim}SDK: getEpochInfo()${C.reset}`);
|
|
341
|
+
console.log();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Supply info via SDK
|
|
345
|
+
if (supplyData) {
|
|
346
|
+
console.log(` ${C.bright}── Token Supply ────────────────────────────────────────────${C.reset}`);
|
|
347
|
+
const totalAETH = supplyData.total ? (Number(supplyData.total) / 1e9).toFixed(2) : 'N/A';
|
|
348
|
+
const circAETH = supplyData.circulating ? (Number(supplyData.circulating) / 1e9).toFixed(2) : 'N/A';
|
|
349
|
+
console.log(` ${C.dim}Total Supply:${C.reset} ${C.green}${totalAETH} AETH${C.reset}`);
|
|
350
|
+
console.log(` ${C.dim}Circulating:${C.reset} ${C.cyan}${circAETH} AETH${C.reset}`);
|
|
351
|
+
console.log(` ${C.dim}SDK: getSupply()${C.reset}`);
|
|
352
|
+
console.log();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Version info via SDK
|
|
356
|
+
if (versionData) {
|
|
357
|
+
const ver = versionData.aetherCore || versionData.version || versionData.solana_core;
|
|
358
|
+
if (ver) {
|
|
359
|
+
console.log(` ${C.bright}── Node Version ───────────────────────────────────────────${C.reset}`);
|
|
360
|
+
console.log(` ${C.dim}Version:${C.reset} ${C.green}${ver}${C.reset}`);
|
|
361
|
+
console.log(` ${C.dim}SDK: getVersion()${C.reset}`);
|
|
362
|
+
console.log();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Tips
|
|
367
|
+
if (status.diff === 0 || status.diff === undefined) {
|
|
368
|
+
console.log(` ${C.green}✓ Node is fully synced with the network.${C.reset}`);
|
|
369
|
+
} else if (status.diff > 0) {
|
|
370
|
+
console.log(` ${C.yellow}⏳ Node is catching up — this is normal on first start or after a restart.${C.reset}`);
|
|
371
|
+
console.log(` ${C.dim} For faster sync, try downloading a recent snapshot from a peer.${C.reset}`);
|
|
372
|
+
console.log(` ${C.dim} Check: aether network --peers${C.reset}`);
|
|
373
|
+
}
|
|
374
|
+
console.log();
|
|
375
|
+
console.log(` ${C.dim}Tip: --watch for live view | --json for scripting${C.reset}`);
|
|
376
|
+
console.log(` ${C.dim}SDK Methods: getSlot(), getBlockHeight(), getEpochInfo(), getHealth(), getVersion()${C.reset}`);
|
|
377
|
+
console.log();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Watch mode
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
async function watchMode(rpc) {
|
|
385
|
+
const readline = require('readline');
|
|
386
|
+
let running = true;
|
|
387
|
+
|
|
388
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
389
|
+
const drainStdin = () => {
|
|
390
|
+
rl.close();
|
|
391
|
+
running = false;
|
|
392
|
+
};
|
|
393
|
+
process.stdin.on('data', drainStdin);
|
|
394
|
+
process.stdin.resume();
|
|
395
|
+
|
|
396
|
+
console.log(` ${C.cyan}Live sync monitoring started.${C.reset} ${C.dim}Press Ctrl+C to stop.${C.reset}\n`);
|
|
397
|
+
|
|
398
|
+
while (running) {
|
|
399
|
+
// Move cursor up and clear lines for clean refresh
|
|
400
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
// All SDK calls
|
|
404
|
+
const [nodeSlot, blockHeight, epochData, versionData, healthData, supplyData] =
|
|
405
|
+
await Promise.all([
|
|
406
|
+
getSlot(rpc),
|
|
407
|
+
getBlockHeight(rpc),
|
|
408
|
+
getEpoch(rpc),
|
|
409
|
+
getVersion(rpc),
|
|
410
|
+
getHealth(rpc),
|
|
411
|
+
getSupply(rpc),
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
// For network slot, we use the same RPC (in real scenario, could be different)
|
|
415
|
+
const networkSlot = nodeSlot;
|
|
416
|
+
|
|
417
|
+
renderSync({ nodeSlot, networkSlot, blockHeight, epochData, versionData, healthData, supplyData, asJson: false }, rpc);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.log(` ${C.red}✗ Error fetching data:${C.reset} ${err.message}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!running) break;
|
|
423
|
+
await new Promise((res) => setTimeout(res, REFRESH_INTERVAL_MS));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
process.stdin.pause();
|
|
427
|
+
process.stdin.off('data', drainStdin);
|
|
428
|
+
console.log(`\n ${C.dim}Stopped.${C.reset}\n`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Main
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
async function snapshotCommand() {
|
|
436
|
+
const opts = parseArgs();
|
|
437
|
+
const rpc = opts.rpc;
|
|
438
|
+
|
|
439
|
+
if (opts.watch) {
|
|
440
|
+
await watchMode(rpc);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// All SDK calls in parallel
|
|
445
|
+
const [nodeSlot, blockHeight, epochData, versionData, healthData, supplyData] =
|
|
446
|
+
await Promise.all([
|
|
447
|
+
getSlot(rpc),
|
|
448
|
+
getBlockHeight(rpc),
|
|
449
|
+
getEpoch(rpc),
|
|
450
|
+
getVersion(rpc),
|
|
451
|
+
getHealth(rpc),
|
|
452
|
+
getSupply(rpc),
|
|
453
|
+
]);
|
|
454
|
+
|
|
455
|
+
// For single-RPC mode, node and network are the same
|
|
456
|
+
const networkSlot = nodeSlot;
|
|
457
|
+
|
|
458
|
+
renderSync({ nodeSlot, networkSlot, blockHeight, epochData, versionData, healthData, supplyData, asJson: opts.asJson }, rpc);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = { snapshotCommand };
|
|
462
|
+
|
|
463
|
+
if (require.main === module) {
|
|
464
|
+
snapshotCommand().catch((err) => {
|
|
465
|
+
console.error(`\n${C.red}✗ Snapshot command failed:${C.reset} ${err.message}`);
|
|
466
|
+
console.error(` ${C.dim}Check that your validator is running and RPC is accessible.${C.reset}`);
|
|
467
|
+
console.error(` ${C.dim}Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
});
|
|
470
|
+
}
|