aether-hub 1.2.6 → 1.2.7

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.
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli claim
4
+ *
5
+ * Claim accumulated staking rewards for a wallet.
6
+ * Fetches pending rewards from the chain and submits a claim transaction.
7
+ *
8
+ * Usage:
9
+ * aether claim --address <addr> [--json] [--rpc <url>]
10
+ *
11
+ * Examples:
12
+ * aether claim --address ATHxxx
13
+ * aether claim --address ATHxxx --json
14
+ */
15
+
16
+ const https = require('https');
17
+ const http = require('http');
18
+
19
+ // ANSI colours
20
+ const C = {
21
+ reset: '\x1b[0m',
22
+ bright: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ red: '\x1b[31m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ cyan: '\x1b[36m',
28
+ magenta: '\x1b[35m',
29
+ };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Config
33
+ // ---------------------------------------------------------------------------
34
+
35
+ function getDefaultRpc() {
36
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // HTTP helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function httpRequest(rpcUrl, pathStr, options = {}, timeoutMs = 8000) {
44
+ return new Promise((resolve, reject) => {
45
+ const url = new URL(pathStr, rpcUrl);
46
+ const lib = url.protocol === 'https:' ? https : http;
47
+ const reqOptions = {
48
+ hostname: url.hostname,
49
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
50
+ path: url.pathname + url.search,
51
+ method: options.method || 'GET',
52
+ headers: { 'Content-Type': 'application/json', ...options.headers },
53
+ };
54
+ const req = lib.request(reqOptions, (res) => {
55
+ let data = '';
56
+ res.on('data', (chunk) => data += chunk);
57
+ res.on('end', () => {
58
+ try { resolve(JSON.parse(data)); }
59
+ catch { resolve(data); }
60
+ });
61
+ });
62
+ req.on('error', reject);
63
+ req.setTimeout(timeoutMs, () => {
64
+ req.destroy();
65
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
66
+ });
67
+ if (options.body) req.write(options.body);
68
+ req.end();
69
+ });
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Argument parsing
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function parseArgs() {
77
+ const args = process.argv.slice(2);
78
+ const result = { address: null, json: false };
79
+
80
+ for (let i = 0; i < args.length; i++) {
81
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
82
+ result.address = args[i + 1];
83
+ i++;
84
+ } else if (args[i] === '--json' || args[i] === '--json-output') {
85
+ result.json = true;
86
+ } else if (args[i] === '--rpc' && args[i + 1]) {
87
+ result.rpc = args[i + 1];
88
+ i++;
89
+ } else if (args[i] === '--help' || args[i] === '-h') {
90
+ result.help = true;
91
+ } else if (args[i] === '--dry-run') {
92
+ result.dryRun = true;
93
+ }
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Format helpers
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function formatAether(lamports) {
104
+ const aeth = (lamports || 0) / 1e9;
105
+ return aeth.toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 }) + ' AETH';
106
+ }
107
+
108
+ function formatFlux(lamports) {
109
+ const flux = (lamports || 0) / 1e6;
110
+ return flux.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' FLUX';
111
+ }
112
+
113
+ function shortPubkey(pubkey) {
114
+ if (!pubkey || pubkey.length < 16) return pubkey || 'unknown';
115
+ return pubkey.slice(0, 8) + '...' + pubkey.slice(-8);
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Main
120
+ // ---------------------------------------------------------------------------
121
+
122
+ async function claimCommand() {
123
+ const opts = parseArgs();
124
+
125
+ if (opts.help) {
126
+ console.log(`
127
+ ${C.bright}${C.cyan}claim${C.reset} — Claim accumulated staking rewards for a wallet
128
+
129
+ ${C.bright}USAGE${C.reset}
130
+ aether claim --address <addr> [--json] [--rpc <url>] [--dry-run]
131
+
132
+ ${C.bright}OPTIONS${C.reset}
133
+ --address <addr> Wallet address (ATH...)
134
+ --json Output raw JSON
135
+ --rpc <url> RPC endpoint (default: AETHER_RPC or localhost:8899)
136
+ --dry-run Preview claim without submitting transaction
137
+ --help Show this help
138
+
139
+ ${C.bright}EXAMPLES${C.reset}
140
+ aether claim --address ATH3abc...
141
+ aether claim --address ATH3abc... --dry-run
142
+ aether claim --address ATH3abc... --json
143
+ `);
144
+ return;
145
+ }
146
+
147
+ if (!opts.address) {
148
+ console.log(` ${C.red}✗ Missing --address${C.reset}\n`);
149
+ console.log(` Usage: aether claim --address <addr> [--json] [--dry-run]\n`);
150
+ return;
151
+ }
152
+
153
+ const rpcUrl = opts.rpc || getDefaultRpc();
154
+ const address = opts.address;
155
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
156
+
157
+ if (!opts.json) {
158
+ console.log(`\n${C.bright}${C.cyan}── Claim Staking Rewards ────────────────────────────────${C.reset}\n`);
159
+ console.log(` ${C.dim}Wallet:${C.reset} ${address}`);
160
+ console.log(` ${C.dim}RPC: ${C.reset} ${rpcUrl}`);
161
+ if (opts.dryRun) console.log(` ${C.yellow}(dry-run mode - no transaction will be submitted)${C.reset}`);
162
+ console.log();
163
+ }
164
+
165
+ try {
166
+ // Step 1: Fetch stake positions to calculate pending rewards
167
+ const stakeRes = await httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`);
168
+
169
+ let stakeAccounts = [];
170
+ if (Array.isArray(stakeRes)) {
171
+ stakeAccounts = stakeRes;
172
+ } else if (stakeRes && typeof stakeRes === 'object') {
173
+ stakeAccounts = stakeRes.accounts || stakeRes.stake_accounts || stakeRes.data || [];
174
+ }
175
+
176
+ if (!opts.json) {
177
+ console.log(` ${C.dim}Fetching stake positions...${C.reset}`);
178
+ }
179
+
180
+ if (!stakeAccounts || stakeAccounts.length === 0) {
181
+ console.log(` ${C.yellow}? No active stake positions found.${C.reset}`);
182
+ console.log(` ${C.dim} Stake AETH with: ${C.cyan}aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
183
+ return;
184
+ }
185
+
186
+ // Calculate total pending rewards
187
+ let totalPendingRewards = 0;
188
+ const rewardBreakdown = [];
189
+
190
+ for (const acc of stakeAccounts) {
191
+ const pendingRewards = acc.pending_rewards || acc.pendingRewards || acc.rewards || 0;
192
+ const stakeLamports = acc.stake_lamports || acc.lamports || 0;
193
+ const validator = acc.validator || acc.delegate || acc.validator_address || 'unknown';
194
+
195
+ totalPendingRewards += pendingRewards;
196
+ rewardBreakdown.push({
197
+ stakeAcct: acc.pubkey || acc.publicKey || acc.account || 'unknown',
198
+ validator,
199
+ stakeLamports,
200
+ pendingRewards,
201
+ });
202
+ }
203
+
204
+ if (!opts.json) {
205
+ console.log(` ${C.bright}Stake Positions (${stakeAccounts.length})${C.reset}\n`);
206
+
207
+ for (const pos of rewardBreakdown) {
208
+ const shortVal = shortPubkey(pos.validator);
209
+ const shortAcct = shortPubkey(pos.stakeAcct);
210
+ console.log(` ${C.dim}├─ ${C.reset}${shortAcct} → ${C.cyan}${shortVal}${C.reset}`);
211
+ console.log(` │ ${C.dim}Staked:${C.reset} ${formatAether(pos.stakeLamports)}`);
212
+ console.log(` │ ${C.green}Pending:${C.reset} ${formatFlux(pos.pendingRewards)}\n`);
213
+ }
214
+
215
+ console.log(` ${C.dim}────────────────────────────────────────${C.reset}`);
216
+ console.log(` ${C.bright}Total Pending Rewards:${C.reset} ${C.green}${formatFlux(totalPendingRewards)}${C.reset}\n`);
217
+ }
218
+
219
+ // Step 2: If not dry-run, submit claim transaction
220
+ if (opts.dryRun) {
221
+ console.log(` ${C.yellow}⚠ Dry run - not submitting claim transaction${C.reset}\n`);
222
+ if (opts.json) {
223
+ console.log(JSON.stringify({
224
+ wallet_address: address,
225
+ dry_run: true,
226
+ stake_count: stakeAccounts.length,
227
+ total_pending_flux: totalPendingRewards,
228
+ total_pending_aeth: (totalPendingRewards / 1e6).toFixed(2),
229
+ breakdown: rewardBreakdown.map(r => ({
230
+ stake_account: r.stakeAcct,
231
+ validator: r.validator,
232
+ pending_flux: r.pendingRewards,
233
+ })),
234
+ }, null, 2));
235
+ }
236
+ return;
237
+ }
238
+
239
+ // Step 3: Submit claim transaction
240
+ if (!opts.json) {
241
+ console.log(` ${C.dim}Submitting claim transaction...${C.reset}`);
242
+ }
243
+
244
+ const claimBody = JSON.stringify({
245
+ address: rawAddr,
246
+ stake_accounts: rewardBreakdown.map(r => r.stakeAcct),
247
+ });
248
+
249
+ const claimRes = await httpRequest(rpcUrl, '/v1/claim', {
250
+ method: 'POST',
251
+ body: claimBody,
252
+ });
253
+
254
+ if (opts.json) {
255
+ console.log(JSON.stringify({
256
+ wallet_address: address,
257
+ success: !claimRes.error,
258
+ total_claimed_flux: claimRes.claimed || totalPendingRewards,
259
+ tx_signature: claimRes.signature || claimRes.txid || null,
260
+ block_height: claimRes.block_height || null,
261
+ claimed_at: new Date().toISOString(),
262
+ }, null, 2));
263
+ return;
264
+ }
265
+
266
+ if (claimRes.error) {
267
+ console.log(` ${C.red}✗ Claim failed:${C.reset} ${claimRes.error}\n`);
268
+ process.exit(1);
269
+ }
270
+
271
+ console.log(` ${C.green}✓ Rewards claimed!${C.reset}`);
272
+ console.log(` ${C.dim} Amount:${C.reset} ${C.green}${formatFlux(claimRes.claimed || totalPendingRewards)}${C.reset}`);
273
+ if (claimRes.signature || claimRes.txid) {
274
+ console.log(` ${C.dim} Tx:${C.reset} ${shortPubkey(claimRes.signature || claimRes.txid)}`);
275
+ }
276
+ console.log();
277
+
278
+ } catch (err) {
279
+ if (opts.json) {
280
+ console.log(JSON.stringify({ address, error: err.message }, null, 2));
281
+ } else {
282
+ console.log(` ${C.red}✗ Failed to claim rewards:${C.reset} ${err.message}\n`);
283
+ }
284
+ process.exit(1);
285
+ }
286
+ }
287
+
288
+ module.exports = { claimCommand };
289
+
290
+ if (require.main === module) {
291
+ claimCommand();
292
+ }