aether-hub 1.0.6 → 1.1.1
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/network.js +503 -0
- package/commands/rewards.js +600 -0
- package/commands/snapshot.js +509 -0
- package/commands/validators.js +326 -0
- package/commands/wallet.js +48 -23
- package/index.js +371 -326
- 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
|
@@ -737,29 +737,6 @@ async function stakeWallet(rl) {
|
|
|
737
737
|
return;
|
|
738
738
|
}
|
|
739
739
|
|
|
740
|
-
// Derive the full wallet object (secret key needed for signing)
|
|
741
|
-
let keyPair;
|
|
742
|
-
try {
|
|
743
|
-
// Re-derive from public key stored in wallet file
|
|
744
|
-
// The secret key isn't stored — we'd need the mnemonic to re-derive.
|
|
745
|
-
// For signing, the CLI requires the wallet to have been created/imported in this session.
|
|
746
|
-
// We use bs58 decoded publicKey + nacl key derivation from stored entropy.
|
|
747
|
-
// Since we store only publicKey, we need the secret key for signing.
|
|
748
|
-
// Workaround: accept a --sign-with <secretkeybase58> flag for now, or
|
|
749
|
-
// require the wallet to be "active" via a session.
|
|
750
|
-
// For simplicity, derive a keypair using a stored seed phrase approach.
|
|
751
|
-
// The wallet.json only has public_key. We need nacl sign keypair.
|
|
752
|
-
// Let's require the secret key be provided for stake/transfer.
|
|
753
|
-
console.log(` ${C.red}✗ Signing requires the wallet secret key.${C.reset}`);
|
|
754
|
-
console.log(` ${C.dim}The wallet must be created/imported in this session to access the secret key.${C.reset}`);
|
|
755
|
-
console.log(` ${C.dim}For staking, use the JS SDK's offline signing flow instead.${C.reset}`);
|
|
756
|
-
console.log(` ${C.dim}See: aether-cli sdk js${C.reset}\n`);
|
|
757
|
-
return;
|
|
758
|
-
} catch (e) {
|
|
759
|
-
console.log(` ${C.red}✗ Failed to load wallet keys: ${e.message}${C.reset}\n`);
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
740
|
// Prompt for missing values interactively
|
|
764
741
|
if (!validator) {
|
|
765
742
|
console.log(` ${C.cyan}Enter validator address:${C.reset}`);
|
|
@@ -784,6 +761,30 @@ async function stakeWallet(rl) {
|
|
|
784
761
|
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
|
|
785
762
|
console.log();
|
|
786
763
|
|
|
764
|
+
// Ask for mnemonic to derive signing keypair
|
|
765
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
|
|
766
|
+
const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
|
|
767
|
+
console.log();
|
|
768
|
+
|
|
769
|
+
let keyPair;
|
|
770
|
+
try {
|
|
771
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
772
|
+
} catch (e) {
|
|
773
|
+
console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
774
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Verify the derived address matches the wallet
|
|
779
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
780
|
+
if (derivedAddress !== address) {
|
|
781
|
+
console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
|
|
782
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
783
|
+
console.log(` ${C.dim} Expected: ${address}${C.reset}`);
|
|
784
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
787
788
|
const confirm = await question(rl, ` ${C.yellow}Confirm stake? [y/N]${C.reset} > ${C.reset}`);
|
|
788
789
|
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
789
790
|
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|
|
@@ -894,6 +895,30 @@ async function transferWallet(rl) {
|
|
|
894
895
|
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
|
|
895
896
|
console.log();
|
|
896
897
|
|
|
898
|
+
// Ask for mnemonic to derive signing keypair
|
|
899
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
|
|
900
|
+
const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
|
|
901
|
+
console.log();
|
|
902
|
+
|
|
903
|
+
let keyPair;
|
|
904
|
+
try {
|
|
905
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
906
|
+
} catch (e) {
|
|
907
|
+
console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
908
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Verify the derived address matches the wallet
|
|
913
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
914
|
+
if (derivedAddress !== address) {
|
|
915
|
+
console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
|
|
916
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
917
|
+
console.log(` ${C.dim} Expected: ${address}${C.reset}`);
|
|
918
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
897
922
|
const confirm = await question(rl, ` ${C.yellow}Confirm transfer? [y/N]${C.reset} > ${C.reset}`);
|
|
898
923
|
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
899
924
|
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|