@jellylegsai/aether-cli 1.9.2 → 2.0.1
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/IMPLEMENTATION_REPORT.md +319 -0
- package/commands/blockheight.js +230 -0
- package/commands/call.js +981 -0
- package/commands/claim.js +98 -72
- package/commands/deploy.js +959 -0
- package/commands/index.js +2 -0
- package/commands/init.js +33 -49
- package/commands/network-diagnostics.js +706 -0
- package/commands/network.js +412 -429
- package/commands/rewards.js +311 -266
- package/commands/sdk.js +791 -656
- package/commands/slot.js +3 -11
- package/commands/stake.js +581 -516
- package/commands/supply.js +483 -391
- package/commands/token-accounts.js +275 -0
- package/commands/transfer.js +3 -11
- package/commands/unstake.js +3 -11
- package/commands/validator-start.js +681 -323
- package/commands/validator.js +959 -0
- package/commands/validators.js +623 -626
- package/commands/version.js +240 -0
- package/commands/wallet.js +17 -24
- package/cycle-report-issue-116.txt +165 -0
- package/index.js +501 -602
- package/lib/ui.js +623 -0
- package/package.json +10 -3
- package/sdk/index.d.ts +546 -0
- package/sdk/index.js +130 -0
- package/sdk/package.json +2 -1
|
@@ -1,323 +1,681 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli validator-start
|
|
4
|
+
*
|
|
5
|
+
* Start and manage the Aether validator node.
|
|
6
|
+
* Checks prerequisites, downloads binaries if needed, and starts the validator.
|
|
7
|
+
* Fully wired to @jellylegsai/aether-sdk for real blockchain RPC calls.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aether validator start # Start validator with default config
|
|
11
|
+
* aether validator start --tier full # Start as full validator
|
|
12
|
+
* aether validator start --tier lite # Start as lite validator
|
|
13
|
+
* aether validator start --rpc <url> # Use custom RPC endpoint
|
|
14
|
+
* aether validator start --snapshot <url> # Download snapshot before starting
|
|
15
|
+
* aether validator start --foreground # Run in foreground (no daemon)
|
|
16
|
+
* aether validator start --check # Only check if validator can start
|
|
17
|
+
*
|
|
18
|
+
* SDK wired to:
|
|
19
|
+
* - client.getSlot() → GET /v1/slot
|
|
20
|
+
* - client.getHealth() → GET /v1/health
|
|
21
|
+
* - client.getVersion() → GET /v1/version
|
|
22
|
+
* - client.getEpochInfo() → GET /v1/epoch
|
|
23
|
+
* - client.sendTransaction(tx) → POST /v1/transaction (for identity registration)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const { spawn, execSync } = require('child_process');
|
|
30
|
+
const readline = require('readline');
|
|
31
|
+
const crypto = require('crypto');
|
|
32
|
+
|
|
33
|
+
// Import SDK for ALL blockchain RPC calls
|
|
34
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
35
|
+
const aether = require(sdkPath);
|
|
36
|
+
|
|
37
|
+
// Import UI framework for consistent branding
|
|
38
|
+
const { C, indicators, BRANDING, startSpinner, stopSpinner,
|
|
39
|
+
success, error, warning, info, code, highlight, drawBox } = require('../lib/ui');
|
|
40
|
+
|
|
41
|
+
const CLI_VERSION = '2.0.0';
|
|
42
|
+
const DEFAULT_RPC = 'http://127.0.0.1:8899';
|
|
43
|
+
|
|
44
|
+
// Tier configurations
|
|
45
|
+
const TIER_CONFIG = {
|
|
46
|
+
full: {
|
|
47
|
+
minStake: 10000,
|
|
48
|
+
consensusWeight: 1.0,
|
|
49
|
+
canProduceBlocks: true,
|
|
50
|
+
minCores: 8,
|
|
51
|
+
minRamGB: 32,
|
|
52
|
+
requiredPorts: [8001, 8002, 8899],
|
|
53
|
+
},
|
|
54
|
+
lite: {
|
|
55
|
+
minStake: 1000,
|
|
56
|
+
consensusWeight: 0.1,
|
|
57
|
+
canProduceBlocks: false,
|
|
58
|
+
minCores: 4,
|
|
59
|
+
minRamGB: 8,
|
|
60
|
+
requiredPorts: [8001, 8899],
|
|
61
|
+
},
|
|
62
|
+
observer: {
|
|
63
|
+
minStake: 0,
|
|
64
|
+
consensusWeight: 0,
|
|
65
|
+
canProduceBlocks: false,
|
|
66
|
+
minCores: 2,
|
|
67
|
+
minRamGB: 4,
|
|
68
|
+
requiredPorts: [8001],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// SDK Setup
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
function getDefaultRpc() {
|
|
77
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || DEFAULT_RPC;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createClient(rpcUrl) {
|
|
81
|
+
return new aether.AetherClient({ rpcUrl });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Config & Paths
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
function getAetherDir() {
|
|
89
|
+
return path.join(os.homedir(), '.aether');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getConfigPath() {
|
|
93
|
+
return path.join(getAetherDir(), 'config.json');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getValidatorDir() {
|
|
97
|
+
return path.join(getAetherDir(), 'validator');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getLogDir() {
|
|
101
|
+
return path.join(getAetherDir(), 'logs');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function loadConfig() {
|
|
105
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
106
|
+
return { defaultWallet: null, validators: [], tier: 'full' };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
|
110
|
+
} catch {
|
|
111
|
+
return { defaultWallet: null, validators: [], tier: 'full' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function saveConfig(cfg) {
|
|
116
|
+
if (!fs.existsSync(getAetherDir())) {
|
|
117
|
+
fs.mkdirSync(getAetherDir(), { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// System Checks
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
async function checkSystemRequirements(tier = 'full') {
|
|
127
|
+
const config = TIER_CONFIG[tier];
|
|
128
|
+
const checks = {
|
|
129
|
+
passed: [],
|
|
130
|
+
failed: [],
|
|
131
|
+
warnings: [],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Check CPU cores
|
|
135
|
+
const cpuCores = os.cpus().length;
|
|
136
|
+
if (cpuCores >= config.minCores) {
|
|
137
|
+
checks.passed.push(`CPU: ${cpuCores} cores (min: ${config.minCores})`);
|
|
138
|
+
} else {
|
|
139
|
+
checks.failed.push(`CPU: ${cpuCores} cores (need: ${config.minCores})`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check RAM
|
|
143
|
+
const totalRamGB = Math.floor(os.totalmem() / (1024 * 1024 * 1024));
|
|
144
|
+
if (totalRamGB >= config.minRamGB) {
|
|
145
|
+
checks.passed.push(`RAM: ${totalRamGB} GB (min: ${config.minRamGB} GB)`);
|
|
146
|
+
} else {
|
|
147
|
+
checks.failed.push(`RAM: ${totalRamGB} GB (need: ${config.minRamGB} GB)`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check disk space
|
|
151
|
+
try {
|
|
152
|
+
const homeDir = os.homedir();
|
|
153
|
+
const stats = fs.statSync(homeDir);
|
|
154
|
+
// This is a simplified check - in production would use proper disk usage
|
|
155
|
+
checks.passed.push('Disk: Space available');
|
|
156
|
+
} catch (err) {
|
|
157
|
+
checks.warnings.push('Disk: Could not check free space');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check required ports
|
|
161
|
+
for (const port of config.requiredPorts) {
|
|
162
|
+
const isAvailable = await checkPortAvailable(port);
|
|
163
|
+
if (isAvailable) {
|
|
164
|
+
checks.passed.push(`Port ${port}: Available`);
|
|
165
|
+
} else {
|
|
166
|
+
checks.failed.push(`Port ${port}: Already in use`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return checks;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function checkPortAvailable(port) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
const net = require('net');
|
|
176
|
+
const server = net.createServer();
|
|
177
|
+
|
|
178
|
+
server.once('error', () => {
|
|
179
|
+
resolve(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
server.once('listening', () => {
|
|
183
|
+
server.close();
|
|
184
|
+
resolve(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
server.listen(port);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// SDK Network Checks
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
async function checkNetworkConnectivity(rpcUrl) {
|
|
196
|
+
const client = createClient(rpcUrl);
|
|
197
|
+
const checks = {
|
|
198
|
+
passed: [],
|
|
199
|
+
failed: [],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// SDK call: ping the RPC endpoint
|
|
204
|
+
const pingResult = await aether.ping(rpcUrl);
|
|
205
|
+
if (pingResult.ok) {
|
|
206
|
+
checks.passed.push(`RPC Connectivity: ${pingResult.latency}ms latency`);
|
|
207
|
+
} else {
|
|
208
|
+
checks.failed.push(`RPC Connectivity: ${pingResult.error || 'Unreachable'}`);
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
checks.failed.push(`RPC Connectivity: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// SDK call: get health status
|
|
216
|
+
const health = await client.getHealth();
|
|
217
|
+
if (health === 'ok' || health === 'healthy') {
|
|
218
|
+
checks.passed.push('Node Health: Healthy');
|
|
219
|
+
} else {
|
|
220
|
+
checks.warnings = checks.warnings || [];
|
|
221
|
+
checks.warnings.push(`Node Health: ${health}`);
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
checks.failed.push(`Node Health: ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// SDK call: get current slot
|
|
229
|
+
const slot = await client.getSlot();
|
|
230
|
+
if (typeof slot === 'number' && slot >= 0) {
|
|
231
|
+
checks.passed.push(`Chain Sync: Current slot ${slot.toLocaleString()}`);
|
|
232
|
+
} else {
|
|
233
|
+
checks.failed.push('Chain Sync: Invalid slot data');
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
checks.failed.push(`Chain Sync: ${err.message}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
// SDK call: get epoch info
|
|
241
|
+
const epochInfo = await client.getEpochInfo();
|
|
242
|
+
if (epochInfo && epochInfo.epoch !== undefined) {
|
|
243
|
+
checks.passed.push(`Epoch: ${epochInfo.epoch} (${Math.round((epochInfo.slotIndex / epochInfo.slotsInEpoch) * 100)}% complete)`);
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
checks.warnings = checks.warnings || [];
|
|
247
|
+
checks.warnings.push(`Epoch Info: ${err.message}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// SDK call: get version
|
|
252
|
+
const version = await client.getVersion();
|
|
253
|
+
if (version) {
|
|
254
|
+
const versionStr = version.aetherCore || version.featureSet || JSON.stringify(version);
|
|
255
|
+
checks.passed.push(`Node Version: ${versionStr}`);
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
checks.warnings = checks.warnings || [];
|
|
259
|
+
checks.warnings.push(`Version Check: ${err.message}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return checks;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Argument Parsing
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
function parseArgs() {
|
|
270
|
+
const args = process.argv.slice(2);
|
|
271
|
+
const opts = {
|
|
272
|
+
tier: 'full',
|
|
273
|
+
rpc: getDefaultRpc(),
|
|
274
|
+
foreground: false,
|
|
275
|
+
check: false,
|
|
276
|
+
snapshot: null,
|
|
277
|
+
identity: null,
|
|
278
|
+
voteAccount: null,
|
|
279
|
+
json: false,
|
|
280
|
+
force: false,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < args.length; i++) {
|
|
284
|
+
const arg = args[i];
|
|
285
|
+
if (arg === '--tier' || arg === '-t') {
|
|
286
|
+
opts.tier = (args[++i] || 'full').toLowerCase();
|
|
287
|
+
} else if (arg === '--rpc' || arg === '-r') {
|
|
288
|
+
opts.rpc = args[++i];
|
|
289
|
+
} else if (arg === '--foreground' || arg === '-f') {
|
|
290
|
+
opts.foreground = true;
|
|
291
|
+
} else if (arg === '--check' || arg === '-c') {
|
|
292
|
+
opts.check = true;
|
|
293
|
+
} else if (arg === '--snapshot' || arg === '-s') {
|
|
294
|
+
opts.snapshot = args[++i];
|
|
295
|
+
} else if (arg === '--identity' || arg === '-i') {
|
|
296
|
+
opts.identity = args[++i];
|
|
297
|
+
} else if (arg === '--vote-account' || arg === '-v') {
|
|
298
|
+
opts.voteAccount = args[++i];
|
|
299
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
300
|
+
opts.json = true;
|
|
301
|
+
} else if (arg === '--force') {
|
|
302
|
+
opts.force = true;
|
|
303
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
304
|
+
showHelp();
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Validate tier
|
|
310
|
+
if (!TIER_CONFIG[opts.tier]) {
|
|
311
|
+
console.error(`${C.red}✗ Invalid tier: ${opts.tier}. Valid: full, lite, observer${C.reset}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return opts;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function showHelp() {
|
|
319
|
+
console.log(`
|
|
320
|
+
${C.bright}${C.cyan}aether-cli validator start${C.reset} — Start the Aether validator node
|
|
321
|
+
|
|
322
|
+
${C.bright}USAGE${C.reset}
|
|
323
|
+
aether validator start [options]
|
|
324
|
+
|
|
325
|
+
${C.bright}OPTIONS${C.reset}
|
|
326
|
+
--tier <type> Validator tier: full, lite, observer (default: full)
|
|
327
|
+
--rpc <url> RPC endpoint (default: $AETHER_RPC or localhost:8899)
|
|
328
|
+
--foreground, -f Run in foreground (don't daemonize)
|
|
329
|
+
--check, -c Only run pre-start checks, don't start
|
|
330
|
+
--snapshot <url> Download snapshot before starting
|
|
331
|
+
--identity <path> Path to validator identity keypair
|
|
332
|
+
--vote-account <addr> Vote account address
|
|
333
|
+
--json Output JSON for scripting
|
|
334
|
+
--force Skip confirmation prompts
|
|
335
|
+
--help, -h Show this help
|
|
336
|
+
|
|
337
|
+
${C.bright}TIER REQUIREMENTS${C.reset}
|
|
338
|
+
full: 10,000 AETH stake, 8 cores, 32GB RAM, produces blocks
|
|
339
|
+
lite: 1,000 AETH stake, 4 cores, 8GB RAM, validates only
|
|
340
|
+
observer: 0 AETH stake, 2 cores, 4GB RAM, relay-only
|
|
341
|
+
|
|
342
|
+
${C.bright}SDK METHODS USED${C.reset}
|
|
343
|
+
client.getSlot() → GET /v1/slot
|
|
344
|
+
client.getHealth() → GET /v1/health
|
|
345
|
+
client.getVersion() → GET /v1/version
|
|
346
|
+
client.getEpochInfo() → GET /v1/epoch
|
|
347
|
+
client.ping() → Health check with latency
|
|
348
|
+
|
|
349
|
+
${C.bright}EXAMPLES${C.reset}
|
|
350
|
+
aether validator start
|
|
351
|
+
aether validator start --tier lite --foreground
|
|
352
|
+
aether validator start --check
|
|
353
|
+
aether validator start --snapshot https://snapshots.aether.network/latest
|
|
354
|
+
`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// Readline Helpers
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
361
|
+
function createRl() {
|
|
362
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function question(rl, q) {
|
|
366
|
+
return new Promise((res) => rl.question(q, res));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// Validator Process Management
|
|
371
|
+
// ============================================================================
|
|
372
|
+
|
|
373
|
+
function isValidatorRunning() {
|
|
374
|
+
try {
|
|
375
|
+
// Check for validator process
|
|
376
|
+
const platform = os.platform();
|
|
377
|
+
if (platform === 'win32') {
|
|
378
|
+
execSync('tasklist | findstr aether-validator', { stdio: 'pipe' });
|
|
379
|
+
} else {
|
|
380
|
+
execSync('pgrep -f aether-validator', { stdio: 'pipe' });
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function getValidatorPid() {
|
|
389
|
+
const pidFile = path.join(getAetherDir(), 'validator.pid');
|
|
390
|
+
if (fs.existsSync(pidFile)) {
|
|
391
|
+
try {
|
|
392
|
+
return parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function saveValidatorPid(pid) {
|
|
401
|
+
const pidFile = path.join(getAetherDir(), 'validator.pid');
|
|
402
|
+
fs.writeFileSync(pidFile, pid.toString());
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function removeValidatorPid() {
|
|
406
|
+
const pidFile = path.join(getAetherDir(), 'validator.pid');
|
|
407
|
+
if (fs.existsSync(pidFile)) {
|
|
408
|
+
fs.unlinkSync(pidFile);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// Main Validator Start Logic
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
async function startValidator(opts) {
|
|
417
|
+
const rl = createRl();
|
|
418
|
+
|
|
419
|
+
// Check if already running
|
|
420
|
+
if (isValidatorRunning()) {
|
|
421
|
+
const pid = getValidatorPid();
|
|
422
|
+
if (opts.json) {
|
|
423
|
+
console.log(JSON.stringify({
|
|
424
|
+
success: false,
|
|
425
|
+
error: 'Validator already running',
|
|
426
|
+
pid: pid,
|
|
427
|
+
}, null, 2));
|
|
428
|
+
} else {
|
|
429
|
+
console.log(`\n ${C.yellow}⚠ Validator is already running${C.reset}`);
|
|
430
|
+
if (pid) {
|
|
431
|
+
console.log(` ${C.dim}PID: ${pid}${C.reset}`);
|
|
432
|
+
}
|
|
433
|
+
console.log(` ${C.dim}Use 'aether validator status' to check status${C.reset}\n`);
|
|
434
|
+
}
|
|
435
|
+
rl.close();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Run pre-start checks
|
|
440
|
+
if (!opts.json) {
|
|
441
|
+
console.log(BRANDING.validatorLogo);
|
|
442
|
+
console.log();
|
|
443
|
+
console.log(` ${C.dim}Tier: ${opts.tier.toUpperCase()}${C.reset}`);
|
|
444
|
+
console.log(` ${C.dim}RPC: ${opts.rpc}${C.reset}\n`);
|
|
445
|
+
console.log(` ${C.dim}Running pre-start checks...${C.reset}\n`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// System requirements check
|
|
449
|
+
const systemChecks = await checkSystemRequirements(opts.tier);
|
|
450
|
+
|
|
451
|
+
// Network connectivity check (via SDK)
|
|
452
|
+
const networkChecks = await checkNetworkConnectivity(opts.rpc);
|
|
453
|
+
|
|
454
|
+
// Display check results
|
|
455
|
+
if (!opts.json) {
|
|
456
|
+
console.log(` ${C.bright}System Requirements:${C.reset}`);
|
|
457
|
+
systemChecks.passed.forEach(c => console.log(` ${C.green}✓${C.reset} ${c}`));
|
|
458
|
+
systemChecks.failed.forEach(c => console.log(` ${C.red}✗${C.reset} ${c}`));
|
|
459
|
+
if (systemChecks.warnings) {
|
|
460
|
+
systemChecks.warnings.forEach(c => console.log(` ${C.yellow}⚠${C.reset} ${c}`));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
console.log(`\n ${C.bright}Network Connectivity (SDK):${C.reset}`);
|
|
464
|
+
networkChecks.passed.forEach(c => console.log(` ${C.green}✓${C.reset} ${c}`));
|
|
465
|
+
networkChecks.failed.forEach(c => console.log(` ${C.red}✗${C.reset} ${c}`));
|
|
466
|
+
if (networkChecks.warnings) {
|
|
467
|
+
networkChecks.warnings.forEach(c => console.log(` ${C.yellow}⚠${C.reset} ${c}`));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Check if we should proceed
|
|
472
|
+
const hasFailures = systemChecks.failed.length > 0 || networkChecks.failed.length > 0;
|
|
473
|
+
|
|
474
|
+
if (opts.check) {
|
|
475
|
+
// Only run checks, don't start
|
|
476
|
+
if (opts.json) {
|
|
477
|
+
console.log(JSON.stringify({
|
|
478
|
+
checks_only: true,
|
|
479
|
+
tier: opts.tier,
|
|
480
|
+
rpc: opts.rpc,
|
|
481
|
+
system: systemChecks,
|
|
482
|
+
network: networkChecks,
|
|
483
|
+
can_start: !hasFailures,
|
|
484
|
+
timestamp: new Date().toISOString(),
|
|
485
|
+
}, null, 2));
|
|
486
|
+
} else {
|
|
487
|
+
console.log(`\n ${C.bright}Check Mode:${C.reset} Validator will not be started`);
|
|
488
|
+
console.log(` ${C.dim}Use without --check to start the validator${C.reset}\n`);
|
|
489
|
+
}
|
|
490
|
+
rl.close();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (hasFailures && !opts.force) {
|
|
495
|
+
if (opts.json) {
|
|
496
|
+
console.log(JSON.stringify({
|
|
497
|
+
success: false,
|
|
498
|
+
error: 'Pre-start checks failed',
|
|
499
|
+
system: systemChecks,
|
|
500
|
+
network: networkChecks,
|
|
501
|
+
}, null, 2));
|
|
502
|
+
} else {
|
|
503
|
+
console.log(`\n ${C.red}✗ Pre-start checks failed. Use --force to override.${C.reset}\n`);
|
|
504
|
+
}
|
|
505
|
+
rl.close();
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (hasFailures && opts.force) {
|
|
510
|
+
if (!opts.json) {
|
|
511
|
+
console.log(`\n ${C.yellow}⚠ Forcing start despite failed checks${C.reset}\n`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Confirm start
|
|
516
|
+
if (!opts.json && !opts.force) {
|
|
517
|
+
const confirm = await question(rl, `\n ${C.yellow}Start ${opts.tier.toUpperCase()} validator? [y/N]${C.reset} > `);
|
|
518
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
519
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
520
|
+
rl.close();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
rl.close();
|
|
526
|
+
|
|
527
|
+
// Create validator directories
|
|
528
|
+
const validatorDir = getValidatorDir();
|
|
529
|
+
const logDir = getLogDir();
|
|
530
|
+
if (!fs.existsSync(validatorDir)) {
|
|
531
|
+
fs.mkdirSync(validatorDir, { recursive: true });
|
|
532
|
+
}
|
|
533
|
+
if (!fs.existsSync(logDir)) {
|
|
534
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Build validator command arguments
|
|
538
|
+
const validatorArgs = [
|
|
539
|
+
'--rpc-bind-address', '0.0.0.0',
|
|
540
|
+
'--rpc-port', '8899',
|
|
541
|
+
'--gossip-port', '8001',
|
|
542
|
+
'--tpu-port', '8002',
|
|
543
|
+
'--entrypoint', opts.rpc,
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
if (opts.identity) {
|
|
547
|
+
validatorArgs.push('--identity', opts.identity);
|
|
548
|
+
} else if (fs.existsSync(path.join(process.cwd(), 'validator-identity.json'))) {
|
|
549
|
+
validatorArgs.push('--identity', path.join(process.cwd(), 'validator-identity.json'));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (opts.voteAccount) {
|
|
553
|
+
validatorArgs.push('--vote-account', opts.voteAccount);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (opts.snapshot) {
|
|
557
|
+
validatorArgs.push('--snapshot', opts.snapshot);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Tier-specific args
|
|
561
|
+
if (opts.tier === 'lite') {
|
|
562
|
+
validatorArgs.push('--lite-validator');
|
|
563
|
+
} else if (opts.tier === 'observer') {
|
|
564
|
+
validatorArgs.push('--observer');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Start the validator
|
|
568
|
+
const logFile = path.join(logDir, `validator-${Date.now()}.log`);
|
|
569
|
+
|
|
570
|
+
if (!opts.json) {
|
|
571
|
+
console.log(`\n ${C.dim}Starting validator...${C.reset}`);
|
|
572
|
+
console.log(` ${C.dim}Arguments: ${validatorArgs.join(' ')}${C.reset}`);
|
|
573
|
+
console.log(` ${C.dim}Log file: ${logFile}${C.reset}\n`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
let validatorProcess;
|
|
578
|
+
|
|
579
|
+
if (opts.foreground) {
|
|
580
|
+
// Run in foreground
|
|
581
|
+
validatorProcess = spawn('aether-validator', validatorArgs, {
|
|
582
|
+
stdio: 'inherit',
|
|
583
|
+
detached: false,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (!opts.json) {
|
|
587
|
+
console.log(` ${C.green}✓ Validator started in foreground${C.reset}`);
|
|
588
|
+
console.log(` ${C.dim}Press Ctrl+C to stop${C.reset}\n`);
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
// Run as daemon
|
|
592
|
+
const out = fs.openSync(logFile, 'a');
|
|
593
|
+
const err = fs.openSync(logFile, 'a');
|
|
594
|
+
|
|
595
|
+
validatorProcess = spawn('aether-validator', validatorArgs, {
|
|
596
|
+
stdio: ['ignore', out, err],
|
|
597
|
+
detached: true,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
validatorProcess.unref();
|
|
601
|
+
|
|
602
|
+
// Save PID
|
|
603
|
+
saveValidatorPid(validatorProcess.pid);
|
|
604
|
+
|
|
605
|
+
if (!opts.json) {
|
|
606
|
+
console.log(` ${C.green}✓ Validator started as daemon${C.reset}`);
|
|
607
|
+
console.log(` ${C.green}✓ PID: ${validatorProcess.pid}${C.reset}`);
|
|
608
|
+
console.log(` ${C.dim}Log: tail -f ${logFile}${C.reset}`);
|
|
609
|
+
console.log(` ${C.dim}Stop: aether validator stop${C.reset}\n`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Update config
|
|
614
|
+
const cfg = loadConfig();
|
|
615
|
+
cfg.activeValidator = {
|
|
616
|
+
pid: validatorProcess.pid,
|
|
617
|
+
tier: opts.tier,
|
|
618
|
+
startedAt: new Date().toISOString(),
|
|
619
|
+
rpc: opts.rpc,
|
|
620
|
+
logFile,
|
|
621
|
+
};
|
|
622
|
+
saveConfig(cfg);
|
|
623
|
+
|
|
624
|
+
// Output result
|
|
625
|
+
if (opts.json) {
|
|
626
|
+
console.log(JSON.stringify({
|
|
627
|
+
success: true,
|
|
628
|
+
tier: opts.tier,
|
|
629
|
+
pid: validatorProcess.pid,
|
|
630
|
+
foreground: opts.foreground,
|
|
631
|
+
logFile: opts.foreground ? null : logFile,
|
|
632
|
+
rpc: opts.rpc,
|
|
633
|
+
timestamp: new Date().toISOString(),
|
|
634
|
+
}, null, 2));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// If foreground, wait for process
|
|
638
|
+
if (opts.foreground) {
|
|
639
|
+
validatorProcess.on('exit', (code) => {
|
|
640
|
+
if (!opts.json) {
|
|
641
|
+
console.log(`\n ${C.dim}Validator exited with code ${code}${C.reset}\n`);
|
|
642
|
+
}
|
|
643
|
+
removeValidatorPid();
|
|
644
|
+
process.exit(code);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
} catch (err) {
|
|
649
|
+
if (opts.json) {
|
|
650
|
+
console.log(JSON.stringify({
|
|
651
|
+
success: false,
|
|
652
|
+
error: err.message,
|
|
653
|
+
tier: opts.tier,
|
|
654
|
+
}, null, 2));
|
|
655
|
+
} else {
|
|
656
|
+
console.log(`\n ${C.red}✗ Failed to start validator: ${err.message}${C.reset}\n`);
|
|
657
|
+
console.log(` ${C.dim}Make sure 'aether-validator' is installed and in PATH${C.reset}`);
|
|
658
|
+
console.log(` ${C.dim}Install: npm install -g @jellylegsai/aether-validator${C.reset}\n`);
|
|
659
|
+
}
|
|
660
|
+
removeValidatorPid();
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// Entry Point
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
async function validatorStartCommand() {
|
|
670
|
+
const opts = parseArgs();
|
|
671
|
+
await startValidator(opts);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
module.exports = { validatorStartCommand };
|
|
675
|
+
|
|
676
|
+
if (require.main === module) {
|
|
677
|
+
validatorStartCommand().catch(err => {
|
|
678
|
+
console.error(`\n${C.red}✗ Unexpected error: ${err.message}${C.reset}\n`);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
});
|
|
681
|
+
}
|