@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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/aether-cli-1.0.0.tgz +0 -0
  4. package/aether-cli-1.8.0.tgz +0 -0
  5. package/aether-hub-1.0.5.tgz +0 -0
  6. package/aether-hub-1.1.8.tgz +0 -0
  7. package/aether-hub-1.2.1.tgz +0 -0
  8. package/commands/account.js +280 -0
  9. package/commands/apy.js +499 -0
  10. package/commands/balance.js +241 -0
  11. package/commands/blockhash.js +181 -0
  12. package/commands/broadcast.js +387 -0
  13. package/commands/claim.js +490 -0
  14. package/commands/config.js +851 -0
  15. package/commands/delegations.js +582 -0
  16. package/commands/doctor.js +769 -0
  17. package/commands/emergency.js +667 -0
  18. package/commands/epoch.js +275 -0
  19. package/commands/fees.js +276 -0
  20. package/commands/index.js +78 -0
  21. package/commands/info.js +495 -0
  22. package/commands/init.js +816 -0
  23. package/commands/install.js +666 -0
  24. package/commands/kyc.js +272 -0
  25. package/commands/logs.js +315 -0
  26. package/commands/monitor.js +431 -0
  27. package/commands/multisig.js +701 -0
  28. package/commands/network.js +429 -0
  29. package/commands/nft.js +857 -0
  30. package/commands/ping.js +266 -0
  31. package/commands/price.js +253 -0
  32. package/commands/rewards.js +931 -0
  33. package/commands/sdk-test.js +477 -0
  34. package/commands/sdk.js +656 -0
  35. package/commands/slot.js +155 -0
  36. package/commands/snapshot.js +470 -0
  37. package/commands/stake-info.js +139 -0
  38. package/commands/stake-positions.js +205 -0
  39. package/commands/stake.js +516 -0
  40. package/commands/stats.js +396 -0
  41. package/commands/status.js +327 -0
  42. package/commands/supply.js +391 -0
  43. package/commands/tps.js +238 -0
  44. package/commands/transfer.js +495 -0
  45. package/commands/tx-history.js +346 -0
  46. package/commands/unstake.js +597 -0
  47. package/commands/validator-info.js +657 -0
  48. package/commands/validator-register.js +593 -0
  49. package/commands/validator-start.js +323 -0
  50. package/commands/validator-status.js +227 -0
  51. package/commands/validators.js +626 -0
  52. package/commands/wallet.js +1570 -0
  53. package/index.js +593 -0
  54. package/lib/errors.js +398 -0
  55. package/package.json +76 -0
  56. package/sdk/README.md +210 -0
  57. package/sdk/index.js +1639 -0
  58. package/sdk/package.json +34 -0
  59. package/sdk/rpc.js +254 -0
  60. package/sdk/test.js +85 -0
  61. package/test/doctor.test.js +76 -0
  62. package/validator-identity.json +4 -0
