@jellylegsai/aether-cli 1.9.2 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IMPLEMENTATION_REPORT.md +319 -0
- package/commands/blockheight.js +230 -0
- package/commands/call.js +981 -0
- package/commands/claim.js +98 -72
- package/commands/deploy.js +959 -0
- package/commands/index.js +2 -0
- package/commands/init.js +33 -49
- package/commands/network-diagnostics.js +706 -0
- package/commands/network.js +412 -429
- package/commands/rewards.js +311 -266
- package/commands/sdk.js +791 -656
- package/commands/slot.js +3 -11
- package/commands/stake.js +581 -516
- package/commands/supply.js +483 -391
- package/commands/token-accounts.js +275 -0
- package/commands/transfer.js +3 -11
- package/commands/unstake.js +3 -11
- package/commands/validator-start.js +681 -323
- package/commands/validator.js +959 -0
- package/commands/validators.js +623 -626
- package/commands/version.js +240 -0
- package/commands/wallet.js +17 -24
- package/cycle-report-issue-116.txt +165 -0
- package/index.js +501 -602
- package/lib/ui.js +623 -0
- package/package.json +10 -3
- package/sdk/index.d.ts +546 -0
- package/sdk/index.js +130 -0
- package/sdk/package.json +2 -1
package/commands/stake.js
CHANGED
|
@@ -1,516 +1,581 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* aether-cli stake
|
|
4
|
-
*
|
|
5
|
-
* First-class stake command - stake AETH to a validator.
|
|
6
|
-
* Fully wired to @
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* aether stake --validator <addr> --amount <aeth> [--address <wallet>]
|
|
10
|
-
* aether stake --validator ATHxxx... --amount 1000
|
|
11
|
-
* aether stake --validator ATHxxx... --amount 1000 --address ATHxxx...
|
|
12
|
-
* aether stake --list-validators # Show available validators to stake to
|
|
13
|
-
* aether stake --dry-run # Preview without submitting
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const os = require('os');
|
|
19
|
-
const readline = require('readline');
|
|
20
|
-
const nacl = require('tweetnacl');
|
|
21
|
-
const bs58 = require('bs58').default;
|
|
22
|
-
const bip39 = require('bip39');
|
|
23
|
-
|
|
24
|
-
// Import SDK for real blockchain RPC calls
|
|
25
|
-
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
26
|
-
const aether = require(sdkPath);
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
const C
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.log(`
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
${C.bright}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
${C.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
${C.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (!opts.json) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
else if (arg === '--
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
opts
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (!
|
|
342
|
-
console.log(`\n ${
|
|
343
|
-
console.log(` ${C.dim}
|
|
344
|
-
rl.close();
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
console.log(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (!
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
console.log(
|
|
408
|
-
rl.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
console.log(
|
|
492
|
-
console.log(`
|
|
493
|
-
console.log(`
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli stake
|
|
4
|
+
*
|
|
5
|
+
* First-class stake command - stake AETH to a validator.
|
|
6
|
+
* Fully wired to @jellylegsai/aether-sdk for real blockchain RPC calls.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* aether stake --validator <addr> --amount <aeth> [--address <wallet>]
|
|
10
|
+
* aether stake --validator ATHxxx... --amount 1000
|
|
11
|
+
* aether stake --validator ATHxxx... --amount 1000 --address ATHxxx...
|
|
12
|
+
* aether stake --list-validators # Show available validators to stake to
|
|
13
|
+
* aether stake --dry-run # Preview without submitting
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const readline = require('readline');
|
|
20
|
+
const nacl = require('tweetnacl');
|
|
21
|
+
const bs58 = require('bs58').default;
|
|
22
|
+
const bip39 = require('bip39');
|
|
23
|
+
|
|
24
|
+
// Import SDK for real blockchain RPC calls
|
|
25
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
26
|
+
const aether = require(sdkPath);
|
|
27
|
+
|
|
28
|
+
// Import UI framework for consistent branding
|
|
29
|
+
const { BRANDING, C, indicators, startSpinner, stopSpinner, drawBox, drawTable,
|
|
30
|
+
success, error, warning, info, code, highlight, value } = require('../lib/ui');
|
|
31
|
+
|
|
32
|
+
const CLI_VERSION = '2.0.0';
|
|
33
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// SDK Setup
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
function getDefaultRpc() {
|
|
40
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createClient(rpcUrl) {
|
|
44
|
+
return new aether.AetherClient({ rpcUrl });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Config & Wallet
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
function getAetherDir() {
|
|
52
|
+
return path.join(os.homedir(), '.aether');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getConfigPath() {
|
|
56
|
+
return path.join(getAetherDir(), 'config.json');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadConfig() {
|
|
60
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
61
|
+
return { defaultWallet: null };
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
|
65
|
+
} catch {
|
|
66
|
+
return { defaultWallet: null };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadWallet(address) {
|
|
71
|
+
const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
|
|
72
|
+
if (!fs.existsSync(fp)) return null;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Crypto Helpers
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
function deriveKeypair(mnemonic) {
|
|
85
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
86
|
+
throw new Error('Invalid mnemonic phrase');
|
|
87
|
+
}
|
|
88
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
89
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
90
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
91
|
+
return {
|
|
92
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
93
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatAddress(publicKey) {
|
|
98
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function signTransaction(tx, secretKey) {
|
|
102
|
+
const txBytes = Buffer.from(JSON.stringify(tx));
|
|
103
|
+
const sig = nacl.sign.detached(txBytes, secretKey);
|
|
104
|
+
return bs58.encode(sig);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Format Helpers
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
function formatAether(lamports) {
|
|
112
|
+
const aeth = Number(lamports) / 1e9;
|
|
113
|
+
if (aeth === 0) return '0 AETH';
|
|
114
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function shortAddress(addr) {
|
|
118
|
+
if (!addr || addr.length < 16) return addr || 'unknown';
|
|
119
|
+
return addr.slice(0, 8) + '...' + addr.slice(-8);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatPercent(val) {
|
|
123
|
+
if (val === undefined || val === null) return 'N/A';
|
|
124
|
+
return val.toFixed(2) + '%';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Readline Helpers
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
function createRl() {
|
|
132
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function question(rl, q) {
|
|
136
|
+
return new Promise((res) => rl.question(q, res));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function askMnemonic(rl, promptText) {
|
|
140
|
+
console.log(`\n${C.cyan}${promptText}${C.reset}`);
|
|
141
|
+
console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
|
|
142
|
+
const raw = await question(rl, ` > ${C.reset}`);
|
|
143
|
+
return raw.trim().toLowerCase();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Fetch Validators via SDK
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
async function fetchValidators(rpcUrl) {
|
|
151
|
+
const client = createClient(rpcUrl);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const validators = await client.getValidators();
|
|
155
|
+
if (!Array.isArray(validators)) return [];
|
|
156
|
+
return validators.map(v => ({
|
|
157
|
+
address: v.vote_account || v.pubkey || v.address || v.identity,
|
|
158
|
+
identity: v.identity || v.node_pubkey,
|
|
159
|
+
stake: v.stake_lamports || v.activated_stake || v.stake || 0,
|
|
160
|
+
commission: v.commission || v.commission_bps || 0,
|
|
161
|
+
apy: v.apy || v.return_rate || 0,
|
|
162
|
+
name: v.name || v.moniker || 'Unknown',
|
|
163
|
+
tier: v.tier || 'unknown',
|
|
164
|
+
active: v.active !== false && v.delinquent !== true,
|
|
165
|
+
}));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Show Help
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
function showHelp() {
|
|
176
|
+
console.log(BRANDING.commandBanner('stake', 'Stake AETH to a validator'));
|
|
177
|
+
|
|
178
|
+
console.log(`\n ${C.bright}USAGE${C.reset}`);
|
|
179
|
+
console.log(` ${code('aether stake --validator <addr> --amount <aeth> [--address <wallet>]')}`);
|
|
180
|
+
|
|
181
|
+
console.log(`\n ${C.bright}REQUIRED${C.reset}`);
|
|
182
|
+
console.log(` ${code('--validator <addr>')} Validator address to stake to`);
|
|
183
|
+
console.log(` ${code('--amount <aeth>')} Amount to stake in AETH`);
|
|
184
|
+
|
|
185
|
+
console.log(`\n ${C.bright}OPTIONS${C.reset}`);
|
|
186
|
+
console.log(` ${code('--address <addr>')} Wallet address (default: configured default)`);
|
|
187
|
+
console.log(` ${code('--rpc <url>')} RPC endpoint (default: AETHER_RPC env or localhost:8899)`);
|
|
188
|
+
console.log(` ${code('--json')} Output JSON for scripting`);
|
|
189
|
+
console.log(` ${code('--dry-run')} Preview stake without submitting`);
|
|
190
|
+
console.log(` ${code('--list-validators')} Show available validators on the network`);
|
|
191
|
+
console.log(` ${code('--force')} Skip confirmation prompts`);
|
|
192
|
+
|
|
193
|
+
console.log(`\n ${C.bright}SDK METHODS USED${C.reset}`);
|
|
194
|
+
console.log(` ${C.dim}client.getValidators() → GET /v1/validators${C.reset}`);
|
|
195
|
+
console.log(` ${C.dim}client.getAccountInfo() → GET /v1/account/<addr>${C.reset}`);
|
|
196
|
+
console.log(` ${C.dim}client.getSlot() → GET /v1/slot${C.reset}`);
|
|
197
|
+
console.log(` ${C.dim}client.sendTransaction() → POST /v1/transaction${C.reset}`);
|
|
198
|
+
|
|
199
|
+
console.log(`\n ${C.bright}EXAMPLES${C.reset}`);
|
|
200
|
+
console.log(` ${C.dim}$${C.reset} ${code('aether stake --validator ATHxxx... --amount 1000')}`);
|
|
201
|
+
console.log(` ${C.dim}$${C.reset} ${code('aether stake --validator ATHxxx... --amount 1000 --address ATHxxx...')}`);
|
|
202
|
+
console.log(` ${C.dim}$${C.reset} ${code('aether stake --list-validators')}`);
|
|
203
|
+
console.log(` ${C.dim}$${C.reset} ${code('aether stake --validator ATHxxx... --amount 1000 --dry-run')}`);
|
|
204
|
+
|
|
205
|
+
console.log(`\n ${C.bright}MINIMUM STAKE AMOUNTS${C.reset}`);
|
|
206
|
+
console.log(` Full: 10,000 AETH`);
|
|
207
|
+
console.log(` Lite: 1,000 AETH`);
|
|
208
|
+
console.log(` Observer: 0 AETH`);
|
|
209
|
+
|
|
210
|
+
console.log(`\n ${C.bright}NOTES${C.reset}`);
|
|
211
|
+
console.log(` ${C.dim}◆ Staked AETH begins earning rewards after one epoch (~2 days)${C.reset}`);
|
|
212
|
+
console.log(` ${C.dim}◆ Use 'aether stake-positions' to view your delegations${C.reset}`);
|
|
213
|
+
console.log(` ${C.dim}◆ Use 'aether unstake' to withdraw (has cooldown period)${C.reset}`);
|
|
214
|
+
console.log();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// List Validators Command
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
async function listValidatorsCommand(opts) {
|
|
222
|
+
if (!opts.json) {
|
|
223
|
+
console.log(BRANDING.commandBanner('stake --list-validators', 'Available Validators'));
|
|
224
|
+
console.log(`\n ${C.dim}Fetching validators from ${opts.rpc}...${C.reset}\n`);
|
|
225
|
+
startSpinner('Querying validators');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const validators = await fetchValidators(opts.rpc);
|
|
229
|
+
|
|
230
|
+
if (!opts.json) {
|
|
231
|
+
stopSpinner(true, 'Validators retrieved');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (validators.length === 0) {
|
|
235
|
+
if (opts.json) {
|
|
236
|
+
console.log(JSON.stringify({
|
|
237
|
+
success: false,
|
|
238
|
+
error: 'No validators found',
|
|
239
|
+
rpc: opts.rpc,
|
|
240
|
+
}, null, 2));
|
|
241
|
+
} else {
|
|
242
|
+
console.log(`\n ${warning('No validators found.')}`);
|
|
243
|
+
console.log(` ${C.dim}Check your RPC endpoint: ${opts.rpc}${C.reset}\n`);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
validators.sort((a, b) => b.stake - a.stake);
|
|
249
|
+
|
|
250
|
+
if (opts.json) {
|
|
251
|
+
console.log(JSON.stringify({
|
|
252
|
+
success: true,
|
|
253
|
+
count: validators.length,
|
|
254
|
+
validators: validators.map(v => ({
|
|
255
|
+
address: v.address,
|
|
256
|
+
name: v.name,
|
|
257
|
+
stake_aeth: v.stake / 1e9,
|
|
258
|
+
apy: v.apy,
|
|
259
|
+
tier: v.tier,
|
|
260
|
+
})),
|
|
261
|
+
}, null, 2));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Build table rows
|
|
266
|
+
const rows = validators.slice(0, 15).map((v, i) => {
|
|
267
|
+
const status = v.active ? indicators.success : indicators.warning;
|
|
268
|
+
const name = (v.name || 'Unknown').slice(0, 20);
|
|
269
|
+
const addr = shortAddress(v.address);
|
|
270
|
+
const stake = formatAether(v.stake);
|
|
271
|
+
const apy = formatPercent(v.apy);
|
|
272
|
+
return [status, `${i + 1}`, name, addr, stake, apy];
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
console.log(`\n ${C.bright}Found ${C.cyan}${validators.length}${C.reset} validators\n`);
|
|
276
|
+
|
|
277
|
+
console.log(drawTable(
|
|
278
|
+
['', '#', 'Name', 'Address', 'Stake', 'APY'],
|
|
279
|
+
rows,
|
|
280
|
+
{ borderStyle: 'single', headerColor: C.cyan + C.bright }
|
|
281
|
+
));
|
|
282
|
+
|
|
283
|
+
if (validators.length > 15) {
|
|
284
|
+
console.log(`\n ${C.dim}... and ${validators.length - 15} more validators (use --json for full list)${C.reset}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(`\n ${C.dim}To stake: ${code('aether stake --validator <addr> --amount <aeth>')}${C.reset}\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Main Stake Logic
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
async function stakeCommand() {
|
|
295
|
+
const opts = {
|
|
296
|
+
validator: null,
|
|
297
|
+
amount: null,
|
|
298
|
+
address: null,
|
|
299
|
+
rpc: getDefaultRpc(),
|
|
300
|
+
json: false,
|
|
301
|
+
dryRun: false,
|
|
302
|
+
listValidators: false,
|
|
303
|
+
force: false,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Parse args
|
|
307
|
+
const args = process.argv.slice(3);
|
|
308
|
+
for (let i = 0; i < args.length; i++) {
|
|
309
|
+
const arg = args[i];
|
|
310
|
+
if (arg === '--validator' || arg === '-v') opts.validator = args[++i];
|
|
311
|
+
else if (arg === '--amount' || arg === '-m') {
|
|
312
|
+
const val = parseFloat(args[++i]);
|
|
313
|
+
if (!isNaN(val) && val > 0) opts.amount = val;
|
|
314
|
+
}
|
|
315
|
+
else if (arg === '--address' || arg === '-a') opts.address = args[++i];
|
|
316
|
+
else if (arg === '--rpc' || arg === '-r') opts.rpc = args[++i];
|
|
317
|
+
else if (arg === '--json' || arg === '-j') opts.json = true;
|
|
318
|
+
else if (arg === '--dry-run') opts.dryRun = true;
|
|
319
|
+
else if (arg === '--list-validators' || arg === '-l') opts.listValidators = true;
|
|
320
|
+
else if (arg === '--force' || arg === '-f') opts.force = true;
|
|
321
|
+
else if (arg === '--help' || arg === '-h') {
|
|
322
|
+
showHelp();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// List validators mode
|
|
328
|
+
if (opts.listValidators) {
|
|
329
|
+
await listValidatorsCommand(opts);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const rl = createRl();
|
|
334
|
+
|
|
335
|
+
// Resolve wallet address
|
|
336
|
+
if (!opts.address) {
|
|
337
|
+
const cfg = loadConfig();
|
|
338
|
+
opts.address = cfg.defaultWallet;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!opts.address) {
|
|
342
|
+
console.log(`\n ${error('No wallet address.')} Use --address <addr> or set a default.`);
|
|
343
|
+
console.log(` ${C.dim}Usage: aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
|
|
344
|
+
rl.close();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check wallet exists
|
|
349
|
+
const wallet = loadWallet(opts.address);
|
|
350
|
+
if (!wallet) {
|
|
351
|
+
console.log(`\n ${error('Wallet not found:')} ${opts.address}`);
|
|
352
|
+
console.log(` ${C.dim}Import it: ${code('aether wallet import')}${C.reset}\n`);
|
|
353
|
+
rl.close();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Fetch balance via SDK
|
|
358
|
+
let balance = 0;
|
|
359
|
+
const client = createClient(opts.rpc);
|
|
360
|
+
const rawAddr = opts.address.startsWith('ATH') ? opts.address.slice(3) : opts.address;
|
|
361
|
+
|
|
362
|
+
if (!opts.json) {
|
|
363
|
+
startSpinner('Fetching wallet balance');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const account = await client.getAccountInfo(rawAddr);
|
|
368
|
+
balance = account.lamports || 0;
|
|
369
|
+
} catch (err) {
|
|
370
|
+
if (!opts.json) console.log(` ${warning('Could not fetch balance:')} ${err.message}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!opts.json) {
|
|
374
|
+
stopSpinner(true, 'Balance retrieved');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Interactive validator selection
|
|
378
|
+
let validator = opts.validator;
|
|
379
|
+
if (!validator) {
|
|
380
|
+
if (!opts.json) {
|
|
381
|
+
console.log(`\n ${C.dim}Fetching validators...${C.reset}`);
|
|
382
|
+
startSpinner('Querying network');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const validators = await fetchValidators(opts.rpc);
|
|
386
|
+
|
|
387
|
+
if (!opts.json) {
|
|
388
|
+
stopSpinner(true, 'Validators retrieved');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (validators.length === 0) {
|
|
392
|
+
console.log(`\n ${error('No validators found.')}\n`);
|
|
393
|
+
rl.close();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
validators.sort((a, b) => b.stake - a.stake);
|
|
398
|
+
console.log(`\n ${C.bright}Select a validator:${C.reset}\n`);
|
|
399
|
+
|
|
400
|
+
validators.slice(0, 10).forEach((v, i) => {
|
|
401
|
+
const name = (v.name || 'Unknown').slice(0, 18).padEnd(18);
|
|
402
|
+
const stake = formatAether(v.stake);
|
|
403
|
+
const apy = formatPercent(v.apy);
|
|
404
|
+
console.log(` ${C.green}${i + 1})${C.reset} ${name} | ${stake} | ${apy}`);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
console.log(`\n ${C.dim}Enter number [1-10] or validator address${C.reset}`);
|
|
408
|
+
const choice = await question(rl, ` Validator > ${C.reset}`);
|
|
409
|
+
const choiceNum = parseInt(choice.trim(), 10);
|
|
410
|
+
|
|
411
|
+
if (!isNaN(choiceNum) && choiceNum >= 1 && choiceNum <= 10) {
|
|
412
|
+
validator = validators[choiceNum - 1].address;
|
|
413
|
+
} else {
|
|
414
|
+
validator = choice.trim();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Resolve amount
|
|
419
|
+
let amount = opts.amount;
|
|
420
|
+
if (!amount) {
|
|
421
|
+
console.log(`\n ${C.cyan}Available:${C.reset} ${formatAether(balance)}`);
|
|
422
|
+
console.log(` ${C.dim}Minimum: Full=10K, Lite=1K, Observer=0${C.reset}`);
|
|
423
|
+
const amt = await question(rl, ` Amount (AETH) > ${C.reset}`);
|
|
424
|
+
amount = parseFloat(amt);
|
|
425
|
+
if (isNaN(amount) || amount <= 0) {
|
|
426
|
+
console.log(`\n ${error('Invalid amount.')}\n`);
|
|
427
|
+
rl.close();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const stakeLamports = Math.round(amount * 1e9);
|
|
433
|
+
const feeBuffer = 0.005 * 1e9;
|
|
434
|
+
|
|
435
|
+
if (stakeLamports + feeBuffer > balance) {
|
|
436
|
+
console.log(`\n ${error('Insufficient balance.')}`);
|
|
437
|
+
console.log(` Requested: ${formatAether(stakeLamports)}`);
|
|
438
|
+
console.log(` Balance: ${formatAether(balance)}\n`);
|
|
439
|
+
rl.close();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Display summary in a box
|
|
444
|
+
if (!opts.json) {
|
|
445
|
+
console.log();
|
|
446
|
+
console.log(drawBox(
|
|
447
|
+
`
|
|
448
|
+
${C.bright}STAKE SUMMARY${C.reset}
|
|
449
|
+
|
|
450
|
+
${C.cyan}Wallet:${C.reset} ${C.bright}${opts.address}${C.reset}
|
|
451
|
+
${C.cyan}Validator:${C.reset} ${C.bright}${shortAddress(validator)}${C.reset}
|
|
452
|
+
${C.cyan}Amount:${C.reset} ${C.green}${formatAether(stakeLamports)}${C.reset}
|
|
453
|
+
${C.cyan}Balance:${C.reset} ${formatAether(balance)}
|
|
454
|
+
`.trim(),
|
|
455
|
+
{ style: 'single', title: 'TRANSACTION', titleColor: C.cyan }
|
|
456
|
+
));
|
|
457
|
+
console.log();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Dry run
|
|
461
|
+
if (opts.dryRun) {
|
|
462
|
+
console.log(JSON.stringify({
|
|
463
|
+
dry_run: true,
|
|
464
|
+
wallet: opts.address,
|
|
465
|
+
validator: validator,
|
|
466
|
+
stake_lamports: stakeLamports,
|
|
467
|
+
stake_aeth: amount,
|
|
468
|
+
balance_aeth: balance / 1e9,
|
|
469
|
+
rpc: opts.rpc,
|
|
470
|
+
cli_version: CLI_VERSION,
|
|
471
|
+
}, null, 2));
|
|
472
|
+
rl.close();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Sign
|
|
477
|
+
console.log(`${warning('Signing requires your wallet passphrase.')}`);
|
|
478
|
+
const mnemonic = await askMnemonic(rl, 'Enter passphrase to sign this transaction');
|
|
479
|
+
|
|
480
|
+
let keyPair;
|
|
481
|
+
try {
|
|
482
|
+
keyPair = deriveKeypair(mnemonic);
|
|
483
|
+
} catch (e) {
|
|
484
|
+
console.log(`\n ${error('Failed to derive keypair:')} ${e.message}\n`);
|
|
485
|
+
rl.close();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
490
|
+
if (derivedAddress !== opts.address) {
|
|
491
|
+
console.log(`\n ${error('Passphrase mismatch!')}`);
|
|
492
|
+
console.log(` Expected: ${opts.address}`);
|
|
493
|
+
console.log(` Derived: ${derivedAddress}\n`);
|
|
494
|
+
rl.close();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Confirm
|
|
499
|
+
if (!opts.force) {
|
|
500
|
+
const confirm = await question(rl, `\n ${warning('Confirm stake? [y/N]')} > `);
|
|
501
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
502
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
503
|
+
rl.close();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Get slot and build transaction
|
|
509
|
+
let slot = 0;
|
|
510
|
+
try { slot = await client.getSlot(); } catch (e) {}
|
|
511
|
+
|
|
512
|
+
const tx = {
|
|
513
|
+
signer: rawAddr,
|
|
514
|
+
tx_type: 'Stake',
|
|
515
|
+
payload: {
|
|
516
|
+
type: 'Stake',
|
|
517
|
+
data: { validator: validator, amount: stakeLamports },
|
|
518
|
+
},
|
|
519
|
+
fee: 5000,
|
|
520
|
+
slot: slot,
|
|
521
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
525
|
+
|
|
526
|
+
if (!opts.json) {
|
|
527
|
+
console.log(`\n ${C.dim}Submitting to ${opts.rpc}...${C.reset}`);
|
|
528
|
+
startSpinner('Submitting transaction');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const result = await client.sendTransaction(tx);
|
|
533
|
+
if (result.error) throw new Error(result.error);
|
|
534
|
+
|
|
535
|
+
if (!opts.json) {
|
|
536
|
+
stopSpinner(true, 'Transaction submitted');
|
|
537
|
+
|
|
538
|
+
console.log();
|
|
539
|
+
console.log(BRANDING.successBanner('Stake transaction submitted!'));
|
|
540
|
+
console.log(`\n ${C.cyan}Wallet:${C.reset} ${opts.address}`);
|
|
541
|
+
console.log(` ${C.cyan}Validator:${C.reset} ${validator}`);
|
|
542
|
+
console.log(` ${C.cyan}Amount:${C.reset} ${C.bright}${formatAether(stakeLamports)}${C.reset}`);
|
|
543
|
+
if (result.signature) console.log(` ${C.cyan}Signature:${C.reset} ${result.signature.slice(0, 40)}...`);
|
|
544
|
+
console.log(` ${C.cyan}Slot:${C.reset} ${result.slot || slot}`);
|
|
545
|
+
console.log();
|
|
546
|
+
console.log(` ${info('Stake will activate in the next epoch')}`);
|
|
547
|
+
console.log(` ${C.dim}Check: ${code(`aether stake-positions --address ${opts.address}`)}${C.reset}\n`);
|
|
548
|
+
} else {
|
|
549
|
+
console.log(JSON.stringify({
|
|
550
|
+
success: true,
|
|
551
|
+
wallet: opts.address,
|
|
552
|
+
validator: validator,
|
|
553
|
+
amount: stakeLamports,
|
|
554
|
+
signature: result.signature,
|
|
555
|
+
slot: result.slot || slot,
|
|
556
|
+
}, null, 2));
|
|
557
|
+
}
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (!opts.json) {
|
|
560
|
+
stopSpinner(false, 'Transaction failed');
|
|
561
|
+
console.log(`\n ${error('Stake failed:')} ${err.message}\n`);
|
|
562
|
+
} else {
|
|
563
|
+
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
rl.close();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ============================================================================
|
|
571
|
+
// Entry Point
|
|
572
|
+
// ============================================================================
|
|
573
|
+
|
|
574
|
+
module.exports = { stakeCommand };
|
|
575
|
+
|
|
576
|
+
if (require.main === module) {
|
|
577
|
+
stakeCommand().catch(err => {
|
|
578
|
+
console.error(`\n${error('Stake command failed:')} ${err.message}`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
});
|
|
581
|
+
}
|