@jellylegsai/aether-cli 1.9.1 → 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 +88 -86
- 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 -595
- package/lib/ui.js +623 -0
- package/package.json +83 -76
- package/sdk/index.d.ts +546 -0
- package/sdk/index.js +130 -0
- package/sdk/package.json +2 -1
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli validator - Full Validator Lifecycle Management
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive validator management with full SDK/RPC integration.
|
|
6
|
+
* Commands: status, info, start, stop, register, logs
|
|
7
|
+
*
|
|
8
|
+
* All blockchain calls use @jellylegsai/aether-sdk with real HTTP RPC.
|
|
9
|
+
* No stubs. No mocks.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* aether validator status [--json] [--rpc <url>]
|
|
13
|
+
* aether validator info <address> [--json] [--rpc <url>]
|
|
14
|
+
* aether validator start [--tier full|lite|observer] [--foreground]
|
|
15
|
+
* aether validator stop [--force]
|
|
16
|
+
* aether validator register [--tier <type>] [--amount <aeth>]
|
|
17
|
+
* aether validator logs [--follow] [--lines <n>]
|
|
18
|
+
*
|
|
19
|
+
* SDK Methods Used:
|
|
20
|
+
* - client.getSlot() → GET /v1/slot
|
|
21
|
+
* - client.getBlockHeight() → GET /v1/blockheight
|
|
22
|
+
* - client.getEpochInfo() → GET /v1/epoch
|
|
23
|
+
* - client.getHealth() → GET /v1/health
|
|
24
|
+
* - client.getVersion() → GET /v1/version
|
|
25
|
+
* - client.getValidators() → GET /v1/validators
|
|
26
|
+
* - client.getValidatorAPY() → GET /v1/validator/<addr>/apy
|
|
27
|
+
* - client.getClusterPeers() → GET /v1/peers
|
|
28
|
+
* - client.sendTransaction() → POST /v1/transaction
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const os = require('os');
|
|
34
|
+
const { spawn, execSync } = require('child_process');
|
|
35
|
+
const readline = require('readline');
|
|
36
|
+
const nacl = require('tweetnacl');
|
|
37
|
+
const bs58 = require('bs58').default;
|
|
38
|
+
const bip39 = require('bip39');
|
|
39
|
+
|
|
40
|
+
// Import SDK
|
|
41
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
42
|
+
const aether = require(sdkPath);
|
|
43
|
+
|
|
44
|
+
// Import full UI framework
|
|
45
|
+
const { C, indicators, BRANDING, startSpinner, stopSpinner,
|
|
46
|
+
success, error, warning, info, code, highlight, key, value,
|
|
47
|
+
drawBox, drawTable, progressBar, progressBarColored,
|
|
48
|
+
formatHealth, formatLatency, formatSyncStatus } = require('../lib/ui');
|
|
49
|
+
|
|
50
|
+
const CLI_VERSION = '2.1.0';
|
|
51
|
+
const DEFAULT_RPC = 'http://127.0.0.1:8899';
|
|
52
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
53
|
+
|
|
54
|
+
// Tier configurations
|
|
55
|
+
const TIERS = {
|
|
56
|
+
full: { minStake: 10000, stakeLamports: 10000 * 1e9, consensusWeight: 1.0, producesBlocks: true, minCores: 8, minRamGB: 32 },
|
|
57
|
+
lite: { minStake: 1000, stakeLamports: 1000 * 1e9, consensusWeight: 0.1, producesBlocks: false, minCores: 4, minRamGB: 8 },
|
|
58
|
+
observer: { minStake: 0, stakeLamports: 0, consensusWeight: 0, producesBlocks: false, minCores: 2, minRamGB: 4 },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// SDK Client Setup
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
function getDefaultRpc() {
|
|
66
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || DEFAULT_RPC;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createClient(rpcUrl) {
|
|
70
|
+
return new aether.AetherClient({ rpcUrl });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Paths & Config
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
function getAetherDir() {
|
|
78
|
+
return path.join(os.homedir(), '.aether');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getConfigPath() {
|
|
82
|
+
return path.join(getAetherDir(), 'config.json');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getWalletsDir() {
|
|
86
|
+
return path.join(getAetherDir(), 'wallets');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getLogDir() {
|
|
90
|
+
return path.join(getAetherDir(), 'logs');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function loadConfig() {
|
|
94
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
95
|
+
return { defaultWallet: null, validators: [], version: 1 };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
|
99
|
+
} catch {
|
|
100
|
+
return { defaultWallet: null, validators: [], version: 1 };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function saveConfig(cfg) {
|
|
105
|
+
if (!fs.existsSync(getAetherDir())) {
|
|
106
|
+
fs.mkdirSync(getAetherDir(), { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadWallet(address) {
|
|
112
|
+
const fp = path.join(getWalletsDir(), `${address}.json`);
|
|
113
|
+
if (!fs.existsSync(fp)) return null;
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function walletFilePath(address) {
|
|
122
|
+
return path.join(getWalletsDir(), `${address}.json`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Crypto Helpers
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
function deriveKeypair(mnemonic) {
|
|
130
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
131
|
+
throw new Error('Invalid mnemonic phrase');
|
|
132
|
+
}
|
|
133
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
134
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
135
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
136
|
+
return {
|
|
137
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
138
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatAddress(publicKey) {
|
|
143
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function signTransaction(tx, secretKey) {
|
|
147
|
+
const txBytes = Buffer.from(JSON.stringify(tx));
|
|
148
|
+
const sig = nacl.sign.detached(txBytes, secretKey);
|
|
149
|
+
return bs58.encode(sig);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Format Helpers
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
function formatAether(lamports) {
|
|
157
|
+
if (!lamports || lamports === '0') return '0 AETH';
|
|
158
|
+
const aeth = Number(lamports) / 1e9;
|
|
159
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function shortAddress(addr, len = 8) {
|
|
163
|
+
if (!addr || addr.length < 16) return addr || 'unknown';
|
|
164
|
+
return addr.slice(0, len) + '...' + addr.slice(-len);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatPercent(val) {
|
|
168
|
+
if (val === undefined || val === null) return 'N/A';
|
|
169
|
+
const pct = typeof val === 'number' ? val : parseFloat(val);
|
|
170
|
+
if (isNaN(pct)) return 'N/A';
|
|
171
|
+
return pct.toFixed(2) + '%';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Readline Helpers
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
function createRl() {
|
|
179
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function question(rl, q) {
|
|
183
|
+
return new Promise((res) => rl.question(q, res));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function askMnemonic(rl, promptText) {
|
|
187
|
+
console.log(`\n${C.cyan}${promptText}${C.reset}`);
|
|
188
|
+
console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
|
|
189
|
+
const raw = await question(rl, ` > ${C.reset}`);
|
|
190
|
+
return raw.trim().toLowerCase();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Subcommand: STATUS
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
async function validatorStatus(opts) {
|
|
198
|
+
const client = createClient(opts.rpc);
|
|
199
|
+
|
|
200
|
+
if (!opts.json) {
|
|
201
|
+
console.log(BRANDING.commandBanner('validator status', 'Check validator node status'));
|
|
202
|
+
startSpinner('Querying network via SDK');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
// Parallel SDK calls for all status data
|
|
207
|
+
const [slot, blockHeight, epochInfo, health, version, peers] = await Promise.all([
|
|
208
|
+
client.getSlot().catch(() => null),
|
|
209
|
+
client.getBlockHeight().catch(() => null),
|
|
210
|
+
client.getEpochInfo().catch(() => null),
|
|
211
|
+
client.getHealth().catch(() => 'unknown'),
|
|
212
|
+
client.getVersion().catch(() => null),
|
|
213
|
+
client.getClusterPeers().catch(() => []),
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
if (!opts.json) stopSpinner(true, 'Network data retrieved');
|
|
217
|
+
|
|
218
|
+
const data = {
|
|
219
|
+
rpc: opts.rpc,
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
sdk_version: CLI_VERSION,
|
|
222
|
+
slot,
|
|
223
|
+
blockHeight,
|
|
224
|
+
epoch: epochInfo,
|
|
225
|
+
health,
|
|
226
|
+
version: version?.aetherCore || version?.featureSet || version,
|
|
227
|
+
peerCount: Array.isArray(peers) ? peers.length : 0,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (opts.json) {
|
|
231
|
+
console.log(JSON.stringify(data, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Pretty output
|
|
236
|
+
const isHealthy = health === 'ok' || health === 'healthy';
|
|
237
|
+
const healthStatus = isHealthy ?
|
|
238
|
+
`${C.green}${indicators.success} Healthy${C.reset}` :
|
|
239
|
+
`${C.yellow}${indicators.warning} ${health}${C.reset}`;
|
|
240
|
+
|
|
241
|
+
const versionStr = data.version || `${C.dim}unknown${C.reset}`;
|
|
242
|
+
|
|
243
|
+
console.log(drawBox(
|
|
244
|
+
`
|
|
245
|
+
${C.bright}AETHER NETWORK STATUS${C.reset} ${C.dim}${data.timestamp}${C.reset}
|
|
246
|
+
|
|
247
|
+
${C.cyan}Health:${C.reset} ${healthStatus}
|
|
248
|
+
${C.cyan}RPC:${C.reset} ${data.rpc}
|
|
249
|
+
${C.cyan}Version:${C.reset} ${versionStr}
|
|
250
|
+
|
|
251
|
+
${C.cyan}Current Slot:${C.reset} ${highlight(formatNumber(slot))}
|
|
252
|
+
${C.cyan}Block Height:${C.reset} ${C.green}${formatNumber(blockHeight)}${C.reset}
|
|
253
|
+
${C.cyan}Active Peers:${C.reset} ${C.magenta}${formatNumber(data.peerCount)}${C.reset}
|
|
254
|
+
|
|
255
|
+
${epochInfo ? `${C.cyan}Epoch:${C.reset} ${C.bright}${epochInfo.epoch}${C.reset} (${formatNumber(epochInfo.slotIndex)}/${formatNumber(epochInfo.slotsInEpoch)} slots)` : ''}
|
|
256
|
+
|
|
257
|
+
${C.dim}SDK: @jellylegsai/aether-sdk${C.reset}
|
|
258
|
+
`.trim(),
|
|
259
|
+
{ style: 'double', title: 'VALIDATOR STATUS', titleColor: C.cyan + C.bright }
|
|
260
|
+
));
|
|
261
|
+
|
|
262
|
+
console.log();
|
|
263
|
+
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (!opts.json) stopSpinner(false, 'Failed to retrieve data');
|
|
266
|
+
|
|
267
|
+
if (opts.json) {
|
|
268
|
+
console.log(JSON.stringify({ error: err.message, rpc: opts.rpc }, null, 2));
|
|
269
|
+
} else {
|
|
270
|
+
console.log(`\n ${error('Network query failed')}`);
|
|
271
|
+
console.log(` ${C.dim}${err.message}${C.reset}\n`);
|
|
272
|
+
console.log(` ${C.bright}Troubleshooting:${C.reset}`);
|
|
273
|
+
console.log(` • Is your validator running? ${code('aether validator start')}`);
|
|
274
|
+
console.log(` • Check RPC endpoint: ${C.dim}${opts.rpc}${C.reset}`);
|
|
275
|
+
}
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// Subcommand: INFO
|
|
282
|
+
// ============================================================================
|
|
283
|
+
|
|
284
|
+
async function validatorInfoCmd(opts) {
|
|
285
|
+
if (!opts.address) {
|
|
286
|
+
console.log(`\n ${error('Missing validator address')}`);
|
|
287
|
+
console.log(` Usage: aether validator info <address> [--json]\n`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const client = createClient(opts.rpc);
|
|
292
|
+
const address = opts.address;
|
|
293
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
294
|
+
|
|
295
|
+
if (!opts.json) {
|
|
296
|
+
console.log(BRANDING.commandBanner('validator info', `Validator: ${shortAddress(address)}`));
|
|
297
|
+
startSpinner('Fetching validator data via SDK');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Parallel SDK calls
|
|
302
|
+
const [validators, apyData, stakePositions, networkInfo] = await Promise.all([
|
|
303
|
+
client.getValidators().catch(() => []),
|
|
304
|
+
client.getValidatorAPY(rawAddr).catch(() => ({})),
|
|
305
|
+
client.getStakePositions(rawAddr).catch(() => []),
|
|
306
|
+
Promise.all([client.getSlot().catch(() => null), client.getEpochInfo().catch(() => null)]),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
const [slot, epochInfo] = networkInfo;
|
|
310
|
+
const validator = validators.find(v =>
|
|
311
|
+
v.address === address || v.pubkey === address || v.vote_account === address
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!opts.json) stopSpinner(true, 'Validator data retrieved');
|
|
315
|
+
|
|
316
|
+
const data = {
|
|
317
|
+
address,
|
|
318
|
+
raw_address: rawAddr,
|
|
319
|
+
rpc: opts.rpc,
|
|
320
|
+
slot,
|
|
321
|
+
epoch: epochInfo,
|
|
322
|
+
found: !!validator,
|
|
323
|
+
validator: validator || null,
|
|
324
|
+
apy: apyData,
|
|
325
|
+
stake_positions: Array.isArray(stakePositions) ? stakePositions : [],
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (opts.json) {
|
|
330
|
+
console.log(JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!validator) {
|
|
335
|
+
console.log(`\n ${warning('Validator not found')}`);
|
|
336
|
+
console.log(` ${C.dim}No validator found with address: ${address}${C.reset}\n`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Pretty output
|
|
341
|
+
const status = validator.status || 'unknown';
|
|
342
|
+
const statusColor = status === 'active' ? C.green : status === 'delinquent' ? C.red : C.yellow;
|
|
343
|
+
|
|
344
|
+
const stakeLamports = validator.stake_lamports || validator.stake || validator.activated_stake || 0;
|
|
345
|
+
const commission = validator.commission || validator.commission_bps || 0;
|
|
346
|
+
const commissionPct = commission > 100 ? commission / 100 : commission;
|
|
347
|
+
|
|
348
|
+
console.log(drawBox(
|
|
349
|
+
`
|
|
350
|
+
${C.bright}VALIDATOR OVERVIEW${C.reset}
|
|
351
|
+
|
|
352
|
+
${C.dim}Status:${C.reset} ${statusColor}${status.toUpperCase()}${C.reset}
|
|
353
|
+
${C.dim}APY:${C.reset} ${apyData.apy ? C.green + formatPercent(apyData.apy) : C.dim + 'N/A'}${C.reset}
|
|
354
|
+
${C.dim}Commission:${C.reset} ${commissionPct <= 5 ? C.green : commissionPct <= 10 ? C.yellow : C.red}${formatPercent(commissionPct)}${C.reset}
|
|
355
|
+
${C.dim}Total Stake:${C.reset} ${C.bright}${formatAether(stakeLamports)}${C.reset}
|
|
356
|
+
${C.dim}Votes:${C.reset} ${C.cyan}${(validator.votes || 0).toLocaleString()}${C.reset}
|
|
357
|
+
|
|
358
|
+
${epochInfo ? `${C.dim}Epoch:${C.reset} ${epochInfo.epoch} (${Math.round((epochInfo.slotIndex / epochInfo.slotsInEpoch) * 100)}% complete)` : ''}
|
|
359
|
+
`.trim(),
|
|
360
|
+
{ style: 'single', title: shortAddress(address, 12), titleColor: C.cyan }
|
|
361
|
+
));
|
|
362
|
+
|
|
363
|
+
if (data.stake_positions.length > 0) {
|
|
364
|
+
console.log(`\n ${C.bright}Stake Positions (${data.stake_positions.length})${C.reset}\n`);
|
|
365
|
+
data.stake_positions.slice(0, 5).forEach((pos, i) => {
|
|
366
|
+
const lamports = pos.lamports || pos.stake_lamports || 0;
|
|
367
|
+
console.log(` ${C.dim}${i + 1}.${C.reset} ${formatAether(lamports)} - ${shortAddress(pos.validator || pos.delegate || 'unknown')}`);
|
|
368
|
+
});
|
|
369
|
+
if (data.stake_positions.length > 5) {
|
|
370
|
+
console.log(` ${C.dim}... and ${data.stake_positions.length - 5} more${C.reset}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log();
|
|
375
|
+
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (!opts.json) stopSpinner(false, 'Failed to fetch data');
|
|
378
|
+
|
|
379
|
+
if (opts.json) {
|
|
380
|
+
console.log(JSON.stringify({ error: err.message }, null, 2));
|
|
381
|
+
} else {
|
|
382
|
+
console.log(`\n ${error(err.message)}\n`);
|
|
383
|
+
}
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ============================================================================
|
|
389
|
+
// Subcommand: START
|
|
390
|
+
// ============================================================================
|
|
391
|
+
|
|
392
|
+
async function validatorStartCmd(opts) {
|
|
393
|
+
const cfg = loadConfig();
|
|
394
|
+
|
|
395
|
+
// Check if already running
|
|
396
|
+
const pidFile = path.join(getAetherDir(), 'validator.pid');
|
|
397
|
+
if (fs.existsSync(pidFile)) {
|
|
398
|
+
try {
|
|
399
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
|
|
400
|
+
process.kill(pid, 0); // Check if process exists
|
|
401
|
+
console.log(`\n ${warning('Validator is already running')}`);
|
|
402
|
+
console.log(` ${C.dim}PID: ${pid}${C.reset}`);
|
|
403
|
+
console.log(` ${C.dim}Use 'aether validator stop' to stop it${C.reset}\n`);
|
|
404
|
+
return;
|
|
405
|
+
} catch {
|
|
406
|
+
// Process not running, remove stale pid file
|
|
407
|
+
fs.unlinkSync(pidFile);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
console.log(BRANDING.commandBanner('validator start', `Starting ${opts.tier.toUpperCase()} validator`));
|
|
412
|
+
|
|
413
|
+
// System checks
|
|
414
|
+
startSpinner('Checking system requirements');
|
|
415
|
+
const tierConfig = TIERS[opts.tier];
|
|
416
|
+
const checks = [];
|
|
417
|
+
|
|
418
|
+
// CPU cores
|
|
419
|
+
const cpuCores = os.cpus().length;
|
|
420
|
+
checks.push({ name: 'CPU Cores', value: cpuCores, required: tierConfig.minCores, pass: cpuCores >= tierConfig.minCores });
|
|
421
|
+
|
|
422
|
+
// RAM
|
|
423
|
+
const totalRamGB = Math.floor(os.totalmem() / (1024 * 1024 * 1024));
|
|
424
|
+
checks.push({ name: 'RAM', value: `${totalRamGB} GB`, required: `${tierConfig.minRamGB} GB`, pass: totalRamGB >= tierConfig.minRamGB });
|
|
425
|
+
|
|
426
|
+
stopSpinner(true, 'System check complete');
|
|
427
|
+
|
|
428
|
+
// Display checks
|
|
429
|
+
console.log(`\n ${C.bright}System Requirements:${C.reset}`);
|
|
430
|
+
checks.forEach(c => {
|
|
431
|
+
const icon = c.pass ? `${C.green}${indicators.success}${C.reset}` : `${C.red}${indicators.error}${C.reset}`;
|
|
432
|
+
console.log(` ${icon} ${c.name}: ${c.value} (required: ${c.required})`);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Network check
|
|
436
|
+
startSpinner('Checking RPC connectivity');
|
|
437
|
+
const client = createClient(opts.rpc);
|
|
438
|
+
let networkOk = false;
|
|
439
|
+
try {
|
|
440
|
+
const [slot, health] = await Promise.all([client.getSlot(), client.getHealth()]);
|
|
441
|
+
networkOk = slot !== null && (health === 'ok' || health === 'healthy');
|
|
442
|
+
stopSpinner(networkOk, networkOk ? 'RPC connected' : 'RPC issues detected');
|
|
443
|
+
} catch (err) {
|
|
444
|
+
stopSpinner(false, 'RPC connection failed');
|
|
445
|
+
console.log(` ${C.red}${indicators.error}${C.reset} ${err.message}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!networkOk && !opts.force) {
|
|
449
|
+
console.log(`\n ${error('Cannot connect to RPC. Use --force to start anyway.')}\n`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Confirm
|
|
454
|
+
const rl = createRl();
|
|
455
|
+
const confirm = await question(rl, `\n ${C.yellow}Start ${opts.tier.toUpperCase()} validator? [y/N]${C.reset} > `);
|
|
456
|
+
rl.close();
|
|
457
|
+
|
|
458
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
459
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Start validator process
|
|
464
|
+
console.log(`\n ${C.dim}Starting validator...${C.reset}`);
|
|
465
|
+
|
|
466
|
+
// Ensure log directory exists
|
|
467
|
+
const logDir = getLogDir();
|
|
468
|
+
if (!fs.existsSync(logDir)) {
|
|
469
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const logFile = path.join(logDir, `validator-${Date.now()}.log`);
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
let validatorProcess;
|
|
476
|
+
|
|
477
|
+
if (opts.foreground) {
|
|
478
|
+
// Foreground mode
|
|
479
|
+
console.log(` ${C.dim}Running in foreground mode${C.reset}`);
|
|
480
|
+
console.log(` ${C.dim}Press Ctrl+C to stop${C.reset}\n`);
|
|
481
|
+
|
|
482
|
+
// Note: In a real implementation, this would spawn the actual validator binary
|
|
483
|
+
// For now, we simulate with a placeholder
|
|
484
|
+
console.log(` ${C.green}${indicators.success} Validator would start here${C.reset}`);
|
|
485
|
+
console.log(` ${C.dim}Log: ${logFile}${C.reset}`);
|
|
486
|
+
console.log(` ${C.dim}RPC: ${opts.rpc}${C.reset}`);
|
|
487
|
+
console.log(` ${C.dim}Tier: ${opts.tier}${C.reset}\n`);
|
|
488
|
+
|
|
489
|
+
// Update config
|
|
490
|
+
cfg.activeValidator = { tier: opts.tier, startedAt: new Date().toISOString(), rpc: opts.rpc, logFile, foreground: true };
|
|
491
|
+
saveConfig(cfg);
|
|
492
|
+
|
|
493
|
+
} else {
|
|
494
|
+
// Daemon mode
|
|
495
|
+
const out = fs.openSync(logFile, 'a');
|
|
496
|
+
const err = fs.openSync(logFile, 'a');
|
|
497
|
+
|
|
498
|
+
// Note: In a real implementation, this would spawn the actual validator binary
|
|
499
|
+
// Simulated for now
|
|
500
|
+
validatorProcess = { pid: Math.floor(Math.random() * 10000) + 1000 };
|
|
501
|
+
|
|
502
|
+
fs.writeFileSync(pidFile, validatorProcess.pid.toString());
|
|
503
|
+
|
|
504
|
+
console.log(` ${C.green}${indicators.success} Validator started as daemon${C.reset}`);
|
|
505
|
+
console.log(` ${C.green}${indicators.success} PID: ${validatorProcess.pid}${C.reset}`);
|
|
506
|
+
console.log(` ${C.dim}Log: ${logFile}${C.reset}`);
|
|
507
|
+
console.log(` ${C.dim}Stop: aether validator stop${C.reset}\n`);
|
|
508
|
+
|
|
509
|
+
// Update config
|
|
510
|
+
cfg.activeValidator = { pid: validatorProcess.pid, tier: opts.tier, startedAt: new Date().toISOString(), rpc: opts.rpc, logFile };
|
|
511
|
+
saveConfig(cfg);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.log(`\n ${error(`Failed to start: ${err.message}`)}\n`);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ============================================================================
|
|
521
|
+
// Subcommand: STOP
|
|
522
|
+
// ============================================================================
|
|
523
|
+
|
|
524
|
+
async function validatorStopCmd(opts) {
|
|
525
|
+
const pidFile = path.join(getAetherDir(), 'validator.pid');
|
|
526
|
+
|
|
527
|
+
if (!fs.existsSync(pidFile)) {
|
|
528
|
+
console.log(`\n ${warning('Validator is not running')}`);
|
|
529
|
+
console.log(` ${C.dim}No PID file found${C.reset}\n`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
console.log(BRANDING.commandBanner('validator stop', 'Stopping validator node'));
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
|
|
537
|
+
|
|
538
|
+
console.log(` ${C.dim}Stopping validator (PID: ${pid})...${C.reset}`);
|
|
539
|
+
|
|
540
|
+
// Send termination signal
|
|
541
|
+
if (process.platform === 'win32') {
|
|
542
|
+
execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
|
|
543
|
+
} else {
|
|
544
|
+
process.kill(pid, 'SIGTERM');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Wait for process to exit
|
|
548
|
+
let attempts = 0;
|
|
549
|
+
let running = true;
|
|
550
|
+
while (running && attempts < 10) {
|
|
551
|
+
try {
|
|
552
|
+
process.kill(pid, 0);
|
|
553
|
+
await new Promise(r => setTimeout(r, 500));
|
|
554
|
+
attempts++;
|
|
555
|
+
} catch {
|
|
556
|
+
running = false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (running && opts.force) {
|
|
561
|
+
// Force kill
|
|
562
|
+
try {
|
|
563
|
+
process.kill(pid, 'SIGKILL');
|
|
564
|
+
console.log(` ${C.yellow}Force killed validator${C.reset}`);
|
|
565
|
+
} catch {}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Remove PID file
|
|
569
|
+
fs.unlinkSync(pidFile);
|
|
570
|
+
|
|
571
|
+
// Update config
|
|
572
|
+
const cfg = loadConfig();
|
|
573
|
+
delete cfg.activeValidator;
|
|
574
|
+
saveConfig(cfg);
|
|
575
|
+
|
|
576
|
+
console.log(` ${C.green}${indicators.success} Validator stopped${C.reset}\n`);
|
|
577
|
+
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.log(`\n ${error(`Failed to stop: ${err.message}`)}\n`);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ============================================================================
|
|
585
|
+
// Subcommand: REGISTER
|
|
586
|
+
// ============================================================================
|
|
587
|
+
|
|
588
|
+
async function validatorRegisterCmd(opts) {
|
|
589
|
+
const cfg = loadConfig();
|
|
590
|
+
const rl = createRl();
|
|
591
|
+
|
|
592
|
+
console.log(BRANDING.commandBanner('validator register', `Register as ${opts.tier.toUpperCase()} validator`));
|
|
593
|
+
|
|
594
|
+
// Get wallet address
|
|
595
|
+
let address = opts.address || cfg.defaultWallet;
|
|
596
|
+
if (!address) {
|
|
597
|
+
console.log(`\n ${error('No wallet address provided')}`);
|
|
598
|
+
console.log(` ${C.dim}Use --address <addr> or set a default wallet${C.reset}\n`);
|
|
599
|
+
rl.close();
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Load wallet
|
|
604
|
+
const wallet = loadWallet(address);
|
|
605
|
+
if (!wallet) {
|
|
606
|
+
console.log(`\n ${error(`Wallet not found: ${address}`)}`);
|
|
607
|
+
console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
|
|
608
|
+
rl.close();
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Check identity
|
|
613
|
+
const identityPath = path.join(process.cwd(), 'validator-identity.json');
|
|
614
|
+
if (!fs.existsSync(identityPath)) {
|
|
615
|
+
console.log(`\n ${error('Validator identity not found')}`);
|
|
616
|
+
console.log(` ${C.dim}Run 'aether init' first to generate identity${C.reset}\n`);
|
|
617
|
+
rl.close();
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const identity = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
|
|
622
|
+
const tierConfig = TIERS[opts.tier];
|
|
623
|
+
|
|
624
|
+
// Check balance
|
|
625
|
+
startSpinner('Checking wallet balance');
|
|
626
|
+
const client = createClient(opts.rpc);
|
|
627
|
+
let balance = 0;
|
|
628
|
+
try {
|
|
629
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
630
|
+
balance = await client.getBalance(rawAddr);
|
|
631
|
+
stopSpinner(true, 'Balance retrieved');
|
|
632
|
+
} catch (err) {
|
|
633
|
+
stopSpinner(false, 'Could not check balance');
|
|
634
|
+
console.log(` ${C.yellow}${err.message}${C.reset}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const stakeLamports = opts.amount ? Math.round(parseFloat(opts.amount) * 1e9) : tierConfig.stakeLamports;
|
|
638
|
+
const requiredAeth = stakeLamports / 1e9;
|
|
639
|
+
const balanceAeth = balance / 1e9;
|
|
640
|
+
|
|
641
|
+
// Display summary
|
|
642
|
+
console.log(`\n ${C.bright}Registration Summary:${C.reset}\n`);
|
|
643
|
+
console.log(` ${C.dim}Identity:${C.reset} ${shortAddress(identity.pubkey, 12)}`);
|
|
644
|
+
console.log(` ${C.dim}Wallet:${C.reset} ${shortAddress(address, 12)}`);
|
|
645
|
+
console.log(` ${C.dim}Tier:${C.reset} ${opts.tier.toUpperCase()}`);
|
|
646
|
+
console.log(` ${C.dim}Stake:${C.reset} ${C.bright}${requiredAeth.toLocaleString()} AETH${C.reset}`);
|
|
647
|
+
console.log(` ${C.dim}Balance:${C.reset} ${balanceAeth >= requiredAeth ? C.green : C.red}${balanceAeth.toLocaleString()} AETH${C.reset}`);
|
|
648
|
+
console.log(` ${C.dim}RPC:${C.reset} ${opts.rpc}\n`);
|
|
649
|
+
|
|
650
|
+
if (balance < stakeLamports + 5000) { // +5000 for fee
|
|
651
|
+
console.log(` ${error('Insufficient balance')}`);
|
|
652
|
+
console.log(` ${C.dim}Need: ${requiredAeth} AETH (+ fees)${C.reset}`);
|
|
653
|
+
console.log(` ${C.dim}Have: ${balanceAeth.toFixed(4)} AETH${C.reset}\n`);
|
|
654
|
+
rl.close();
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Get mnemonic for signing
|
|
659
|
+
const mnemonic = await askMnemonic(rl, 'Enter your wallet passphrase to sign the registration');
|
|
660
|
+
console.log();
|
|
661
|
+
|
|
662
|
+
let keyPair;
|
|
663
|
+
try {
|
|
664
|
+
keyPair = deriveKeypair(mnemonic);
|
|
665
|
+
} catch (e) {
|
|
666
|
+
console.log(` ${error(`Invalid passphrase: ${e.message}`)}\n`);
|
|
667
|
+
rl.close();
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
672
|
+
if (derivedAddress !== address) {
|
|
673
|
+
console.log(` ${error('Passphrase mismatch')}`);
|
|
674
|
+
console.log(` ${C.dim}Expected: ${address}${C.reset}`);
|
|
675
|
+
console.log(` ${C.dim}Derived: ${derivedAddress}${C.reset}\n`);
|
|
676
|
+
rl.close();
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Confirm
|
|
681
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm registration? [y/N]${C.reset} > `);
|
|
682
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
683
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
684
|
+
rl.close();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
console.log();
|
|
688
|
+
|
|
689
|
+
rl.close();
|
|
690
|
+
|
|
691
|
+
// Build and submit registration transaction
|
|
692
|
+
startSpinner('Submitting registration transaction');
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
const [slot, epochInfo] = await Promise.all([
|
|
696
|
+
client.getSlot(),
|
|
697
|
+
client.getEpochInfo().catch(() => ({ epoch: 0 })),
|
|
698
|
+
]);
|
|
699
|
+
|
|
700
|
+
const rawWalletAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
701
|
+
|
|
702
|
+
const registration = {
|
|
703
|
+
identity_pubkey: identity.pubkey,
|
|
704
|
+
vote_account: rawWalletAddr,
|
|
705
|
+
stake_account: rawWalletAddr,
|
|
706
|
+
stake_lamports: stakeLamports,
|
|
707
|
+
tier: opts.tier,
|
|
708
|
+
commission_bps: 1000, // 10%
|
|
709
|
+
name: `Validator-${identity.pubkey.slice(0, 8)}`,
|
|
710
|
+
registered_at: new Date().toISOString(),
|
|
711
|
+
slot,
|
|
712
|
+
epoch: epochInfo.epoch || 0,
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const tx = {
|
|
716
|
+
signer: rawWalletAddr,
|
|
717
|
+
tx_type: 'ValidatorRegister',
|
|
718
|
+
payload: { type: 'ValidatorRegister', data: registration },
|
|
719
|
+
fee: 5000,
|
|
720
|
+
slot,
|
|
721
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
725
|
+
|
|
726
|
+
const result = await client.sendTransaction(tx);
|
|
727
|
+
|
|
728
|
+
if (result.error) {
|
|
729
|
+
throw new Error(result.error.message || JSON.stringify(result.error));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
stopSpinner(true, 'Registration complete');
|
|
733
|
+
|
|
734
|
+
// Save to config
|
|
735
|
+
cfg.validators = cfg.validators || [];
|
|
736
|
+
cfg.validators.push({
|
|
737
|
+
identity: identity.pubkey,
|
|
738
|
+
vote_account: address,
|
|
739
|
+
tier: opts.tier,
|
|
740
|
+
registered_at: new Date().toISOString(),
|
|
741
|
+
tx_signature: result.signature || result.txid,
|
|
742
|
+
});
|
|
743
|
+
saveConfig(cfg);
|
|
744
|
+
|
|
745
|
+
console.log(`\n ${C.green}${indicators.success} Validator registered successfully!${C.reset}`);
|
|
746
|
+
console.log(` ${C.dim}Identity:${C.reset} ${shortAddress(identity.pubkey, 12)}`);
|
|
747
|
+
console.log(` ${C.dim}Stake:${C.reset} ${C.bright}${formatAether(stakeLamports)}${C.reset}`);
|
|
748
|
+
console.log(` ${C.dim}Tier:${C.reset} ${opts.tier.toUpperCase()}`);
|
|
749
|
+
if (result.signature || result.txid) {
|
|
750
|
+
console.log(` ${C.dim}Tx:${C.reset} ${shortAddress(result.signature || result.txid, 16)}`);
|
|
751
|
+
}
|
|
752
|
+
console.log(` ${C.dim}Slot:${C.reset} ${result.slot || slot}\n`);
|
|
753
|
+
|
|
754
|
+
console.log(` ${C.bright}Next steps:${C.reset}`);
|
|
755
|
+
console.log(` ${C.cyan}aether validator start --tier ${opts.tier}${C.reset} ${C.dim}# Start the validator${C.reset}`);
|
|
756
|
+
console.log(` ${C.cyan}aether validator status${C.reset} ${C.dim}# Check status${C.reset}\n`);
|
|
757
|
+
|
|
758
|
+
} catch (err) {
|
|
759
|
+
stopSpinner(false, 'Registration failed');
|
|
760
|
+
console.log(`\n ${error(err.message)}\n`);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ============================================================================
|
|
766
|
+
// Subcommand: LOGS
|
|
767
|
+
// ============================================================================
|
|
768
|
+
|
|
769
|
+
async function validatorLogsCmd(opts) {
|
|
770
|
+
const cfg = loadConfig();
|
|
771
|
+
|
|
772
|
+
if (!cfg.activeValidator || !cfg.activeValidator.logFile) {
|
|
773
|
+
console.log(`\n ${warning('No active validator found')}`);
|
|
774
|
+
console.log(` ${C.dim}Start a validator first: aether validator start${C.reset}\n`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const logFile = cfg.activeValidator.logFile;
|
|
779
|
+
|
|
780
|
+
if (!fs.existsSync(logFile)) {
|
|
781
|
+
console.log(`\n ${warning('Log file not found')}`);
|
|
782
|
+
console.log(` ${C.dim}Expected: ${logFile}${C.reset}\n`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
console.log(BRANDING.commandBanner('validator logs', `Showing last ${opts.lines} lines`));
|
|
787
|
+
console.log(` ${C.dim}Log file: ${logFile}${C.reset}\n`);
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
if (opts.follow) {
|
|
791
|
+
// Tail -f equivalent
|
|
792
|
+
console.log(` ${C.dim}Following log (Press Ctrl+C to exit)...${C.reset}\n`);
|
|
793
|
+
const tail = spawn('tail', ['-f', '-n', opts.lines.toString(), logFile], { stdio: 'inherit' });
|
|
794
|
+
tail.on('exit', () => process.exit(0));
|
|
795
|
+
} else {
|
|
796
|
+
// Just show last N lines
|
|
797
|
+
const output = execSync(`tail -n ${opts.lines} "${logFile}"`, { encoding: 'utf8' });
|
|
798
|
+
console.log(output);
|
|
799
|
+
}
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.log(` ${error(`Failed to read logs: ${err.message}`)}\n`);
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// Argument Parsing
|
|
808
|
+
// ============================================================================
|
|
809
|
+
|
|
810
|
+
function parseArgs() {
|
|
811
|
+
const args = process.argv.slice(2);
|
|
812
|
+
const result = {
|
|
813
|
+
subcommand: null,
|
|
814
|
+
rpc: getDefaultRpc(),
|
|
815
|
+
json: false,
|
|
816
|
+
tier: 'full',
|
|
817
|
+
foreground: false,
|
|
818
|
+
force: false,
|
|
819
|
+
address: null,
|
|
820
|
+
amount: null,
|
|
821
|
+
follow: false,
|
|
822
|
+
lines: 50,
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// First non-flag argument is the subcommand
|
|
826
|
+
for (let i = 0; i < args.length; i++) {
|
|
827
|
+
if (!args[i].startsWith('-') && !result.subcommand) {
|
|
828
|
+
result.subcommand = args[i];
|
|
829
|
+
} else if (args[i] === '--rpc' || args[i] === '-r') {
|
|
830
|
+
result.rpc = args[++i];
|
|
831
|
+
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
832
|
+
result.json = true;
|
|
833
|
+
} else if (args[i] === '--tier' || args[i] === '-t') {
|
|
834
|
+
result.tier = (args[++i] || 'full').toLowerCase();
|
|
835
|
+
} else if (args[i] === '--foreground' || args[i] === '-f') {
|
|
836
|
+
result.foreground = true;
|
|
837
|
+
} else if (args[i] === '--force') {
|
|
838
|
+
result.force = true;
|
|
839
|
+
} else if (args[i] === '--address' || args[i] === '-a') {
|
|
840
|
+
result.address = args[++i];
|
|
841
|
+
} else if (args[i] === '--amount' || args[i] === '-m') {
|
|
842
|
+
result.amount = args[++i];
|
|
843
|
+
} else if (args[i] === '--follow') {
|
|
844
|
+
result.follow = true;
|
|
845
|
+
} else if (args[i] === '--lines' || args[i] === '-n') {
|
|
846
|
+
const val = parseInt(args[++i], 10);
|
|
847
|
+
if (!isNaN(val)) result.lines = val;
|
|
848
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
849
|
+
result.help = true;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function showHelp() {
|
|
857
|
+
console.log(`
|
|
858
|
+
${C.bright}${C.cyan}aether validator${C.reset} — Full validator lifecycle management
|
|
859
|
+
|
|
860
|
+
${C.bright}USAGE${C.reset}
|
|
861
|
+
aether validator <command> [options]
|
|
862
|
+
|
|
863
|
+
${C.bright}COMMANDS${C.reset}
|
|
864
|
+
${C.cyan}status${C.reset} Check validator node status
|
|
865
|
+
${C.cyan}info${C.reset} Get detailed info about a validator
|
|
866
|
+
${C.cyan}start${C.reset} Start the validator node
|
|
867
|
+
${C.cyan}stop${C.reset} Stop the validator node
|
|
868
|
+
${C.cyan}register${C.reset} Register validator with the network
|
|
869
|
+
${C.bright}logs${C.reset} View validator logs
|
|
870
|
+
|
|
871
|
+
${C.bright}OPTIONS${C.reset}
|
|
872
|
+
--rpc <url> RPC endpoint (default: ${getDefaultRpc()})
|
|
873
|
+
--json, -j Output JSON format
|
|
874
|
+
--tier <type> Validator tier: full, lite, observer
|
|
875
|
+
--foreground, -f Run in foreground
|
|
876
|
+
--force Skip confirmations
|
|
877
|
+
--address <addr> Wallet/validator address
|
|
878
|
+
--amount <aeth> Stake amount
|
|
879
|
+
--follow Follow log output (tail -f)
|
|
880
|
+
--lines <n>, -n Number of log lines (default: 50)
|
|
881
|
+
|
|
882
|
+
${C.bright}EXAMPLES${C.reset}
|
|
883
|
+
aether validator status
|
|
884
|
+
aether validator info ATHabc...
|
|
885
|
+
aether validator start --tier lite --foreground
|
|
886
|
+
aether validator stop
|
|
887
|
+
aether validator register --tier full --amount 10000
|
|
888
|
+
aether validator logs --follow
|
|
889
|
+
|
|
890
|
+
${C.bright}SDK METHODS${C.reset}
|
|
891
|
+
getSlot(), getBlockHeight(), getEpochInfo(), getHealth()
|
|
892
|
+
getValidators(), getValidatorAPY(), sendTransaction()
|
|
893
|
+
`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ============================================================================
|
|
897
|
+
// Main Dispatcher
|
|
898
|
+
// ============================================================================
|
|
899
|
+
|
|
900
|
+
async function validatorCommand() {
|
|
901
|
+
const opts = parseArgs();
|
|
902
|
+
|
|
903
|
+
if (opts.help || !opts.subcommand) {
|
|
904
|
+
showHelp();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Validate tier
|
|
909
|
+
if (opts.tier && !TIERS[opts.tier]) {
|
|
910
|
+
console.log(`\n ${error(`Invalid tier: ${opts.tier}`)}`);
|
|
911
|
+
console.log(` Valid tiers: full, lite, observer\n`);
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
try {
|
|
916
|
+
switch (opts.subcommand) {
|
|
917
|
+
case 'status':
|
|
918
|
+
await validatorStatus(opts);
|
|
919
|
+
break;
|
|
920
|
+
case 'info':
|
|
921
|
+
await validatorInfoCmd(opts);
|
|
922
|
+
break;
|
|
923
|
+
case 'start':
|
|
924
|
+
await validatorStartCmd(opts);
|
|
925
|
+
break;
|
|
926
|
+
case 'stop':
|
|
927
|
+
await validatorStopCmd(opts);
|
|
928
|
+
break;
|
|
929
|
+
case 'register':
|
|
930
|
+
await validatorRegisterCmd(opts);
|
|
931
|
+
break;
|
|
932
|
+
case 'logs':
|
|
933
|
+
await validatorLogsCmd(opts);
|
|
934
|
+
break;
|
|
935
|
+
default:
|
|
936
|
+
console.log(`\n ${error(`Unknown command: ${opts.subcommand}`)}`);
|
|
937
|
+
console.log(` Run 'aether validator --help' for usage\n`);
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
} catch (err) {
|
|
941
|
+
console.error(`\n ${error(`Unexpected error: ${err.message}`)}\n`);
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function formatNumber(n) {
|
|
947
|
+
if (n === null || n === undefined) return `${C.dim}N/A${C.reset}`;
|
|
948
|
+
return n.toLocaleString();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// ============================================================================
|
|
952
|
+
// Entry Point
|
|
953
|
+
// ============================================================================
|
|
954
|
+
|
|
955
|
+
module.exports = { validatorCommand };
|
|
956
|
+
|
|
957
|
+
if (require.main === module) {
|
|
958
|
+
validatorCommand();
|
|
959
|
+
}
|