@@ -0,0 +1,593 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli validator-register
4
+ *
5
+ * Register a validator with the Aether network via RPC.
6
+ * This is the FINAL step of validator onboarding - submits identity
7
+ * and stake to the chain, creating an on-chain validator record.
8
+ *
9
+ * Usage:
10
+ * aether validator register --identity <path> --validator <addr> --amount <aeth>
11
+ * aether validator register --tier full --stake 10000
12
+ * aether validator register --json
13
+ *
14
+ * SDK wired to: POST /v1/validator/register, GET /v1/validators, GET /v1/epoch
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const readline = require('readline');
21
+ const crypto = require('crypto');
22
+ const nacl = require('tweetnacl');
23
+ const bs58 = require('bs58').default;
24
+ const bip39 = require('bip39');
25
+
26
+ // Import SDK for blockchain RPC calls
27
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
28
+ const aether = require(sdkPath);
29
+
30
+ // ANSI colours
31
+ const C = {
32
+ reset: '\x1b[0m',
33
+ bright: '\x1b[1m',
34
+ dim: '\x1b[2m',
35
+ red: '\x1b[31m',
36
+ green: '\x1b[32m',
37
+ yellow: '\x1b[33m',
38
+ cyan: '\x1b[36m',
39
+ magenta: '\x1b[35m',
40
+ };
41
+
42
+ const CLI_VERSION = '1.0.0';
43
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
44
+
45
+ // Tier requirements
46
+ const TIER_REQUIREMENTS = {
47
+ full: { minStake: 10000, minCores: 8, minRam: 32, minDisk: 512 },
48
+ lite: { minStake: 1000, minCores: 4, minRam: 8, minDisk: 100 },
49
+ observer: { minStake: 0, minCores: 2, minRam: 4, minDisk: 50 },
50
+ };
51
+
52
+ // ============================================================================
53
+ // Config & Paths
54
+ // ============================================================================
55
+
56
+ function getAetherDir() {
57
+ return path.join(os.homedir(), '.aether');
58
+ }
59
+
60
+ function getConfigPath() {
61
+ return path.join(getAetherDir(), 'config.json');
62
+ }
63
+
64
+ function loadConfig() {
65
+ if (!fs.existsSync(getConfigPath())) {
66
+ return { defaultWallet: null, validators: [] };
67
+ }
68
+ try {
69
+ return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
70
+ } catch {
71
+ return { defaultWallet: null, validators: [] };
72
+ }
73
+ }
74
+
75
+ function saveConfig(cfg) {
76
+ if (!fs.existsSync(getAetherDir())) {
77
+ fs.mkdirSync(getAetherDir(), { recursive: true });
78
+ }
79
+ fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
80
+ }
81
+
82
+ function loadWallet(address) {
83
+ const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
84
+ if (!fs.existsSync(fp)) return null;
85
+ try {
86
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function getDefaultRpc() {
93
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
94
+ }
95
+
96
+ function createClient(rpcUrl) {
97
+ return new aether.AetherClient({ rpcUrl });
98
+ }
99
+
100
+ // ============================================================================
101
+ // Argument Parsing
102
+ // ============================================================================
103
+
104
+ function parseArgs() {
105
+ // Handle both direct invocation and subcommand invocation
106
+ // Direct: node validator-register.js --wallet ...
107
+ // Subcommand: aether validator register --wallet ...
108
+ let args = process.argv.slice(2);
109
+
110
+ // If called as subcommand, strip 'validator' and 'register' from args
111
+ if (args[0] === 'validator' && args[1] === 'register') {
112
+ args = args.slice(2);
113
+ } else if (args[0] === 'register') {
114
+ args = args.slice(1);
115
+ }
116
+
117
+ const opts = {
118
+ identity: null,
119
+ wallet: null,
120
+ validator: null,
121
+ amount: null,
122
+ tier: 'full',
123
+ commission: 10, // Default 10% commission
124
+ name: null,
125
+ rpc: getDefaultRpc(),
126
+ json: false,
127
+ dryRun: false,
128
+ force: false,
129
+ };
130
+
131
+ for (let i = 0; i < args.length; i++) {
132
+ const arg = args[i];
133
+ if (arg === '--identity' || arg === '-i') {
134
+ opts.identity = args[++i];
135
+ } else if (arg === '--wallet' || arg === '-w') {
136
+ opts.wallet = args[++i];
137
+ } else if (arg === '--validator' || arg === '-v') {
138
+ opts.validator = args[++i];
139
+ } else if (arg === '--amount' || arg === '-a') {
140
+ const val = args[++i];
141
+ opts.amount = parseFloat(val);
142
+ if (isNaN(opts.amount)) {
143
+ console.error(` ${C.red}✗ Invalid amount:${C.reset} ${val}`);
144
+ process.exit(1);
145
+ }
146
+ } else if (arg === '--tier' || arg === '-t') {
147
+ opts.tier = (args[++i] || 'full').toLowerCase();
148
+ if (!['full', 'lite', 'observer'].includes(opts.tier)) {
149
+ console.error(` ${C.red}✗ Invalid tier:${C.reset} ${opts.tier}. Valid: full, lite, observer`);
150
+ process.exit(1);
151
+ }
152
+ } else if (arg === '--commission' || arg === '-c') {
153
+ const val = parseInt(args[++i], 10);
154
+ if (isNaN(val) || val < 0 || val > 100) {
155
+ console.error(` ${C.red}✗ Invalid commission:${C.reset} must be 0-100`);
156
+ process.exit(1);
157
+ }
158
+ opts.commission = val;
159
+ } else if (arg === '--name' || arg === '-n') {
160
+ opts.name = args[++i];
161
+ } else if (arg === '--rpc' || arg === '-r') {
162
+ opts.rpc = args[++i];
163
+ } else if (arg === '--json' || arg === '-j') {
164
+ opts.json = true;
165
+ } else if (arg === '--dry-run') {
166
+ opts.dryRun = true;
167
+ } else if (arg === '--force' || arg === '-f') {
168
+ opts.force = true;
169
+ } else if (arg === '--help' || arg === '-h') {
170
+ showHelp();
171
+ process.exit(0);
172
+ }
173
+ }
174
+
175
+ return opts;
176
+ }
177
+
178
+ function showHelp() {
179
+ console.log(`
180
+ ${C.bright}${C.cyan}aether-cli validator register${C.reset} — Register validator with the network
181
+
182
+ ${C.bright}USAGE${C.reset}
183
+ aether validator register --wallet <addr> --amount <aeth> [options]
184
+
185
+ ${C.bright}REQUIRED${C.reset}
186
+ --wallet <addr> Wallet address to stake from (ATH...)
187
+ --amount <aeth> Amount of AETH to stake (minimum per tier)
188
+
189
+ ${C.bright}OPTIONS${C.reset}
190
+ --identity <path> Path to validator-identity.json (default: ./validator-identity.json)
191
+ --validator <addr> Validator vote account address (default: derive from wallet)
192
+ --tier <type> Validator tier: full, lite, observer (default: full)
193
+ --commission <n> Commission percentage 0-100 (default: 10)
194
+ --name <string> Validator name/moniker
195
+ --rpc <url> RPC endpoint (default: $AETHER_RPC or localhost:8899)
196
+ --dry-run Preview registration without submitting
197
+ --json Output JSON for scripting
198
+ --force Skip confirmation prompts
199
+ --help Show this help
200
+
201
+ ${C.bright}TIER REQUIREMENTS${C.reset}
202
+ full: 10,000 AETH stake, 8 cores, 32GB RAM, 512GB SSD
203
+ lite: 1,000 AETH stake, 4 cores, 8GB RAM, 100GB SSD
204
+ observer: 0 AETH stake, 2 cores, 4GB RAM, 50GB disk
205
+
206
+ ${C.bright}SDK METHODS USED${C.reset}
207
+ client.getSlot() → GET /v1/slot
208
+ client.getEpochInfo() → GET /v1/epoch
209
+ client.getBalance() → GET /v1/account/<addr>
210
+ client.sendTransaction() → POST /v1/validator/register
211
+ client.getValidators() → GET /v1/validators (verify)
212
+
213
+ ${C.bright}EXAMPLES${C.reset}
214
+ aether validator register --wallet ATHxxx... --amount 10000 --tier full
215
+ aether validator register --wallet ATHxxx... --amount 1000 --tier lite --name "MyLiteNode"
216
+ aether validator register --wallet ATHxxx... --amount 0 --tier observer --json
217
+ `);
218
+ }
219
+
220
+ // ============================================================================
221
+ // Crypto Helpers
222
+ // ============================================================================
223
+
224
+ function deriveKeypair(mnemonic) {
225
+ if (!bip39.validateMnemonic(mnemonic)) {
226
+ throw new Error('Invalid mnemonic phrase');
227
+ }
228
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
229
+ const seed32 = seedBuffer.slice(0, 32);
230
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
231
+ return {
232
+ publicKey: Buffer.from(keyPair.publicKey),
233
+ secretKey: Buffer.from(keyPair.secretKey),
234
+ };
235
+ }
236
+
237
+ function formatAddress(publicKey) {
238
+ return 'ATH' + bs58.encode(publicKey);
239
+ }
240
+
241
+ function signTransaction(tx, secretKey) {
242
+ const txBytes = Buffer.from(JSON.stringify(tx));
243
+ const sig = nacl.sign.detached(txBytes, secretKey);
244
+ return bs58.encode(sig);
245
+ }
246
+
247
+ // ============================================================================
248
+ // Validation Helpers
249
+ // ============================================================================
250
+
251
+ async function validatePrerequisites(opts, client) {
252
+ const errors = [];
253
+
254
+ // Check identity file
255
+ const identityPath = opts.identity || path.join(process.cwd(), 'validator-identity.json');
256
+ if (!fs.existsSync(identityPath)) {
257
+ errors.push(`Identity file not found: ${identityPath}`);
258
+ errors.push(`Run 'aether init' first to generate validator identity`);
259
+ } else {
260
+ try {
261
+ const identity = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
262
+ if (!identity.pubkey || !identity.secret) {
263
+ errors.push('Invalid identity file: missing pubkey or secret');
264
+ }
265
+ } catch (e) {
266
+ errors.push(`Failed to parse identity file: ${e.message}`);
267
+ }
268
+ }
269
+
270
+ // Check wallet exists
271
+ if (!opts.wallet) {
272
+ errors.push('No wallet address provided (--wallet)');
273
+ } else {
274
+ const wallet = loadWallet(opts.wallet);
275
+ if (!wallet) {
276
+ errors.push(`Wallet not found: ${opts.wallet}`);
277
+ errors.push(`Run 'aether wallet import' to add this wallet`);
278
+ }
279
+ }
280
+
281
+ // Check minimum stake for tier
282
+ const minStake = TIER_REQUIREMENTS[opts.tier].minStake;
283
+ if (opts.amount === null || opts.amount === undefined) {
284
+ errors.push(`No stake amount provided (--amount)`);
285
+ } else if (opts.amount < minStake) {
286
+ errors.push(`Stake amount ${opts.amount} AETH below minimum ${minStake} AETH for ${opts.tier} tier`);
287
+ }
288
+
289
+ // Check RPC connectivity via SDK
290
+ try {
291
+ const slot = await client.getSlot();
292
+ if (typeof slot !== 'number') {
293
+ errors.push('RPC endpoint not responding with valid slot data');
294
+ }
295
+ } catch (e) {
296
+ errors.push(`RPC endpoint unreachable: ${e.message}`);
297
+ }
298
+
299
+ // Check wallet balance via SDK
300
+ if (opts.wallet) {
301
+ try {
302
+ const rawAddr = opts.wallet.startsWith('ATH') ? opts.wallet.slice(3) : opts.wallet;
303
+ const balance = await client.getBalance(rawAddr);
304
+ const required = opts.amount * 1e9 + 0.005 * 1e9; // stake + fee buffer
305
+ if (balance < required) {
306
+ errors.push(`Insufficient balance: ${(balance / 1e9).toFixed(4)} AETH, need ${(required / 1e9).toFixed(4)} AETH`);
307
+ }
308
+ } catch (e) {
309
+ errors.push(`Failed to check wallet balance: ${e.message}`);
310
+ }
311
+ }
312
+
313
+ return errors;
314
+ }
315
+
316
+ function formatAether(lamports) {
317
+ const aeth = (lamports || 0) / 1e9;
318
+ return aeth.toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 }) + ' AETH';
319
+ }
320
+
321
+ // ============================================================================
322
+ // Readline Helpers
323
+ // ============================================================================
324
+
325
+ function createRl() {
326
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
327
+ }
328
+
329
+ function question(rl, q) {
330
+ return new Promise((res) => rl.question(q, res));
331
+ }
332
+
333
+ async function askMnemonic(rl, promptText) {
334
+ console.log(`\n${C.cyan}${promptText}${C.reset}`);
335
+ console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
336
+ const raw = await question(rl, ` > ${C.reset}`);
337
+ return raw.trim().toLowerCase();
338
+ }
339
+
340
+ // ============================================================================
341
+ // Core Registration Logic
342
+ // ============================================================================
343
+
344
+ async function registerValidator(opts) {
345
+ const client = createClient(opts.rpc);
346
+ const rl = createRl();
347
+
348
+ // Parse identity
349
+ const identityPath = opts.identity || path.join(process.cwd(), 'validator-identity.json');
350
+ let identity;
351
+ try {
352
+ identity = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
353
+ } catch (e) {
354
+ console.error(`\n ${C.red}✗ Failed to load identity:${C.reset} ${e.message}`);
355
+ process.exit(1);
356
+ }
357
+
358
+ // Validate prerequisites
359
+ if (!opts.json) {
360
+ console.log(`\n${C.bright}${C.cyan}── Validating Prerequisites ─────────────────────────────${C.reset}\n`);
361
+ console.log(` ${C.dim}Checking identity, wallet, RPC, and balance...${C.reset}`);
362
+ }
363
+
364
+ const errors = await validatePrerequisites(opts, client);
365
+ if (errors.length > 0) {
366
+ if (opts.json) {
367
+ console.log(JSON.stringify({ success: false, errors }, null, 2));
368
+ } else {
369
+ console.log(`\n ${C.red}✗ Validation failed:${C.reset}\n`);
370
+ errors.forEach(e => console.log(` ${C.red}•${C.reset} ${e}`));
371
+ console.log();
372
+ }
373
+ rl.close();
374
+ process.exit(1);
375
+ }
376
+
377
+ if (!opts.json) {
378
+ console.log(` ${C.green}✓ All prerequisites validated${C.reset}\n`);
379
+ }
380
+
381
+ // Fetch current network state via SDK
382
+ const [slot, epochInfo] = await Promise.all([
383
+ client.getSlot().catch(() => null),
384
+ client.getEpochInfo().catch(() => null),
385
+ ]);
386
+
387
+ // Build registration transaction
388
+ const lamports = Math.round(opts.amount * 1e9);
389
+ const validatorAddr = opts.validator || opts.wallet;
390
+ const rawValidatorAddr = validatorAddr.startsWith('ATH') ? validatorAddr.slice(3) : validatorAddr;
391
+ const rawWalletAddr = opts.wallet.startsWith('ATH') ? opts.wallet.slice(3) : opts.wallet;
392
+
393
+ const registration = {
394
+ identity_pubkey: identity.pubkey,
395
+ vote_account: rawValidatorAddr,
396
+ stake_account: rawWalletAddr,
397
+ stake_lamports: lamports,
398
+ tier: opts.tier,
399
+ commission_bps: opts.commission * 100, // Convert to basis points
400
+ name: opts.name || `Validator-${identity.pubkey.slice(0, 8)}`,
401
+ registered_at: new Date().toISOString(),
402
+ slot: slot || 0,
403
+ epoch: epochInfo?.epoch || 0,
404
+ };
405
+
406
+ if (opts.json) {
407
+ if (opts.dryRun) {
408
+ console.log(JSON.stringify({
409
+ dry_run: true,
410
+ registration,
411
+ identity_path: identityPath,
412
+ rpc: opts.rpc,
413
+ timestamp: new Date().toISOString(),
414
+ }, null, 2));
415
+ rl.close();
416
+ return;
417
+ }
418
+ } else {
419
+ // Display registration summary
420
+ console.log(`${C.bright}${C.cyan}── Validator Registration ─────────────────────────────────${C.reset}\n`);
421
+ console.log(` ${C.green}★${C.reset} Identity: ${C.bright}${identity.pubkey.slice(0, 20)}...${C.reset}`);
422
+ console.log(` ${C.green}★${C.reset} Vote Acct: ${C.bright}${validatorAddr}${C.reset}`);
423
+ console.log(` ${C.green}★${C.reset} Stake From: ${C.bright}${opts.wallet}${C.reset}`);
424
+ console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${formatAether(lamports)}${C.reset}`);
425
+ console.log(` ${C.green}★${C.reset} Tier: ${C.bright}${opts.tier.toUpperCase()}${C.reset}`);
426
+ console.log(` ${C.green}★${C.reset} Commission: ${C.bright}${opts.commission}%${C.reset}`);
427
+ console.log(` ${C.green}★${C.reset} Name: ${C.bright}${registration.name}${C.reset}`);
428
+ console.log(` ${C.dim} RPC: ${opts.rpc}${C.reset}`);
429
+ console.log(` ${C.dim} Current Slot: ${slot || 'unknown'}${C.reset}`);
430
+ console.log(` ${C.dim} Epoch: ${epochInfo?.epoch || 'unknown'}${C.reset}\n`);
431
+
432
+ if (opts.dryRun) {
433
+ console.log(` ${C.yellow}⚠ Dry run mode - no transaction submitted${C.reset}\n`);
434
+ rl.close();
435
+ return;
436
+ }
437
+ }
438
+
439
+ // Get mnemonic for signing
440
+ let keyPair;
441
+ if (!opts.json) {
442
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}\n`);
443
+ }
444
+
445
+ try {
446
+ const mnemonic = await askMnemonic(rl, 'Enter your wallet passphrase to sign the registration');
447
+ keyPair = deriveKeypair(mnemonic);
448
+
449
+ // Verify derived address matches wallet
450
+ const derivedAddr = formatAddress(keyPair.publicKey);
451
+ if (derivedAddr !== opts.wallet) {
452
+ if (!opts.json) {
453
+ console.log(`\n ${C.red}✗ Passphrase mismatch!${C.reset}`);
454
+ console.log(` ${C.dim} Derived: ${derivedAddr}${C.reset}`);
455
+ console.log(` ${C.dim} Expected: ${opts.wallet}${C.reset}\n`);
456
+ } else {
457
+ console.log(JSON.stringify({ success: false, error: 'Passphrase mismatch' }, null, 2));
458
+ }
459
+ rl.close();
460
+ process.exit(1);
461
+ }
462
+ } catch (e) {
463
+ if (!opts.json) {
464
+ console.log(`\n ${C.red}✗ Failed to derive keypair:${C.reset} ${e.message}\n`);
465
+ } else {
466
+ console.log(JSON.stringify({ success: false, error: e.message }, null, 2));
467
+ }
468
+ rl.close();
469
+ process.exit(1);
470
+ }
471
+
472
+ // Confirm registration
473
+ if (!opts.json && !opts.force) {
474
+ const confirm = await question(rl, ` ${C.yellow}Confirm registration? [y/N]${C.reset} > `);
475
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
476
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
477
+ rl.close();
478
+ return;
479
+ }
480
+ console.log();
481
+ }
482
+
483
+ rl.close();
484
+
485
+ // Build and sign registration transaction
486
+ const tx = {
487
+ signer: rawWalletAddr,
488
+ tx_type: 'ValidatorRegister',
489
+ payload: {
490
+ type: 'ValidatorRegister',
491
+ data: registration,
492
+ },
493
+ fee: 5000, // Registration fee in lamports
494
+ slot: slot || 0,
495
+ timestamp: Math.floor(Date.now() / 1000),
496
+ };
497
+
498
+ // Sign with wallet keypair
499
+ tx.signature = signTransaction(tx, keyPair.secretKey);
500
+
501
+ if (!opts.json) {
502
+ console.log(` ${C.dim}Submitting registration to ${opts.rpc}...${C.reset}`);
503
+ }
504
+
505
+ // Submit via SDK
506
+ try {
507
+ const result = await client.sendTransaction(tx);
508
+
509
+ if (result.error) {
510
+ throw new Error(result.error.message || JSON.stringify(result.error));
511
+ }
512
+
513
+ // Save to local config
514
+ const cfg = loadConfig();
515
+ if (!cfg.validators) cfg.validators = [];
516
+ cfg.validators.push({
517
+ identity: identity.pubkey,
518
+ vote_account: validatorAddr,
519
+ tier: opts.tier,
520
+ registered_at: new Date().toISOString(),
521
+ tx_signature: result.signature || result.txid,
522
+ });
523
+ saveConfig(cfg);
524
+
525
+ if (opts.json) {
526
+ console.log(JSON.stringify({
527
+ success: true,
528
+ registration: {
529
+ identity: identity.pubkey,
530
+ vote_account: validatorAddr,
531
+ stake_lamports: lamports,
532
+ tier: opts.tier,
533
+ commission_bps: opts.commission,
534
+ },
535
+ tx_signature: result.signature || result.txid,
536
+ slot: result.slot || slot,
537
+ rpc: opts.rpc,
538
+ timestamp: new Date().toISOString(),
539
+ }, null, 2));
540
+ } else {
541
+ console.log(`\n ${C.green}✓ Validator registered successfully!${C.reset}\n`);
542
+ console.log(` ${C.dim}Identity:${C.reset} ${identity.pubkey}`);
543
+ console.log(` ${C.dim}Vote Acct:${C.reset} ${validatorAddr}`);
544
+ console.log(` ${C.dim}Tier:${C.reset} ${opts.tier.toUpperCase()}`);
545
+ console.log(` ${C.dim}Stake:${C.reset} ${formatAether(lamports)}`);
546
+ if (result.signature || result.txid) {
547
+ console.log(` ${C.dim}Signature:${C.reset} ${(result.signature || result.txid).slice(0, 40)}...`);
548
+ }
549
+ if (result.slot) {
550
+ console.log(` ${C.dim}Slot:${C.reset} ${result.slot}`);
551
+ }
552
+ console.log();
553
+ console.log(` ${C.dim}Next steps:${C.reset}`);
554
+ console.log(` ${C.cyan}aether validator status${C.reset} Check validator status`);
555
+ console.log(` ${C.cyan}aether validators list${C.reset} View all validators`);
556
+ console.log(` ${C.cyan}aether delegations list${C.reset} View your delegations\n`);
557
+ }
558
+ } catch (err) {
559
+ if (opts.json) {
560
+ console.log(JSON.stringify({
561
+ success: false,
562
+ error: err.message,
563
+ registration: { identity: identity.pubkey, vote_account: validatorAddr },
564
+ }, null, 2));
565
+ } else {
566
+ console.log(`\n ${C.red}✗ Registration failed:${C.reset} ${err.message}\n`);
567
+ console.log(` ${C.dim}Common causes:${C.reset}`);
568
+ console.log(` • Validator with this identity already registered`);
569
+ console.log(` • Insufficient balance for stake + fees`);
570
+ console.log(` • RPC endpoint not accepting transactions`);
571
+ console.log(` • Network epoch boundary - retry in a few slots\n`);
572
+ }
573
+ process.exit(1);
574
+ }
575
+ }
576
+
577
+ // ============================================================================
578
+ // Main Entry Point
579
+ // ============================================================================
580
+
581
+ async function validatorRegisterCommand() {
582
+ const opts = parseArgs();
583
+ await registerValidator(opts);
584
+ }
585
+
586
+ module.exports = { validatorRegisterCommand };
587
+
588
+ if (require.main === module) {
589
+ validatorRegisterCommand().catch(err => {
590
+ console.error(`\n${C.red}✗ Unexpected error:${C.reset} ${err.message}\n`);
591
+ process.exit(1);
592
+ });
593
+ }