aether-hub 1.1.1 → 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/emergency.js +657 -0
- package/commands/rewards.js +600 -600
- package/commands/wallet.js +168 -0
- package/index.js +18 -0
- package/package.json +1 -1
package/commands/rewards.js
CHANGED
|
@@ -1,600 +1,600 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* aether-cli rewards
|
|
4
|
-
*
|
|
5
|
-
* View staking rewards earned from delegated stake accounts.
|
|
6
|
-
* Shows accumulated rewards, estimated APY, and claimable amounts.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* aether rewards list --address <addr> List all rewards per stake account
|
|
10
|
-
* aether rewards list --address <addr> --json JSON output for scripting
|
|
11
|
-
* aether rewards claim --address <addr> --account <stakeAcct> [--json]
|
|
12
|
-
* aether rewards summary --address <addr> One-line summary of total rewards
|
|
13
|
-
*
|
|
14
|
-
* Requires AETHER_RPC env var or local node running (default: http://127.0.0.1:8899)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const http = require('http');
|
|
18
|
-
const https = require('https');
|
|
19
|
-
const readline = require('readline');
|
|
20
|
-
const crypto = require('crypto');
|
|
21
|
-
const bs58 = require('bs58').default;
|
|
22
|
-
const bip39 = require('bip39');
|
|
23
|
-
const nacl = require('tweetnacl');
|
|
24
|
-
|
|
25
|
-
// ANSI colours
|
|
26
|
-
const C = {
|
|
27
|
-
reset: '\x1b[0m',
|
|
28
|
-
bright: '\x1b[1m',
|
|
29
|
-
dim: '\x1b[2m',
|
|
30
|
-
red: '\x1b[31m',
|
|
31
|
-
green: '\x1b[32m',
|
|
32
|
-
yellow: '\x1b[33m',
|
|
33
|
-
cyan: '\x1b[36m',
|
|
34
|
-
magenta: '\x1b[35m',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
38
|
-
const CLI_VERSION = '1.0.5';
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Paths & config
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
function getAetherDir() {
|
|
45
|
-
return require('path').join(require('os').homedir(), '.aether');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function loadConfig() {
|
|
49
|
-
const p = require('path').join(getAetherDir(), 'config.json');
|
|
50
|
-
if (!require('fs').existsSync(p)) return { defaultWallet: null };
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(require('fs').readFileSync(p, 'utf8'));
|
|
53
|
-
} catch {
|
|
54
|
-
return { defaultWallet: null };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function loadWallet(address) {
|
|
59
|
-
const fp = require('path').join(getAetherDir(), 'wallets', `${address}.json`);
|
|
60
|
-
if (!require('fs').existsSync(fp)) return null;
|
|
61
|
-
return JSON.parse(require('fs').readFileSync(fp, 'utf8'));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Crypto helpers
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
function deriveKeypair(mnemonic) {
|
|
69
|
-
if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic');
|
|
70
|
-
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
71
|
-
const seed32 = seedBuffer.slice(0, 32);
|
|
72
|
-
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
73
|
-
return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function formatAddress(publicKey) {
|
|
77
|
-
return 'ATH' + bs58.encode(publicKey);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// HTTP helpers
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
function httpRequest(rpcUrl, path) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
const url = new URL(path, rpcUrl);
|
|
87
|
-
const lib = url.protocol === 'https:' ? https : http;
|
|
88
|
-
const req = lib.request({
|
|
89
|
-
hostname: url.hostname,
|
|
90
|
-
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
91
|
-
path: url.pathname + url.search,
|
|
92
|
-
method: 'GET',
|
|
93
|
-
timeout: 8000,
|
|
94
|
-
headers: { 'Content-Type': 'application/json' },
|
|
95
|
-
}, (res) => {
|
|
96
|
-
let data = '';
|
|
97
|
-
res.on('data', (chunk) => data += chunk);
|
|
98
|
-
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
|
|
99
|
-
});
|
|
100
|
-
req.on('error', reject);
|
|
101
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
102
|
-
req.end();
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function httpPost(rpcUrl, path, body) {
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
const url = new URL(path, rpcUrl);
|
|
109
|
-
const lib = url.protocol === 'https:' ? https : http;
|
|
110
|
-
const bodyStr = JSON.stringify(body);
|
|
111
|
-
const req = lib.request({
|
|
112
|
-
hostname: url.hostname,
|
|
113
|
-
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
114
|
-
path: url.pathname + url.search,
|
|
115
|
-
method: 'POST',
|
|
116
|
-
timeout: 15000,
|
|
117
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
|
|
118
|
-
}, (res) => {
|
|
119
|
-
let data = '';
|
|
120
|
-
res.on('data', (chunk) => data += chunk);
|
|
121
|
-
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
|
|
122
|
-
});
|
|
123
|
-
req.on('error', reject);
|
|
124
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
125
|
-
req.write(bodyStr);
|
|
126
|
-
req.end();
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getDefaultRpc() {
|
|
131
|
-
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function formatAether(lamports) {
|
|
135
|
-
const aeth = lamports / 1e9;
|
|
136
|
-
if (aeth === 0) return '0 AETH';
|
|
137
|
-
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function formatAethFull(lamports) {
|
|
141
|
-
return (lamports / 1e9).toFixed(6) + ' AETH';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// Rewards calculation helpers
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Fetch stake account info and compute rewards from epoch history.
|
|
150
|
-
* Uses the stake account's delegated stake + activation/deactivation epochs
|
|
151
|
-
* to estimate rewards accrued.
|
|
152
|
-
*/
|
|
153
|
-
async function fetchStakeRewards(rpc, stakeAddress) {
|
|
154
|
-
try {
|
|
155
|
-
// Fetch stake account data
|
|
156
|
-
const stakeData = await httpRequest(rpc, `/v1/stake-account/${stakeAddress}`);
|
|
157
|
-
if (!stakeData || stakeData.error) {
|
|
158
|
-
return { stakeAddress, error: stakeData.error || 'Failed to fetch stake account' };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const delegatedStake = BigInt(stakeData.delegated_stake || 0);
|
|
162
|
-
const activationEpoch = stakeData.activation_epoch || 0;
|
|
163
|
-
const deactivationEpoch = stakeData.deactivation_epoch || null;
|
|
164
|
-
const stakeType = stakeData.stake_type || 'unknown';
|
|
165
|
-
|
|
166
|
-
// Fetch epoch info for current epoch
|
|
167
|
-
const epochInfo = await httpRequest(rpc, '/v1/epoch-info');
|
|
168
|
-
const currentEpoch = epochInfo.epoch || 0;
|
|
169
|
-
const rewardsPerEpoch = BigInt(epochInfo.rewards_per_epoch || '2000000000'); // default ~2 AETH
|
|
170
|
-
|
|
171
|
-
// Calculate active epochs
|
|
172
|
-
const activeFromEpoch = activationEpoch;
|
|
173
|
-
const activeToEpoch = deactivationEpoch || currentEpoch;
|
|
174
|
-
const activeEpochs = Math.max(0, activeToEpoch - activeFromEpoch);
|
|
175
|
-
|
|
176
|
-
// Rewards accrue proportional to stake share (simplified — assumes network-wide pool)
|
|
177
|
-
// APY is estimated from rewards_per_epoch vs total staked
|
|
178
|
-
const totalNetworkStake = BigInt(epochInfo.total_staked || '1000000000000'); // fallback
|
|
179
|
-
const rewardsRate = Number(rewardsPerEpoch * BigInt(365)) / Number(totalNetworkStake);
|
|
180
|
-
const apyBps = Math.round(rewardsRate * 10000); // basis points
|
|
181
|
-
|
|
182
|
-
// Compute estimated rewards accumulated
|
|
183
|
-
const stakeAeth = Number(delegatedStake) / 1e9;
|
|
184
|
-
const epochDuration = 432000; // seconds per epoch (approx 3.5 days on Aether)
|
|
185
|
-
const yearEpochs = Math.round(31557600 / epochDuration); // ~73 epochs/year
|
|
186
|
-
const estimatedAnnualRewards = stakeAeth * (apyBps / 10000);
|
|
187
|
-
const estimatedRewards = (estimatedAnnualRewards / yearEpochs) * activeEpochs;
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
stakeAddress,
|
|
191
|
-
delegatedStake: delegatedStake.toString(),
|
|
192
|
-
delegatedStakeFormatted: formatAether(delegatedStake),
|
|
193
|
-
activationEpoch: activeFromEpoch,
|
|
194
|
-
deactivationEpoch,
|
|
195
|
-
isActive: deactivationEpoch === null,
|
|
196
|
-
activeEpochs,
|
|
197
|
-
estimatedRewards: Math.round(estimatedRewards * 1e9),
|
|
198
|
-
estimatedRewardsFormatted: formatAether(Math.round(estimatedRewards * 1e9)),
|
|
199
|
-
apyBps,
|
|
200
|
-
stakeType,
|
|
201
|
-
};
|
|
202
|
-
} catch (err) {
|
|
203
|
-
return { stakeAddress, error: err.message };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Fetch all stake accounts for a wallet address.
|
|
209
|
-
* Returns array of stake account pubkeys from wallet's session data.
|
|
210
|
-
*/
|
|
211
|
-
async function fetchWalletStakeAccounts(walletAddress) {
|
|
212
|
-
const sessionsDir = require('path').join(getAetherDir(), 'sessions');
|
|
213
|
-
if (!require('fs').existsSync(sessionsDir)) return [];
|
|
214
|
-
|
|
215
|
-
const files = require('fs').readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
|
|
216
|
-
const stakeAccounts = [];
|
|
217
|
-
|
|
218
|
-
for (const file of files) {
|
|
219
|
-
try {
|
|
220
|
-
const session = JSON.parse(require('fs').readFileSync(require('path').join(sessionsDir, file), 'utf8'));
|
|
221
|
-
if (session.wallet_address === walletAddress && session.stake_account) {
|
|
222
|
-
stakeAccounts.push(session.stake_account);
|
|
223
|
-
}
|
|
224
|
-
} catch {}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return stakeAccounts;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Fetch wallet info from chain (account data)
|
|
232
|
-
*/
|
|
233
|
-
async function fetchAccountInfo(rpc, address) {
|
|
234
|
-
try {
|
|
235
|
-
return await httpRequest(rpc, `/v1/account/${address}`);
|
|
236
|
-
} catch {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
// Rewards list command
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
async function rewardsList(args) {
|
|
246
|
-
const rpc = args.rpc || getDefaultRpc();
|
|
247
|
-
const isJson = args.json || false;
|
|
248
|
-
let address = args.address || null;
|
|
249
|
-
|
|
250
|
-
// Interactive address prompt if not provided
|
|
251
|
-
if (!address) {
|
|
252
|
-
const config = loadConfig();
|
|
253
|
-
const rl = createRl();
|
|
254
|
-
const answer = await question(rl, `\n${C.cyan}Enter wallet address (or press Enter for default): ${C.reset}`);
|
|
255
|
-
rl.close();
|
|
256
|
-
|
|
257
|
-
if (!answer.trim()) {
|
|
258
|
-
if (!config.defaultWallet) {
|
|
259
|
-
console.log(`\n${C.red}✗ No default wallet and no address provided.${C.reset}`);
|
|
260
|
-
console.log(` ${C.dim}Set a default wallet first: aether wallet default${C.reset}\n`);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
address = config.defaultWallet;
|
|
264
|
-
} else {
|
|
265
|
-
address = answer.trim();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Validate address format (ATH...)
|
|
270
|
-
if (!address.startsWith('ATH') || address.length < 30) {
|
|
271
|
-
// Try loading from config if it looks like a nickname
|
|
272
|
-
const config = loadConfig();
|
|
273
|
-
if (config.defaultWallet) address = config.defaultWallet;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
console.log(`\n${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
|
|
277
|
-
console.log(`${C.bright}${C.cyan}║ Staking Rewards — ${address.substring(0, 12)}... ║${C.reset}`);
|
|
278
|
-
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
|
|
279
|
-
|
|
280
|
-
// Fetch stake accounts for this wallet
|
|
281
|
-
const stakeAccounts = await fetchWalletStakeAccounts(address);
|
|
282
|
-
|
|
283
|
-
if (stakeAccounts.length === 0) {
|
|
284
|
-
console.log(` ${C.yellow}⚠ No stake accounts found for this wallet.${C.reset}`);
|
|
285
|
-
console.log(` ${C.dim}Stake AETH first: aether stake --address ${address} --validator <val> --amount <aeth>${C.reset}\n`);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Fetch rewards for each stake account
|
|
290
|
-
const rewardsResults = await Promise.all(
|
|
291
|
-
stakeAccounts.map(sa => fetchStakeRewards(rpc, sa))
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
let totalEstimatedRewards = BigInt(0);
|
|
295
|
-
let totalDelegatedStake = BigInt(0);
|
|
296
|
-
let activeCount = 0;
|
|
297
|
-
const rows = [];
|
|
298
|
-
|
|
299
|
-
for (const result of rewardsResults) {
|
|
300
|
-
if (result.error) {
|
|
301
|
-
rows.push({ status: 'error', ...result });
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
totalEstimatedRewards += BigInt(result.estimatedRewards);
|
|
306
|
-
totalDelegatedStake += BigInt(result.delegatedStake);
|
|
307
|
-
if (result.isActive) activeCount++;
|
|
308
|
-
|
|
309
|
-
rows.push(result);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (isJson) {
|
|
313
|
-
console.log(JSON.stringify({
|
|
314
|
-
address,
|
|
315
|
-
totalEstimatedRewards: totalEstimatedRewards.toString(),
|
|
316
|
-
totalEstimatedRewardsFormatted: formatAether(totalEstimatedRewards),
|
|
317
|
-
totalDelegatedStake: totalDelegatedStake.toString(),
|
|
318
|
-
totalDelegatedStakeFormatted: formatAether(totalDelegatedStake),
|
|
319
|
-
activeStakeAccounts: activeCount,
|
|
320
|
-
totalStakeAccounts: rows.length,
|
|
321
|
-
stakeAccounts: rows,
|
|
322
|
-
}, null, 2));
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ASCII table header
|
|
327
|
-
console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
328
|
-
console.log(` ${C.dim}│${C.reset} ${C.bright}Stake Account${C.reset} ${C.bright}Delegated${C.reset} ${C.bright}Est. Rewards${C.reset} ${C.bright}APY${C.reset} ${C.bright}Status${C.reset} ${C.dim}│${C.reset}`);
|
|
329
|
-
console.log(` ${C.dim}├─────────────────────────────────────────────────────────────┤${C.reset}`);
|
|
330
|
-
|
|
331
|
-
for (const r of rows) {
|
|
332
|
-
const shortAddr = r.stakeAddress ? r.stakeAddress.substring(0, 14) + '...' : 'unknown';
|
|
333
|
-
const delegated = r.delegatedStakeFormatted || '—';
|
|
334
|
-
const estRew = r.estimatedRewardsFormatted || '—';
|
|
335
|
-
const apy = r.apyBps ? `${(r.apyBps / 100).toFixed(2)}%` : '—';
|
|
336
|
-
const status = r.isActive
|
|
337
|
-
? `${C.green}● Active${C.reset}`
|
|
338
|
-
: r.deactivationEpoch
|
|
339
|
-
? `${C.yellow}○ Deactivated${C.reset}`
|
|
340
|
-
: `${C.red}✗ Error${C.reset}`;
|
|
341
|
-
const statusColor = r.isActive ? C.green : r.deactivationEpoch ? C.yellow : C.red;
|
|
342
|
-
|
|
343
|
-
console.log(
|
|
344
|
-
` ${C.dim}│${C.reset} ${shortAddr.padEnd(20)} ${delegated.padEnd(13)} ${estRew.padEnd(15)} ${apy.padEnd(7)} ${statusColor}${r.isActive ? '● Active' : r.deactivationEpoch ? '○ Deact.' : '✗ Err'}${C.reset} ${C.dim}│${C.reset}`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
console.log(` ${C.dim}└─────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
349
|
-
console.log();
|
|
350
|
-
console.log(` ${C.bright}Total Delegated:${C.reset} ${C.cyan}${formatAether(totalDelegatedStake)}${C.reset}`);
|
|
351
|
-
console.log(` ${C.bright}Total Est. Rewards:${C.reset} ${C.green}${formatAether(totalEstimatedRewards)}${C.reset}`);
|
|
352
|
-
console.log(` ${C.bright}Active Accounts:${C.reset} ${activeCount} of ${rows.length}`);
|
|
353
|
-
console.log();
|
|
354
|
-
console.log(` ${C.dim}Run "aether rewards claim --address ${address}" to claim unclaimed rewards.${C.reset}\n`);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// ---------------------------------------------------------------------------
|
|
358
|
-
// Rewards summary command (one-line)
|
|
359
|
-
// ---------------------------------------------------------------------------
|
|
360
|
-
|
|
361
|
-
async function rewardsSummary(args) {
|
|
362
|
-
const rpc = args.rpc || getDefaultRpc();
|
|
363
|
-
let address = args.address || null;
|
|
364
|
-
|
|
365
|
-
if (!address) {
|
|
366
|
-
const config = loadConfig();
|
|
367
|
-
if (!config.defaultWallet) {
|
|
368
|
-
console.log(`${C.red}✗ No default wallet and no address provided.${C.reset}`);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
address = config.defaultWallet;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const stakeAccounts = await fetchWalletStakeAccounts(address);
|
|
375
|
-
if (stakeAccounts.length === 0) {
|
|
376
|
-
console.log(`${C.yellow}⚠ No stake accounts for ${address.substring(0, 12)}...${C.reset}`);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const results = await Promise.all(stakeAccounts.map(sa => fetchStakeRewards(rpc, sa)));
|
|
381
|
-
let totalRewards = BigInt(0);
|
|
382
|
-
let totalStake = BigInt(0);
|
|
383
|
-
let activeCount = 0;
|
|
384
|
-
|
|
385
|
-
for (const r of results) {
|
|
386
|
-
if (!r.error) {
|
|
387
|
-
totalRewards += BigInt(r.estimatedRewards);
|
|
388
|
-
totalStake += BigInt(r.delegatedStake);
|
|
389
|
-
if (r.isActive) activeCount++;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
console.log(`${C.cyan}${address.substring(0, 12)}...${C.reset} │ Stake: ${C.cyan}${formatAether(totalStake)}${C.reset} │ Est.Rewards: ${C.green}${formatAether(totalRewards)}${C.reset} │ Active: ${activeCount}/${results.length}`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ---------------------------------------------------------------------------
|
|
397
|
-
// Rewards claim command
|
|
398
|
-
// ---------------------------------------------------------------------------
|
|
399
|
-
|
|
400
|
-
async function rewardsClaim(args) {
|
|
401
|
-
const rpc = args.rpc || getDefaultRpc();
|
|
402
|
-
const isJson = args.json || false;
|
|
403
|
-
let address = args.address || null;
|
|
404
|
-
let stakeAccount = args.account || null;
|
|
405
|
-
|
|
406
|
-
const config = loadConfig();
|
|
407
|
-
const rl = createRl();
|
|
408
|
-
|
|
409
|
-
if (!address) {
|
|
410
|
-
const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
|
|
411
|
-
address = ans.trim();
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (!stakeAccount) {
|
|
415
|
-
const stakeAccounts = await fetchWalletStakeAccounts(address);
|
|
416
|
-
if (stakeAccounts.length === 0) {
|
|
417
|
-
console.log(`\n${C.red}✗ No stake accounts found for this wallet.${C.reset}\n`);
|
|
418
|
-
rl.close();
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (stakeAccounts.length === 1) {
|
|
422
|
-
stakeAccount = stakeAccounts[0];
|
|
423
|
-
} else {
|
|
424
|
-
console.log(`\n${C.cyan}Select stake account:${C.reset}`);
|
|
425
|
-
stakeAccounts.forEach((sa, i) => {
|
|
426
|
-
console.log(` ${i + 1}) ${sa.substring(0, 20)}...`);
|
|
427
|
-
});
|
|
428
|
-
const ans = await question(rl, `${C.cyan}Enter number: ${C.reset}`);
|
|
429
|
-
const idx = parseInt(ans.trim()) - 1;
|
|
430
|
-
if (idx < 0 || idx >= stakeAccounts.length) {
|
|
431
|
-
console.log(`\n${C.red}Invalid selection.${C.reset}\n`);
|
|
432
|
-
rl.close();
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
stakeAccount = stakeAccounts[idx];
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
rl.close();
|
|
440
|
-
|
|
441
|
-
// Load wallet for signing
|
|
442
|
-
const wallet = loadWallet(address);
|
|
443
|
-
if (!wallet) {
|
|
444
|
-
console.log(`\n${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
|
|
445
|
-
console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
console.log(`\n${C.bright}${C.cyan}╔════════════════════════════════════════╗${C.reset}`);
|
|
450
|
-
console.log(`${C.bright}${C.cyan}║ Claim Staking Rewards ║${C.reset}`);
|
|
451
|
-
console.log(`${C.bright}${C.cyan}╚════════════════════════════════════════╝${C.reset}\n`);
|
|
452
|
-
console.log(` ${C.dim}Wallet:${C.reset} ${address.substring(0, 16)}...`);
|
|
453
|
-
console.log(` ${C.dim}Stake Account:${C.reset} ${stakeAccount.substring(0, 16)}...`);
|
|
454
|
-
|
|
455
|
-
// Fetch current rewards for this stake account
|
|
456
|
-
const rewardData = await fetchStakeRewards(rpc, stakeAccount);
|
|
457
|
-
if (rewardData.error) {
|
|
458
|
-
console.log(`\n${C.red}✗ Failed to fetch stake account: ${rewardData.error}${C.reset}\n`);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
console.log(` ${C.dim}Delegated Stake:${C.reset} ${rewardData.delegatedStakeFormatted}`);
|
|
463
|
-
console.log(` ${C.dim}Est. Accumulated:${C.reset} ${rewardData.estimatedRewardsFormatted}`);
|
|
464
|
-
console.log(` ${C.dim}APY:${C.reset} ${(rewardData.apyBps / 100).toFixed(2)}%`);
|
|
465
|
-
|
|
466
|
-
const estimatedRewards = rewardData.estimatedRewards;
|
|
467
|
-
if (BigInt(estimatedRewards) === BigInt(0)) {
|
|
468
|
-
console.log(`\n${C.yellow}⚠ No rewards accumulated yet.${C.reset}\n`);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const confirm = await question(rl, `\n ${C.yellow}Claim ${rewardData.estimatedRewardsFormatted}? [y/N]${C.reset} > `);
|
|
473
|
-
if (confirm.trim().toLowerCase() !== 'y') {
|
|
474
|
-
console.log(`${C.dim}Cancelled.${C.reset}\n`);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Derive keypair from mnemonic for signing
|
|
479
|
-
let keypair;
|
|
480
|
-
try {
|
|
481
|
-
const mnemonic = wallet.mnemonic;
|
|
482
|
-
keypair = deriveKeypair(mnemonic);
|
|
483
|
-
} catch (err) {
|
|
484
|
-
console.log(`\n${C.red}✗ Failed to derive keypair: ${err.message}${C.reset}\n`);
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Build claim transaction
|
|
489
|
-
const tx = {
|
|
490
|
-
type: 'ClaimRewards',
|
|
491
|
-
from: address,
|
|
492
|
-
stake_account: stakeAccount,
|
|
493
|
-
lamports: estimatedRewards,
|
|
494
|
-
timestamp: Math.floor(Date.now() / 1000),
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
// Sign transaction
|
|
498
|
-
const txData = JSON.stringify(tx);
|
|
499
|
-
const txHash = crypto.createHash('sha256').update(txData).digest('hex');
|
|
500
|
-
const signature = nacl.hash(Buffer.from(txHash, 'hex'));
|
|
501
|
-
const signatureB58 = bs58.encode(signature.slice(0, 64));
|
|
502
|
-
|
|
503
|
-
tx.signature = signatureB58;
|
|
504
|
-
|
|
505
|
-
// Submit transaction
|
|
506
|
-
try {
|
|
507
|
-
const result = await httpPost(rpc, '/v1/tx', tx);
|
|
508
|
-
|
|
509
|
-
if (isJson) {
|
|
510
|
-
console.log(JSON.stringify({ success: true, tx: tx, result }, null, 2));
|
|
511
|
-
} else {
|
|
512
|
-
if (result.success || result.txid) {
|
|
513
|
-
console.log(`\n${C.green}✓ Rewards claimed successfully!${C.reset}`);
|
|
514
|
-
console.log(` ${C.dim}TX ID: ${result.txid || signatureB58.substring(0, 20)}...${C.reset}`);
|
|
515
|
-
console.log(` ${C.dim}Amount: ${rewardData.estimatedRewardsFormatted}${C.reset}`);
|
|
516
|
-
console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
|
|
517
|
-
} else {
|
|
518
|
-
console.log(`\n${C.red}✗ Claim failed: ${result.error || JSON.stringify(result)}${C.reset}\n`);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} catch (err) {
|
|
522
|
-
if (isJson) {
|
|
523
|
-
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
524
|
-
} else {
|
|
525
|
-
console.log(`\n${C.red}✗ Failed to submit claim transaction: ${err.message}${C.reset}`);
|
|
526
|
-
console.log(` ${C.dim}The rewards are accumulated on-chain and can be claimed later.${C.reset}\n`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// ---------------------------------------------------------------------------
|
|
532
|
-
// Parse CLI args
|
|
533
|
-
// ---------------------------------------------------------------------------
|
|
534
|
-
|
|
535
|
-
function parseArgs() {
|
|
536
|
-
const args = process.argv.slice(3); // [node, index.js, rewards, <subcmd>, ...]
|
|
537
|
-
return args;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function createRl() {
|
|
541
|
-
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function question(rl, q) {
|
|
545
|
-
return new Promise((res) => rl.question(q, res));
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// ---------------------------------------------------------------------------
|
|
549
|
-
// Main entry point
|
|
550
|
-
// ---------------------------------------------------------------------------
|
|
551
|
-
|
|
552
|
-
async function main() {
|
|
553
|
-
const rawArgs = parseArgs();
|
|
554
|
-
const subcmd = rawArgs[0] || 'list';
|
|
555
|
-
|
|
556
|
-
// Parse common flags from all args
|
|
557
|
-
const allArgs = rawArgs.slice(1);
|
|
558
|
-
const rpcIndex = allArgs.findIndex(a => a === '--rpc');
|
|
559
|
-
const rpc = rpcIndex !== -1 ? allArgs[rpcIndex + 1] : getDefaultRpc();
|
|
560
|
-
|
|
561
|
-
const parsed = {
|
|
562
|
-
rpc,
|
|
563
|
-
json: allArgs.includes('--json'),
|
|
564
|
-
address: null,
|
|
565
|
-
account: null,
|
|
566
|
-
};
|
|
567
|
-
|
|
568
|
-
// Extract --address and --account flags
|
|
569
|
-
const addrIdx = allArgs.findIndex(a => a === '--address');
|
|
570
|
-
if (addrIdx !== -1 && allArgs[addrIdx + 1]) parsed.address = allArgs[addrIdx + 1];
|
|
571
|
-
|
|
572
|
-
const acctIdx = allArgs.findIndex(a => a === '--account');
|
|
573
|
-
if (acctIdx !== -1 && allArgs[acctIdx + 1]) parsed.account = allArgs[acctIdx + 1];
|
|
574
|
-
|
|
575
|
-
switch (subcmd) {
|
|
576
|
-
case 'list':
|
|
577
|
-
await rewardsList(parsed);
|
|
578
|
-
break;
|
|
579
|
-
case 'summary':
|
|
580
|
-
await rewardsSummary(parsed);
|
|
581
|
-
break;
|
|
582
|
-
case 'claim':
|
|
583
|
-
await rewardsClaim(parsed);
|
|
584
|
-
break;
|
|
585
|
-
default:
|
|
586
|
-
console.log(`\n${C.cyan}Usage:${C.reset}`);
|
|
587
|
-
console.log(` aether rewards list --address <addr> List all staking rewards`);
|
|
588
|
-
console.log(` aether rewards summary --address <addr> One-line rewards summary`);
|
|
589
|
-
console.log(` aether rewards claim --address <addr> [--account <stakeAcct>] Claim rewards`);
|
|
590
|
-
console.log();
|
|
591
|
-
console.log(` ${C.dim}--json Output as JSON`);
|
|
592
|
-
console.log(` --rpc <url> Use specific RPC endpoint${C.reset}\n`);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
main().catch(err => {
|
|
597
|
-
console.error(`\n${C.red}Error running rewards command:${C.reset}`, err.message, '\n');
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
module.exports = { rewardsCommand: main };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli rewards
|
|
4
|
+
*
|
|
5
|
+
* View staking rewards earned from delegated stake accounts.
|
|
6
|
+
* Shows accumulated rewards, estimated APY, and claimable amounts.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* aether rewards list --address <addr> List all rewards per stake account
|
|
10
|
+
* aether rewards list --address <addr> --json JSON output for scripting
|
|
11
|
+
* aether rewards claim --address <addr> --account <stakeAcct> [--json]
|
|
12
|
+
* aether rewards summary --address <addr> One-line summary of total rewards
|
|
13
|
+
*
|
|
14
|
+
* Requires AETHER_RPC env var or local node running (default: http://127.0.0.1:8899)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const https = require('https');
|
|
19
|
+
const readline = require('readline');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const bs58 = require('bs58').default;
|
|
22
|
+
const bip39 = require('bip39');
|
|
23
|
+
const nacl = require('tweetnacl');
|
|
24
|
+
|
|
25
|
+
// ANSI colours
|
|
26
|
+
const C = {
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
bright: '\x1b[1m',
|
|
29
|
+
dim: '\x1b[2m',
|
|
30
|
+
red: '\x1b[31m',
|
|
31
|
+
green: '\x1b[32m',
|
|
32
|
+
yellow: '\x1b[33m',
|
|
33
|
+
cyan: '\x1b[36m',
|
|
34
|
+
magenta: '\x1b[35m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
38
|
+
const CLI_VERSION = '1.0.5';
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Paths & config
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function getAetherDir() {
|
|
45
|
+
return require('path').join(require('os').homedir(), '.aether');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadConfig() {
|
|
49
|
+
const p = require('path').join(getAetherDir(), 'config.json');
|
|
50
|
+
if (!require('fs').existsSync(p)) return { defaultWallet: null };
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(require('fs').readFileSync(p, 'utf8'));
|
|
53
|
+
} catch {
|
|
54
|
+
return { defaultWallet: null };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadWallet(address) {
|
|
59
|
+
const fp = require('path').join(getAetherDir(), 'wallets', `${address}.json`);
|
|
60
|
+
if (!require('fs').existsSync(fp)) return null;
|
|
61
|
+
return JSON.parse(require('fs').readFileSync(fp, 'utf8'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Crypto helpers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function deriveKeypair(mnemonic) {
|
|
69
|
+
if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic');
|
|
70
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
71
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
72
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
73
|
+
return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatAddress(publicKey) {
|
|
77
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// HTTP helpers
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
function httpRequest(rpcUrl, path) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const url = new URL(path, rpcUrl);
|
|
87
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
88
|
+
const req = lib.request({
|
|
89
|
+
hostname: url.hostname,
|
|
90
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
91
|
+
path: url.pathname + url.search,
|
|
92
|
+
method: 'GET',
|
|
93
|
+
timeout: 8000,
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
}, (res) => {
|
|
96
|
+
let data = '';
|
|
97
|
+
res.on('data', (chunk) => data += chunk);
|
|
98
|
+
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
|
|
99
|
+
});
|
|
100
|
+
req.on('error', reject);
|
|
101
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
102
|
+
req.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function httpPost(rpcUrl, path, body) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const url = new URL(path, rpcUrl);
|
|
109
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
110
|
+
const bodyStr = JSON.stringify(body);
|
|
111
|
+
const req = lib.request({
|
|
112
|
+
hostname: url.hostname,
|
|
113
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
114
|
+
path: url.pathname + url.search,
|
|
115
|
+
method: 'POST',
|
|
116
|
+
timeout: 15000,
|
|
117
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
|
|
118
|
+
}, (res) => {
|
|
119
|
+
let data = '';
|
|
120
|
+
res.on('data', (chunk) => data += chunk);
|
|
121
|
+
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ raw: data }); } });
|
|
122
|
+
});
|
|
123
|
+
req.on('error', reject);
|
|
124
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
125
|
+
req.write(bodyStr);
|
|
126
|
+
req.end();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getDefaultRpc() {
|
|
131
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatAether(lamports) {
|
|
135
|
+
const aeth = lamports / 1e9;
|
|
136
|
+
if (aeth === 0) return '0 AETH';
|
|
137
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatAethFull(lamports) {
|
|
141
|
+
return (lamports / 1e9).toFixed(6) + ' AETH';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Rewards calculation helpers
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fetch stake account info and compute rewards from epoch history.
|
|
150
|
+
* Uses the stake account's delegated stake + activation/deactivation epochs
|
|
151
|
+
* to estimate rewards accrued.
|
|
152
|
+
*/
|
|
153
|
+
async function fetchStakeRewards(rpc, stakeAddress) {
|
|
154
|
+
try {
|
|
155
|
+
// Fetch stake account data
|
|
156
|
+
const stakeData = await httpRequest(rpc, `/v1/stake-account/${stakeAddress}`);
|
|
157
|
+
if (!stakeData || stakeData.error) {
|
|
158
|
+
return { stakeAddress, error: stakeData.error || 'Failed to fetch stake account' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const delegatedStake = BigInt(stakeData.delegated_stake || 0);
|
|
162
|
+
const activationEpoch = stakeData.activation_epoch || 0;
|
|
163
|
+
const deactivationEpoch = stakeData.deactivation_epoch || null;
|
|
164
|
+
const stakeType = stakeData.stake_type || 'unknown';
|
|
165
|
+
|
|
166
|
+
// Fetch epoch info for current epoch
|
|
167
|
+
const epochInfo = await httpRequest(rpc, '/v1/epoch-info');
|
|
168
|
+
const currentEpoch = epochInfo.epoch || 0;
|
|
169
|
+
const rewardsPerEpoch = BigInt(epochInfo.rewards_per_epoch || '2000000000'); // default ~2 AETH
|
|
170
|
+
|
|
171
|
+
// Calculate active epochs
|
|
172
|
+
const activeFromEpoch = activationEpoch;
|
|
173
|
+
const activeToEpoch = deactivationEpoch || currentEpoch;
|
|
174
|
+
const activeEpochs = Math.max(0, activeToEpoch - activeFromEpoch);
|
|
175
|
+
|
|
176
|
+
// Rewards accrue proportional to stake share (simplified — assumes network-wide pool)
|
|
177
|
+
// APY is estimated from rewards_per_epoch vs total staked
|
|
178
|
+
const totalNetworkStake = BigInt(epochInfo.total_staked || '1000000000000'); // fallback
|
|
179
|
+
const rewardsRate = Number(rewardsPerEpoch * BigInt(365)) / Number(totalNetworkStake);
|
|
180
|
+
const apyBps = Math.round(rewardsRate * 10000); // basis points
|
|
181
|
+
|
|
182
|
+
// Compute estimated rewards accumulated
|
|
183
|
+
const stakeAeth = Number(delegatedStake) / 1e9;
|
|
184
|
+
const epochDuration = 432000; // seconds per epoch (approx 3.5 days on Aether)
|
|
185
|
+
const yearEpochs = Math.round(31557600 / epochDuration); // ~73 epochs/year
|
|
186
|
+
const estimatedAnnualRewards = stakeAeth * (apyBps / 10000);
|
|
187
|
+
const estimatedRewards = (estimatedAnnualRewards / yearEpochs) * activeEpochs;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
stakeAddress,
|
|
191
|
+
delegatedStake: delegatedStake.toString(),
|
|
192
|
+
delegatedStakeFormatted: formatAether(delegatedStake),
|
|
193
|
+
activationEpoch: activeFromEpoch,
|
|
194
|
+
deactivationEpoch,
|
|
195
|
+
isActive: deactivationEpoch === null,
|
|
196
|
+
activeEpochs,
|
|
197
|
+
estimatedRewards: Math.round(estimatedRewards * 1e9),
|
|
198
|
+
estimatedRewardsFormatted: formatAether(Math.round(estimatedRewards * 1e9)),
|
|
199
|
+
apyBps,
|
|
200
|
+
stakeType,
|
|
201
|
+
};
|
|
202
|
+
} catch (err) {
|
|
203
|
+
return { stakeAddress, error: err.message };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Fetch all stake accounts for a wallet address.
|
|
209
|
+
* Returns array of stake account pubkeys from wallet's session data.
|
|
210
|
+
*/
|
|
211
|
+
async function fetchWalletStakeAccounts(walletAddress) {
|
|
212
|
+
const sessionsDir = require('path').join(getAetherDir(), 'sessions');
|
|
213
|
+
if (!require('fs').existsSync(sessionsDir)) return [];
|
|
214
|
+
|
|
215
|
+
const files = require('fs').readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
|
|
216
|
+
const stakeAccounts = [];
|
|
217
|
+
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
try {
|
|
220
|
+
const session = JSON.parse(require('fs').readFileSync(require('path').join(sessionsDir, file), 'utf8'));
|
|
221
|
+
if (session.wallet_address === walletAddress && session.stake_account) {
|
|
222
|
+
stakeAccounts.push(session.stake_account);
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return stakeAccounts;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Fetch wallet info from chain (account data)
|
|
232
|
+
*/
|
|
233
|
+
async function fetchAccountInfo(rpc, address) {
|
|
234
|
+
try {
|
|
235
|
+
return await httpRequest(rpc, `/v1/account/${address}`);
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Rewards list command
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
async function rewardsList(args) {
|
|
246
|
+
const rpc = args.rpc || getDefaultRpc();
|
|
247
|
+
const isJson = args.json || false;
|
|
248
|
+
let address = args.address || null;
|
|
249
|
+
|
|
250
|
+
// Interactive address prompt if not provided
|
|
251
|
+
if (!address) {
|
|
252
|
+
const config = loadConfig();
|
|
253
|
+
const rl = createRl();
|
|
254
|
+
const answer = await question(rl, `\n${C.cyan}Enter wallet address (or press Enter for default): ${C.reset}`);
|
|
255
|
+
rl.close();
|
|
256
|
+
|
|
257
|
+
if (!answer.trim()) {
|
|
258
|
+
if (!config.defaultWallet) {
|
|
259
|
+
console.log(`\n${C.red}✗ No default wallet and no address provided.${C.reset}`);
|
|
260
|
+
console.log(` ${C.dim}Set a default wallet first: aether wallet default${C.reset}\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
address = config.defaultWallet;
|
|
264
|
+
} else {
|
|
265
|
+
address = answer.trim();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Validate address format (ATH...)
|
|
270
|
+
if (!address.startsWith('ATH') || address.length < 30) {
|
|
271
|
+
// Try loading from config if it looks like a nickname
|
|
272
|
+
const config = loadConfig();
|
|
273
|
+
if (config.defaultWallet) address = config.defaultWallet;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(`\n${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
|
|
277
|
+
console.log(`${C.bright}${C.cyan}║ Staking Rewards — ${address.substring(0, 12)}... ║${C.reset}`);
|
|
278
|
+
console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
|
|
279
|
+
|
|
280
|
+
// Fetch stake accounts for this wallet
|
|
281
|
+
const stakeAccounts = await fetchWalletStakeAccounts(address);
|
|
282
|
+
|
|
283
|
+
if (stakeAccounts.length === 0) {
|
|
284
|
+
console.log(` ${C.yellow}⚠ No stake accounts found for this wallet.${C.reset}`);
|
|
285
|
+
console.log(` ${C.dim}Stake AETH first: aether stake --address ${address} --validator <val> --amount <aeth>${C.reset}\n`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Fetch rewards for each stake account
|
|
290
|
+
const rewardsResults = await Promise.all(
|
|
291
|
+
stakeAccounts.map(sa => fetchStakeRewards(rpc, sa))
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
let totalEstimatedRewards = BigInt(0);
|
|
295
|
+
let totalDelegatedStake = BigInt(0);
|
|
296
|
+
let activeCount = 0;
|
|
297
|
+
const rows = [];
|
|
298
|
+
|
|
299
|
+
for (const result of rewardsResults) {
|
|
300
|
+
if (result.error) {
|
|
301
|
+
rows.push({ status: 'error', ...result });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
totalEstimatedRewards += BigInt(result.estimatedRewards);
|
|
306
|
+
totalDelegatedStake += BigInt(result.delegatedStake);
|
|
307
|
+
if (result.isActive) activeCount++;
|
|
308
|
+
|
|
309
|
+
rows.push(result);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (isJson) {
|
|
313
|
+
console.log(JSON.stringify({
|
|
314
|
+
address,
|
|
315
|
+
totalEstimatedRewards: totalEstimatedRewards.toString(),
|
|
316
|
+
totalEstimatedRewardsFormatted: formatAether(totalEstimatedRewards),
|
|
317
|
+
totalDelegatedStake: totalDelegatedStake.toString(),
|
|
318
|
+
totalDelegatedStakeFormatted: formatAether(totalDelegatedStake),
|
|
319
|
+
activeStakeAccounts: activeCount,
|
|
320
|
+
totalStakeAccounts: rows.length,
|
|
321
|
+
stakeAccounts: rows,
|
|
322
|
+
}, null, 2));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ASCII table header
|
|
327
|
+
console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────┐${C.reset}`);
|
|
328
|
+
console.log(` ${C.dim}│${C.reset} ${C.bright}Stake Account${C.reset} ${C.bright}Delegated${C.reset} ${C.bright}Est. Rewards${C.reset} ${C.bright}APY${C.reset} ${C.bright}Status${C.reset} ${C.dim}│${C.reset}`);
|
|
329
|
+
console.log(` ${C.dim}├─────────────────────────────────────────────────────────────┤${C.reset}`);
|
|
330
|
+
|
|
331
|
+
for (const r of rows) {
|
|
332
|
+
const shortAddr = r.stakeAddress ? r.stakeAddress.substring(0, 14) + '...' : 'unknown';
|
|
333
|
+
const delegated = r.delegatedStakeFormatted || '—';
|
|
334
|
+
const estRew = r.estimatedRewardsFormatted || '—';
|
|
335
|
+
const apy = r.apyBps ? `${(r.apyBps / 100).toFixed(2)}%` : '—';
|
|
336
|
+
const status = r.isActive
|
|
337
|
+
? `${C.green}● Active${C.reset}`
|
|
338
|
+
: r.deactivationEpoch
|
|
339
|
+
? `${C.yellow}○ Deactivated${C.reset}`
|
|
340
|
+
: `${C.red}✗ Error${C.reset}`;
|
|
341
|
+
const statusColor = r.isActive ? C.green : r.deactivationEpoch ? C.yellow : C.red;
|
|
342
|
+
|
|
343
|
+
console.log(
|
|
344
|
+
` ${C.dim}│${C.reset} ${shortAddr.padEnd(20)} ${delegated.padEnd(13)} ${estRew.padEnd(15)} ${apy.padEnd(7)} ${statusColor}${r.isActive ? '● Active' : r.deactivationEpoch ? '○ Deact.' : '✗ Err'}${C.reset} ${C.dim}│${C.reset}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(` ${C.dim}└─────────────────────────────────────────────────────────────┘${C.reset}`);
|
|
349
|
+
console.log();
|
|
350
|
+
console.log(` ${C.bright}Total Delegated:${C.reset} ${C.cyan}${formatAether(totalDelegatedStake)}${C.reset}`);
|
|
351
|
+
console.log(` ${C.bright}Total Est. Rewards:${C.reset} ${C.green}${formatAether(totalEstimatedRewards)}${C.reset}`);
|
|
352
|
+
console.log(` ${C.bright}Active Accounts:${C.reset} ${activeCount} of ${rows.length}`);
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(` ${C.dim}Run "aether rewards claim --address ${address}" to claim unclaimed rewards.${C.reset}\n`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Rewards summary command (one-line)
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
async function rewardsSummary(args) {
|
|
362
|
+
const rpc = args.rpc || getDefaultRpc();
|
|
363
|
+
let address = args.address || null;
|
|
364
|
+
|
|
365
|
+
if (!address) {
|
|
366
|
+
const config = loadConfig();
|
|
367
|
+
if (!config.defaultWallet) {
|
|
368
|
+
console.log(`${C.red}✗ No default wallet and no address provided.${C.reset}`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
address = config.defaultWallet;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const stakeAccounts = await fetchWalletStakeAccounts(address);
|
|
375
|
+
if (stakeAccounts.length === 0) {
|
|
376
|
+
console.log(`${C.yellow}⚠ No stake accounts for ${address.substring(0, 12)}...${C.reset}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const results = await Promise.all(stakeAccounts.map(sa => fetchStakeRewards(rpc, sa)));
|
|
381
|
+
let totalRewards = BigInt(0);
|
|
382
|
+
let totalStake = BigInt(0);
|
|
383
|
+
let activeCount = 0;
|
|
384
|
+
|
|
385
|
+
for (const r of results) {
|
|
386
|
+
if (!r.error) {
|
|
387
|
+
totalRewards += BigInt(r.estimatedRewards);
|
|
388
|
+
totalStake += BigInt(r.delegatedStake);
|
|
389
|
+
if (r.isActive) activeCount++;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log(`${C.cyan}${address.substring(0, 12)}...${C.reset} │ Stake: ${C.cyan}${formatAether(totalStake)}${C.reset} │ Est.Rewards: ${C.green}${formatAether(totalRewards)}${C.reset} │ Active: ${activeCount}/${results.length}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Rewards claim command
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
async function rewardsClaim(args) {
|
|
401
|
+
const rpc = args.rpc || getDefaultRpc();
|
|
402
|
+
const isJson = args.json || false;
|
|
403
|
+
let address = args.address || null;
|
|
404
|
+
let stakeAccount = args.account || null;
|
|
405
|
+
|
|
406
|
+
const config = loadConfig();
|
|
407
|
+
const rl = createRl();
|
|
408
|
+
|
|
409
|
+
if (!address) {
|
|
410
|
+
const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
|
|
411
|
+
address = ans.trim();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!stakeAccount) {
|
|
415
|
+
const stakeAccounts = await fetchWalletStakeAccounts(address);
|
|
416
|
+
if (stakeAccounts.length === 0) {
|
|
417
|
+
console.log(`\n${C.red}✗ No stake accounts found for this wallet.${C.reset}\n`);
|
|
418
|
+
rl.close();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (stakeAccounts.length === 1) {
|
|
422
|
+
stakeAccount = stakeAccounts[0];
|
|
423
|
+
} else {
|
|
424
|
+
console.log(`\n${C.cyan}Select stake account:${C.reset}`);
|
|
425
|
+
stakeAccounts.forEach((sa, i) => {
|
|
426
|
+
console.log(` ${i + 1}) ${sa.substring(0, 20)}...`);
|
|
427
|
+
});
|
|
428
|
+
const ans = await question(rl, `${C.cyan}Enter number: ${C.reset}`);
|
|
429
|
+
const idx = parseInt(ans.trim()) - 1;
|
|
430
|
+
if (idx < 0 || idx >= stakeAccounts.length) {
|
|
431
|
+
console.log(`\n${C.red}Invalid selection.${C.reset}\n`);
|
|
432
|
+
rl.close();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
stakeAccount = stakeAccounts[idx];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
rl.close();
|
|
440
|
+
|
|
441
|
+
// Load wallet for signing
|
|
442
|
+
const wallet = loadWallet(address);
|
|
443
|
+
if (!wallet) {
|
|
444
|
+
console.log(`\n${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
|
|
445
|
+
console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log(`\n${C.bright}${C.cyan}╔════════════════════════════════════════╗${C.reset}`);
|
|
450
|
+
console.log(`${C.bright}${C.cyan}║ Claim Staking Rewards ║${C.reset}`);
|
|
451
|
+
console.log(`${C.bright}${C.cyan}╚════════════════════════════════════════╝${C.reset}\n`);
|
|
452
|
+
console.log(` ${C.dim}Wallet:${C.reset} ${address.substring(0, 16)}...`);
|
|
453
|
+
console.log(` ${C.dim}Stake Account:${C.reset} ${stakeAccount.substring(0, 16)}...`);
|
|
454
|
+
|
|
455
|
+
// Fetch current rewards for this stake account
|
|
456
|
+
const rewardData = await fetchStakeRewards(rpc, stakeAccount);
|
|
457
|
+
if (rewardData.error) {
|
|
458
|
+
console.log(`\n${C.red}✗ Failed to fetch stake account: ${rewardData.error}${C.reset}\n`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
console.log(` ${C.dim}Delegated Stake:${C.reset} ${rewardData.delegatedStakeFormatted}`);
|
|
463
|
+
console.log(` ${C.dim}Est. Accumulated:${C.reset} ${rewardData.estimatedRewardsFormatted}`);
|
|
464
|
+
console.log(` ${C.dim}APY:${C.reset} ${(rewardData.apyBps / 100).toFixed(2)}%`);
|
|
465
|
+
|
|
466
|
+
const estimatedRewards = rewardData.estimatedRewards;
|
|
467
|
+
if (BigInt(estimatedRewards) === BigInt(0)) {
|
|
468
|
+
console.log(`\n${C.yellow}⚠ No rewards accumulated yet.${C.reset}\n`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const confirm = await question(rl, `\n ${C.yellow}Claim ${rewardData.estimatedRewardsFormatted}? [y/N]${C.reset} > `);
|
|
473
|
+
if (confirm.trim().toLowerCase() !== 'y') {
|
|
474
|
+
console.log(`${C.dim}Cancelled.${C.reset}\n`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Derive keypair from mnemonic for signing
|
|
479
|
+
let keypair;
|
|
480
|
+
try {
|
|
481
|
+
const mnemonic = wallet.mnemonic;
|
|
482
|
+
keypair = deriveKeypair(mnemonic);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.log(`\n${C.red}✗ Failed to derive keypair: ${err.message}${C.reset}\n`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Build claim transaction
|
|
489
|
+
const tx = {
|
|
490
|
+
type: 'ClaimRewards',
|
|
491
|
+
from: address,
|
|
492
|
+
stake_account: stakeAccount,
|
|
493
|
+
lamports: estimatedRewards,
|
|
494
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Sign transaction
|
|
498
|
+
const txData = JSON.stringify(tx);
|
|
499
|
+
const txHash = crypto.createHash('sha256').update(txData).digest('hex');
|
|
500
|
+
const signature = nacl.hash(Buffer.from(txHash, 'hex'));
|
|
501
|
+
const signatureB58 = bs58.encode(signature.slice(0, 64));
|
|
502
|
+
|
|
503
|
+
tx.signature = signatureB58;
|
|
504
|
+
|
|
505
|
+
// Submit transaction
|
|
506
|
+
try {
|
|
507
|
+
const result = await httpPost(rpc, '/v1/tx', tx);
|
|
508
|
+
|
|
509
|
+
if (isJson) {
|
|
510
|
+
console.log(JSON.stringify({ success: true, tx: tx, result }, null, 2));
|
|
511
|
+
} else {
|
|
512
|
+
if (result.success || result.txid) {
|
|
513
|
+
console.log(`\n${C.green}✓ Rewards claimed successfully!${C.reset}`);
|
|
514
|
+
console.log(` ${C.dim}TX ID: ${result.txid || signatureB58.substring(0, 20)}...${C.reset}`);
|
|
515
|
+
console.log(` ${C.dim}Amount: ${rewardData.estimatedRewardsFormatted}${C.reset}`);
|
|
516
|
+
console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
|
|
517
|
+
} else {
|
|
518
|
+
console.log(`\n${C.red}✗ Claim failed: ${result.error || JSON.stringify(result)}${C.reset}\n`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch (err) {
|
|
522
|
+
if (isJson) {
|
|
523
|
+
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
524
|
+
} else {
|
|
525
|
+
console.log(`\n${C.red}✗ Failed to submit claim transaction: ${err.message}${C.reset}`);
|
|
526
|
+
console.log(` ${C.dim}The rewards are accumulated on-chain and can be claimed later.${C.reset}\n`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
// Parse CLI args
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
function parseArgs() {
|
|
536
|
+
const args = process.argv.slice(3); // [node, index.js, rewards, <subcmd>, ...]
|
|
537
|
+
return args;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function createRl() {
|
|
541
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function question(rl, q) {
|
|
545
|
+
return new Promise((res) => rl.question(q, res));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
// Main entry point
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
async function main() {
|
|
553
|
+
const rawArgs = parseArgs();
|
|
554
|
+
const subcmd = rawArgs[0] || 'list';
|
|
555
|
+
|
|
556
|
+
// Parse common flags from all args
|
|
557
|
+
const allArgs = rawArgs.slice(1);
|
|
558
|
+
const rpcIndex = allArgs.findIndex(a => a === '--rpc');
|
|
559
|
+
const rpc = rpcIndex !== -1 ? allArgs[rpcIndex + 1] : getDefaultRpc();
|
|
560
|
+
|
|
561
|
+
const parsed = {
|
|
562
|
+
rpc,
|
|
563
|
+
json: allArgs.includes('--json'),
|
|
564
|
+
address: null,
|
|
565
|
+
account: null,
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// Extract --address and --account flags
|
|
569
|
+
const addrIdx = allArgs.findIndex(a => a === '--address');
|
|
570
|
+
if (addrIdx !== -1 && allArgs[addrIdx + 1]) parsed.address = allArgs[addrIdx + 1];
|
|
571
|
+
|
|
572
|
+
const acctIdx = allArgs.findIndex(a => a === '--account');
|
|
573
|
+
if (acctIdx !== -1 && allArgs[acctIdx + 1]) parsed.account = allArgs[acctIdx + 1];
|
|
574
|
+
|
|
575
|
+
switch (subcmd) {
|
|
576
|
+
case 'list':
|
|
577
|
+
await rewardsList(parsed);
|
|
578
|
+
break;
|
|
579
|
+
case 'summary':
|
|
580
|
+
await rewardsSummary(parsed);
|
|
581
|
+
break;
|
|
582
|
+
case 'claim':
|
|
583
|
+
await rewardsClaim(parsed);
|
|
584
|
+
break;
|
|
585
|
+
default:
|
|
586
|
+
console.log(`\n${C.cyan}Usage:${C.reset}`);
|
|
587
|
+
console.log(` aether rewards list --address <addr> List all staking rewards`);
|
|
588
|
+
console.log(` aether rewards summary --address <addr> One-line rewards summary`);
|
|
589
|
+
console.log(` aether rewards claim --address <addr> [--account <stakeAcct>] Claim rewards`);
|
|
590
|
+
console.log();
|
|
591
|
+
console.log(` ${C.dim}--json Output as JSON`);
|
|
592
|
+
console.log(` --rpc <url> Use specific RPC endpoint${C.reset}\n`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
main().catch(err => {
|
|
597
|
+
console.error(`\n${C.red}Error running rewards command:${C.reset}`, err.message, '\n');
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
module.exports = { rewardsCommand: main };
|