@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
|
@@ -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
|
+
}
|