aether-hub 1.2.8 → 1.3.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.
@@ -1,412 +1,412 @@
1
- #!/usr/bin/env node
2
- /**
3
- * aether-cli delegations
4
- *
5
- * View stake delegations and accumulated rewards for a wallet.
6
- * Also supports claiming rewards from a stake account.
7
- *
8
- * Usage:
9
- * aether delegations list --address <addr> List all stake delegations
10
- * aether delegations list --address <addr> --json JSON output
11
- * aether delegations claim --address <addr> --account <stakeAcct> [--json]
12
- *
13
- * SDK wired to: GET /v1/slot, GET /v1/account/<addr>, GET /v1/stake/<addr>
14
- */
15
-
16
- const path = require('path');
17
- const readline = require('readline');
18
- const crypto = require('crypto');
19
- const bs58 = require('bs58').default;
20
- const bip39 = require('bip39');
21
- const nacl = require('tweetnacl');
22
-
23
- // ANSI colours
24
- const C = {
25
- reset: '\x1b[0m',
26
- bright: '\x1b[1m',
27
- dim: '\x1b[2m',
28
- red: '\x1b[31m',
29
- green: '\x1b[32m',
30
- yellow: '\x1b[33m',
31
- cyan: '\x1b[36m',
32
- magenta: '\x1b[35m',
33
- };
34
-
35
- const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
36
- const CLI_VERSION = '1.0.6';
37
-
38
- // ---------------------------------------------------------------------------
39
- // SDK Import
40
- // ---------------------------------------------------------------------------
41
-
42
- const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
43
- const aether = require(sdkPath);
44
-
45
- // ---------------------------------------------------------------------------
46
- // Paths & config
47
- // ---------------------------------------------------------------------------
48
-
49
- function getAetherDir() {
50
- return path.join(require('os').homedir(), '.aether');
51
- }
52
-
53
- function loadConfig() {
54
- const p = path.join(getAetherDir(), 'config.json');
55
- if (!require('fs').existsSync(p)) return { defaultWallet: null };
56
- try {
57
- return JSON.parse(require('fs').readFileSync(p, 'utf8'));
58
- } catch {
59
- return { defaultWallet: null };
60
- }
61
- }
62
-
63
- function loadWallet(address) {
64
- const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
65
- if (!require('fs').existsSync(fp)) return null;
66
- return JSON.parse(require('fs').readFileSync(fp, 'utf8'));
67
- }
68
-
69
- // ---------------------------------------------------------------------------
70
- // Crypto helpers (mirrored from wallet.js)
71
- // ---------------------------------------------------------------------------
72
-
73
- function deriveKeypair(mnemonic) {
74
- if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic');
75
- const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
76
- const seed32 = seedBuffer.slice(0, 32);
77
- const keyPair = nacl.sign.keyPair.fromSeed(seed32);
78
- return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
79
- }
80
-
81
- function formatAddress(publicKey) {
82
- return 'ATH' + bs58.encode(publicKey);
83
- }
84
-
85
- // ---------------------------------------------------------------------------
86
- // Config helpers
87
- // ---------------------------------------------------------------------------
88
-
89
- function getDefaultRpc() {
90
- return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
91
- }
92
-
93
- function createClient(rpcUrl) {
94
- return new aether.AetherClient({ rpcUrl });
95
- }
96
-
97
- function formatAether(lamports) {
98
- const aeth = lamports / 1e9;
99
- if (aeth === 0) return '0 AETH';
100
- return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
101
- }
102
-
103
- // ---------------------------------------------------------------------------
104
- // Parse CLI args
105
- // ---------------------------------------------------------------------------
106
-
107
- function parseArgs() {
108
- const args = process.argv.slice(3); // [node, index.js, delegations, <subcmd>, ...]
109
- return args;
110
- }
111
-
112
- function createRl() {
113
- return readline.createInterface({ input: process.stdin, output: process.stdout });
114
- }
115
-
116
- function question(rl, q) {
117
- return new Promise((res) => rl.question(q, res));
118
- }
119
-
120
- async function askMnemonic(rl, prompt) {
121
- console.log(`\n${C.cyan}${prompt}${C.reset}`);
122
- console.log(`${C.dim}Enter your 12 or 24-word passphrase, one space-separated line:${C.reset}`);
123
- const raw = await question(rl, ` > ${C.reset}`);
124
- return raw.trim().toLowerCase();
125
- }
126
-
127
- // ---------------------------------------------------------------------------
128
- // LIST DELEGATIONS — uses SDK
129
- // ---------------------------------------------------------------------------
130
-
131
- async function listDelegations(args) {
132
- const rl = createRl();
133
- let address = null;
134
- let asJson = false;
135
- let rpcUrl = getDefaultRpc();
136
-
137
- for (let i = 0; i < args.length; i++) {
138
- if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) address = args[++i];
139
- else if (args[i] === '--json' || args[i] === '-j') asJson = true;
140
- else if ((args[i] === '--rpc' || args[i] === '-r') && args[i + 1]) rpcUrl = args[++i];
141
- }
142
-
143
- if (!address) {
144
- const cfg = loadConfig();
145
- address = cfg.defaultWallet;
146
- }
147
-
148
- if (!address) {
149
- console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
150
- console.log(` ${C.dim}Usage: aether delegations list --address <addr> [--json]${C.reset}\n`);
151
- rl.close();
152
- return;
153
- }
154
-
155
- const client = createClient(rpcUrl);
156
- const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
157
-
158
- try {
159
- // Real chain RPC calls via SDK
160
- const [account, stakeAccounts] = await Promise.all([
161
- client.getAccountInfo(rawAddr).catch(() => null),
162
- client.getStakePositions(rawAddr).catch(() => []),
163
- ]);
164
-
165
- if (asJson) {
166
- console.log(JSON.stringify({
167
- address,
168
- rpc: rpcUrl,
169
- account: account && !account.error ? { lamports: account.lamports } : null,
170
- delegations: stakeAccounts,
171
- cli_version: CLI_VERSION,
172
- fetched_at: new Date().toISOString(),
173
- }, null, 2));
174
- rl.close();
175
- return;
176
- }
177
-
178
- console.log(`\n${C.bright}${C.cyan}── Stake Delegations ─────────────────────────────────────${C.reset}\n`);
179
- console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
180
- console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
181
- if (account && !account.error) {
182
- console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(account.lamports || 0)}${C.reset}`);
183
- }
184
- console.log();
185
-
186
- if (!stakeAccounts || stakeAccounts.length === 0) {
187
- console.log(` ${C.dim}No stake delegations found for this wallet.${C.reset}`);
188
- console.log(` ${C.dim}Delegate with:${C.reset} ${C.cyan}aether stake --address ${address} --validator <val> --amount <aeth>${C.reset}\n`);
189
- rl.close();
190
- return;
191
- }
192
-
193
- const typeColors = {
194
- Stake: C.green,
195
- Unstake: C.yellow,
196
- ClaimRewards: C.magenta,
197
- };
198
-
199
- for (const stake of stakeAccounts) {
200
- const status = stake.status || stake.state || 'active';
201
- const statusColor = status === 'active' ? C.green : status === 'unstaked' ? C.yellow : C.red;
202
- const validator = stake.validator || stake.delegation?.validator || 'unknown';
203
- const amount = stake.lamports || stake.amount || stake.delegation?.lamports || 0;
204
- const rewards = stake.rewards || stake.pending_rewards || 0;
205
- const stakeAcct = stake.pubkey || stake.publicKey || stake.account || 'unknown';
206
-
207
- console.log(` ${C.bright}┌─ ${stakeAcct}${C.reset}`);
208
- console.log(` │ Validator: ${C.cyan}${validator}${C.reset}`);
209
- console.log(` │ Amount: ${C.bright}${formatAether(amount)}${C.reset}`);
210
- if (rewards > 0) {
211
- console.log(` │ ${C.magenta}★ Rewards: ${formatAether(rewards)}${C.reset}`);
212
- }
213
- console.log(` │ Status: ${statusColor}${status}${C.reset}`);
214
- console.log(` ${C.dim}└${C.reset}`);
215
- console.log();
216
- }
217
-
218
- // Summary
219
- const totalDelegated = stakeAccounts.reduce((sum, s) => sum + (s.lamports || s.amount || s.delegation?.lamports || 0), 0);
220
- const totalRewards = stakeAccounts.reduce((sum, s) => sum + (s.rewards || s.pending_rewards || 0), 0);
221
- console.log(` ${C.dim}────────────────────────────────────────${C.reset}`);
222
- console.log(` ${C.dim}Total delegated: ${C.reset}${C.bright}${formatAether(totalDelegated)}${C.reset}`);
223
- if (totalRewards > 0) {
224
- console.log(` ${C.dim}Total rewards: ${C.reset}${C.bright}${C.magenta}${formatAether(totalRewards)}${C.reset}`);
225
- console.log(` ${C.dim} Claim with: aether delegations claim --address ${address} --account <stake_account>${C.reset}`);
226
- }
227
- console.log();
228
- rl.close();
229
- } catch (err) {
230
- console.log(` ${C.red}✗ Failed to fetch delegations:${C.reset} ${err.message}`);
231
- console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
232
- rl.close();
233
- process.exit(1);
234
- }
235
- }
236
-
237
- // ---------------------------------------------------------------------------
238
- // CLAIM REWARDS — uses SDK for fetch, wallet for signing
239
- // ---------------------------------------------------------------------------
240
-
241
- async function claimRewards(args) {
242
- const rl = createRl();
243
-
244
- let address = null;
245
- let stakeAccount = null;
246
- let asJson = false;
247
- let rpcUrl = getDefaultRpc();
248
-
249
- for (let i = 0; i < args.length; i++) {
250
- if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) address = args[++i];
251
- else if ((args[i] === '--account' || args[i] === '-s') && args[i + 1]) stakeAccount = args[++i];
252
- else if (args[i] === '--json' || args[i] === '-j') asJson = true;
253
- else if ((args[i] === '--rpc' || args[i] === '-r') && args[i + 1]) rpcUrl = args[++i];
254
- }
255
-
256
- if (!address) {
257
- const cfg = loadConfig();
258
- address = cfg.defaultWallet;
259
- }
260
-
261
- if (!address) {
262
- console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
263
- console.log(` ${C.dim}Usage: aether delegations claim --address <addr> --account <stakeAcct>${C.reset}\n`);
264
- rl.close();
265
- return;
266
- }
267
-
268
- const client = createClient(rpcUrl);
269
-
270
- // If no stake account specified, fetch list via SDK
271
- if (!stakeAccount) {
272
- const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
273
- let stakeAccounts = await client.getStakePositions(rawAddr).catch(() => []);
274
-
275
- if (!stakeAccounts || stakeAccounts.length === 0) {
276
- console.log(` ${C.red}✗ No stake accounts found.${C.reset} Use ${C.cyan}--account <stakeAcct>${C.reset} to specify one.\n`);
277
- rl.close();
278
- return;
279
- }
280
-
281
- console.log(`\n${C.bright}${C.cyan}── Select Stake Account ──────────────────────────────────${C.reset}\n`);
282
- for (let i = 0; i < stakeAccounts.length; i++) {
283
- const s = stakeAccounts[i];
284
- const rewards = s.rewards || s.pending_rewards || 0;
285
- const validator = s.validator || s.delegation?.validator || 'unknown';
286
- console.log(` ${C.green}${i + 1})${C.reset} ${s.pubkey || s.publicKey || s.account}`);
287
- console.log(` Validator: ${C.cyan}${validator}${C.reset} Rewards: ${C.magenta}${formatAether(rewards)}${C.reset}`);
288
- }
289
- console.log();
290
- const choice = await question(rl, ` ${C.cyan}Select account [1-${stakeAccounts.length}]:${C.reset} `);
291
- const idx = parseInt(choice.trim(), 10) - 1;
292
- if (isNaN(idx) || idx < 0 || idx >= stakeAccounts.length) {
293
- console.log(` ${C.red}Invalid selection.${C.reset}\n`);
294
- rl.close();
295
- return;
296
- }
297
- stakeAccount = stakeAccounts[idx].pubkey || stakeAccounts[idx].publicKey || stakeAccounts[idx].account;
298
- }
299
-
300
- const wallet = loadWallet(address);
301
- if (!wallet) {
302
- console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
303
- rl.close();
304
- return;
305
- }
306
-
307
- console.log(`\n${C.bright}${C.cyan}── Claim Rewards ─────────────────────────────────────────${C.reset}\n`);
308
- console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
309
- console.log(` ${C.green}★${C.reset} Stake acct: ${C.bright}${stakeAccount}${C.reset}`);
310
- console.log();
311
-
312
- // Ask for mnemonic to derive signing keypair
313
- console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
314
- const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
315
- console.log();
316
-
317
- let keyPair;
318
- try {
319
- keyPair = deriveKeypair(mnemonic);
320
- } catch (e) {
321
- console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}\n`);
322
- rl.close();
323
- return;
324
- }
325
-
326
- const derivedAddress = formatAddress(keyPair.publicKey);
327
- if (derivedAddress !== address) {
328
- console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
329
- console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
330
- console.log(` ${C.dim} Expected: ${address}${C.reset}\n`);
331
- rl.close();
332
- return;
333
- }
334
-
335
- const confirm = await question(rl, ` ${C.yellow}Confirm claim? [y/N]${C.reset} > ${C.reset}`);
336
- if (!confirm.trim().toLowerCase().startsWith('y')) {
337
- console.log(` ${C.dim}Cancelled.${C.reset}\n`);
338
- rl.close();
339
- return;
340
- }
341
-
342
- // Build claim rewards transaction
343
- const tx = {
344
- signer: address.startsWith('ATH') ? address.slice(3) : address,
345
- tx_type: 'ClaimRewards',
346
- payload: {
347
- type: 'ClaimRewards',
348
- data: {
349
- stake_account: stakeAccount,
350
- },
351
- },
352
- fee: 0,
353
- slot: 0,
354
- timestamp: Math.floor(Date.now() / 1000),
355
- };
356
-
357
- console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
358
-
359
- try {
360
- const result = await client.sendTransaction(tx);
361
-
362
- if (result.error) {
363
- console.log(`\n ${C.red}✗ Claim failed:${C.reset} ${result.error}\n`);
364
- rl.close();
365
- process.exit(1);
366
- }
367
-
368
- const sig = result.signature || result.tx_signature || result.id || JSON.stringify(result);
369
- console.log(`\n${C.green}✓ Rewards claim submitted!${C.reset}`);
370
- console.log(` ${C.dim}Signature: ${sig}${C.reset}`);
371
- console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
372
- rl.close();
373
- } catch (err) {
374
- console.log(` ${C.red}✗ Failed to submit claim:${C.reset} ${err.message}`);
375
- console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
376
- rl.close();
377
- process.exit(1);
378
- }
379
- }
380
-
381
- // ---------------------------------------------------------------------------
382
- // Main dispatcher
383
- // ---------------------------------------------------------------------------
384
-
385
- async function delegationsCommand() {
386
- const args = parseArgs();
387
- const subcmd = args[0];
388
-
389
- const rl = createRl();
390
- try {
391
- if (!subcmd || subcmd === 'list') {
392
- await listDelegations(args);
393
- } else if (subcmd === 'claim') {
394
- await claimRewards(args);
395
- } else {
396
- console.log(`\n ${C.red}Unknown subcommand:${C.reset} ${subcmd}`);
397
- console.log(`\n Usage:`);
398
- console.log(` ${C.cyan}aether delegations list --address <addr>${C.reset} List stake delegations`);
399
- console.log(` ${C.cyan}aether delegations claim --address <addr> --account <stakeAcct>${C.reset} Claim rewards`);
400
- console.log();
401
- process.exit(1);
402
- }
403
- } finally {
404
- rl.close();
405
- }
406
- }
407
-
408
- module.exports = { delegationsCommand };
409
-
410
- if (require.main === module) {
411
- delegationsCommand();
412
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli delegations
4
+ *
5
+ * View stake delegations and accumulated rewards for a wallet.
6
+ * Also supports claiming rewards from a stake account.
7
+ *
8
+ * Usage:
9
+ * aether delegations list --address <addr> List all stake delegations
10
+ * aether delegations list --address <addr> --json JSON output
11
+ * aether delegations claim --address <addr> --account <stakeAcct> [--json]
12
+ *
13
+ * SDK wired to: GET /v1/slot, GET /v1/account/<addr>, GET /v1/stake/<addr>
14
+ */
15
+
16
+ const path = require('path');
17
+ const readline = require('readline');
18
+ const crypto = require('crypto');
19
+ const bs58 = require('bs58').default;
20
+ const bip39 = require('bip39');
21
+ const nacl = require('tweetnacl');
22
+
23
+ // ANSI colours
24
+ const C = {
25
+ reset: '\x1b[0m',
26
+ bright: '\x1b[1m',
27
+ dim: '\x1b[2m',
28
+ red: '\x1b[31m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ cyan: '\x1b[36m',
32
+ magenta: '\x1b[35m',
33
+ };
34
+
35
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
36
+ const CLI_VERSION = '1.0.6';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // SDK Import
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
43
+ const aether = require(sdkPath);
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Paths & config
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function getAetherDir() {
50
+ return path.join(require('os').homedir(), '.aether');
51
+ }
52
+
53
+ function loadConfig() {
54
+ const p = path.join(getAetherDir(), 'config.json');
55
+ if (!require('fs').existsSync(p)) return { defaultWallet: null };
56
+ try {
57
+ return JSON.parse(require('fs').readFileSync(p, 'utf8'));
58
+ } catch {
59
+ return { defaultWallet: null };
60
+ }
61
+ }
62
+
63
+ function loadWallet(address) {
64
+ const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
65
+ if (!require('fs').existsSync(fp)) return null;
66
+ return JSON.parse(require('fs').readFileSync(fp, 'utf8'));
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Crypto helpers (mirrored from wallet.js)
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function deriveKeypair(mnemonic) {
74
+ if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic');
75
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
76
+ const seed32 = seedBuffer.slice(0, 32);
77
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
78
+ return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
79
+ }
80
+
81
+ function formatAddress(publicKey) {
82
+ return 'ATH' + bs58.encode(publicKey);
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Config helpers
87
+ // ---------------------------------------------------------------------------
88
+
89
+ function getDefaultRpc() {
90
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
91
+ }
92
+
93
+ function createClient(rpcUrl) {
94
+ return new aether.AetherClient({ rpcUrl });
95
+ }
96
+
97
+ function formatAether(lamports) {
98
+ const aeth = lamports / 1e9;
99
+ if (aeth === 0) return '0 AETH';
100
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Parse CLI args
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function parseArgs() {
108
+ const args = process.argv.slice(3); // [node, index.js, delegations, <subcmd>, ...]
109
+ return args;
110
+ }
111
+
112
+ function createRl() {
113
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
114
+ }
115
+
116
+ function question(rl, q) {
117
+ return new Promise((res) => rl.question(q, res));
118
+ }
119
+
120
+ async function askMnemonic(rl, prompt) {
121
+ console.log(`\n${C.cyan}${prompt}${C.reset}`);
122
+ console.log(`${C.dim}Enter your 12 or 24-word passphrase, one space-separated line:${C.reset}`);
123
+ const raw = await question(rl, ` > ${C.reset}`);
124
+ return raw.trim().toLowerCase();
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // LIST DELEGATIONS — uses SDK
129
+ // ---------------------------------------------------------------------------
130
+
131
+ async function listDelegations(args) {
132
+ const rl = createRl();
133
+ let address = null;
134
+ let asJson = false;
135
+ let rpcUrl = getDefaultRpc();
136
+
137
+ for (let i = 0; i < args.length; i++) {
138
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) address = args[++i];
139
+ else if (args[i] === '--json' || args[i] === '-j') asJson = true;
140
+ else if ((args[i] === '--rpc' || args[i] === '-r') && args[i + 1]) rpcUrl = args[++i];
141
+ }
142
+
143
+ if (!address) {
144
+ const cfg = loadConfig();
145
+ address = cfg.defaultWallet;
146
+ }
147
+
148
+ if (!address) {
149
+ console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
150
+ console.log(` ${C.dim}Usage: aether delegations list --address <addr> [--json]${C.reset}\n`);
151
+ rl.close();
152
+ return;
153
+ }
154
+
155
+ const client = createClient(rpcUrl);
156
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
157
+
158
+ try {
159
+ // Real chain RPC calls via SDK
160
+ const [account, stakeAccounts] = await Promise.all([
161
+ client.getAccountInfo(rawAddr).catch(() => null),
162
+ client.getStakePositions(rawAddr).catch(() => []),
163
+ ]);
164
+
165
+ if (asJson) {
166
+ console.log(JSON.stringify({
167
+ address,
168
+ rpc: rpcUrl,
169
+ account: account && !account.error ? { lamports: account.lamports } : null,
170
+ delegations: stakeAccounts,
171
+ cli_version: CLI_VERSION,
172
+ fetched_at: new Date().toISOString(),
173
+ }, null, 2));
174
+ rl.close();
175
+ return;
176
+ }
177
+
178
+ console.log(`\n${C.bright}${C.cyan}── Stake Delegations ─────────────────────────────────────${C.reset}\n`);
179
+ console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
180
+ console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
181
+ if (account && !account.error) {
182
+ console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(account.lamports || 0)}${C.reset}`);
183
+ }
184
+ console.log();
185
+
186
+ if (!stakeAccounts || stakeAccounts.length === 0) {
187
+ console.log(` ${C.dim}No stake delegations found for this wallet.${C.reset}`);
188
+ console.log(` ${C.dim}Delegate with:${C.reset} ${C.cyan}aether stake --address ${address} --validator <val> --amount <aeth>${C.reset}\n`);
189
+ rl.close();
190
+ return;
191
+ }
192
+
193
+ const typeColors = {
194
+ Stake: C.green,
195
+ Unstake: C.yellow,
196
+ ClaimRewards: C.magenta,
197
+ };
198
+
199
+ for (const stake of stakeAccounts) {
200
+ const status = stake.status || stake.state || 'active';
201
+ const statusColor = status === 'active' ? C.green : status === 'unstaked' ? C.yellow : C.red;
202
+ const validator = stake.validator || stake.delegation?.validator || 'unknown';
203
+ const amount = stake.lamports || stake.amount || stake.delegation?.lamports || 0;
204
+ const rewards = stake.rewards || stake.pending_rewards || 0;
205
+ const stakeAcct = stake.pubkey || stake.publicKey || stake.account || 'unknown';
206
+
207
+ console.log(` ${C.bright}┌─ ${stakeAcct}${C.reset}`);
208
+ console.log(` │ Validator: ${C.cyan}${validator}${C.reset}`);
209
+ console.log(` │ Amount: ${C.bright}${formatAether(amount)}${C.reset}`);
210
+ if (rewards > 0) {
211
+ console.log(` │ ${C.magenta}★ Rewards: ${formatAether(rewards)}${C.reset}`);
212
+ }
213
+ console.log(` │ Status: ${statusColor}${status}${C.reset}`);
214
+ console.log(` ${C.dim}└${C.reset}`);
215
+ console.log();
216
+ }
217
+
218
+ // Summary
219
+ const totalDelegated = stakeAccounts.reduce((sum, s) => sum + (s.lamports || s.amount || s.delegation?.lamports || 0), 0);
220
+ const totalRewards = stakeAccounts.reduce((sum, s) => sum + (s.rewards || s.pending_rewards || 0), 0);
221
+ console.log(` ${C.dim}────────────────────────────────────────${C.reset}`);
222
+ console.log(` ${C.dim}Total delegated: ${C.reset}${C.bright}${formatAether(totalDelegated)}${C.reset}`);
223
+ if (totalRewards > 0) {
224
+ console.log(` ${C.dim}Total rewards: ${C.reset}${C.bright}${C.magenta}${formatAether(totalRewards)}${C.reset}`);
225
+ console.log(` ${C.dim} Claim with: aether delegations claim --address ${address} --account <stake_account>${C.reset}`);
226
+ }
227
+ console.log();
228
+ rl.close();
229
+ } catch (err) {
230
+ console.log(` ${C.red}✗ Failed to fetch delegations:${C.reset} ${err.message}`);
231
+ console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
232
+ rl.close();
233
+ process.exit(1);
234
+ }
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // CLAIM REWARDS — uses SDK for fetch, wallet for signing
239
+ // ---------------------------------------------------------------------------
240
+
241
+ async function claimRewards(args) {
242
+ const rl = createRl();
243
+
244
+ let address = null;
245
+ let stakeAccount = null;
246
+ let asJson = false;
247
+ let rpcUrl = getDefaultRpc();
248
+
249
+ for (let i = 0; i < args.length; i++) {
250
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) address = args[++i];
251
+ else if ((args[i] === '--account' || args[i] === '-s') && args[i + 1]) stakeAccount = args[++i];
252
+ else if (args[i] === '--json' || args[i] === '-j') asJson = true;
253
+ else if ((args[i] === '--rpc' || args[i] === '-r') && args[i + 1]) rpcUrl = args[++i];
254
+ }
255
+
256
+ if (!address) {
257
+ const cfg = loadConfig();
258
+ address = cfg.defaultWallet;
259
+ }
260
+
261
+ if (!address) {
262
+ console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
263
+ console.log(` ${C.dim}Usage: aether delegations claim --address <addr> --account <stakeAcct>${C.reset}\n`);
264
+ rl.close();
265
+ return;
266
+ }
267
+
268
+ const client = createClient(rpcUrl);
269
+
270
+ // If no stake account specified, fetch list via SDK
271
+ if (!stakeAccount) {
272
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
273
+ let stakeAccounts = await client.getStakePositions(rawAddr).catch(() => []);
274
+
275
+ if (!stakeAccounts || stakeAccounts.length === 0) {
276
+ console.log(` ${C.red}✗ No stake accounts found.${C.reset} Use ${C.cyan}--account <stakeAcct>${C.reset} to specify one.\n`);
277
+ rl.close();
278
+ return;
279
+ }
280
+
281
+ console.log(`\n${C.bright}${C.cyan}── Select Stake Account ──────────────────────────────────${C.reset}\n`);
282
+ for (let i = 0; i < stakeAccounts.length; i++) {
283
+ const s = stakeAccounts[i];
284
+ const rewards = s.rewards || s.pending_rewards || 0;
285
+ const validator = s.validator || s.delegation?.validator || 'unknown';
286
+ console.log(` ${C.green}${i + 1})${C.reset} ${s.pubkey || s.publicKey || s.account}`);
287
+ console.log(` Validator: ${C.cyan}${validator}${C.reset} Rewards: ${C.magenta}${formatAether(rewards)}${C.reset}`);
288
+ }
289
+ console.log();
290
+ const choice = await question(rl, ` ${C.cyan}Select account [1-${stakeAccounts.length}]:${C.reset} `);
291
+ const idx = parseInt(choice.trim(), 10) - 1;
292
+ if (isNaN(idx) || idx < 0 || idx >= stakeAccounts.length) {
293
+ console.log(` ${C.red}Invalid selection.${C.reset}\n`);
294
+ rl.close();
295
+ return;
296
+ }
297
+ stakeAccount = stakeAccounts[idx].pubkey || stakeAccounts[idx].publicKey || stakeAccounts[idx].account;
298
+ }
299
+
300
+ const wallet = loadWallet(address);
301
+ if (!wallet) {
302
+ console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
303
+ rl.close();
304
+ return;
305
+ }
306
+
307
+ console.log(`\n${C.bright}${C.cyan}── Claim Rewards ─────────────────────────────────────────${C.reset}\n`);
308
+ console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
309
+ console.log(` ${C.green}★${C.reset} Stake acct: ${C.bright}${stakeAccount}${C.reset}`);
310
+ console.log();
311
+
312
+ // Ask for mnemonic to derive signing keypair
313
+ console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
314
+ const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
315
+ console.log();
316
+
317
+ let keyPair;
318
+ try {
319
+ keyPair = deriveKeypair(mnemonic);
320
+ } catch (e) {
321
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}\n`);
322
+ rl.close();
323
+ return;
324
+ }
325
+
326
+ const derivedAddress = formatAddress(keyPair.publicKey);
327
+ if (derivedAddress !== address) {
328
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
329
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
330
+ console.log(` ${C.dim} Expected: ${address}${C.reset}\n`);
331
+ rl.close();
332
+ return;
333
+ }
334
+
335
+ const confirm = await question(rl, ` ${C.yellow}Confirm claim? [y/N]${C.reset} > ${C.reset}`);
336
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
337
+ console.log(` ${C.dim}Cancelled.${C.reset}\n`);
338
+ rl.close();
339
+ return;
340
+ }
341
+
342
+ // Build claim rewards transaction
343
+ const tx = {
344
+ signer: address.startsWith('ATH') ? address.slice(3) : address,
345
+ tx_type: 'ClaimRewards',
346
+ payload: {
347
+ type: 'ClaimRewards',
348
+ data: {
349
+ stake_account: stakeAccount,
350
+ },
351
+ },
352
+ fee: 0,
353
+ slot: 0,
354
+ timestamp: Math.floor(Date.now() / 1000),
355
+ };
356
+
357
+ console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
358
+
359
+ try {
360
+ const result = await client.sendTransaction(tx);
361
+
362
+ if (result.error) {
363
+ console.log(`\n ${C.red}✗ Claim failed:${C.reset} ${result.error}\n`);
364
+ rl.close();
365
+ process.exit(1);
366
+ }
367
+
368
+ const sig = result.signature || result.tx_signature || result.id || JSON.stringify(result);
369
+ console.log(`\n${C.green}✓ Rewards claim submitted!${C.reset}`);
370
+ console.log(` ${C.dim}Signature: ${sig}${C.reset}`);
371
+ console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
372
+ rl.close();
373
+ } catch (err) {
374
+ console.log(` ${C.red}✗ Failed to submit claim:${C.reset} ${err.message}`);
375
+ console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
376
+ rl.close();
377
+ process.exit(1);
378
+ }
379
+ }
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Main dispatcher
383
+ // ---------------------------------------------------------------------------
384
+
385
+ async function delegationsCommand() {
386
+ const args = parseArgs();
387
+ const subcmd = args[0];
388
+
389
+ const rl = createRl();
390
+ try {
391
+ if (!subcmd || subcmd === 'list') {
392
+ await listDelegations(args);
393
+ } else if (subcmd === 'claim') {
394
+ await claimRewards(args);
395
+ } else {
396
+ console.log(`\n ${C.red}Unknown subcommand:${C.reset} ${subcmd}`);
397
+ console.log(`\n Usage:`);
398
+ console.log(` ${C.cyan}aether delegations list --address <addr>${C.reset} List stake delegations`);
399
+ console.log(` ${C.cyan}aether delegations claim --address <addr> --account <stakeAcct>${C.reset} Claim rewards`);
400
+ console.log();
401
+ process.exit(1);
402
+ }
403
+ } finally {
404
+ rl.close();
405
+ }
406
+ }
407
+
408
+ module.exports = { delegationsCommand };
409
+
410
+ if (require.main === module) {
411
+ delegationsCommand();
412
+ }