@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,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 @jellylegs/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
|
+
// ANSI colours
|
|
29
|
+
const C = {
|
|
30
|
+
reset: '\x1b[0m',
|
|
31
|
+
bright: '\x1b[1m',
|
|
32
|
+
dim: '\x1b[2m',
|
|
33
|
+
red: '\x1b[31m',
|
|
34
|
+
green: '\x1b[32m',
|
|
35
|
+
yellow: '\x1b[33m',
|
|
36
|
+
cyan: '\x1b[36m',
|
|
37
|
+
magenta: '\x1b[35m',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const CLI_VERSION = '1.0.0';
|
|
41
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// SDK Setup
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
function getDefaultRpc() {
|
|
48
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createClient(rpcUrl) {
|
|
52
|
+
return new aether.AetherClient({ rpcUrl });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Config & Wallet
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
function getAetherDir() {
|
|
60
|
+
return path.join(os.homedir(), '.aether');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getConfigPath() {
|
|
64
|
+
return path.join(getAetherDir(), 'config.json');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadConfig() {
|
|
68
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
69
|
+
return { defaultWallet: null };
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
|
73
|
+
} catch {
|
|
74
|
+
return { defaultWallet: null };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadWallet(address) {
|
|
79
|
+
const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
|
|
80
|
+
if (!fs.existsSync(fp)) return null;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Crypto Helpers
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
function deriveKeypair(mnemonic) {
|
|
93
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
94
|
+
throw new Error('Invalid mnemonic phrase');
|
|
95
|
+
}
|
|
96
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
97
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
98
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
99
|
+
return {
|
|
100
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
101
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatAddress(publicKey) {
|
|
106
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function signTransaction(tx, secretKey) {
|
|
110
|
+
const txBytes = Buffer.from(JSON.stringify(tx));
|
|
111
|
+
const sig = nacl.sign.detached(txBytes, secretKey);
|
|
112
|
+
return bs58.encode(sig);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Format Helpers
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
function formatAether(lamports) {
|
|
120
|
+
const aeth = Number(lamports) / 1e9;
|
|
121
|
+
if (aeth === 0) return '0 AETH';
|
|
122
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function shortAddress(addr) {
|
|
126
|
+
if (!addr || addr.length < 16) return addr || 'unknown';
|
|
127
|
+
return addr.slice(0, 8) + '...' + addr.slice(-8);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatPercent(val) {
|
|
131
|
+
if (val === undefined || val === null) return 'N/A';
|
|
132
|
+
return val.toFixed(2) + '%';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Readline Helpers
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
function createRl() {
|
|
140
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function question(rl, q) {
|
|
144
|
+
return new Promise((res) => rl.question(q, res));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function askMnemonic(rl, promptText) {
|
|
148
|
+
console.log(`\n${C.cyan}${promptText}${C.reset}`);
|
|
149
|
+
console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
|
|
150
|
+
const raw = await question(rl, ` > ${C.reset}`);
|
|
151
|
+
return raw.trim().toLowerCase();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Fetch Validators via SDK
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
async function fetchValidators(rpcUrl) {
|
|
159
|
+
const client = createClient(rpcUrl);
|
|
160
|
+
try {
|
|
161
|
+
const validators = await client.getValidators();
|
|
162
|
+
if (!Array.isArray(validators)) return [];
|
|
163
|
+
return validators.map(v => ({
|
|
164
|
+
address: v.vote_account || v.pubkey || v.address || v.identity,
|
|
165
|
+
identity: v.identity || v.node_pubkey,
|
|
166
|
+
stake: v.stake_lamports || v.activated_stake || v.stake || 0,
|
|
167
|
+
commission: v.commission || v.commission_bps || 0,
|
|
168
|
+
apy: v.apy || v.return_rate || 0,
|
|
169
|
+
name: v.name || v.moniker || 'Unknown',
|
|
170
|
+
tier: v.tier || 'unknown',
|
|
171
|
+
active: v.active !== false && v.delinquent !== true,
|
|
172
|
+
}));
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Show Help
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
function showHelp() {
|
|
183
|
+
console.log(`
|
|
184
|
+
${C.bright}${C.cyan}aether-cli stake${C.reset} — Stake AETH to a validator
|
|
185
|
+
|
|
186
|
+
${C.bright}USAGE${C.reset}
|
|
187
|
+
aether stake --validator <addr> --amount <aeth> [--address <wallet>]
|
|
188
|
+
|
|
189
|
+
${C.bright}REQUIRED${C.reset}
|
|
190
|
+
--validator <addr> Validator address to stake to
|
|
191
|
+
--amount <aeth> Amount to stake in AETH
|
|
192
|
+
|
|
193
|
+
${C.bright}OPTIONS${C.reset}
|
|
194
|
+
--address <addr> Wallet address (default: configured default)
|
|
195
|
+
--rpc <url> RPC endpoint (default: AETHER_RPC env or localhost:8899)
|
|
196
|
+
--json Output JSON for scripting
|
|
197
|
+
--dry-run Preview stake without submitting
|
|
198
|
+
--list-validators Show available validators on the network
|
|
199
|
+
--force Skip confirmation prompts
|
|
200
|
+
|
|
201
|
+
${C.bright}SDK METHODS USED${C.reset}
|
|
202
|
+
client.getValidators() → GET /v1/validators
|
|
203
|
+
client.getAccountInfo() → GET /v1/account/<addr>
|
|
204
|
+
client.getSlot() → GET /v1/slot
|
|
205
|
+
client.sendTransaction() → POST /v1/transaction
|
|
206
|
+
|
|
207
|
+
${C.bright}EXAMPLES${C.reset}
|
|
208
|
+
aether stake --validator ATHxxx... --amount 1000
|
|
209
|
+
aether stake --validator ATHxxx... --amount 1000 --address ATHxxx...
|
|
210
|
+
aether stake --list-validators
|
|
211
|
+
aether stake --validator ATHxxx... --amount 1000 --dry-run
|
|
212
|
+
|
|
213
|
+
${C.bright}MINIMUM STAKE AMOUNTS${C.reset}
|
|
214
|
+
Full: 10,000 AETH
|
|
215
|
+
Lite: 1,000 AETH
|
|
216
|
+
Observer: 0 AETH
|
|
217
|
+
|
|
218
|
+
${C.bright}NOTES${C.reset}
|
|
219
|
+
• Staked AETH begins earning rewards after one epoch (~2 days)
|
|
220
|
+
• Use 'aether stake-positions' to view your delegations
|
|
221
|
+
• Use 'aether unstake' to withdraw (has cooldown period)
|
|
222
|
+
`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// List Validators Command
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
async function listValidatorsCommand(opts) {
|
|
230
|
+
if (!opts.json) {
|
|
231
|
+
console.log(`\n${C.bright}${C.cyan}── Available Validators ──────────────────────────────────────${C.reset}\n`);
|
|
232
|
+
console.log(` ${C.dim}Fetching validators from ${opts.rpc}...${C.reset}\n`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const validators = await fetchValidators(opts.rpc);
|
|
236
|
+
|
|
237
|
+
if (validators.length === 0) {
|
|
238
|
+
if (opts.json) {
|
|
239
|
+
console.log(JSON.stringify({
|
|
240
|
+
success: false,
|
|
241
|
+
error: 'No validators found',
|
|
242
|
+
rpc: opts.rpc,
|
|
243
|
+
}, null, 2));
|
|
244
|
+
} else {
|
|
245
|
+
console.log(` ${C.yellow}⚠ No validators found.${C.reset}`);
|
|
246
|
+
console.log(` ${C.dim} Check your RPC endpoint: ${opts.rpc}${C.reset}\n`);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
validators.sort((a, b) => b.stake - a.stake);
|
|
252
|
+
|
|
253
|
+
if (opts.json) {
|
|
254
|
+
console.log(JSON.stringify({
|
|
255
|
+
success: true,
|
|
256
|
+
count: validators.length,
|
|
257
|
+
validators: validators.map(v => ({
|
|
258
|
+
address: v.address,
|
|
259
|
+
name: v.name,
|
|
260
|
+
stake_aeth: v.stake / 1e9,
|
|
261
|
+
apy: v.apy,
|
|
262
|
+
tier: v.tier,
|
|
263
|
+
})),
|
|
264
|
+
}, null, 2));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(` ${C.bright}Found ${validators.length} validators${C.reset}\n`);
|
|
269
|
+
validators.slice(0, 15).forEach((v, i) => {
|
|
270
|
+
const status = v.active ? C.green + '●' : C.yellow + '○';
|
|
271
|
+
const name = (v.name || 'Unknown').slice(0, 20).padEnd(20);
|
|
272
|
+
const addr = shortAddress(v.address);
|
|
273
|
+
const stake = formatAether(v.stake);
|
|
274
|
+
const apy = formatPercent(v.apy);
|
|
275
|
+
console.log(` ${status}${C.reset} ${(i + 1).toString().padStart(2)} ${name} ${addr} ${stake} ${apy}`);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
console.log(`\n ${C.dim}To stake: aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Main Stake Logic
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
async function stakeCommand() {
|
|
286
|
+
const opts = {
|
|
287
|
+
validator: null,
|
|
288
|
+
amount: null,
|
|
289
|
+
address: null,
|
|
290
|
+
rpc: getDefaultRpc(),
|
|
291
|
+
json: false,
|
|
292
|
+
dryRun: false,
|
|
293
|
+
listValidators: false,
|
|
294
|
+
force: false,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Parse args
|
|
298
|
+
const args = process.argv.slice(3);
|
|
299
|
+
for (let i = 0; i < args.length; i++) {
|
|
300
|
+
const arg = args[i];
|
|
301
|
+
if (arg === '--validator' || arg === '-v') opts.validator = args[++i];
|
|
302
|
+
else if (arg === '--amount' || arg === '-m') {
|
|
303
|
+
const val = parseFloat(args[++i]);
|
|
304
|
+
if (!isNaN(val) && val > 0) opts.amount = val;
|
|
305
|
+
}
|
|
306
|
+
else if (arg === '--address' || arg === '-a') opts.address = args[++i];
|
|
307
|
+
else if (arg === '--rpc' || arg === '-r') opts.rpc = args[++i];
|
|
308
|
+
else if (arg === '--json' || arg === '-j') opts.json = true;
|
|
309
|
+
else if (arg === '--dry-run') opts.dryRun = true;
|
|
310
|
+
else if (arg === '--list-validators' || arg === '-l') opts.listValidators = true;
|
|
311
|
+
else if (arg === '--force' || arg === '-f') opts.force = true;
|
|
312
|
+
else if (arg === '--help' || arg === '-h') {
|
|
313
|
+
showHelp();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// List validators mode
|
|
319
|
+
if (opts.listValidators) {
|
|
320
|
+
await listValidatorsCommand(opts);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const rl = createRl();
|
|
325
|
+
|
|
326
|
+
// Resolve wallet address
|
|
327
|
+
if (!opts.address) {
|
|
328
|
+
const cfg = loadConfig();
|
|
329
|
+
opts.address = cfg.defaultWallet;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!opts.address) {
|
|
333
|
+
console.log(`\n ${C.red}✗ No wallet address.${C.reset} Use --address <addr> or set a default.`);
|
|
334
|
+
console.log(` ${C.dim}Usage: aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
|
|
335
|
+
rl.close();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check wallet exists
|
|
340
|
+
const wallet = loadWallet(opts.address);
|
|
341
|
+
if (!wallet) {
|
|
342
|
+
console.log(`\n ${C.red}✗ Wallet not found:${C.reset} ${opts.address}`);
|
|
343
|
+
console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
|
|
344
|
+
rl.close();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Fetch balance via SDK
|
|
349
|
+
let balance = 0;
|
|
350
|
+
const client = createClient(opts.rpc);
|
|
351
|
+
const rawAddr = opts.address.startsWith('ATH') ? opts.address.slice(3) : opts.address;
|
|
352
|
+
try {
|
|
353
|
+
const account = await client.getAccountInfo(rawAddr);
|
|
354
|
+
balance = account.lamports || 0;
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (!opts.json) console.log(` ${C.yellow}⚠ Could not fetch balance: ${err.message}${C.reset}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Interactive validator selection
|
|
360
|
+
let validator = opts.validator;
|
|
361
|
+
if (!validator) {
|
|
362
|
+
console.log(`\n ${C.dim}Fetching validators...${C.reset}`);
|
|
363
|
+
const validators = await fetchValidators(opts.rpc);
|
|
364
|
+
if (validators.length === 0) {
|
|
365
|
+
console.log(` ${C.red}✗ No validators found.${C.reset}\n`);
|
|
366
|
+
rl.close();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
validators.sort((a, b) => b.stake - a.stake);
|
|
370
|
+
console.log(`\n ${C.bright}Select a validator:${C.reset}`);
|
|
371
|
+
validators.slice(0, 10).forEach((v, i) => {
|
|
372
|
+
const name = (v.name || 'Unknown').slice(0, 18).padEnd(18);
|
|
373
|
+
const stake = formatAether(v.stake);
|
|
374
|
+
const apy = formatPercent(v.apy);
|
|
375
|
+
console.log(` ${C.green}${i + 1})${C.reset} ${name} | ${stake} | ${apy}`);
|
|
376
|
+
});
|
|
377
|
+
console.log(`\n ${C.dim}Enter number [1-10] or validator address${C.reset}`);
|
|
378
|
+
const choice = await question(rl, ` Validator > ${C.reset}`);
|
|
379
|
+
const choiceNum = parseInt(choice.trim(), 10);
|
|
380
|
+
if (!isNaN(choiceNum) && choiceNum >= 1 && choiceNum <= 10) {
|
|
381
|
+
validator = validators[choiceNum - 1].address;
|
|
382
|
+
} else {
|
|
383
|
+
validator = choice.trim();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Resolve amount
|
|
388
|
+
let amount = opts.amount;
|
|
389
|
+
if (!amount) {
|
|
390
|
+
console.log(`\n Available: ${formatAether(balance)}`);
|
|
391
|
+
console.log(` Minimum: Full=10K, Lite=1K, Observer=0`);
|
|
392
|
+
const amt = await question(rl, ` Amount (AETH) > ${C.reset}`);
|
|
393
|
+
amount = parseFloat(amt);
|
|
394
|
+
if (isNaN(amount) || amount <= 0) {
|
|
395
|
+
console.log(`\n ${C.red}✗ Invalid amount.${C.reset}\n`);
|
|
396
|
+
rl.close();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const stakeLamports = Math.round(amount * 1e9);
|
|
402
|
+
const feeBuffer = 0.005 * 1e9;
|
|
403
|
+
|
|
404
|
+
if (stakeLamports + feeBuffer > balance) {
|
|
405
|
+
console.log(`\n ${C.red}✗ Insufficient balance.${C.reset}`);
|
|
406
|
+
console.log(` Requested: ${formatAether(stakeLamports)}`);
|
|
407
|
+
console.log(` Balance: ${formatAether(balance)}\n`);
|
|
408
|
+
rl.close();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Summary
|
|
413
|
+
console.log(`\n ${C.green}★${C.reset} Wallet: ${C.bright}${opts.address}${C.reset}`);
|
|
414
|
+
console.log(` ${C.green}★${C.reset} Validator: ${C.bright}${shortAddress(validator)}${C.reset}`);
|
|
415
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${formatAether(stakeLamports)}${C.reset}`);
|
|
416
|
+
console.log();
|
|
417
|
+
|
|
418
|
+
// Dry run
|
|
419
|
+
if (opts.dryRun) {
|
|
420
|
+
console.log(JSON.stringify({
|
|
421
|
+
dry_run: true,
|
|
422
|
+
wallet: opts.address,
|
|
423
|
+
validator: validator,
|
|
424
|
+
stake_lamports: stakeLamports,
|
|
425
|
+
stake_aeth: amount,
|
|
426
|
+
balance_aeth: balance / 1e9,
|
|
427
|
+
rpc: opts.rpc,
|
|
428
|
+
cli_version: CLI_VERSION,
|
|
429
|
+
}, null, 2));
|
|
430
|
+
rl.close();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Sign
|
|
435
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
|
|
436
|
+
const mnemonic = await askMnemonic(rl, 'Enter passphrase to sign this transaction');
|
|
437
|
+
|
|
438
|
+
let keyPair;
|
|
439
|
+
try {
|
|
440
|
+
keyPair = deriveKeypair(mnemonic);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.log(`\n ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}\n`);
|
|
443
|
+
rl.close();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
448
|
+
if (derivedAddress !== opts.address) {
|
|
449
|
+
console.log(`\n ${C.red}✗ Passphrase mismatch!${C.reset}`);
|
|
450
|
+
console.log(` Expected: ${opts.address}`);
|
|
451
|
+
console.log(` Derived: ${derivedAddress}\n`);
|
|
452
|
+
rl.close();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Confirm
|
|
457
|
+
if (!opts.force) {
|
|
458
|
+
const confirm = await question(rl, `\n ${C.yellow}Confirm stake? [y/N]${C.reset} > `);
|
|
459
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
460
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
461
|
+
rl.close();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Get slot and build transaction
|
|
467
|
+
let slot = 0;
|
|
468
|
+
try { slot = await client.getSlot(); } catch (e) {}
|
|
469
|
+
|
|
470
|
+
const tx = {
|
|
471
|
+
signer: rawAddr,
|
|
472
|
+
tx_type: 'Stake',
|
|
473
|
+
payload: {
|
|
474
|
+
type: 'Stake',
|
|
475
|
+
data: { validator: validator, amount: stakeLamports },
|
|
476
|
+
},
|
|
477
|
+
fee: 5000,
|
|
478
|
+
slot: slot,
|
|
479
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
483
|
+
|
|
484
|
+
console.log(`\n ${C.dim}Submitting to ${opts.rpc}...${C.reset}`);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const result = await client.sendTransaction(tx);
|
|
488
|
+
if (result.error) throw new Error(result.error);
|
|
489
|
+
|
|
490
|
+
console.log(`\n${C.green}✓ Stake transaction submitted!${C.reset}`);
|
|
491
|
+
console.log(` Wallet: ${opts.address}`);
|
|
492
|
+
console.log(` Validator: ${validator}`);
|
|
493
|
+
console.log(` Amount: ${formatAether(stakeLamports)}`);
|
|
494
|
+
if (result.signature) console.log(` Signature: ${result.signature.slice(0, 40)}...`);
|
|
495
|
+
console.log(` Slot: ${result.slot || slot}`);
|
|
496
|
+
console.log(`\n${C.green}✓ Stake will activate in the next epoch${C.reset}`);
|
|
497
|
+
console.log(` Check: aether stake-positions --address ${opts.address}\n`);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
console.log(`\n ${C.red}✗ Stake failed:${C.reset} ${err.message}\n`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
rl.close();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// Entry Point
|
|
507
|
+
// ============================================================================
|
|
508
|
+
|
|
509
|
+
module.exports = { stakeCommand };
|
|
510
|
+
|
|
511
|
+
if (require.main === module) {
|
|
512
|
+
stakeCommand().catch(err => {
|
|
513
|
+
console.error(`\n${C.red}✗ Stake command failed:${C.reset} ${err.message}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
});
|
|
516
|
+
}
|