aether-hub 1.1.0 → 1.1.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/commands/delegations.js +462 -0
- package/commands/emergency.js +657 -0
- package/commands/rewards.js +600 -0
- package/commands/snapshot.js +509 -0
- package/commands/validators.js +326 -0
- package/commands/wallet.js +168 -0
- package/index.js +389 -341
- package/package.json +2 -2
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aether-cli validators - Aether Validator Registry
|
|
3
|
+
*
|
|
4
|
+
* List and explore validators available for staking.
|
|
5
|
+
* aether validators list — Show all active validators
|
|
6
|
+
* aether validators list --json — JSON output for scripting
|
|
7
|
+
* aether validators list --tier <full|lite|observer> — Filter by tier
|
|
8
|
+
*
|
|
9
|
+
* @see docs/MINING_VALIDATOR_TOOLS.md for spec
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const https = require('https');
|
|
14
|
+
|
|
15
|
+
// ANSI colours
|
|
16
|
+
const C = {
|
|
17
|
+
reset: '\x1b[0m',
|
|
18
|
+
bright: '\x1b[1m',
|
|
19
|
+
dim: '\x1b[2m',
|
|
20
|
+
red: '\x1b[31m',
|
|
21
|
+
green: '\x1b[32m',
|
|
22
|
+
yellow: '\x1b[33m',
|
|
23
|
+
blue: '\x1b[34m',
|
|
24
|
+
cyan: '\x1b[36m',
|
|
25
|
+
magenta: '\x1b[35m',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_RPC = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// HTTP helpers (mirrors network.js patterns)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function httpRequest(rpcUrl, path) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const url = new URL(path, rpcUrl);
|
|
37
|
+
const isHttps = url.protocol === 'https:';
|
|
38
|
+
const lib = isHttps ? https : http;
|
|
39
|
+
|
|
40
|
+
const req = lib.request({
|
|
41
|
+
hostname: url.hostname,
|
|
42
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
43
|
+
path: url.pathname + url.search,
|
|
44
|
+
method: 'GET',
|
|
45
|
+
timeout: 8000,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
}, (res) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
res.on('data', (chunk) => (data += chunk));
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try { resolve(JSON.parse(data)); }
|
|
52
|
+
catch { resolve({ raw: data }); }
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
req.on('error', reject);
|
|
57
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
58
|
+
req.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Argument parsing
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function parseArgs() {
|
|
67
|
+
const args = process.argv.slice(3); // skip 'validators' and subcommand
|
|
68
|
+
const opts = {
|
|
69
|
+
rpc: DEFAULT_RPC,
|
|
70
|
+
tier: null,
|
|
71
|
+
asJson: false,
|
|
72
|
+
sort: 'stake', // 'stake' | 'name' | 'uptime'
|
|
73
|
+
limit: 50,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
if (args[i] === '--rpc' || args[i] === '-r') {
|
|
78
|
+
opts.rpc = args[++i];
|
|
79
|
+
} else if (args[i] === '--tier' || args[i] === '-t') {
|
|
80
|
+
opts.tier = args[++i]?.toLowerCase();
|
|
81
|
+
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
82
|
+
opts.asJson = true;
|
|
83
|
+
} else if (args[i] === '--sort' || args[i] === '-s') {
|
|
84
|
+
opts.sort = args[++i]?.toLowerCase();
|
|
85
|
+
} else if (args[i] === '--limit' || args[i] === '-l') {
|
|
86
|
+
opts.limit = parseInt(args[++i], 10) || 50;
|
|
87
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
88
|
+
showHelp();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return opts;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function showHelp() {
|
|
97
|
+
console.log(`
|
|
98
|
+
${C.bright}${C.cyan}aether-cli validators${C.reset} - Aether Validator Registry
|
|
99
|
+
|
|
100
|
+
${C.bright}Usage:${C.reset}
|
|
101
|
+
aether validators list [options]
|
|
102
|
+
|
|
103
|
+
${C.bright}Options:${C.reset}
|
|
104
|
+
-r, --rpc <url> RPC endpoint (default: ${DEFAULT_RPC} or $AETHER_RPC)
|
|
105
|
+
-t, --tier <tier> Filter by tier: full, lite, observer
|
|
106
|
+
-s, --sort <field> Sort by: stake (default), name, uptime
|
|
107
|
+
-l, --limit <n> Max validators to show (default: 50)
|
|
108
|
+
-j, --json Output raw JSON
|
|
109
|
+
-h, --help Show this help
|
|
110
|
+
|
|
111
|
+
${C.bright}Examples:${C.reset}
|
|
112
|
+
aether validators list
|
|
113
|
+
aether validators list --tier full
|
|
114
|
+
aether validators list --sort stake --limit 20
|
|
115
|
+
aether validators list --json
|
|
116
|
+
`.trim());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Data fetching
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/** Fetch validators list from RPC */
|
|
124
|
+
async function fetchValidators(rpc) {
|
|
125
|
+
try {
|
|
126
|
+
const res = await httpRequest(rpc, '/v1/validators');
|
|
127
|
+
if (Array.isArray(res)) return res;
|
|
128
|
+
if (res.validators && Array.isArray(res.validators)) return res.validators;
|
|
129
|
+
return [];
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Fetch epoch info for context */
|
|
136
|
+
async function fetchEpoch(rpc) {
|
|
137
|
+
try {
|
|
138
|
+
const res = await httpRequest(rpc, '/v1/epoch');
|
|
139
|
+
return res;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Formatting helpers
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function formatAether(lamports) {
|
|
150
|
+
if (!lamports && lamports !== 0) return '?';
|
|
151
|
+
const aeth = lamports / 1e9;
|
|
152
|
+
if (aeth === 0) return '0';
|
|
153
|
+
return aeth.toFixed(2).replace(/\.?0+$/, '');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatPct(n) {
|
|
157
|
+
if (n === undefined || n === null) return '?';
|
|
158
|
+
return n.toFixed(1) + '%';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function tierBadge(tier) {
|
|
162
|
+
const map = {
|
|
163
|
+
full: `${C.cyan}FULL${C.reset}`,
|
|
164
|
+
lite: `${C.yellow}LITE${C.reset}`,
|
|
165
|
+
observer: `${C.green}OBS${C.reset}`,
|
|
166
|
+
};
|
|
167
|
+
return map[tier?.toLowerCase()] || `${C.dim}?${C.reset}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function scoreColor(score) {
|
|
171
|
+
if (score === undefined || score === null) return C.dim;
|
|
172
|
+
if (score >= 90) return C.green;
|
|
173
|
+
if (score >= 70) return C.yellow;
|
|
174
|
+
return C.red;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function shortenAddr(addr, len = 16) {
|
|
178
|
+
if (!addr) return '?';
|
|
179
|
+
if (addr.length <= len) return addr;
|
|
180
|
+
return addr.slice(0, 8) + '…' + addr.slice(-6);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Renderers
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
function renderList(validators, epochData, opts, rpc) {
|
|
188
|
+
const filtered = opts.tier
|
|
189
|
+
? validators.filter(v => (v.tier || v.node_type || '').toLowerCase() === opts.tier)
|
|
190
|
+
: validators;
|
|
191
|
+
|
|
192
|
+
// Sort
|
|
193
|
+
if (opts.sort === 'name') {
|
|
194
|
+
filtered.sort((a, b) => (a.name || a.address || '').localeCompare(b.name || b.address || ''));
|
|
195
|
+
} else if (opts.sort === 'uptime') {
|
|
196
|
+
filtered.sort((a, b) => (b.uptime || b.score || 0) - (a.uptime || a.score || 0));
|
|
197
|
+
} else {
|
|
198
|
+
// Default: by stake (descending)
|
|
199
|
+
filtered.sort((a, b) => (b.stake || b.stake_amount || 0) - (a.stake || a.stake_amount || 0));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const shown = filtered.slice(0, opts.limit);
|
|
203
|
+
const totalStake = validators.reduce((sum, v) => sum + (v.stake || v.stake_amount || 0), 0);
|
|
204
|
+
const networkScore = validators.reduce((sum, v) => sum + (v.score || v.uptime || 0) * (v.stake || 1), 0) / (totalStake || 1);
|
|
205
|
+
|
|
206
|
+
console.log();
|
|
207
|
+
console.log(`${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
208
|
+
console.log(`${C.bright}${C.cyan}║${C.reset} ${C.bright}AETHER VALIDATOR REGISTRY${C.reset}${C.cyan} ║${C.reset}`);
|
|
209
|
+
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
210
|
+
console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
|
|
211
|
+
console.log(` ${C.dim}Total validators:${C.reset} ${C.bright}${validators.length}${C.reset}`);
|
|
212
|
+
if (opts.tier) console.log(` ${C.dim}Filtered by tier:${C.reset} ${C.bright}${opts.tier}${C.reset}`);
|
|
213
|
+
console.log();
|
|
214
|
+
|
|
215
|
+
// Summary stats
|
|
216
|
+
console.log(` ${C.bright}┌──────────────────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
217
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Total Stake${C.reset} ${C.bright}│${C.reset} ${C.green}${formatAether(totalStake).padEnd(20)} AETH${C.reset}`.padEnd(80) + `${C.bright}│${C.reset}`);
|
|
218
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Network Uptime${C.reset} ${C.bright}│${C.reset} ${scoreColor(networkScore)}${formatPct(networkScore).padEnd(20)}${C.reset}`.padEnd(80) + `${C.bright}│${C.reset}`);
|
|
219
|
+
if (epochData && epochData.epoch !== undefined) {
|
|
220
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}Current Epoch${C.reset} ${C.bright}│${C.reset} ${C.green}${epochData.epoch}${C.reset}`.padEnd(80) + `${C.bright}│${C.reset}`);
|
|
221
|
+
}
|
|
222
|
+
console.log(` ${C.bright}└──────────────────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
223
|
+
console.log();
|
|
224
|
+
|
|
225
|
+
if (shown.length === 0) {
|
|
226
|
+
console.log(` ${C.yellow}⚠ No validators found${C.reset}${opts.tier ? ` for tier "${opts.tier}"` : ''}.`);
|
|
227
|
+
console.log(` ${C.dim}Try without --tier filter or check RPC connectivity.${C.reset}`);
|
|
228
|
+
console.log();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Table header
|
|
233
|
+
console.log(` ${C.bright}┌────┬────────────────────────┬────────┬─────────┬────────┬─────────────┐${C.reset}`);
|
|
234
|
+
console.log(` ${C.bright}│${C.reset} ${C.cyan}#${C.reset} ${C.cyan}Validator${C.reset} ${C.cyan}Tier${C.reset} ${C.cyan}Stake${C.reset} ${C.cyan}Uptime${C.reset} ${C.cyan}Commission${C.reset} ${C.bright}│${C.reset}`);
|
|
235
|
+
console.log(` ${C.bright}├────┼────────────────────────┼────────┼─────────┼────────┼─────────────┤${C.reset}`);
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < shown.length; i++) {
|
|
238
|
+
const v = shown[i];
|
|
239
|
+
const num = (i + 1).toString().padStart(3);
|
|
240
|
+
const addr = shortenAddr(v.address || v.pubkey || v.id);
|
|
241
|
+
const tierStr = tierBadge(v.tier || v.node_type);
|
|
242
|
+
const stake = formatAether(v.stake || v.stake_amount || 0);
|
|
243
|
+
const uptime = formatPct(v.uptime || v.score);
|
|
244
|
+
const commission = formatPct(v.commission || v.fee);
|
|
245
|
+
const row = ` ${C.bright}│${C.reset} ${C.dim}${num}${C.reset} ${addr.padEnd(24)} ${tierStr.padEnd(8)} ${stake.padEnd(9)} ${scoreColor(v.uptime || v.score)}${uptime.padEnd(9)}${C.reset} ${C.dim}${commission}${C.reset} ${C.bright}│${C.reset}`;
|
|
246
|
+
console.log(row);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(` ${C.bright}└────┴────────────────────────┴────────┴─────────┴────────┴─────────────┘${C.reset}`);
|
|
250
|
+
console.log();
|
|
251
|
+
|
|
252
|
+
if (filtered.length > opts.limit) {
|
|
253
|
+
console.log(` ${C.dim}Showing ${opts.limit} of ${filtered.length} validators. Use --limit to see more.${C.reset}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(` ${C.dim}Stake to a validator:${C.reset}`);
|
|
258
|
+
console.log(` ${C.cyan}aether stake --validator <address> --amount <aeth>${C.reset}`);
|
|
259
|
+
console.log(` ${C.cyan}aether stake --validator <address> --amount <aeth> --dry-run${C.reset} ${C.dim}(preview)${C.reset}`);
|
|
260
|
+
console.log();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function renderJson(validators, epochData, opts, rpc) {
|
|
264
|
+
const out = {
|
|
265
|
+
rpc,
|
|
266
|
+
fetchedAt: new Date().toISOString(),
|
|
267
|
+
total: validators.length,
|
|
268
|
+
epoch: epochData?.epoch ?? null,
|
|
269
|
+
validators: validators.map(v => ({
|
|
270
|
+
address: v.address || v.pubkey || v.id,
|
|
271
|
+
name: v.name || null,
|
|
272
|
+
tier: v.tier || v.node_type || null,
|
|
273
|
+
stake: v.stake || v.stake_amount || 0,
|
|
274
|
+
stakeAETH: formatAether(v.stake || v.stake_amount || 0),
|
|
275
|
+
uptime: v.uptime ?? v.score ?? null,
|
|
276
|
+
uptimePct: formatPct(v.uptime ?? v.score),
|
|
277
|
+
commission: v.commission ?? v.fee ?? null,
|
|
278
|
+
commissionPct: formatPct(v.commission ?? v.fee),
|
|
279
|
+
lastSeen: v.last_seen || v.lastActive || null,
|
|
280
|
+
version: v.version || v.clientVersion || null,
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
console.log(JSON.stringify(out, null, 2));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Main
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
async function validatorsListCommand() {
|
|
291
|
+
const opts = parseArgs();
|
|
292
|
+
const rpc = opts.rpc;
|
|
293
|
+
|
|
294
|
+
if (!opts.asJson) {
|
|
295
|
+
console.log(`\n${C.cyan}Fetching validators...${C.reset} ${C.dim}(${rpc})${C.reset}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const [validators, epochData] = await Promise.all([
|
|
299
|
+
fetchValidators(rpc),
|
|
300
|
+
fetchEpoch(rpc),
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
if (validators.length === 0 && !opts.asJson) {
|
|
304
|
+
console.log(`\n ${C.yellow}⚠ No validator data returned from RPC.${C.reset}`);
|
|
305
|
+
console.log(` ${C.dim} Is your validator running and fully synced?${C.reset}`);
|
|
306
|
+
console.log(` ${C.dim} Check: aether-cli network${C.reset}`);
|
|
307
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=http://your-rpc-url${C.reset}\n`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (opts.asJson) {
|
|
312
|
+
renderJson(validators, epochData, opts, rpc);
|
|
313
|
+
} else {
|
|
314
|
+
renderList(validators, epochData, opts, rpc);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = { validatorsListCommand };
|
|
319
|
+
|
|
320
|
+
if (require.main === module) {
|
|
321
|
+
validatorsListCommand().catch((err) => {
|
|
322
|
+
console.error(`\n${C.red}✗ Validators command failed:${C.reset} ${err.message}`);
|
|
323
|
+
console.error(` ${C.dim}Check that your validator is running and RPC is accessible.${C.reset}\n`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
});
|
|
326
|
+
}
|
package/commands/wallet.js
CHANGED
|
@@ -1106,6 +1106,171 @@ async function txHistory(rl) {
|
|
|
1106
1106
|
}
|
|
1107
1107
|
}
|
|
1108
1108
|
|
|
1109
|
+
// ---------------------------------------------------------------------------
|
|
1110
|
+
// UNSTAKE
|
|
1111
|
+
// Submit an Unstake transaction via POST /v1/tx to deactivate stake
|
|
1112
|
+
// ---------------------------------------------------------------------------
|
|
1113
|
+
|
|
1114
|
+
async function unstakeWallet(rl) {
|
|
1115
|
+
console.log(`\n${C.bright}${C.cyan}── Unstake AETH ──────────────────────────────────────────${C.reset}\n`);
|
|
1116
|
+
|
|
1117
|
+
const args = process.argv.slice(4);
|
|
1118
|
+
let address = null;
|
|
1119
|
+
let stakeAccount = null;
|
|
1120
|
+
let amountStr = null;
|
|
1121
|
+
|
|
1122
|
+
for (let i = 0; i < args.length; i++) {
|
|
1123
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
1124
|
+
address = args[i + 1];
|
|
1125
|
+
}
|
|
1126
|
+
if ((args[i] === '--account' || args[i] === '-s') && args[i + 1]) {
|
|
1127
|
+
stakeAccount = args[i + 1];
|
|
1128
|
+
}
|
|
1129
|
+
if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
|
|
1130
|
+
amountStr = args[i + 1];
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (!address) {
|
|
1135
|
+
const cfg = loadConfig();
|
|
1136
|
+
address = cfg.defaultWallet;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (!address) {
|
|
1140
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
1141
|
+
console.log(` ${C.dim}Usage: aether unstake --account <stakeAcct> [--amount <aeth>] [--address <addr>]${C.reset}\n`);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const wallet = loadWallet(address);
|
|
1146
|
+
if (!wallet) {
|
|
1147
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Resolve stake account: --account flag, or query chain for first active stake
|
|
1152
|
+
if (!stakeAccount) {
|
|
1153
|
+
const rpcUrl = getDefaultRpc();
|
|
1154
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
1155
|
+
|
|
1156
|
+
let stakeAccounts = [];
|
|
1157
|
+
try {
|
|
1158
|
+
const res = await httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`);
|
|
1159
|
+
if (res && !res.error) {
|
|
1160
|
+
stakeAccounts = Array.isArray(res) ? res : (res.accounts || []);
|
|
1161
|
+
}
|
|
1162
|
+
} catch { /* no stake accounts */ }
|
|
1163
|
+
|
|
1164
|
+
if (stakeAccounts.length === 0) {
|
|
1165
|
+
console.log(` ${C.red}✗ No active stake accounts found for this wallet.${C.reset}`);
|
|
1166
|
+
console.log(` ${C.dim}Use ${C.cyan}--account <stakeAcct>${C.reset} ${C.dim}to specify a stake account.${C.reset}`);
|
|
1167
|
+
console.log(` ${C.dim}Check delegations: aether delegations list --address ${address}${C.reset}\n`);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Default to first active stake account
|
|
1172
|
+
const active = stakeAccounts.find(s => !s.deactivation_epoch && (s.status === 'active' || s.state === 'active'));
|
|
1173
|
+
stakeAccount = active
|
|
1174
|
+
? (active.pubkey || active.publicKey || active.account)
|
|
1175
|
+
: (stakeAccounts[0].pubkey || stakeAccounts[0].publicKey || stakeAccounts[0].account);
|
|
1176
|
+
|
|
1177
|
+
console.log(` ${C.cyan}Using stake account:${C.reset} ${C.bright}${stakeAccount}${C.reset}`);
|
|
1178
|
+
console.log(` ${C.dim}(override with ${C.cyan}--account <stakeAcct>${C.reset}${C.dim})${C.reset}\n`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Resolve amount: --amount flag, or prompt if partial unstake supported
|
|
1182
|
+
// If no amount provided, unstake entire stake
|
|
1183
|
+
let lamports = null;
|
|
1184
|
+
if (amountStr) {
|
|
1185
|
+
const amount = parseFloat(amountStr);
|
|
1186
|
+
if (isNaN(amount) || amount <= 0) {
|
|
1187
|
+
console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
lamports = Math.round(amount * 1e9);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
1194
|
+
console.log(` ${C.green}★${C.reset} Stake acct: ${C.bright}${stakeAccount}${C.reset}`);
|
|
1195
|
+
if (lamports !== null) {
|
|
1196
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${(lamports / 1e9).toFixed(4)} AETH${C.reset} (${lamports} lamports)`);
|
|
1197
|
+
} else {
|
|
1198
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}FULL STAKE${C.reset}`);
|
|
1199
|
+
}
|
|
1200
|
+
console.log();
|
|
1201
|
+
|
|
1202
|
+
// Ask for mnemonic to derive signing keypair
|
|
1203
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
|
|
1204
|
+
const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
|
|
1205
|
+
console.log();
|
|
1206
|
+
|
|
1207
|
+
let keyPair;
|
|
1208
|
+
try {
|
|
1209
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
1212
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Verify the derived address matches the wallet
|
|
1217
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
1218
|
+
if (derivedAddress !== address) {
|
|
1219
|
+
console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
|
|
1220
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
1221
|
+
console.log(` ${C.dim} Expected: ${address}${C.reset}\n`);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm unstake? [y/N]${C.reset} > ${C.reset}`);
|
|
1226
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
1227
|
+
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Build the unstake transaction
|
|
1232
|
+
const txData = {
|
|
1233
|
+
type: 'Unstake',
|
|
1234
|
+
data: {
|
|
1235
|
+
stake_account: stakeAccount,
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
if (lamports !== null) {
|
|
1239
|
+
txData.data.amount = lamports;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const tx = {
|
|
1243
|
+
signer: address.startsWith('ATH') ? address.slice(3) : address,
|
|
1244
|
+
tx_type: 'Unstake',
|
|
1245
|
+
payload: txData,
|
|
1246
|
+
fee: 0,
|
|
1247
|
+
slot: 0,
|
|
1248
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
const rpcUrl = getDefaultRpc();
|
|
1252
|
+
console.log(` ${C.dim}Submitting to ${rpcUrl}...${C.reset}`);
|
|
1253
|
+
|
|
1254
|
+
try {
|
|
1255
|
+
const result = await httpPost(rpcUrl, '/v1/tx', tx);
|
|
1256
|
+
|
|
1257
|
+
if (result.error) {
|
|
1258
|
+
console.log(`\n ${C.red}✗ Unstake failed:${C.reset} ${result.error}\n`);
|
|
1259
|
+
process.exit(1);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const sig = result.signature || result.tx_signature || result.id || JSON.stringify(result);
|
|
1263
|
+
console.log(`\n${C.green}✓ Unstake transaction submitted!${C.reset}`);
|
|
1264
|
+
console.log(` ${C.dim}Signature:${C.reset} ${sig}`);
|
|
1265
|
+
console.log(` ${C.dim}Stake will deactivate over the next epoch.${C.reset}`);
|
|
1266
|
+
console.log(` ${C.dim}Check status: aether delegations list --address ${address}${C.reset}\n`);
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
|
|
1269
|
+
console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1109
1274
|
// ---------------------------------------------------------------------------
|
|
1110
1275
|
// Main dispatcher
|
|
1111
1276
|
// ---------------------------------------------------------------------------
|
|
@@ -1133,6 +1298,8 @@ async function walletCommand() {
|
|
|
1133
1298
|
await balanceWallet(rl);
|
|
1134
1299
|
} else if (subcmd === 'stake') {
|
|
1135
1300
|
await stakeWallet(rl);
|
|
1301
|
+
} else if (subcmd === 'unstake') {
|
|
1302
|
+
await unstakeWallet(rl);
|
|
1136
1303
|
} else if (subcmd === 'transfer') {
|
|
1137
1304
|
await transferWallet(rl);
|
|
1138
1305
|
} else if (subcmd === 'history' || subcmd === 'tx') {
|
|
@@ -1147,6 +1314,7 @@ async function walletCommand() {
|
|
|
1147
1314
|
console.log(` ${C.cyan}aether wallet connect${C.reset} Connect wallet via browser verification`);
|
|
1148
1315
|
console.log(` ${C.cyan}aether wallet balance${C.reset} Query chain balance for an address`);
|
|
1149
1316
|
console.log(` ${C.cyan}aether wallet stake${C.reset} Stake AETH to a validator`);
|
|
1317
|
+
console.log(` ${C.cyan}aether wallet unstake${C.reset} Unstake AETH — deactivate a stake account`);
|
|
1150
1318
|
console.log(` ${C.cyan}aether wallet transfer${C.reset} Transfer AETH to another address`);
|
|
1151
1319
|
console.log(` ${C.cyan}aether wallet history${C.reset} Show recent transactions for an address`);
|
|
1152
1320
|
console.log();
|