@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,597 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli unstake
|
|
4
|
+
*
|
|
5
|
+
* Unstake AETH from a validator — deactivate a stake account and begin cooldown.
|
|
6
|
+
* Fully wired to @jellylegsai/aether-sdk for real blockchain RPC calls.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* aether unstake --address <wallet> [--account <stakeAcct>] [--amount <aeth>]
|
|
10
|
+
* aether unstake --address ATHxxx... --account Stakexxx... --amount 100
|
|
11
|
+
* aether unstake --address ATHxxx... --json
|
|
12
|
+
*
|
|
13
|
+
* SDK wired to:
|
|
14
|
+
* - client.getSlot() → GET /v1/slot
|
|
15
|
+
* - client.getStakePositions() → GET /v1/stake/<addr>
|
|
16
|
+
* - client.getAccountInfo() → GET /v1/account/<addr>
|
|
17
|
+
* - client.sendTransaction() → POST /v1/transaction
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const readline = require('readline');
|
|
24
|
+
const nacl = require('tweetnacl');
|
|
25
|
+
const bs58 = require('bs58').default;
|
|
26
|
+
const bip39 = require('bip39');
|
|
27
|
+
|
|
28
|
+
// Import SDK for real blockchain RPC calls
|
|
29
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
30
|
+
const aether = require(sdkPath);
|
|
31
|
+
|
|
32
|
+
// ANSI colours
|
|
33
|
+
const C = {
|
|
34
|
+
reset: '\x1b[0m',
|
|
35
|
+
bright: '\x1b[1m',
|
|
36
|
+
dim: '\x1b[2m',
|
|
37
|
+
red: '\x1b[31m',
|
|
38
|
+
green: '\x1b[32m',
|
|
39
|
+
yellow: '\x1b[33m',
|
|
40
|
+
cyan: '\x1b[36m',
|
|
41
|
+
magenta: '\x1b[35m',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const CLI_VERSION = '1.0.0';
|
|
45
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// SDK Setup
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
function getDefaultRpc() {
|
|
52
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createClient(rpcUrl) {
|
|
56
|
+
return new aether.AetherClient({ rpcUrl });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Config & Wallet
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
function getAetherDir() {
|
|
64
|
+
return path.join(os.homedir(), '.aether');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getConfigPath() {
|
|
68
|
+
return path.join(getAetherDir(), 'config.json');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadConfig() {
|
|
72
|
+
if (!fs.existsSync(getConfigPath())) {
|
|
73
|
+
return { defaultWallet: null };
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
return { defaultWallet: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadWallet(address) {
|
|
83
|
+
const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
|
|
84
|
+
if (!fs.existsSync(fp)) return null;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Crypto Helpers
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
function deriveKeypair(mnemonic) {
|
|
97
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
98
|
+
throw new Error('Invalid mnemonic phrase');
|
|
99
|
+
}
|
|
100
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
101
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
102
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
103
|
+
return {
|
|
104
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
105
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatAddress(publicKey) {
|
|
110
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function signTransaction(tx, secretKey) {
|
|
114
|
+
const txBytes = Buffer.from(JSON.stringify(tx));
|
|
115
|
+
const sig = nacl.sign.detached(txBytes, secretKey);
|
|
116
|
+
return bs58.encode(sig);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Format Helpers
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
function formatAether(lamports) {
|
|
124
|
+
if (!lamports || lamports === '0') return '0 AETH';
|
|
125
|
+
const aeth = Number(lamports) / 1e9;
|
|
126
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function shortAddress(addr) {
|
|
130
|
+
if (!addr || addr.length < 16) return addr || 'unknown';
|
|
131
|
+
return addr.slice(0, 8) + '...' + addr.slice(-8);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Argument Parsing
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
function parseArgs() {
|
|
139
|
+
const args = process.argv.slice(2);
|
|
140
|
+
const opts = {
|
|
141
|
+
address: null,
|
|
142
|
+
stakeAccount: null,
|
|
143
|
+
amount: null,
|
|
144
|
+
rpc: getDefaultRpc(),
|
|
145
|
+
json: false,
|
|
146
|
+
dryRun: false,
|
|
147
|
+
force: false,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < args.length; i++) {
|
|
151
|
+
const arg = args[i];
|
|
152
|
+
if (arg === '--address' || arg === '-a') {
|
|
153
|
+
opts.address = args[++i];
|
|
154
|
+
} else if (arg === '--account' || arg === '-s') {
|
|
155
|
+
opts.stakeAccount = args[++i];
|
|
156
|
+
} else if (arg === '--amount' || arg === '-m') {
|
|
157
|
+
const val = parseFloat(args[++i]);
|
|
158
|
+
if (!isNaN(val) && val > 0) {
|
|
159
|
+
opts.amount = val;
|
|
160
|
+
}
|
|
161
|
+
} else if (arg === '--rpc' || arg === '-r') {
|
|
162
|
+
opts.rpc = args[++i];
|
|
163
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
164
|
+
opts.json = true;
|
|
165
|
+
} else if (arg === '--dry-run') {
|
|
166
|
+
opts.dryRun = true;
|
|
167
|
+
} else if (arg === '--force' || arg === '-f') {
|
|
168
|
+
opts.force = true;
|
|
169
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
170
|
+
opts.help = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return opts;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function showHelp() {
|
|
178
|
+
console.log(`
|
|
179
|
+
${C.bright}${C.cyan}aether-cli unstake${C.reset} — Unstake AETH from a validator
|
|
180
|
+
|
|
181
|
+
${C.bright}USAGE${C.reset}
|
|
182
|
+
aether unstake --address <wallet> [--account <stakeAcct>] [--amount <aeth>]
|
|
183
|
+
|
|
184
|
+
${C.bright}REQUIRED${C.reset}
|
|
185
|
+
--address <wallet> Wallet address with the stake account
|
|
186
|
+
|
|
187
|
+
${C.bright}OPTIONS${C.reset}
|
|
188
|
+
--account <addr> Specific stake account to deactivate
|
|
189
|
+
--amount <aeth> Amount to unstake (default: full stake)
|
|
190
|
+
--rpc <url> RPC endpoint (default: $AETHER_RPC or localhost:8899)
|
|
191
|
+
--json Output JSON for scripting
|
|
192
|
+
--dry-run Preview unstake without submitting
|
|
193
|
+
--force Skip confirmation prompts
|
|
194
|
+
--help Show this help
|
|
195
|
+
|
|
196
|
+
${C.bright}SDK METHODS USED${C.reset}
|
|
197
|
+
client.getSlot() → GET /v1/slot
|
|
198
|
+
client.getStakePositions() → GET /v1/stake/<addr>
|
|
199
|
+
client.getAccountInfo() → GET /v1/account/<addr>
|
|
200
|
+
client.sendTransaction() → POST /v1/transaction
|
|
201
|
+
|
|
202
|
+
${C.bright}EXAMPLES${C.reset}
|
|
203
|
+
aether unstake --address ATHxxx...
|
|
204
|
+
aether unstake --address ATHxxx... --account Stakexxx... --amount 500
|
|
205
|
+
aether unstake --address ATHxxx... --json --dry-run
|
|
206
|
+
|
|
207
|
+
${C.bright}NOTES${C.reset}
|
|
208
|
+
• Unstaking begins a cooldown period (typically 1-2 epochs)
|
|
209
|
+
• During cooldown, stake is "deactivating" and earns reduced rewards
|
|
210
|
+
• Once cooldown completes, use 'aether claim' to withdraw to wallet
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Readline Helpers
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
function createRl() {
|
|
219
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function question(rl, q) {
|
|
223
|
+
return new Promise((res) => rl.question(q, res));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function askMnemonic(rl, promptText) {
|
|
227
|
+
console.log(`\n${C.cyan}${promptText}${C.reset}`);
|
|
228
|
+
console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
|
|
229
|
+
const raw = await question(rl, ` > ${C.reset}`);
|
|
230
|
+
return raw.trim().toLowerCase();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Fetch Stake Accounts via SDK
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
async function fetchStakeAccounts(rpcUrl, walletAddress) {
|
|
238
|
+
const client = createClient(rpcUrl);
|
|
239
|
+
const rawAddr = walletAddress.startsWith('ATH') ? walletAddress.slice(3) : walletAddress;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const stakePositions = await client.getStakePositions(rawAddr);
|
|
243
|
+
if (!Array.isArray(stakePositions)) return [];
|
|
244
|
+
|
|
245
|
+
return stakePositions.map(s => ({
|
|
246
|
+
address: s.pubkey || s.publicKey || s.account || s.stake_account,
|
|
247
|
+
validator: s.validator || s.delegate || s.vote_account,
|
|
248
|
+
lamports: s.lamports || s.stake_lamports || s.amount || 0,
|
|
249
|
+
status: s.status || s.state || 'active',
|
|
250
|
+
activationEpoch: s.activation_epoch || s.activationEpoch,
|
|
251
|
+
deactivationEpoch: s.deactivation_epoch || s.deactivationEpoch,
|
|
252
|
+
})).filter(s => s.address);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Main Unstake Logic
|
|
260
|
+
// ============================================================================
|
|
261
|
+
|
|
262
|
+
async function unstakeCommand() {
|
|
263
|
+
const opts = parseArgs();
|
|
264
|
+
const rl = createRl();
|
|
265
|
+
|
|
266
|
+
if (opts.help) {
|
|
267
|
+
showHelp();
|
|
268
|
+
rl.close();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Resolve wallet address
|
|
273
|
+
if (!opts.address) {
|
|
274
|
+
const cfg = loadConfig();
|
|
275
|
+
opts.address = cfg.defaultWallet;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!opts.address) {
|
|
279
|
+
console.log(`\n ${C.red}✗ No wallet address provided.${C.reset}`);
|
|
280
|
+
console.log(` ${C.dim}Usage: aether unstake --address <addr> [--account <stakeAcct>]${C.reset}\n`);
|
|
281
|
+
rl.close();
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const wallet = loadWallet(opts.address);
|
|
286
|
+
if (!wallet) {
|
|
287
|
+
console.log(`\n ${C.red}✗ Wallet not found locally:${C.reset} ${opts.address}`);
|
|
288
|
+
console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
|
|
289
|
+
rl.close();
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const client = createClient(opts.rpc);
|
|
294
|
+
|
|
295
|
+
// Fetch stake accounts via SDK
|
|
296
|
+
if (!opts.json) {
|
|
297
|
+
console.log(`\n${C.bright}${C.cyan}── Unstake AETH ──────────────────────────────────────────${C.reset}\n`);
|
|
298
|
+
console.log(` ${C.dim}Fetching stake accounts via SDK...${C.reset}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const stakeAccounts = await fetchStakeAccounts(opts.rpc, opts.address);
|
|
302
|
+
|
|
303
|
+
if (stakeAccounts.length === 0) {
|
|
304
|
+
if (opts.json) {
|
|
305
|
+
console.log(JSON.stringify({
|
|
306
|
+
success: false,
|
|
307
|
+
error: 'No active stake accounts found',
|
|
308
|
+
address: opts.address,
|
|
309
|
+
suggestion: 'Stake AETH first with: aether stake --validator <addr> --amount <aeth>',
|
|
310
|
+
}, null, 2));
|
|
311
|
+
} else {
|
|
312
|
+
console.log(`\n ${C.yellow}⚠ No active stake accounts found.${C.reset}`);
|
|
313
|
+
console.log(` ${C.dim} Stake AETH first: aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
|
|
314
|
+
}
|
|
315
|
+
rl.close();
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Filter for active stakes only (can't unstake already deactivating)
|
|
320
|
+
const activeStakes = stakeAccounts.filter(s =>
|
|
321
|
+
s.status === 'active' && !s.deactivationEpoch
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (activeStakes.length === 0) {
|
|
325
|
+
if (opts.json) {
|
|
326
|
+
console.log(JSON.stringify({
|
|
327
|
+
success: false,
|
|
328
|
+
error: 'No active stake positions to unstake',
|
|
329
|
+
address: opts.address,
|
|
330
|
+
stake_accounts: stakeAccounts,
|
|
331
|
+
}, null, 2));
|
|
332
|
+
} else {
|
|
333
|
+
console.log(`\n ${C.yellow}⚠ No active stake positions to unstake.${C.reset}`);
|
|
334
|
+
console.log(` ${C.dim} Current status:${C.reset}`);
|
|
335
|
+
stakeAccounts.forEach(s => {
|
|
336
|
+
const status = s.deactivationEpoch ? 'deactivating' : s.status;
|
|
337
|
+
console.log(` ${shortAddress(s.address)} → ${status}`);
|
|
338
|
+
});
|
|
339
|
+
console.log();
|
|
340
|
+
}
|
|
341
|
+
rl.close();
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Select stake account
|
|
346
|
+
let selectedStake = null;
|
|
347
|
+
|
|
348
|
+
if (opts.stakeAccount) {
|
|
349
|
+
// User specified a stake account
|
|
350
|
+
selectedStake = activeStakes.find(s =>
|
|
351
|
+
s.address === opts.stakeAccount ||
|
|
352
|
+
s.address.endsWith(opts.stakeAccount)
|
|
353
|
+
);
|
|
354
|
+
if (!selectedStake) {
|
|
355
|
+
console.log(`\n ${C.red}✗ Stake account not found or not active:${C.reset} ${opts.stakeAccount}`);
|
|
356
|
+
console.log(` ${C.dim}Active stake accounts:${C.reset}`);
|
|
357
|
+
activeStakes.forEach((s, i) => {
|
|
358
|
+
console.log(` ${i + 1}) ${shortAddress(s.address)} → ${formatAether(s.lamports)}`);
|
|
359
|
+
});
|
|
360
|
+
console.log();
|
|
361
|
+
rl.close();
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
} else if (activeStakes.length === 1) {
|
|
365
|
+
// Only one active stake, use it
|
|
366
|
+
selectedStake = activeStakes[0];
|
|
367
|
+
} else {
|
|
368
|
+
// Multiple active stakes - prompt user to select
|
|
369
|
+
if (opts.json) {
|
|
370
|
+
console.log(JSON.stringify({
|
|
371
|
+
success: false,
|
|
372
|
+
error: 'Multiple active stake accounts found. Use --account to specify.',
|
|
373
|
+
address: opts.address,
|
|
374
|
+
active_stakes: activeStakes.map(s => ({
|
|
375
|
+
address: s.address,
|
|
376
|
+
validator: s.validator,
|
|
377
|
+
lamports: s.lamports,
|
|
378
|
+
aeth: formatAether(s.lamports),
|
|
379
|
+
})),
|
|
380
|
+
}, null, 2));
|
|
381
|
+
rl.close();
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log(`\n ${C.bright}Multiple active stake accounts found:${C.reset}\n`);
|
|
386
|
+
activeStakes.forEach((s, i) => {
|
|
387
|
+
const val = s.validator ? shortAddress(s.validator) : 'unknown';
|
|
388
|
+
console.log(` ${C.green}${i + 1})${C.reset} ${shortAddress(s.address)}`);
|
|
389
|
+
console.log(` Validator: ${C.cyan}${val}${C.reset} Amount: ${C.bright}${formatAether(s.lamports)}${C.reset}`);
|
|
390
|
+
});
|
|
391
|
+
console.log();
|
|
392
|
+
|
|
393
|
+
const choice = await question(rl, ` ${C.cyan}Select account [1-${activeStakes.length}]:${C.reset} `);
|
|
394
|
+
const idx = parseInt(choice.trim(), 10) - 1;
|
|
395
|
+
|
|
396
|
+
if (isNaN(idx) || idx < 0 || idx >= activeStakes.length) {
|
|
397
|
+
console.log(`\n ${C.red}✗ Invalid selection.${C.reset}\n`);
|
|
398
|
+
rl.close();
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
selectedStake = activeStakes[idx];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Determine unstake amount
|
|
406
|
+
let unstakeLamports = selectedStake.lamports;
|
|
407
|
+
if (opts.amount) {
|
|
408
|
+
const requestedLamports = Math.round(opts.amount * 1e9);
|
|
409
|
+
if (requestedLamports > selectedStake.lamports) {
|
|
410
|
+
console.log(`\n ${C.red}✗ Requested amount exceeds staked balance.${C.reset}`);
|
|
411
|
+
console.log(` ${C.dim} Requested: ${formatAether(requestedLamports)}${C.reset}`);
|
|
412
|
+
console.log(` ${C.dim} Staked: ${formatAether(selectedStake.lamports)}${C.reset}\n`);
|
|
413
|
+
rl.close();
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
unstakeLamports = requestedLamports;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Display summary
|
|
420
|
+
if (!opts.json) {
|
|
421
|
+
console.log(`\n ${C.green}★${C.reset} Wallet: ${C.bright}${opts.address}${C.reset}`);
|
|
422
|
+
console.log(` ${C.green}★${C.reset} Stake acct: ${C.bright}${shortAddress(selectedStake.address)}${C.reset}`);
|
|
423
|
+
console.log(` ${C.green}★${C.reset} Validator: ${C.bright}${shortAddress(selectedStake.validator || 'unknown')}${C.reset}`);
|
|
424
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${opts.amount ? formatAether(unstakeLamports) : 'FULL STAKE'}${C.reset}`);
|
|
425
|
+
console.log(` ${C.dim}(${unstakeLamports.toLocaleString()} lamports)${C.reset}`);
|
|
426
|
+
console.log();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (opts.dryRun) {
|
|
430
|
+
if (opts.json) {
|
|
431
|
+
console.log(JSON.stringify({
|
|
432
|
+
dry_run: true,
|
|
433
|
+
wallet: opts.address,
|
|
434
|
+
stake_account: selectedStake.address,
|
|
435
|
+
validator: selectedStake.validator,
|
|
436
|
+
unstake_lamports: unstakeLamports,
|
|
437
|
+
unstake_aeth: formatAether(unstakeLamports),
|
|
438
|
+
current_stake_lamports: selectedStake.lamports,
|
|
439
|
+
current_stake_aeth: formatAether(selectedStake.lamports),
|
|
440
|
+
rpc: opts.rpc,
|
|
441
|
+
cli_version: CLI_VERSION,
|
|
442
|
+
timestamp: new Date().toISOString(),
|
|
443
|
+
}, null, 2));
|
|
444
|
+
} else {
|
|
445
|
+
console.log(` ${C.yellow}⚠ Dry run mode - no transaction submitted${C.reset}\n`);
|
|
446
|
+
}
|
|
447
|
+
rl.close();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Get mnemonic for signing
|
|
452
|
+
if (!opts.json) {
|
|
453
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}\n`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let keyPair;
|
|
457
|
+
try {
|
|
458
|
+
const mnemonic = await askMnemonic(rl, 'Enter your wallet passphrase to sign the unstake');
|
|
459
|
+
keyPair = deriveKeypair(mnemonic);
|
|
460
|
+
|
|
461
|
+
// Verify derived address matches
|
|
462
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
463
|
+
if (derivedAddress !== opts.address) {
|
|
464
|
+
if (opts.json) {
|
|
465
|
+
console.log(JSON.stringify({ success: false, error: 'Passphrase mismatch' }, null, 2));
|
|
466
|
+
} else {
|
|
467
|
+
console.log(`\n ${C.red}✗ Passphrase mismatch!${C.reset}`);
|
|
468
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
469
|
+
console.log(` ${C.dim} Expected: ${opts.address}${C.reset}\n`);
|
|
470
|
+
}
|
|
471
|
+
rl.close();
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
} catch (e) {
|
|
475
|
+
if (opts.json) {
|
|
476
|
+
console.log(JSON.stringify({ success: false, error: e.message }, null, 2));
|
|
477
|
+
} else {
|
|
478
|
+
console.log(`\n ${C.red}✗ Failed to derive keypair:${C.reset} ${e.message}\n`);
|
|
479
|
+
}
|
|
480
|
+
rl.close();
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Confirm transaction
|
|
485
|
+
if (!opts.json && !opts.force) {
|
|
486
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm unstake? [y/N]${C.reset} > `);
|
|
487
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
488
|
+
console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
|
|
489
|
+
rl.close();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
console.log();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
rl.close();
|
|
496
|
+
|
|
497
|
+
// Build unstake transaction
|
|
498
|
+
const rawWalletAddr = opts.address.startsWith('ATH') ? opts.address.slice(3) : opts.address;
|
|
499
|
+
|
|
500
|
+
// Fetch current slot via SDK
|
|
501
|
+
let currentSlot = 0;
|
|
502
|
+
try {
|
|
503
|
+
currentSlot = await client.getSlot();
|
|
504
|
+
} catch (e) {
|
|
505
|
+
// Continue with slot 0
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const tx = {
|
|
509
|
+
signer: rawWalletAddr,
|
|
510
|
+
tx_type: 'Unstake',
|
|
511
|
+
payload: {
|
|
512
|
+
type: 'Unstake',
|
|
513
|
+
data: {
|
|
514
|
+
stake_account: selectedStake.address,
|
|
515
|
+
amount: unstakeLamports,
|
|
516
|
+
validator: selectedStake.validator,
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
fee: 5000,
|
|
520
|
+
slot: currentSlot,
|
|
521
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Sign transaction
|
|
525
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
526
|
+
|
|
527
|
+
if (!opts.json) {
|
|
528
|
+
console.log(` ${C.dim}Submitting unstake via SDK to ${opts.rpc}...${C.reset}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Submit via SDK
|
|
532
|
+
try {
|
|
533
|
+
const result = await client.sendTransaction(tx);
|
|
534
|
+
|
|
535
|
+
if (result.error) {
|
|
536
|
+
throw new Error(result.error.message || JSON.stringify(result.error));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (opts.json) {
|
|
540
|
+
console.log(JSON.stringify({
|
|
541
|
+
success: true,
|
|
542
|
+
wallet: opts.address,
|
|
543
|
+
stake_account: selectedStake.address,
|
|
544
|
+
validator: selectedStake.validator,
|
|
545
|
+
unstake_lamports: unstakeLamports,
|
|
546
|
+
unstake_aeth: formatAether(unstakeLamports),
|
|
547
|
+
tx_signature: result.signature || result.txid,
|
|
548
|
+
slot: result.slot || currentSlot,
|
|
549
|
+
rpc: opts.rpc,
|
|
550
|
+
cli_version: CLI_VERSION,
|
|
551
|
+
timestamp: new Date().toISOString(),
|
|
552
|
+
}, null, 2));
|
|
553
|
+
} else {
|
|
554
|
+
console.log(`\n ${C.green}✓ Unstake transaction submitted!${C.reset}\n`);
|
|
555
|
+
console.log(` ${C.dim}Stake Account:${C.reset} ${shortAddress(selectedStake.address)}`);
|
|
556
|
+
console.log(` ${C.dim}Amount: ${C.reset}${C.bright}${formatAether(unstakeLamports)}${C.reset}`);
|
|
557
|
+
if (result.signature || result.txid) {
|
|
558
|
+
console.log(` ${C.dim}Signature: ${C.reset}${C.cyan}${(result.signature || result.txid).slice(0, 40)}...${C.reset}`);
|
|
559
|
+
}
|
|
560
|
+
console.log(` ${C.dim}Slot: ${C.reset}${result.slot || currentSlot}`);
|
|
561
|
+
console.log();
|
|
562
|
+
console.log(` ${C.yellow}⚠ Cooldown period started${C.reset}`);
|
|
563
|
+
console.log(` ${C.dim} Your stake is now deactivating. Rewards will be reduced during cooldown.${C.reset}`);
|
|
564
|
+
console.log(` ${C.dim} Check status: aether stake-positions --address ${opts.address}${C.reset}`);
|
|
565
|
+
console.log(` ${C.dim} After cooldown: aether claim --address ${opts.address}${C.reset}\n`);
|
|
566
|
+
}
|
|
567
|
+
} catch (err) {
|
|
568
|
+
if (opts.json) {
|
|
569
|
+
console.log(JSON.stringify({
|
|
570
|
+
success: false,
|
|
571
|
+
error: err.message,
|
|
572
|
+
wallet: opts.address,
|
|
573
|
+
stake_account: selectedStake.address,
|
|
574
|
+
}, null, 2));
|
|
575
|
+
} else {
|
|
576
|
+
console.log(`\n ${C.red}✗ Unstake failed:${C.reset} ${err.message}\n`);
|
|
577
|
+
console.log(` ${C.dim}Common causes:${C.reset}`);
|
|
578
|
+
console.log(` • Stake account already deactivating`);
|
|
579
|
+
console.log(` • Insufficient balance for transaction fee`);
|
|
580
|
+
console.log(` • RPC endpoint not accepting transactions\n`);
|
|
581
|
+
}
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ============================================================================
|
|
587
|
+
// Entry Point
|
|
588
|
+
// ============================================================================
|
|
589
|
+
|
|
590
|
+
module.exports = { unstakeCommand };
|
|
591
|
+
|
|
592
|
+
if (require.main === module) {
|
|
593
|
+
unstakeCommand().catch(err => {
|
|
594
|
+
console.error(`\n${C.red}✗ Unstake command failed:${C.reset} ${err.message}\n`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
});
|
|
597
|
+
}
|