@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,851 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli config
|
|
4
|
+
*
|
|
5
|
+
* Centralized configuration management for the Aether CLI.
|
|
6
|
+
* Persistently store settings like RPC URL, default wallet, preferences.
|
|
7
|
+
* All RPC URLs are validated with real HTTP calls before saving.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aether config get <key> Get a config value
|
|
11
|
+
* aether config set <key> <value> Set a config value (validates RPC URLs)
|
|
12
|
+
* aether config list Show all config
|
|
13
|
+
* aether config init Create default config
|
|
14
|
+
* aether config reset Reset to defaults
|
|
15
|
+
* aether config validate Validate current config (test RPC, etc.)
|
|
16
|
+
* aether config import --file <path> Import config from JSON file
|
|
17
|
+
* aether config export --file <path> Export config to JSON file
|
|
18
|
+
*
|
|
19
|
+
* Config keys:
|
|
20
|
+
* rpc.url Default RPC endpoint (validates on set)
|
|
21
|
+
* rpc.backup Backup RPC endpoint
|
|
22
|
+
* wallet.default Default wallet address
|
|
23
|
+
* wallet.keypair Path to keypair file
|
|
24
|
+
* validator.tier Default validator tier (full|lite|observer)
|
|
25
|
+
* output.format Default output format (text|json)
|
|
26
|
+
* output.colors Enable/disable ANSI colors (true|false)
|
|
27
|
+
* network.timeout RPC timeout in ms (default: 10000)
|
|
28
|
+
*
|
|
29
|
+
* SDK wired to: GET /v1/health, GET /v1/slot, GET /v1/version
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const os = require('os');
|
|
35
|
+
const readline = require('readline');
|
|
36
|
+
|
|
37
|
+
// Import SDK for RPC validation
|
|
38
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
39
|
+
const aether = require(sdkPath);
|
|
40
|
+
|
|
41
|
+
// ANSI colours
|
|
42
|
+
const C = {
|
|
43
|
+
reset: '\x1b[0m',
|
|
44
|
+
bright: '\x1b[1m',
|
|
45
|
+
dim: '\x1b[2m',
|
|
46
|
+
red: '\x1b[31m',
|
|
47
|
+
green: '\x1b[32m',
|
|
48
|
+
yellow: '\x1b[33m',
|
|
49
|
+
cyan: '\x1b[36m',
|
|
50
|
+
magenta: '\x1b[35m',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const CLI_VERSION = '1.3.0';
|
|
54
|
+
const CONFIG_VERSION = 2;
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Paths & Config Management
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function getAetherDir() {
|
|
61
|
+
return path.join(os.homedir(), '.aether');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getConfigPath() {
|
|
65
|
+
return path.join(getAetherDir(), 'config.json');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensureAetherDir() {
|
|
69
|
+
const dir = getAetherDir();
|
|
70
|
+
if (!fs.existsSync(dir)) {
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Default configuration values
|
|
77
|
+
*/
|
|
78
|
+
function getDefaultConfig() {
|
|
79
|
+
return {
|
|
80
|
+
version: CONFIG_VERSION,
|
|
81
|
+
created_at: new Date().toISOString(),
|
|
82
|
+
updated_at: new Date().toISOString(),
|
|
83
|
+
rpc: {
|
|
84
|
+
url: 'http://127.0.0.1:8899',
|
|
85
|
+
backup: null,
|
|
86
|
+
timeout: 10000,
|
|
87
|
+
},
|
|
88
|
+
wallet: {
|
|
89
|
+
default: null,
|
|
90
|
+
keypair: null,
|
|
91
|
+
},
|
|
92
|
+
validator: {
|
|
93
|
+
tier: 'full',
|
|
94
|
+
identity: null,
|
|
95
|
+
},
|
|
96
|
+
output: {
|
|
97
|
+
format: 'text',
|
|
98
|
+
colors: true,
|
|
99
|
+
},
|
|
100
|
+
network: {
|
|
101
|
+
explorer: 'https://explorer.aether.network',
|
|
102
|
+
faucet: null,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load config from disk
|
|
109
|
+
*/
|
|
110
|
+
function loadConfig() {
|
|
111
|
+
const configPath = getConfigPath();
|
|
112
|
+
if (!fs.existsSync(configPath)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
117
|
+
return migrateConfig(config);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Save config to disk
|
|
125
|
+
*/
|
|
126
|
+
function saveConfig(config) {
|
|
127
|
+
ensureAetherDir();
|
|
128
|
+
config.updated_at = new Date().toISOString();
|
|
129
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
|
|
130
|
+
return config;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Migrate old config versions
|
|
135
|
+
*/
|
|
136
|
+
function migrateConfig(config) {
|
|
137
|
+
if (!config.version || config.version < CONFIG_VERSION) {
|
|
138
|
+
// Merge with defaults to add any missing keys
|
|
139
|
+
const defaults = getDefaultConfig();
|
|
140
|
+
const migrated = {
|
|
141
|
+
...defaults,
|
|
142
|
+
...config,
|
|
143
|
+
version: CONFIG_VERSION,
|
|
144
|
+
rpc: { ...defaults.rpc, ...config.rpc },
|
|
145
|
+
wallet: { ...defaults.wallet, ...config.wallet },
|
|
146
|
+
validator: { ...defaults.validator, ...config.validator },
|
|
147
|
+
output: { ...defaults.output, ...config.output },
|
|
148
|
+
network: { ...defaults.network, ...config.network },
|
|
149
|
+
};
|
|
150
|
+
return migrated;
|
|
151
|
+
}
|
|
152
|
+
return config;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Validation Functions (with REAL RPC calls)
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate an RPC URL with real HTTP call
|
|
161
|
+
*/
|
|
162
|
+
async function validateRpcUrl(url) {
|
|
163
|
+
const errors = [];
|
|
164
|
+
|
|
165
|
+
// Basic URL validation
|
|
166
|
+
if (!url || typeof url !== 'string') {
|
|
167
|
+
return { valid: false, error: 'URL is required' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = new URL(url);
|
|
173
|
+
} catch {
|
|
174
|
+
return { valid: false, error: 'Invalid URL format' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
178
|
+
return { valid: false, error: 'URL must be http:// or https://' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Real RPC validation via SDK
|
|
182
|
+
try {
|
|
183
|
+
const client = new aether.AetherClient({ rpcUrl: url, timeoutMs: 5000 });
|
|
184
|
+
const start = Date.now();
|
|
185
|
+
const [health, slot] = await Promise.all([
|
|
186
|
+
client.getHealth().catch(() => null),
|
|
187
|
+
client.getSlot().catch(() => null),
|
|
188
|
+
]);
|
|
189
|
+
const latency = Date.now() - start;
|
|
190
|
+
|
|
191
|
+
if (slot === null && health === null) {
|
|
192
|
+
return {
|
|
193
|
+
valid: false,
|
|
194
|
+
error: 'RPC endpoint did not respond to health/slot checks',
|
|
195
|
+
url,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
valid: true,
|
|
201
|
+
url,
|
|
202
|
+
latency,
|
|
203
|
+
health: health || 'unknown',
|
|
204
|
+
slot,
|
|
205
|
+
};
|
|
206
|
+
} catch (err) {
|
|
207
|
+
return {
|
|
208
|
+
valid: false,
|
|
209
|
+
error: `RPC validation failed: ${err.message}`,
|
|
210
|
+
url,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Validate wallet address format
|
|
217
|
+
*/
|
|
218
|
+
function validateAddress(addr) {
|
|
219
|
+
if (!addr || typeof addr !== 'string') {
|
|
220
|
+
return { valid: false, error: 'Address is required' };
|
|
221
|
+
}
|
|
222
|
+
if (!addr.startsWith('ATH')) {
|
|
223
|
+
return { valid: false, error: 'Address must start with ATH' };
|
|
224
|
+
}
|
|
225
|
+
if (addr.length < 36) {
|
|
226
|
+
return { valid: false, error: 'Address too short' };
|
|
227
|
+
}
|
|
228
|
+
return { valid: true, address: addr };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Validate tier value
|
|
233
|
+
*/
|
|
234
|
+
function validateTier(tier) {
|
|
235
|
+
const validTiers = ['full', 'lite', 'observer'];
|
|
236
|
+
if (!tier || typeof tier !== 'string') {
|
|
237
|
+
return { valid: false, error: 'Tier is required' };
|
|
238
|
+
}
|
|
239
|
+
if (!validTiers.includes(tier.toLowerCase())) {
|
|
240
|
+
return { valid: false, error: `Tier must be one of: ${validTiers.join(', ')}` };
|
|
241
|
+
}
|
|
242
|
+
return { valid: true, tier: tier.toLowerCase() };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validate output format
|
|
247
|
+
*/
|
|
248
|
+
function validateFormat(format) {
|
|
249
|
+
const validFormats = ['text', 'json'];
|
|
250
|
+
if (!format || typeof format !== 'string') {
|
|
251
|
+
return { valid: false, error: 'Format is required' };
|
|
252
|
+
}
|
|
253
|
+
if (!validFormats.includes(format.toLowerCase())) {
|
|
254
|
+
return { valid: false, error: `Format must be one of: ${validFormats.join(', ')}` };
|
|
255
|
+
}
|
|
256
|
+
return { valid: true, format: format.toLowerCase() };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Validate boolean string
|
|
261
|
+
*/
|
|
262
|
+
function validateBoolean(value) {
|
|
263
|
+
const truthy = ['true', 'yes', '1', 'on'];
|
|
264
|
+
const falsy = ['false', 'no', '0', 'off'];
|
|
265
|
+
const lower = String(value).toLowerCase();
|
|
266
|
+
if (truthy.includes(lower)) return { valid: true, value: true };
|
|
267
|
+
if (falsy.includes(lower)) return { valid: true, value: false };
|
|
268
|
+
return { valid: false, error: 'Must be true/false, yes/no, 1/0, or on/off' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate timeout
|
|
273
|
+
*/
|
|
274
|
+
function validateTimeout(value) {
|
|
275
|
+
const num = parseInt(value, 10);
|
|
276
|
+
if (isNaN(num) || num < 1000) {
|
|
277
|
+
return { valid: false, error: 'Timeout must be at least 1000ms' };
|
|
278
|
+
}
|
|
279
|
+
if (num > 60000) {
|
|
280
|
+
return { valid: false, error: 'Timeout cannot exceed 60000ms' };
|
|
281
|
+
}
|
|
282
|
+
return { valid: true, timeout: num };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Config Value Getters/Setters
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
const CONFIG_SCHEMA = {
|
|
290
|
+
'rpc.url': {
|
|
291
|
+
category: 'rpc',
|
|
292
|
+
key: 'url',
|
|
293
|
+
validate: validateRpcUrl,
|
|
294
|
+
description: 'Default RPC endpoint URL',
|
|
295
|
+
},
|
|
296
|
+
'rpc.backup': {
|
|
297
|
+
category: 'rpc',
|
|
298
|
+
key: 'backup',
|
|
299
|
+
validate: validateRpcUrl,
|
|
300
|
+
description: 'Backup RPC endpoint URL',
|
|
301
|
+
},
|
|
302
|
+
'rpc.timeout': {
|
|
303
|
+
category: 'rpc',
|
|
304
|
+
key: 'timeout',
|
|
305
|
+
validate: validateTimeout,
|
|
306
|
+
description: 'RPC timeout in milliseconds',
|
|
307
|
+
},
|
|
308
|
+
'wallet.default': {
|
|
309
|
+
category: 'wallet',
|
|
310
|
+
key: 'default',
|
|
311
|
+
validate: validateAddress,
|
|
312
|
+
description: 'Default wallet address',
|
|
313
|
+
},
|
|
314
|
+
'wallet.keypair': {
|
|
315
|
+
category: 'wallet',
|
|
316
|
+
key: 'keypair',
|
|
317
|
+
validate: (v) => ({ valid: fs.existsSync(v), path: v, error: fs.existsSync(v) ? null : 'File does not exist' }),
|
|
318
|
+
description: 'Path to keypair file',
|
|
319
|
+
},
|
|
320
|
+
'validator.tier': {
|
|
321
|
+
category: 'validator',
|
|
322
|
+
key: 'tier',
|
|
323
|
+
validate: validateTier,
|
|
324
|
+
description: 'Default validator tier (full|lite|observer)',
|
|
325
|
+
},
|
|
326
|
+
'validator.identity': {
|
|
327
|
+
category: 'validator',
|
|
328
|
+
key: 'identity',
|
|
329
|
+
validate: (v) => ({ valid: true, path: v }),
|
|
330
|
+
description: 'Path to validator identity file',
|
|
331
|
+
},
|
|
332
|
+
'output.format': {
|
|
333
|
+
category: 'output',
|
|
334
|
+
key: 'format',
|
|
335
|
+
validate: validateFormat,
|
|
336
|
+
description: 'Default output format (text|json)',
|
|
337
|
+
},
|
|
338
|
+
'output.colors': {
|
|
339
|
+
category: 'output',
|
|
340
|
+
key: 'colors',
|
|
341
|
+
validate: validateBoolean,
|
|
342
|
+
description: 'Enable ANSI colors (true|false)',
|
|
343
|
+
},
|
|
344
|
+
'network.explorer': {
|
|
345
|
+
category: 'network',
|
|
346
|
+
key: 'explorer',
|
|
347
|
+
validate: (v) => ({ valid: true, url: v }),
|
|
348
|
+
description: 'Block explorer URL',
|
|
349
|
+
},
|
|
350
|
+
'network.faucet': {
|
|
351
|
+
category: 'network',
|
|
352
|
+
key: 'faucet',
|
|
353
|
+
validate: (v) => ({ valid: true, url: v }),
|
|
354
|
+
description: 'Testnet faucet URL',
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
function getConfigValue(config, key) {
|
|
359
|
+
const schema = CONFIG_SCHEMA[key];
|
|
360
|
+
if (!schema) return { exists: false };
|
|
361
|
+
|
|
362
|
+
const value = config[schema.category]?.[schema.key];
|
|
363
|
+
return { exists: true, value, schema };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function setConfigValue(config, key, value) {
|
|
367
|
+
const schema = CONFIG_SCHEMA[key];
|
|
368
|
+
if (!schema) {
|
|
369
|
+
return { success: false, error: `Unknown config key: ${key}` };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Validate the value
|
|
373
|
+
const validation = await schema.validate(value);
|
|
374
|
+
if (!validation.valid) {
|
|
375
|
+
return { success: false, error: validation.error };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Set the value
|
|
379
|
+
config[schema.category][schema.key] = validation[schema.key] ?? validation.value ?? validation.url ?? validation.path ?? value;
|
|
380
|
+
return { success: true, value: config[schema.category][schema.key] };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Command Implementations
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
async function configGet(args) {
|
|
388
|
+
const key = args[0];
|
|
389
|
+
const config = loadConfig() || getDefaultConfig();
|
|
390
|
+
|
|
391
|
+
if (!key) {
|
|
392
|
+
console.log(`\n ${C.red}✗ Config key required${C.reset}`);
|
|
393
|
+
console.log(` ${C.dim}Usage: aether config get <key>${C.reset}`);
|
|
394
|
+
console.log(` ${C.dim}Run 'aether config list' to see all keys${C.reset}\n`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const result = getConfigValue(config, key);
|
|
399
|
+
if (!result.exists) {
|
|
400
|
+
console.log(`\n ${C.red}✗ Unknown config key:${C.reset} ${key}`);
|
|
401
|
+
console.log(` ${C.dim}Run 'aether config list' to see available keys${C.reset}\n`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(`\n ${C.bright}${C.cyan}── Config Value ──${C.reset}\n`);
|
|
406
|
+
console.log(` ${C.cyan}Key:${C.reset} ${key}`);
|
|
407
|
+
console.log(` ${C.cyan}Description:${C.reset} ${result.schema.description}`);
|
|
408
|
+
console.log(` ${C.cyan}Value:${C.reset} ${result.value !== null ? C.bright + result.value + C.reset : C.dim + '(not set)' + C.reset}`);
|
|
409
|
+
console.log();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function configSet(args) {
|
|
413
|
+
const key = args[0];
|
|
414
|
+
const value = args[1];
|
|
415
|
+
|
|
416
|
+
if (!key || value === undefined) {
|
|
417
|
+
console.log(`\n ${C.red}✗ Key and value required${C.reset}`);
|
|
418
|
+
console.log(` ${C.dim}Usage: aether config set <key> <value>${C.reset}\n`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let config = loadConfig();
|
|
423
|
+
if (!config) {
|
|
424
|
+
config = getDefaultConfig();
|
|
425
|
+
console.log(` ${C.yellow}⚠ No existing config. Creating new config file...${C.reset}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const schema = CONFIG_SCHEMA[key];
|
|
429
|
+
if (!schema) {
|
|
430
|
+
console.log(`\n ${C.red}✗ Unknown config key:${C.reset} ${key}`);
|
|
431
|
+
console.log(` ${C.dim}Run 'aether config list' to see available keys${C.reset}\n`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Show validation message for RPC URLs
|
|
436
|
+
if (key.includes('rpc.')) {
|
|
437
|
+
console.log(`\n ${C.dim}Validating RPC endpoint...${C.reset}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const result = await setConfigValue(config, key, value);
|
|
441
|
+
|
|
442
|
+
if (!result.success) {
|
|
443
|
+
console.log(`\n ${C.red}✗ Validation failed:${C.reset} ${result.error}\n`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
saveConfig(config);
|
|
448
|
+
|
|
449
|
+
console.log(`\n ${C.green}✓ Config updated${C.reset}\n`);
|
|
450
|
+
console.log(` ${C.cyan}Key:${C.reset} ${key}`);
|
|
451
|
+
console.log(` ${C.cyan}Value:${C.reset} ${C.bright}${result.value}${C.reset}`);
|
|
452
|
+
|
|
453
|
+
// Show extra info for RPC
|
|
454
|
+
if (key === 'rpc.url' && result.latency) {
|
|
455
|
+
const latencyColor = result.latency < 50 ? C.green : result.latency < 200 ? C.cyan : C.yellow;
|
|
456
|
+
console.log(` ${C.cyan}Health:${C.reset} ${C.green}✓${C.reset} Online (${latencyColor}${result.latency}ms${C.reset})`);
|
|
457
|
+
if (result.slot) {
|
|
458
|
+
console.log(` ${C.cyan}Slot:${C.reset} ${result.slot.toLocaleString()}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
console.log();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function configList(args, asJson = false) {
|
|
466
|
+
const config = loadConfig() || getDefaultConfig();
|
|
467
|
+
const isCompact = args.includes('--compact');
|
|
468
|
+
|
|
469
|
+
if (asJson) {
|
|
470
|
+
console.log(JSON.stringify(config, null, 2));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
console.log(`\n ${C.bright}${C.cyan}── Aether CLI Configuration ──${C.reset}\n`);
|
|
475
|
+
console.log(` ${C.dim}Config file: ${getConfigPath()}${C.reset}`);
|
|
476
|
+
console.log(` ${C.dim}Version: ${config.version}${C.reset}\n`);
|
|
477
|
+
|
|
478
|
+
// Group by category
|
|
479
|
+
const categories = {};
|
|
480
|
+
Object.entries(CONFIG_SCHEMA).forEach(([key, schema]) => {
|
|
481
|
+
if (!categories[schema.category]) {
|
|
482
|
+
categories[schema.category] = [];
|
|
483
|
+
}
|
|
484
|
+
const value = config[schema.category]?.[schema.key];
|
|
485
|
+
categories[schema.category].push({ key, value, schema });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
for (const [category, items] of Object.entries(categories)) {
|
|
489
|
+
console.log(` ${C.bright}${C.cyan}[${category.toUpperCase()}]${C.reset}`);
|
|
490
|
+
for (const item of items) {
|
|
491
|
+
const displayValue = item.value !== null
|
|
492
|
+
? C.bright + String(item.value) + C.reset
|
|
493
|
+
: C.dim + '(not set)' + C.reset;
|
|
494
|
+
if (isCompact) {
|
|
495
|
+
console.log(` ${C.dim}${item.key}:${C.reset} ${displayValue}`);
|
|
496
|
+
} else {
|
|
497
|
+
console.log(` ${C.cyan}${item.key}${C.reset}`);
|
|
498
|
+
console.log(` ${C.dim}${item.schema.description}${C.reset}`);
|
|
499
|
+
console.log(` ${C.dim}Value:${C.reset} ${displayValue}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
console.log();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
console.log(` ${C.dim}Tip: Use ${C.cyan}aether config set <key> <value>${C.reset}${C.dim} to change settings${C.reset}`);
|
|
506
|
+
console.log(` ${C.dim} Run ${C.cyan}aether config validate${C.reset}${C.dim} to test your configuration${C.reset}\n`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function configInit(args) {
|
|
510
|
+
const force = args.includes('--force');
|
|
511
|
+
const configPath = getConfigPath();
|
|
512
|
+
|
|
513
|
+
if (fs.existsSync(configPath) && !force) {
|
|
514
|
+
console.log(`\n ${C.yellow}⚠ Config file already exists${C.reset}`);
|
|
515
|
+
console.log(` ${C.dim}Path: ${configPath}${C.reset}`);
|
|
516
|
+
console.log(` ${C.dim}Use --force to overwrite${C.reset}\n`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const config = getDefaultConfig();
|
|
521
|
+
saveConfig(config);
|
|
522
|
+
|
|
523
|
+
console.log(`\n ${C.green}✓ Configuration initialized${C.reset}\n`);
|
|
524
|
+
console.log(` ${C.dim}Path: ${configPath}${C.reset}`);
|
|
525
|
+
console.log(` ${C.dim}Edit with:${C.reset} ${C.cyan}aether config set <key> <value>${C.reset}`);
|
|
526
|
+
console.log(` ${C.dim}View with:${C.reset} ${C.cyan}aether config list${C.reset}\n`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function configReset(args) {
|
|
530
|
+
const confirmed = args.includes('--yes');
|
|
531
|
+
|
|
532
|
+
if (!confirmed) {
|
|
533
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
534
|
+
const answer = await new Promise(resolve => {
|
|
535
|
+
rl.question(`\n ${C.yellow}⚠ This will reset all configuration to defaults.${C.reset}\n Continue? [y/N] `, resolve);
|
|
536
|
+
});
|
|
537
|
+
rl.close();
|
|
538
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
539
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const config = getDefaultConfig();
|
|
545
|
+
saveConfig(config);
|
|
546
|
+
|
|
547
|
+
console.log(`\n ${C.green}✓ Configuration reset to defaults${C.reset}\n`);
|
|
548
|
+
console.log(` ${C.dim}Config file: ${getConfigPath()}${C.reset}\n`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function configValidate(args, asJson = false) {
|
|
552
|
+
const config = loadConfig();
|
|
553
|
+
|
|
554
|
+
if (!config) {
|
|
555
|
+
const error = { error: 'No configuration file found. Run: aether config init' };
|
|
556
|
+
if (asJson) {
|
|
557
|
+
console.log(JSON.stringify(error, null, 2));
|
|
558
|
+
} else {
|
|
559
|
+
console.log(`\n ${C.red}✗ No configuration found${C.reset}`);
|
|
560
|
+
console.log(` ${C.dim}Run: aether config init${C.reset}\n`);
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const results = {
|
|
566
|
+
config_valid: true,
|
|
567
|
+
checks: [],
|
|
568
|
+
timestamp: new Date().toISOString(),
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
if (!asJson) {
|
|
572
|
+
console.log(`\n ${C.bright}${C.cyan}── Configuration Validation ──${C.reset}\n`);
|
|
573
|
+
console.log(` ${C.dim}Testing RPC endpoints and settings...${C.reset}\n`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Test primary RPC
|
|
577
|
+
if (config.rpc?.url) {
|
|
578
|
+
if (!asJson) console.log(` ${C.dim}Testing primary RPC: ${config.rpc.url}${C.reset}`);
|
|
579
|
+
const rpcTest = await validateRpcUrl(config.rpc.url);
|
|
580
|
+
results.checks.push({
|
|
581
|
+
name: 'Primary RPC',
|
|
582
|
+
key: 'rpc.url',
|
|
583
|
+
passed: rpcTest.valid,
|
|
584
|
+
details: rpcTest.valid
|
|
585
|
+
? { latency: rpcTest.latency, slot: rpcTest.slot, health: rpcTest.health }
|
|
586
|
+
: { error: rpcTest.error },
|
|
587
|
+
});
|
|
588
|
+
if (!asJson) {
|
|
589
|
+
const icon = rpcTest.valid ? `${C.green}✓` : `${C.red}✗`;
|
|
590
|
+
const status = rpcTest.valid ? `${C.green}Online` : `${C.red}Failed`;
|
|
591
|
+
console.log(` ${icon} Primary RPC: ${status}${C.reset}`);
|
|
592
|
+
if (rpcTest.valid) {
|
|
593
|
+
const latencyColor = rpcTest.latency < 50 ? C.green : rpcTest.latency < 200 ? C.cyan : C.yellow;
|
|
594
|
+
console.log(` ${C.dim}Latency: ${latencyColor}${rpcTest.latency}ms${C.reset}`);
|
|
595
|
+
console.log(` ${C.dim}Slot: ${rpcTest.slot?.toLocaleString()}${C.reset}`);
|
|
596
|
+
} else {
|
|
597
|
+
console.log(` ${C.red}Error: ${rpcTest.error}${C.reset}`);
|
|
598
|
+
results.config_valid = false;
|
|
599
|
+
}
|
|
600
|
+
} else if (!rpcTest.valid) {
|
|
601
|
+
results.config_valid = false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Test backup RPC if set
|
|
606
|
+
if (config.rpc?.backup) {
|
|
607
|
+
if (!asJson) console.log(`\n ${C.dim}Testing backup RPC: ${config.rpc.backup}${C.reset}`);
|
|
608
|
+
const backupTest = await validateRpcUrl(config.rpc.backup);
|
|
609
|
+
results.checks.push({
|
|
610
|
+
name: 'Backup RPC',
|
|
611
|
+
key: 'rpc.backup',
|
|
612
|
+
passed: backupTest.valid,
|
|
613
|
+
details: backupTest.valid
|
|
614
|
+
? { latency: backupTest.latency, slot: backupTest.slot }
|
|
615
|
+
: { error: backupTest.error },
|
|
616
|
+
});
|
|
617
|
+
if (!asJson) {
|
|
618
|
+
const icon = backupTest.valid ? `${C.green}✓` : `${C.yellow}⚠`;
|
|
619
|
+
const status = backupTest.valid ? `${C.green}Online` : `${C.yellow}Unreachable`;
|
|
620
|
+
console.log(` ${icon} Backup RPC: ${status}${C.reset}`);
|
|
621
|
+
if (backupTest.valid) {
|
|
622
|
+
const latencyColor = backupTest.latency < 50 ? C.green : backupTest.latency < 200 ? C.cyan : C.yellow;
|
|
623
|
+
console.log(` ${C.dim}Latency: ${latencyColor}${backupTest.latency}ms${C.reset}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Validate default wallet
|
|
629
|
+
if (config.wallet?.default) {
|
|
630
|
+
const addrTest = validateAddress(config.wallet.default);
|
|
631
|
+
results.checks.push({
|
|
632
|
+
name: 'Default Wallet',
|
|
633
|
+
key: 'wallet.default',
|
|
634
|
+
passed: addrTest.valid,
|
|
635
|
+
details: addrTest.valid ? { address: addrTest.address } : { error: addrTest.error },
|
|
636
|
+
});
|
|
637
|
+
if (!asJson) {
|
|
638
|
+
const icon = addrTest.valid ? `${C.green}✓` : `${C.yellow}⚠`;
|
|
639
|
+
const status = addrTest.valid ? `${C.green}Valid` : `${C.yellow}Invalid format`;
|
|
640
|
+
console.log(`\n ${icon} Default wallet: ${status}${C.reset}`);
|
|
641
|
+
if (!addrTest.valid) {
|
|
642
|
+
console.log(` ${C.red}Error: ${addrTest.error}${C.reset}`);
|
|
643
|
+
} else {
|
|
644
|
+
console.log(` ${C.dim}Address: ${config.wallet.default}${C.reset}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Validate tier
|
|
650
|
+
if (config.validator?.tier) {
|
|
651
|
+
const tierTest = validateTier(config.validator.tier);
|
|
652
|
+
results.checks.push({
|
|
653
|
+
name: 'Validator Tier',
|
|
654
|
+
key: 'validator.tier',
|
|
655
|
+
passed: tierTest.valid,
|
|
656
|
+
details: tierTest.valid ? { tier: tierTest.tier } : { error: tierTest.error },
|
|
657
|
+
});
|
|
658
|
+
if (!asJson) {
|
|
659
|
+
const icon = tierTest.valid ? `${C.green}✓` : `${C.red}✗`;
|
|
660
|
+
console.log(`\n ${icon} Validator tier: ${tierTest.valid ? C.green + tierTest.tier : C.red + 'Invalid'}${C.reset}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Summary
|
|
665
|
+
const passed = results.checks.filter(c => c.passed).length;
|
|
666
|
+
const total = results.checks.length;
|
|
667
|
+
|
|
668
|
+
if (asJson) {
|
|
669
|
+
results.summary = { passed, total, success_rate: Math.round((passed / total) * 100) };
|
|
670
|
+
console.log(JSON.stringify(results, null, 2));
|
|
671
|
+
} else {
|
|
672
|
+
console.log(`\n ${C.bright}Validation Summary:${C.reset} ${passed}/${total} checks passed`);
|
|
673
|
+
if (passed === total) {
|
|
674
|
+
console.log(`\n ${C.green}✓ Configuration is valid and RPC is reachable${C.reset}\n`);
|
|
675
|
+
} else if (results.config_valid) {
|
|
676
|
+
console.log(`\n ${C.yellow}⚠ Some optional checks failed, but config is usable${C.reset}\n`);
|
|
677
|
+
} else {
|
|
678
|
+
console.log(`\n ${C.red}✗ Configuration has errors that need to be fixed${C.reset}`);
|
|
679
|
+
console.log(` ${C.dim}Run: aether config set rpc.url <working-url>${C.reset}\n`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function configExport(args) {
|
|
685
|
+
const fileIdx = args.findIndex(a => a === '--file' || a === '-f');
|
|
686
|
+
const filePath = fileIdx !== -1 && args[fileIdx + 1] ? args[fileIdx + 1] : null;
|
|
687
|
+
|
|
688
|
+
if (!filePath) {
|
|
689
|
+
console.log(`\n ${C.red}✗ --file required${C.reset}`);
|
|
690
|
+
console.log(` ${C.dim}Usage: aether config export --file <path>${C.reset}\n`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const config = loadConfig() || getDefaultConfig();
|
|
695
|
+
const exportData = {
|
|
696
|
+
...config,
|
|
697
|
+
exported_at: new Date().toISOString(),
|
|
698
|
+
cli_version: CLI_VERSION,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
fs.writeFileSync(filePath, JSON.stringify(exportData, null, 2));
|
|
703
|
+
console.log(`\n ${C.green}✓ Configuration exported${C.reset}\n`);
|
|
704
|
+
console.log(` ${C.dim}File: ${filePath}${C.reset}\n`);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
console.log(`\n ${C.red}✗ Export failed:${C.reset} ${err.message}\n`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function configImport(args) {
|
|
711
|
+
const fileIdx = args.findIndex(a => a === '--file' || a === '-f');
|
|
712
|
+
const filePath = fileIdx !== -1 && args[fileIdx + 1] ? args[fileIdx + 1] : null;
|
|
713
|
+
const force = args.includes('--force');
|
|
714
|
+
|
|
715
|
+
if (!filePath) {
|
|
716
|
+
console.log(`\n ${C.red}✗ --file required${C.reset}`);
|
|
717
|
+
console.log(` ${C.dim}Usage: aether config import --file <path>${C.reset}\n`);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!fs.existsSync(filePath)) {
|
|
722
|
+
console.log(`\n ${C.red}✗ File not found:${C.reset} ${filePath}\n`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
const imported = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
728
|
+
const configPath = getConfigPath();
|
|
729
|
+
|
|
730
|
+
if (fs.existsSync(configPath) && !force) {
|
|
731
|
+
console.log(`\n ${C.yellow}⚠ Existing config will be overwritten${C.reset}`);
|
|
732
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
733
|
+
const answer = await new Promise(resolve => {
|
|
734
|
+
rl.question(` Continue? [y/N] `, resolve);
|
|
735
|
+
});
|
|
736
|
+
rl.close();
|
|
737
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
738
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const migrated = migrateConfig(imported);
|
|
744
|
+
saveConfig(migrated);
|
|
745
|
+
|
|
746
|
+
console.log(`\n ${C.green}✓ Configuration imported${C.reset}\n`);
|
|
747
|
+
console.log(` ${C.dim}From: ${filePath}${C.reset}`);
|
|
748
|
+
console.log(` ${C.dim}To: ${configPath}${C.reset}\n`);
|
|
749
|
+
} catch (err) {
|
|
750
|
+
console.log(`\n ${C.red}✗ Import failed:${C.reset} ${err.message}\n`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
// CLI Parser & Dispatcher
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
|
|
758
|
+
function parseArgs() {
|
|
759
|
+
// argv = [node, index.js, config, <subcmd>, ...]
|
|
760
|
+
return process.argv.slice(3);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function showHelp() {
|
|
764
|
+
console.log(`
|
|
765
|
+
${C.bright}${C.cyan}aether-cli config${C.reset} — Configuration Management
|
|
766
|
+
|
|
767
|
+
${C.bright}Usage:${C.reset}
|
|
768
|
+
aether config get <key> Get a config value
|
|
769
|
+
aether config set <key> <value> Set a config value (validates RPC URLs)
|
|
770
|
+
aether config list [--compact] Show all configuration
|
|
771
|
+
aether config init [--force] Create default config
|
|
772
|
+
aether config reset --yes Reset to defaults
|
|
773
|
+
aether config validate [--json] Validate config with real RPC calls
|
|
774
|
+
aether config export --file <path> Export config to JSON
|
|
775
|
+
aether config import --file <path> Import config from JSON
|
|
776
|
+
|
|
777
|
+
${C.bright}Config Keys:${C.reset}
|
|
778
|
+
rpc.url Default RPC endpoint (validates on set)
|
|
779
|
+
rpc.backup Backup RPC endpoint
|
|
780
|
+
rpc.timeout RPC timeout in milliseconds
|
|
781
|
+
wallet.default Default wallet address
|
|
782
|
+
wallet.keypair Path to keypair file
|
|
783
|
+
validator.tier Default tier (full|lite|observer)
|
|
784
|
+
validator.identity Path to validator identity
|
|
785
|
+
output.format Output format (text|json)
|
|
786
|
+
output.colors Enable colors (true|false)
|
|
787
|
+
network.explorer Block explorer URL
|
|
788
|
+
network.faucet Testnet faucet URL
|
|
789
|
+
|
|
790
|
+
${C.bright}Examples:${C.reset}
|
|
791
|
+
aether config set rpc.url http://localhost:8899
|
|
792
|
+
aether config set wallet.default ATHabc...
|
|
793
|
+
aether config set validator.tier full
|
|
794
|
+
aether config validate
|
|
795
|
+
aether config export --file ~/aether-config-backup.json
|
|
796
|
+
|
|
797
|
+
${C.dim}SDK Validation:${C.reset}
|
|
798
|
+
Setting rpc.url performs a real HTTP health check via GET /v1/health
|
|
799
|
+
`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function configCommand() {
|
|
803
|
+
const args = parseArgs();
|
|
804
|
+
const subcmd = args[0];
|
|
805
|
+
const subargs = args.slice(1);
|
|
806
|
+
const asJson = args.includes('--json') || args.includes('-j');
|
|
807
|
+
|
|
808
|
+
switch (subcmd) {
|
|
809
|
+
case 'get':
|
|
810
|
+
await configGet(subargs);
|
|
811
|
+
break;
|
|
812
|
+
case 'set':
|
|
813
|
+
await configSet(subargs);
|
|
814
|
+
break;
|
|
815
|
+
case 'list':
|
|
816
|
+
await configList(subargs, asJson);
|
|
817
|
+
break;
|
|
818
|
+
case 'init':
|
|
819
|
+
await configInit(subargs);
|
|
820
|
+
break;
|
|
821
|
+
case 'reset':
|
|
822
|
+
await configReset(subargs);
|
|
823
|
+
break;
|
|
824
|
+
case 'validate':
|
|
825
|
+
await configValidate(subargs, asJson);
|
|
826
|
+
break;
|
|
827
|
+
case 'export':
|
|
828
|
+
await configExport(subargs);
|
|
829
|
+
break;
|
|
830
|
+
case 'import':
|
|
831
|
+
await configImport(subargs);
|
|
832
|
+
break;
|
|
833
|
+
case '--help':
|
|
834
|
+
case '-h':
|
|
835
|
+
case 'help':
|
|
836
|
+
default:
|
|
837
|
+
showHelp();
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Export for module use
|
|
843
|
+
module.exports = { configCommand };
|
|
844
|
+
|
|
845
|
+
// Run if called directly
|
|
846
|
+
if (require.main === module) {
|
|
847
|
+
configCommand().catch(err => {
|
|
848
|
+
console.error(`\n${C.red}✗ Config command failed:${C.reset}`, err.message, '\n');
|
|
849
|
+
process.exit(1);
|
|
850
|
+
});
|
|
851
|
+
}
|