@jellylegsai/aether-cli 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/aether-cli-1.0.0.tgz +0 -0
- package/aether-cli-1.8.0.tgz +0 -0
- package/aether-hub-1.0.5.tgz +0 -0
- package/aether-hub-1.1.8.tgz +0 -0
- package/aether-hub-1.2.1.tgz +0 -0
- package/commands/account.js +280 -0
- package/commands/apy.js +499 -0
- package/commands/balance.js +241 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +387 -0
- package/commands/claim.js +490 -0
- package/commands/config.js +851 -0
- package/commands/delegations.js +582 -0
- package/commands/doctor.js +769 -0
- package/commands/emergency.js +667 -0
- package/commands/epoch.js +275 -0
- package/commands/fees.js +276 -0
- package/commands/index.js +78 -0
- package/commands/info.js +495 -0
- package/commands/init.js +816 -0
- package/commands/install.js +666 -0
- package/commands/kyc.js +272 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/multisig.js +701 -0
- package/commands/network.js +429 -0
- package/commands/nft.js +857 -0
- package/commands/ping.js +266 -0
- package/commands/price.js +253 -0
- package/commands/rewards.js +931 -0
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +656 -0
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +470 -0
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +205 -0
- package/commands/stake.js +516 -0
- package/commands/stats.js +396 -0
- package/commands/status.js +327 -0
- package/commands/supply.js +391 -0
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +346 -0
- package/commands/unstake.js +597 -0
- package/commands/validator-info.js +657 -0
- package/commands/validator-register.js +593 -0
- package/commands/validator-start.js +323 -0
- package/commands/validator-status.js +227 -0
- package/commands/validators.js +626 -0
- package/commands/wallet.js +1570 -0
- package/index.js +593 -0
- package/lib/errors.js +398 -0
- package/package.json +76 -0
- package/sdk/README.md +210 -0
- package/sdk/index.js +1639 -0
- package/sdk/package.json +34 -0
- package/sdk/rpc.js +254 -0
- package/sdk/test.js +85 -0
- package/test/doctor.test.js +76 -0
- package/validator-identity.json +4 -0
package/commands/init.js
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aether-cli init
|
|
3
|
+
*
|
|
4
|
+
* COMPLETE ONBOARDING WIZARD - Fully wired to SDK
|
|
5
|
+
*
|
|
6
|
+
* Guides users through:
|
|
7
|
+
* 1. Prerequisites check (Node.js, disk space)
|
|
8
|
+
* 2. Tier selection (Full/Lite/Observer)
|
|
9
|
+
* 3. Identity generation (Ed25519 keypair)
|
|
10
|
+
* 4. Wallet creation/import (BIP39)
|
|
11
|
+
* 5. RPC connection & health check
|
|
12
|
+
* 6. Faucet funding (testnet)
|
|
13
|
+
* 7. Validator registration (real SDK calls)
|
|
14
|
+
* 8. Auto-start option
|
|
15
|
+
*
|
|
16
|
+
* All blockchain calls use @jellylegsai/aether-sdk with REAL RPC.
|
|
17
|
+
* No stubs. No mocks.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
const nacl = require('tweetnacl');
|
|
24
|
+
const bs58 = require('bs58').default;
|
|
25
|
+
const bip39 = require('bip39');
|
|
26
|
+
const readline = require('readline');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const http = require('http');
|
|
29
|
+
const https = require('https');
|
|
30
|
+
|
|
31
|
+
// Import SDK
|
|
32
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
33
|
+
const aether = require(sdkPath);
|
|
34
|
+
|
|
35
|
+
// ANSI colors
|
|
36
|
+
const C = {
|
|
37
|
+
reset: '\x1b[0m',
|
|
38
|
+
bright: '\x1b[1m',
|
|
39
|
+
green: '\x1b[32m',
|
|
40
|
+
yellow: '\x1b[33m',
|
|
41
|
+
cyan: '\x1b[36m',
|
|
42
|
+
red: '\x1b[31m',
|
|
43
|
+
dim: '\x1b[2m',
|
|
44
|
+
magenta: '\x1b[35m',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CLI_VERSION = '2.0.0';
|
|
48
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
49
|
+
|
|
50
|
+
// Tier requirements
|
|
51
|
+
const TIER_REQUIREMENTS = {
|
|
52
|
+
full: { minStake: 10000, minCores: 8, minRam: 32, minDisk: 512 },
|
|
53
|
+
lite: { minStake: 1000, minCores: 4, minRam: 8, minDisk: 100 },
|
|
54
|
+
observer: { minStake: 0, minCores: 2, minRam: 4, minDisk: 50 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Paths & Config
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
function getAetherDir() {
|
|
62
|
+
return path.join(os.homedir(), '.aether');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getWalletsDir() {
|
|
66
|
+
return path.join(getAetherDir(), 'wallets');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getConfigPath() {
|
|
70
|
+
return path.join(getAetherDir(), 'config.json');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureDirs() {
|
|
74
|
+
const dirs = [getAetherDir(), getWalletsDir()];
|
|
75
|
+
for (const d of dirs) {
|
|
76
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadConfig() {
|
|
81
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
82
|
+
return { defaultWallet: null, validators: [], version: 1 };
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
|
86
|
+
} catch {
|
|
87
|
+
return { defaultWallet: null, validators: [], version: 1 };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function saveConfig(cfg) {
|
|
92
|
+
ensureDirs();
|
|
93
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function walletFilePath(address) {
|
|
97
|
+
return path.join(getWalletsDir(), `${address}.json`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function saveWalletFile(address, publicKey) {
|
|
101
|
+
ensureDirs();
|
|
102
|
+
const data = {
|
|
103
|
+
version: 1,
|
|
104
|
+
address,
|
|
105
|
+
public_key: bs58.encode(publicKey),
|
|
106
|
+
created_at: new Date().toISOString(),
|
|
107
|
+
derivation_path: DERIVATION_PATH,
|
|
108
|
+
};
|
|
109
|
+
fs.writeFileSync(walletFilePath(address), JSON.stringify(data, null, 2));
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function loadWallet(address) {
|
|
114
|
+
const fp = walletFilePath(address);
|
|
115
|
+
if (!fs.existsSync(fp)) return null;
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Readline Helpers
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
function createRl() {
|
|
128
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function question(rl, q) {
|
|
132
|
+
return new Promise((res) => rl.question(q, res));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function askYesNo(rl, text, defaultYes = true) {
|
|
136
|
+
const suffix = defaultYes ? ' [Y/n]' : ' [y/N]';
|
|
137
|
+
const ans = await question(rl, `${C.cyan}${text}${suffix}:${C.reset} `);
|
|
138
|
+
const normalized = ans.trim().toLowerCase();
|
|
139
|
+
if (normalized === '') return defaultYes;
|
|
140
|
+
return normalized === 'y' || normalized === 'yes';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function askValue(rl, text, defaultVal = '') {
|
|
144
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : '';
|
|
145
|
+
const ans = await question(rl, `${C.cyan}${text}${suffix}:${C.reset} `);
|
|
146
|
+
return ans.trim() || defaultVal;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function askMnemonic(rl, promptText) {
|
|
150
|
+
console.log(`\n${C.cyan}${promptText}${C.reset}`);
|
|
151
|
+
console.log(`${C.dim}Enter your 12 or 24-word mnemonic:${C.reset}`);
|
|
152
|
+
const raw = await question(rl, ` > ${C.reset}`);
|
|
153
|
+
return raw.trim().toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Print Helpers
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
function printBanner() {
|
|
161
|
+
console.log(`
|
|
162
|
+
${C.cyan}╔═══════════════════════════════════════════════════════════════╗
|
|
163
|
+
║ ║
|
|
164
|
+
║ ${C.bright}AETHER VALIDATOR ONBOARDING WIZARD${C.reset}${C.cyan} ║
|
|
165
|
+
║ ${C.dim}v${CLI_VERSION} - Fully wired to SDK${C.reset}${C.cyan} ║
|
|
166
|
+
║ ║
|
|
167
|
+
╚═══════════════════════════════════════════════════════════════╝${C.reset}
|
|
168
|
+
`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function printStep(step, total, title) {
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(`${C.yellow}Step ${step}/${total}:${C.reset} ${C.bright}${title}${C.reset}`);
|
|
174
|
+
console.log(`${C.dim}${'─'.repeat(60)}${C.reset}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printSuccess(msg) {
|
|
178
|
+
console.log(` ${C.green}✓${C.reset} ${msg}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function printWarning(msg) {
|
|
182
|
+
console.log(` ${C.yellow}⚠${C.reset} ${msg}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function printError(msg) {
|
|
186
|
+
console.log(` ${C.red}✗${C.reset} ${msg}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function printInfo(msg) {
|
|
190
|
+
console.log(` ${C.dim}ℹ${C.reset} ${msg}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Crypto Helpers
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
function generateEd25519Identity() {
|
|
198
|
+
const keyPair = nacl.sign.keyPair();
|
|
199
|
+
const seed32 = keyPair.secretKey.slice(0, 32);
|
|
200
|
+
return {
|
|
201
|
+
pubkey: bs58.encode(keyPair.publicKey),
|
|
202
|
+
secret: bs58.encode(seed32),
|
|
203
|
+
publicKey: keyPair.publicKey,
|
|
204
|
+
secretKey: keyPair.secretKey,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function deriveKeypair(mnemonic) {
|
|
209
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
210
|
+
throw new Error('Invalid mnemonic phrase');
|
|
211
|
+
}
|
|
212
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
213
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
214
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
215
|
+
return {
|
|
216
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
217
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function formatAddress(publicKey) {
|
|
222
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function signTransaction(tx, secretKey) {
|
|
226
|
+
const txBytes = Buffer.from(JSON.stringify(tx));
|
|
227
|
+
const sig = nacl.sign.detached(txBytes, secretKey);
|
|
228
|
+
return bs58.encode(sig);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Step 1: Prerequisites Check
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
async function checkPrerequisites(rl) {
|
|
236
|
+
printStep(1, 6, 'Checking Prerequisites');
|
|
237
|
+
|
|
238
|
+
const checks = [];
|
|
239
|
+
|
|
240
|
+
// Check Node.js version
|
|
241
|
+
const nodeVersion = process.version;
|
|
242
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
243
|
+
checks.push({
|
|
244
|
+
name: 'Node.js',
|
|
245
|
+
passed: nodeMajor >= 14,
|
|
246
|
+
message: `Node.js ${nodeVersion}`,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Check disk space
|
|
250
|
+
try {
|
|
251
|
+
let freeGB = 100;
|
|
252
|
+
if (process.platform === 'win32') {
|
|
253
|
+
const output = execSync('powershell -c "(Get-PSDrive -Name C).Free / 1GB"', { encoding: 'utf8' });
|
|
254
|
+
freeGB = parseFloat(output.trim());
|
|
255
|
+
} else {
|
|
256
|
+
const stat = fs.statfsSync('/');
|
|
257
|
+
freeGB = (stat.bsize * stat.bfree) / (1024 * 1024 * 1024);
|
|
258
|
+
}
|
|
259
|
+
checks.push({
|
|
260
|
+
name: 'Disk Space',
|
|
261
|
+
passed: freeGB >= 50,
|
|
262
|
+
message: `${Math.floor(freeGB)} GB free`,
|
|
263
|
+
});
|
|
264
|
+
} catch {
|
|
265
|
+
checks.push({ name: 'Disk Space', passed: true, message: 'Unable to check' });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check SDK imports
|
|
269
|
+
try {
|
|
270
|
+
const sdk = require(sdkPath);
|
|
271
|
+
checks.push({
|
|
272
|
+
name: 'Aether SDK',
|
|
273
|
+
passed: !!sdk.AetherClient,
|
|
274
|
+
message: 'SDK loaded',
|
|
275
|
+
});
|
|
276
|
+
} catch (e) {
|
|
277
|
+
checks.push({ name: 'Aether SDK', passed: false, message: `Failed: ${e.message}` });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let allPassed = true;
|
|
281
|
+
for (const check of checks) {
|
|
282
|
+
const icon = check.passed ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
|
|
283
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
284
|
+
if (!check.passed) allPassed = false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!allPassed) {
|
|
288
|
+
printWarning('Some prerequisites may need attention.');
|
|
289
|
+
const cont = await askYesNo(rl, 'Continue anyway?', false);
|
|
290
|
+
if (!cont) {
|
|
291
|
+
console.log('\nInstall prerequisites and try again.\n');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
printSuccess('All prerequisites met');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// Step 2: Tier Selection
|
|
303
|
+
// ============================================================================
|
|
304
|
+
|
|
305
|
+
async function selectTier(rl) {
|
|
306
|
+
printStep(2, 6, 'Select Validator Tier');
|
|
307
|
+
|
|
308
|
+
console.log(`\n Choose your validator tier based on hardware and stake:\n`);
|
|
309
|
+
console.log(` ${C.bright}[1] FULL${C.reset} - 10K AETH stake, 8 cores, 32GB RAM, 512GB SSD`);
|
|
310
|
+
console.log(` Full consensus weight, block production, voting`);
|
|
311
|
+
console.log();
|
|
312
|
+
console.log(` ${C.bright}[2] LITE${C.reset} - 1K AETH stake, 4 cores, 8GB RAM, 100GB SSD`);
|
|
313
|
+
console.log(` Stake-based weight, voting only`);
|
|
314
|
+
console.log();
|
|
315
|
+
console.log(` ${C.bright}[3] OBSERVER${C.reset} - 0 AETH stake, 2 cores, 4GB RAM, 50GB disk`);
|
|
316
|
+
console.log(` Relay-only, earns FLUX via data relay`);
|
|
317
|
+
console.log();
|
|
318
|
+
|
|
319
|
+
const choice = await askValue(rl, 'Select tier', '1');
|
|
320
|
+
|
|
321
|
+
let tier = 'full';
|
|
322
|
+
let badge = '[FULL]';
|
|
323
|
+
|
|
324
|
+
switch (choice.trim()) {
|
|
325
|
+
case '1':
|
|
326
|
+
case 'full':
|
|
327
|
+
tier = 'full';
|
|
328
|
+
badge = '[FULL]';
|
|
329
|
+
printSuccess('Selected: FULL Validator');
|
|
330
|
+
break;
|
|
331
|
+
case '2':
|
|
332
|
+
case 'lite':
|
|
333
|
+
tier = 'lite';
|
|
334
|
+
badge = '[LITE]';
|
|
335
|
+
printSuccess('Selected: LITE Validator');
|
|
336
|
+
break;
|
|
337
|
+
case '3':
|
|
338
|
+
case 'observer':
|
|
339
|
+
tier = 'observer';
|
|
340
|
+
badge = '[OBSERVER]';
|
|
341
|
+
printSuccess('Selected: OBSERVER Node');
|
|
342
|
+
break;
|
|
343
|
+
default:
|
|
344
|
+
tier = 'full';
|
|
345
|
+
badge = '[FULL]';
|
|
346
|
+
printWarning('Invalid choice, defaulting to FULL');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { tier, badge, minStake: TIER_REQUIREMENTS[tier].minStake };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================================================
|
|
353
|
+
// Step 3: Identity Generation
|
|
354
|
+
// ============================================================================
|
|
355
|
+
|
|
356
|
+
async function generateIdentity(rl) {
|
|
357
|
+
printStep(3, 6, 'Generating Validator Identity');
|
|
358
|
+
|
|
359
|
+
const identityPath = path.join(process.cwd(), 'validator-identity.json');
|
|
360
|
+
|
|
361
|
+
if (fs.existsSync(identityPath)) {
|
|
362
|
+
printWarning('Identity already exists');
|
|
363
|
+
const regen = await askYesNo(rl, 'Regenerate identity?', false);
|
|
364
|
+
if (!regen) {
|
|
365
|
+
const existing = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
|
|
366
|
+
printSuccess(`Using existing identity: ${existing.pubkey.slice(0, 20)}...`);
|
|
367
|
+
return { identityPath, ...existing };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const identity = generateEd25519Identity();
|
|
372
|
+
fs.writeFileSync(identityPath, JSON.stringify(identity, null, 2));
|
|
373
|
+
|
|
374
|
+
printSuccess(`Identity saved to ${path.basename(identityPath)}`);
|
|
375
|
+
console.log(` ${C.dim}Public key: ${identity.pubkey}${C.reset}`);
|
|
376
|
+
|
|
377
|
+
printWarning('IMPORTANT: Backup validator-identity.json!');
|
|
378
|
+
printWarning('If you lose this file, you lose your validator status.');
|
|
379
|
+
|
|
380
|
+
return { identityPath, ...identity };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Step 4: Wallet Setup
|
|
385
|
+
// ============================================================================
|
|
386
|
+
|
|
387
|
+
async function setupWallet(rl) {
|
|
388
|
+
printStep(4, 6, 'Wallet Setup');
|
|
389
|
+
|
|
390
|
+
const cfg = loadConfig();
|
|
391
|
+
|
|
392
|
+
console.log(`\n ${C.green}1)${C.reset} Create new wallet`);
|
|
393
|
+
console.log(` ${C.green}2)${C.reset} Import existing wallet`);
|
|
394
|
+
console.log(` ${C.green}3)${C.reset} Use default wallet (${cfg.defaultWallet || 'none set'})`);
|
|
395
|
+
console.log();
|
|
396
|
+
|
|
397
|
+
const choice = await askValue(rl, 'Choose', '1');
|
|
398
|
+
|
|
399
|
+
let wallet = null;
|
|
400
|
+
let keyPair = null;
|
|
401
|
+
let mnemonic = null;
|
|
402
|
+
|
|
403
|
+
if (choice === '2') {
|
|
404
|
+
// Import
|
|
405
|
+
mnemonic = await askMnemonic(rl, 'Import wallet from mnemonic');
|
|
406
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
407
|
+
printError('Invalid BIP39 mnemonic');
|
|
408
|
+
throw new Error('Invalid mnemonic');
|
|
409
|
+
}
|
|
410
|
+
keyPair = deriveKeypair(mnemonic);
|
|
411
|
+
const address = formatAddress(keyPair.publicKey);
|
|
412
|
+
|
|
413
|
+
if (!loadWallet(address)) {
|
|
414
|
+
saveWalletFile(address, keyPair.publicKey);
|
|
415
|
+
cfg.defaultWallet = address;
|
|
416
|
+
saveConfig(cfg);
|
|
417
|
+
}
|
|
418
|
+
wallet = { address, ...loadWallet(address) };
|
|
419
|
+
printSuccess(`Wallet imported: ${address}`);
|
|
420
|
+
|
|
421
|
+
} else if (choice === '3' && cfg.defaultWallet) {
|
|
422
|
+
// Use default
|
|
423
|
+
wallet = { address: cfg.defaultWallet, ...loadWallet(cfg.defaultWallet) };
|
|
424
|
+
printSuccess(`Using default wallet: ${wallet.address}`);
|
|
425
|
+
|
|
426
|
+
} else {
|
|
427
|
+
// Create new
|
|
428
|
+
mnemonic = bip39.generateMnemonic(128);
|
|
429
|
+
keyPair = deriveKeypair(mnemonic);
|
|
430
|
+
const address = formatAddress(keyPair.publicKey);
|
|
431
|
+
|
|
432
|
+
const words = mnemonic.split(' ');
|
|
433
|
+
console.log(`\n${C.red}${C.bright}╔═══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
434
|
+
console.log(`${C.red}${C.bright}║ YOUR WALLET PASSPHRASE ║${C.reset}`);
|
|
435
|
+
console.log(`${C.red}${C.bright}╚═══════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
436
|
+
console.log(`\n${C.yellow} Write these words down. They cannot be recovered.${C.reset}\n`);
|
|
437
|
+
|
|
438
|
+
for (let i = 0; i < words.length; i += 3) {
|
|
439
|
+
const line = [];
|
|
440
|
+
for (let j = 0; j < 3 && i + j < words.length; j++) {
|
|
441
|
+
line.push(`${C.bright}${i + j + 1}.${C.reset} ${words[i + j].padEnd(15)}`);
|
|
442
|
+
}
|
|
443
|
+
console.log(` ${line.join(' ')}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await question(rl, `\n${C.cyan}Press Enter when you have saved your passphrase...${C.reset}`);
|
|
447
|
+
|
|
448
|
+
saveWalletFile(address, keyPair.publicKey);
|
|
449
|
+
cfg.defaultWallet = address;
|
|
450
|
+
saveConfig(cfg);
|
|
451
|
+
|
|
452
|
+
wallet = { address, ...loadWallet(address) };
|
|
453
|
+
printSuccess(`Wallet created: ${address}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { wallet, keyPair, mnemonic };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// Step 5: RPC Connection & Health Check
|
|
461
|
+
// ============================================================================
|
|
462
|
+
|
|
463
|
+
async function checkRpcConnection(rl) {
|
|
464
|
+
printStep(5, 6, 'RPC Connection & Health Check');
|
|
465
|
+
|
|
466
|
+
const defaultRpc = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
467
|
+
const rpcUrl = await askValue(rl, 'RPC endpoint', defaultRpc);
|
|
468
|
+
|
|
469
|
+
printInfo(`Connecting to ${rpcUrl}...`);
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
const client = new aether.AetherClient({ rpcUrl });
|
|
473
|
+
const [slot, health, version] = await Promise.all([
|
|
474
|
+
client.getSlot().catch(() => null),
|
|
475
|
+
client.getHealth().catch(() => null),
|
|
476
|
+
client.getVersion().catch(() => null),
|
|
477
|
+
]);
|
|
478
|
+
|
|
479
|
+
if (slot === null) {
|
|
480
|
+
throw new Error('RPC not responding with valid slot data');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
printSuccess(`Connected to RPC`);
|
|
484
|
+
console.log(` ${C.dim}Slot:${C.reset} ${slot}`);
|
|
485
|
+
console.log(` ${C.dim}Health:${C.reset} ${health || 'unknown'}`);
|
|
486
|
+
if (version) {
|
|
487
|
+
console.log(` ${C.dim}Version:${C.reset} ${version.aetherCore || JSON.stringify(version)}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return { rpcUrl, slot, client };
|
|
491
|
+
} catch (err) {
|
|
492
|
+
printError(`Failed to connect: ${err.message}`);
|
|
493
|
+
printWarning('You can continue and use a local validator later');
|
|
494
|
+
const cont = await askYesNo(rl, 'Continue with setup?', true);
|
|
495
|
+
if (!cont) {
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
return { rpcUrl, slot: 0, client: null };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ============================================================================
|
|
503
|
+
// Step 6: Faucet Funding (Testnet)
|
|
504
|
+
// ============================================================================
|
|
505
|
+
|
|
506
|
+
async function fundFromFaucet(rl, wallet, rpcUrl, tier) {
|
|
507
|
+
printStep(6, 6, 'Faucet Funding (Testnet)');
|
|
508
|
+
|
|
509
|
+
const minStake = TIER_REQUIREMENTS[tier].minStake;
|
|
510
|
+
|
|
511
|
+
console.log(`\n To register as a ${tier.toUpperCase()} validator, you need ${minStake} AETH.`);
|
|
512
|
+
console.log(` ${C.dim}Testnet faucets provide free AETH for development.${C.reset}\n`);
|
|
513
|
+
|
|
514
|
+
// First check if wallet already has funds
|
|
515
|
+
try {
|
|
516
|
+
const client = new aether.AetherClient({ rpcUrl });
|
|
517
|
+
const rawAddr = wallet.address.startsWith('ATH') ? wallet.address.slice(3) : wallet.address;
|
|
518
|
+
const balance = await client.getBalance(rawAddr);
|
|
519
|
+
const aethBalance = balance / 1e9;
|
|
520
|
+
|
|
521
|
+
if (aethBalance >= minStake) {
|
|
522
|
+
printSuccess(`Wallet already funded: ${aethBalance.toFixed(4)} AETH`);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
printInfo(`Current balance: ${aethBalance.toFixed(4)} AETH`);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
printWarning(`Could not check balance: ${err.message}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Try faucet funding
|
|
532
|
+
printInfo('Requesting testnet AETH from faucet...');
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
// Aether testnet faucet endpoint
|
|
536
|
+
const faucetUrl = process.env.AETHER_FAUCET_URL || 'http://127.0.0.1:8899';
|
|
537
|
+
const faucetEndpoint = `${faucetUrl}/v1/faucet`;
|
|
538
|
+
|
|
539
|
+
const result = await httpPost(faucetEndpoint, {
|
|
540
|
+
address: wallet.address,
|
|
541
|
+
amount: Math.max(minStake * 1e9, 10000 * 1e9), // Request min stake + buffer
|
|
542
|
+
}, 10000);
|
|
543
|
+
|
|
544
|
+
if (result.error) {
|
|
545
|
+
throw new Error(result.error.message || JSON.stringify(result.error));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
printSuccess(`Faucet request submitted!`);
|
|
549
|
+
if (result.signature) {
|
|
550
|
+
console.log(` ${C.dim}Tx:${C.reset} ${result.signature.slice(0, 30)}...`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Wait for funds
|
|
554
|
+
printInfo('Waiting for funds to arrive...');
|
|
555
|
+
await waitForBalance(rpcUrl, wallet.address, minStake * 1e9, 30);
|
|
556
|
+
|
|
557
|
+
return true;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
printWarning(`Faucet request failed: ${err.message}`);
|
|
560
|
+
printInfo('You can fund manually and re-run registration later');
|
|
561
|
+
printInfo(`Address: ${wallet.address}`);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function waitForBalance(rpcUrl, address, minLamports, timeoutSec) {
|
|
567
|
+
const client = new aether.AetherClient({ rpcUrl });
|
|
568
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
569
|
+
|
|
570
|
+
for (let i = 0; i < timeoutSec; i++) {
|
|
571
|
+
try {
|
|
572
|
+
const balance = await client.getBalance(rawAddr);
|
|
573
|
+
if (balance >= minLamports) {
|
|
574
|
+
printSuccess(`Balance confirmed: ${(balance / 1e9).toFixed(4)} AETH`);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
} catch {}
|
|
578
|
+
process.stdout.write('.');
|
|
579
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
580
|
+
}
|
|
581
|
+
console.log();
|
|
582
|
+
printWarning('Timeout waiting for funds');
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function httpPost(url, body, timeout) {
|
|
587
|
+
return new Promise((resolve, reject) => {
|
|
588
|
+
const urlObj = new URL(url);
|
|
589
|
+
const lib = urlObj.protocol === 'https:' ? https : http;
|
|
590
|
+
const bodyStr = JSON.stringify(body);
|
|
591
|
+
|
|
592
|
+
const req = lib.request({
|
|
593
|
+
hostname: urlObj.hostname,
|
|
594
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
595
|
+
path: urlObj.pathname,
|
|
596
|
+
method: 'POST',
|
|
597
|
+
timeout,
|
|
598
|
+
headers: {
|
|
599
|
+
'Content-Type': 'application/json',
|
|
600
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
601
|
+
},
|
|
602
|
+
}, (res) => {
|
|
603
|
+
let data = '';
|
|
604
|
+
res.on('data', chunk => data += chunk);
|
|
605
|
+
res.on('end', () => {
|
|
606
|
+
try {
|
|
607
|
+
resolve(JSON.parse(data));
|
|
608
|
+
} catch {
|
|
609
|
+
resolve({ raw: data });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
req.on('error', reject);
|
|
615
|
+
req.on('timeout', () => {
|
|
616
|
+
req.destroy();
|
|
617
|
+
reject(new Error('Request timeout'));
|
|
618
|
+
});
|
|
619
|
+
req.write(bodyStr);
|
|
620
|
+
req.end();
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ============================================================================
|
|
625
|
+
// Step 7: Validator Registration
|
|
626
|
+
// ============================================================================
|
|
627
|
+
|
|
628
|
+
async function registerValidator(rl, wallet, identity, tier, rpcUrl, keyPair) {
|
|
629
|
+
console.log();
|
|
630
|
+
printStep(6, 6, 'Validator Registration');
|
|
631
|
+
|
|
632
|
+
const minStake = TIER_REQUIREMENTS[tier].minStake;
|
|
633
|
+
const stakeLamports = Math.round(minStake * 1e9);
|
|
634
|
+
const rawWalletAddr = wallet.address.startsWith('ATH') ? wallet.address.slice(3) : wallet.address;
|
|
635
|
+
|
|
636
|
+
printInfo(`Registering validator with ${minStake} AETH stake...`);
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
const client = new aether.AetherClient({ rpcUrl });
|
|
640
|
+
const [slot, epochInfo] = await Promise.all([
|
|
641
|
+
client.getSlot().catch(() => 0),
|
|
642
|
+
client.getEpochInfo().catch(() => ({ epoch: 0 })),
|
|
643
|
+
]);
|
|
644
|
+
|
|
645
|
+
// Build registration transaction
|
|
646
|
+
const registration = {
|
|
647
|
+
identity_pubkey: identity.pubkey,
|
|
648
|
+
vote_account: rawWalletAddr,
|
|
649
|
+
stake_account: rawWalletAddr,
|
|
650
|
+
stake_lamports: stakeLamports,
|
|
651
|
+
tier: tier,
|
|
652
|
+
commission_bps: 1000, // 10%
|
|
653
|
+
name: `Validator-${identity.pubkey.slice(0, 8)}`,
|
|
654
|
+
registered_at: new Date().toISOString(),
|
|
655
|
+
slot: slot,
|
|
656
|
+
epoch: epochInfo.epoch || 0,
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const tx = {
|
|
660
|
+
signer: rawWalletAddr,
|
|
661
|
+
tx_type: 'ValidatorRegister',
|
|
662
|
+
payload: {
|
|
663
|
+
type: 'ValidatorRegister',
|
|
664
|
+
data: registration,
|
|
665
|
+
},
|
|
666
|
+
fee: 5000,
|
|
667
|
+
slot: slot,
|
|
668
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Sign transaction
|
|
672
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
673
|
+
|
|
674
|
+
printInfo(`Submitting registration transaction...`);
|
|
675
|
+
|
|
676
|
+
// Submit via SDK
|
|
677
|
+
const result = await client.sendTransaction(tx);
|
|
678
|
+
|
|
679
|
+
if (result.error) {
|
|
680
|
+
throw new Error(result.error.message || JSON.stringify(result.error));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Save to config
|
|
684
|
+
const cfg = loadConfig();
|
|
685
|
+
cfg.validators = cfg.validators || [];
|
|
686
|
+
cfg.validators.push({
|
|
687
|
+
identity: identity.pubkey,
|
|
688
|
+
vote_account: wallet.address,
|
|
689
|
+
tier: tier,
|
|
690
|
+
registered_at: new Date().toISOString(),
|
|
691
|
+
tx_signature: result.signature || result.txid,
|
|
692
|
+
});
|
|
693
|
+
saveConfig(cfg);
|
|
694
|
+
|
|
695
|
+
printSuccess('Validator registered successfully!');
|
|
696
|
+
console.log(` ${C.dim}Identity:${C.reset} ${identity.pubkey.slice(0, 30)}...`);
|
|
697
|
+
console.log(` ${C.dim}Stake:${C.reset} ${minStake} AETH`);
|
|
698
|
+
console.log(` ${C.dim}Tier:${C.reset} ${tier.toUpperCase()}`);
|
|
699
|
+
if (result.signature || result.txid) {
|
|
700
|
+
console.log(` ${C.dim}Tx:${C.reset} ${(result.signature || result.txid).slice(0, 40)}...`);
|
|
701
|
+
}
|
|
702
|
+
console.log(` ${C.dim}Slot:${C.reset} ${result.slot || slot}`);
|
|
703
|
+
|
|
704
|
+
return { success: true, result };
|
|
705
|
+
} catch (err) {
|
|
706
|
+
printError(`Registration failed: ${err.message}`);
|
|
707
|
+
printInfo('Common causes:');
|
|
708
|
+
printInfo(' • Validator already registered with this identity');
|
|
709
|
+
printInfo(' • Insufficient balance for stake + fees');
|
|
710
|
+
printInfo(' • RPC endpoint not accepting transactions');
|
|
711
|
+
return { success: false, error: err.message };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ============================================================================
|
|
716
|
+
// Completion Summary
|
|
717
|
+
// ============================================================================
|
|
718
|
+
|
|
719
|
+
async function printSummary(identity, wallet, tier, badge) {
|
|
720
|
+
console.log();
|
|
721
|
+
console.log(`${C.green}╔═══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
722
|
+
console.log(`${C.green}║ ║${C.reset}`);
|
|
723
|
+
console.log(`${C.green}║ ${C.bright}✅ VALIDATOR SETUP COMPLETE${C.reset}${C.green} ║${C.reset}`);
|
|
724
|
+
console.log(`${C.green}║ ${C.dim}Tier: ${badge}${C.reset}${C.green} ║${C.reset}`);
|
|
725
|
+
console.log(`${C.green}║ ║${C.reset}`);
|
|
726
|
+
console.log(`${C.green}╚═══════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
727
|
+
console.log();
|
|
728
|
+
|
|
729
|
+
console.log(`${C.bright}Identity:${C.reset}`);
|
|
730
|
+
console.log(` File: validator-identity.json`);
|
|
731
|
+
console.log(` Pubkey: ${identity.pubkey}`);
|
|
732
|
+
console.log();
|
|
733
|
+
|
|
734
|
+
console.log(`${C.bright}Wallet:${C.reset}`);
|
|
735
|
+
console.log(` Address: ${wallet.address}`);
|
|
736
|
+
console.log(` File: ~/.aether/wallets/${wallet.address}.json`);
|
|
737
|
+
console.log();
|
|
738
|
+
|
|
739
|
+
console.log(`${C.bright}Next steps:${C.reset}`);
|
|
740
|
+
console.log(` ${C.cyan}aether validator status${C.reset} Check validator status`);
|
|
741
|
+
console.log(` ${C.cyan}aether validator start${C.reset} Start the validator node`);
|
|
742
|
+
console.log(` ${C.cyan}aether network${C.reset} View network status`);
|
|
743
|
+
console.log(` ${C.cyan}aether wallet balance${C.reset} Check wallet balance`);
|
|
744
|
+
console.log(` ${C.cyan}aether stake-info <addr>${C.reset} Check stake positions`);
|
|
745
|
+
console.log();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ============================================================================
|
|
749
|
+
// Main Init Function
|
|
750
|
+
// ============================================================================
|
|
751
|
+
|
|
752
|
+
async function init() {
|
|
753
|
+
const rl = createRl();
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
printBanner();
|
|
757
|
+
|
|
758
|
+
// Step 1: Prerequisites
|
|
759
|
+
await checkPrerequisites(rl);
|
|
760
|
+
|
|
761
|
+
// Step 2: Tier Selection
|
|
762
|
+
const { tier, badge, minStake } = await selectTier(rl);
|
|
763
|
+
|
|
764
|
+
// Step 3: Identity
|
|
765
|
+
const identity = await generateIdentity(rl);
|
|
766
|
+
|
|
767
|
+
// Step 4: Wallet
|
|
768
|
+
const { wallet, keyPair } = await setupWallet(rl);
|
|
769
|
+
|
|
770
|
+
// Step 5: RPC Connection
|
|
771
|
+
const { rpcUrl, client } = await checkRpcConnection(rl);
|
|
772
|
+
|
|
773
|
+
// Step 6: Funding (if testnet and tier requires stake)
|
|
774
|
+
let funded = false;
|
|
775
|
+
if (tier !== 'observer') {
|
|
776
|
+
funded = await fundFromFaucet(rl, wallet, rpcUrl, tier);
|
|
777
|
+
} else {
|
|
778
|
+
printInfo('Observer tier requires no stake - skipping funding');
|
|
779
|
+
funded = true;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Step 7: Registration
|
|
783
|
+
const registered = await registerValidator(rl, wallet, identity, tier, rpcUrl, keyPair);
|
|
784
|
+
|
|
785
|
+
// Summary
|
|
786
|
+
await printSummary(identity, wallet, tier, badge);
|
|
787
|
+
|
|
788
|
+
// Offer to start validator
|
|
789
|
+
if (registered.success) {
|
|
790
|
+
const startNow = await askYesNo(rl, 'Start validator now?', true);
|
|
791
|
+
if (startNow) {
|
|
792
|
+
console.log();
|
|
793
|
+
printInfo('Starting validator...');
|
|
794
|
+
rl.close();
|
|
795
|
+
const { validatorStart } = require('./validator-start');
|
|
796
|
+
validatorStart({ testnet: true, tier });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
rl.close();
|
|
802
|
+
|
|
803
|
+
} catch (err) {
|
|
804
|
+
rl.close();
|
|
805
|
+
console.error(`\n${C.red}✗ Init failed:${C.reset} ${err.message}\n`);
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Export for module use
|
|
811
|
+
module.exports = { init };
|
|
812
|
+
|
|
813
|
+
// Run if called directly
|
|
814
|
+
if (require.main === module) {
|
|
815
|
+
init();
|
|
816
|
+
}
|