@jellylegsai/aether-cli 1.8.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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/aether-cli-1.0.0.tgz +0 -0
- package/aether-cli-1.8.0.tgz +0 -0
- package/aether-hub-1.0.5.tgz +0 -0
- package/aether-hub-1.1.8.tgz +0 -0
- package/aether-hub-1.2.1.tgz +0 -0
- package/commands/account.js +280 -0
- package/commands/apy.js +499 -0
- package/commands/balance.js +241 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +387 -0
- package/commands/claim.js +490 -0
- package/commands/config.js +851 -0
- package/commands/delegations.js +582 -0
- package/commands/doctor.js +769 -0
- package/commands/emergency.js +667 -0
- package/commands/epoch.js +275 -0
- package/commands/fees.js +276 -0
- package/commands/index.js +78 -0
- package/commands/info.js +495 -0
- package/commands/init.js +816 -0
- package/commands/install.js +666 -0
- package/commands/kyc.js +272 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/multisig.js +701 -0
- package/commands/network.js +429 -0
- package/commands/nft.js +857 -0
- package/commands/ping.js +266 -0
- package/commands/price.js +253 -0
- package/commands/rewards.js +931 -0
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +656 -0
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +470 -0
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +205 -0
- package/commands/stake.js +516 -0
- package/commands/stats.js +396 -0
- package/commands/status.js +327 -0
- package/commands/supply.js +391 -0
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +346 -0
- package/commands/unstake.js +597 -0
- package/commands/validator-info.js +657 -0
- package/commands/validator-register.js +593 -0
- package/commands/validator-start.js +323 -0
- package/commands/validator-status.js +227 -0
- package/commands/validators.js +626 -0
- package/commands/wallet.js +1570 -0
- package/index.js +593 -0
- package/lib/errors.js +398 -0
- package/package.json +76 -0
- package/sdk/README.md +210 -0
- package/sdk/index.js +1639 -0
- package/sdk/package.json +34 -0
- package/sdk/rpc.js +254 -0
- package/sdk/test.js +85 -0
- package/test/doctor.test.js +76 -0
- package/validator-identity.json +4 -0
package/commands/apy.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli apy
|
|
4
|
+
*
|
|
5
|
+
* Estimate APY for a validator or wallet's stake positions.
|
|
6
|
+
* FULLY WIRED TO SDK - Uses @jellylegsai/aether-sdk for all blockchain calls.
|
|
7
|
+
* No manual HTTP - all calls go through AetherClient with real RPC.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aether apy Show network-wide average APY
|
|
11
|
+
* aether apy --validator <addr> APY for a specific validator
|
|
12
|
+
* aether apy --address <addr> APY for a wallet's stake delegations
|
|
13
|
+
* aether apy --json JSON output for scripting/monitoring
|
|
14
|
+
* aether apy --rpc <url> Override default RPC
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
* aether apy --validator ATH3xyz... # Check validator APY
|
|
18
|
+
* aether apy --address ATHabc... # Check your wallet's weighted APY
|
|
19
|
+
* aether apy --json # Machine-readable output
|
|
20
|
+
*
|
|
21
|
+
* SDK Methods Used:
|
|
22
|
+
* - client.getValidatorAPY(address) → GET /v1/validator/<address>/apy
|
|
23
|
+
* - client.getStakePositions(address) → GET /v1/stake/<address>
|
|
24
|
+
* - client.getRewards(address) → GET /v1/rewards/<address>
|
|
25
|
+
* - client.getEpochInfo() → GET /v1/epoch
|
|
26
|
+
* - client.getSupply() → GET /v1/supply
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
// Import SDK — makes REAL HTTP RPC calls to the blockchain
|
|
32
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
33
|
+
const aether = require(sdkPath);
|
|
34
|
+
|
|
35
|
+
// ANSI colours
|
|
36
|
+
const C = {
|
|
37
|
+
reset: '\x1b[0m',
|
|
38
|
+
bright: '\x1b[1m',
|
|
39
|
+
dim: '\x1b[2m',
|
|
40
|
+
red: '\x1b[31m',
|
|
41
|
+
green: '\x1b[32m',
|
|
42
|
+
yellow: '\x1b[33m',
|
|
43
|
+
cyan: '\x1b[36m',
|
|
44
|
+
magenta: '\x1b[35m',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CLI_VERSION = '1.1.0';
|
|
48
|
+
const EPOCHS_PER_YEAR = 2190; // Aether epochs are ~4 hours
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// SDK Client Setup
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function getDefaultRpc() {
|
|
55
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createClient(rpcUrl) {
|
|
59
|
+
return new aether.AetherClient({ rpcUrl });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getDefaultConfig() {
|
|
63
|
+
const fs = require('fs');
|
|
64
|
+
const cfgPath = path.join(require('os').homedir(), '.aether', 'config.json');
|
|
65
|
+
if (!fs.existsSync(cfgPath)) return { defaultWallet: null };
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return { defaultWallet: null };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Argument parsing
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function parseArgs() {
|
|
78
|
+
const args = process.argv.slice(2);
|
|
79
|
+
const opts = {
|
|
80
|
+
rpc: getDefaultRpc(),
|
|
81
|
+
validator: null,
|
|
82
|
+
address: null,
|
|
83
|
+
asJson: false,
|
|
84
|
+
epochs: 14, // default lookback for APY calc
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < args.length; i++) {
|
|
88
|
+
const arg = args[i];
|
|
89
|
+
if (arg === '--rpc' || arg === '-r') {
|
|
90
|
+
opts.rpc = args[++i];
|
|
91
|
+
} else if (arg === '--validator' || arg === '-v') {
|
|
92
|
+
opts.validator = args[++i];
|
|
93
|
+
} else if (arg === '--address' || arg === '-a') {
|
|
94
|
+
opts.address = args[++i];
|
|
95
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
96
|
+
opts.asJson = true;
|
|
97
|
+
} else if (arg === '--epochs' || arg === '-e') {
|
|
98
|
+
const v = parseInt(args[++i], 10);
|
|
99
|
+
if (!isNaN(v) && v > 0 && v <= 100) opts.epochs = v;
|
|
100
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
101
|
+
showHelp();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fall back to default wallet if no address provided
|
|
107
|
+
if (!opts.address && !opts.validator) {
|
|
108
|
+
const cfg = getDefaultConfig();
|
|
109
|
+
opts.address = cfg.defaultWallet;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return opts;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function showHelp() {
|
|
116
|
+
console.log(`
|
|
117
|
+
${C.bright}${C.cyan}aether-cli apy${C.reset} - Validator APY Estimator
|
|
118
|
+
|
|
119
|
+
${C.bright}Usage:${C.reset}
|
|
120
|
+
aether apy Network-wide average APY
|
|
121
|
+
aether apy --validator <addr> APY for a specific validator
|
|
122
|
+
aether apy --address <addr> APY for wallet's stake positions
|
|
123
|
+
aether apy --rpc <url> Override default RPC
|
|
124
|
+
aether apy --json JSON output for scripting
|
|
125
|
+
aether apy --epochs <n> Lookback epochs (default: 14, max: 100)
|
|
126
|
+
|
|
127
|
+
${C.bright}SDK Methods Used:${C.reset}
|
|
128
|
+
client.getValidatorAPY(address) → GET /v1/validator/<address>/apy
|
|
129
|
+
client.getStakePositions(address) → GET /v1/stake/<address>
|
|
130
|
+
client.getRewards(address) → GET /v1/rewards/<address>
|
|
131
|
+
client.getEpochInfo() → GET /v1/epoch
|
|
132
|
+
client.getSupply() → GET /v1/supply
|
|
133
|
+
|
|
134
|
+
${C.bright}Examples:${C.reset}
|
|
135
|
+
aether apy --validator ATH3J8... Check a validator's APY
|
|
136
|
+
aether apy --address ATHabc... Check your wallet's weighted APY
|
|
137
|
+
aether apy --json Machine-readable output
|
|
138
|
+
`.trim());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// APY calculation via SDK (REAL RPC CALLS)
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fetch validator APY via SDK (GET /v1/validator/<address>/apy)
|
|
147
|
+
*/
|
|
148
|
+
async function fetchValidatorApy(client, validatorAddr) {
|
|
149
|
+
try {
|
|
150
|
+
const result = await client.getValidatorAPY(validatorAddr);
|
|
151
|
+
return {
|
|
152
|
+
apy: result.apy ?? result.current_apy ?? result.estimated_apy ?? null,
|
|
153
|
+
commission: result.commission ?? null,
|
|
154
|
+
total_stake: result.total_stake ?? result.stake_lamports ?? null,
|
|
155
|
+
source: 'validator_api',
|
|
156
|
+
raw: result,
|
|
157
|
+
};
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return { error: err.message, source: 'validator_api' };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Calculate wallet APY from stake positions and rewards via SDK
|
|
165
|
+
*/
|
|
166
|
+
async function calculateWalletApy(client, address, epochs) {
|
|
167
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
168
|
+
|
|
169
|
+
// Parallel SDK calls
|
|
170
|
+
const [stakePositions, rewards, epochInfo, supply] = await Promise.all([
|
|
171
|
+
client.getStakePositions(rawAddr).catch(() => []),
|
|
172
|
+
client.getRewards(rawAddr).catch(() => ({ total: 0, pending: 0, history: [] })),
|
|
173
|
+
client.getEpochInfo().catch(() => ({ epoch: 0, slotsInEpoch: 432000, slotIndex: 0 })),
|
|
174
|
+
client.getSupply().catch(() => null),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
const currentEpoch = epochInfo?.epoch ?? 0;
|
|
178
|
+
|
|
179
|
+
// No stake positions
|
|
180
|
+
if (!Array.isArray(stakePositions) || stakePositions.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
apy: null,
|
|
183
|
+
apy_pct: null,
|
|
184
|
+
method: 'no_stake',
|
|
185
|
+
epoch: currentEpoch,
|
|
186
|
+
address,
|
|
187
|
+
note: 'No active stake positions found for this address.',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Calculate total staked
|
|
192
|
+
const totalStakedLamports = stakePositions.reduce((sum, s) => {
|
|
193
|
+
return sum + BigInt(s.lamports || s.stake_lamports || 0);
|
|
194
|
+
}, 0n);
|
|
195
|
+
|
|
196
|
+
if (totalStakedLamports === 0n) {
|
|
197
|
+
return {
|
|
198
|
+
apy: null,
|
|
199
|
+
apy_pct: null,
|
|
200
|
+
method: 'no_active_stake',
|
|
201
|
+
epoch: currentEpoch,
|
|
202
|
+
address,
|
|
203
|
+
note: 'Stake positions found but no active stake amount.',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Get reward history
|
|
208
|
+
const rewardHistory = rewards?.history || rewards?.epochs || [];
|
|
209
|
+
const totalRewardsLamports = BigInt(rewards?.total || rewards?.total_rewards || 0);
|
|
210
|
+
|
|
211
|
+
// If we have reward history, calculate from it
|
|
212
|
+
if (rewardHistory.length > 0 && totalRewardsLamports > 0n) {
|
|
213
|
+
const epochsWithData = Math.min(rewardHistory.length, epochs);
|
|
214
|
+
const avgYieldPerEpoch = Number(totalRewardsLamports) / (Number(totalStakedLamports) * epochsWithData);
|
|
215
|
+
const apy = Math.pow(1 + avgYieldPerEpoch, EPOCHS_PER_YEAR) - 1;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
apy,
|
|
219
|
+
apy_pct: parseFloat((apy * 100).toFixed(2)),
|
|
220
|
+
method: 'reward_history',
|
|
221
|
+
epoch: currentEpoch,
|
|
222
|
+
epochs_used: epochsWithData,
|
|
223
|
+
epochs_with_rewards: rewardHistory.filter(r => (r.rewards || 0) > 0).length,
|
|
224
|
+
total_rewards_lamports: totalRewardsLamports.toString(),
|
|
225
|
+
total_staked_lamports: totalStakedLamports.toString(),
|
|
226
|
+
avg_yield_per_epoch_pct: parseFloat((avgYieldPerEpoch * 100).toFixed(4)),
|
|
227
|
+
address,
|
|
228
|
+
stake_count: stakePositions.length,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// No reward history yet — use network inflation model
|
|
233
|
+
if (supply && !supply.error) {
|
|
234
|
+
// Aether uses ~7% inflation Year 1, declining
|
|
235
|
+
const inflationRate = 0.07;
|
|
236
|
+
return {
|
|
237
|
+
apy: inflationRate,
|
|
238
|
+
apy_pct: 7.0,
|
|
239
|
+
method: 'inflation_model',
|
|
240
|
+
epoch: currentEpoch,
|
|
241
|
+
epochs_used: 0,
|
|
242
|
+
total_staked_lamports: totalStakedLamports.toString(),
|
|
243
|
+
address,
|
|
244
|
+
stake_count: stakePositions.length,
|
|
245
|
+
note: 'No reward history available yet. APY estimated from network inflation model.',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// No data available
|
|
250
|
+
return {
|
|
251
|
+
apy: null,
|
|
252
|
+
apy_pct: null,
|
|
253
|
+
method: 'insufficient_data',
|
|
254
|
+
epoch: currentEpoch,
|
|
255
|
+
address,
|
|
256
|
+
stake_count: stakePositions.length,
|
|
257
|
+
note: 'Stake positions exist but no reward data yet. Check back after epoch ends.',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Estimate APY using SDK
|
|
263
|
+
*/
|
|
264
|
+
async function estimateApy({ rpc, validator, address, epochs }) {
|
|
265
|
+
const client = createClient(rpc);
|
|
266
|
+
|
|
267
|
+
// Validator-specific APY
|
|
268
|
+
if (validator) {
|
|
269
|
+
const rawAddr = validator.startsWith('ATH') ? validator.slice(3) : validator;
|
|
270
|
+
const validatorApy = await fetchValidatorApy(client, rawAddr);
|
|
271
|
+
|
|
272
|
+
if (validatorApy.error) {
|
|
273
|
+
// Fall back to wallet calculation method
|
|
274
|
+
return await calculateWalletApy(client, validator, epochs);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const apy = validatorApy.apy ?? 0;
|
|
278
|
+
return {
|
|
279
|
+
apy,
|
|
280
|
+
apy_pct: apy !== null ? parseFloat((apy * 100).toFixed(2)) : null,
|
|
281
|
+
method: 'validator_api',
|
|
282
|
+
commission: validatorApy.commission,
|
|
283
|
+
total_stake: validatorApy.total_stake,
|
|
284
|
+
validator,
|
|
285
|
+
source: validatorApy.source,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Wallet APY (weighted across all stake positions)
|
|
290
|
+
if (address) {
|
|
291
|
+
return await calculateWalletApy(client, address, epochs);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Network-wide APY
|
|
295
|
+
try {
|
|
296
|
+
const [epochInfo, supply] = await Promise.all([
|
|
297
|
+
client.getEpochInfo().catch(() => null),
|
|
298
|
+
client.getSupply().catch(() => null),
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
if (supply && !supply.error) {
|
|
302
|
+
const inflationRate = 0.07; // 7% base inflation
|
|
303
|
+
return {
|
|
304
|
+
apy: inflationRate,
|
|
305
|
+
apy_pct: 7.0,
|
|
306
|
+
method: 'network_inflation_model',
|
|
307
|
+
epoch: epochInfo?.epoch ?? null,
|
|
308
|
+
note: 'Network-wide APY estimate based on inflation schedule.',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
// Fall through
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
apy: null,
|
|
317
|
+
apy_pct: null,
|
|
318
|
+
method: 'none',
|
|
319
|
+
error: 'Unable to fetch network data. Is your validator running?',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Visual formatters
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
function formatApyBar(apyPct, maxPct = 20) {
|
|
328
|
+
const totalBars = 20;
|
|
329
|
+
const filled = Math.min(totalBars, Math.round((apyPct / maxPct) * totalBars));
|
|
330
|
+
return C.green + '█'.repeat(filled) + C.dim + '░'.repeat(totalBars - filled) + C.reset;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getApyColor(apyPct) {
|
|
334
|
+
if (apyPct === null || apyPct === undefined) return C.dim;
|
|
335
|
+
if (apyPct >= 8) return C.green;
|
|
336
|
+
if (apyPct >= 4) return C.cyan;
|
|
337
|
+
if (apyPct >= 2) return C.yellow;
|
|
338
|
+
return C.red;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Output formatters
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function outputHuman(apyData, opts) {
|
|
346
|
+
const { rpc, validator, address } = opts;
|
|
347
|
+
|
|
348
|
+
console.log(`\n${C.bright}${C.cyan}── Validator APY Estimate ──────────────────────────────${C.reset}\n`);
|
|
349
|
+
|
|
350
|
+
const targetLabel = validator
|
|
351
|
+
? `Validator: ${C.bright}${validator}${C.reset}`
|
|
352
|
+
: address
|
|
353
|
+
? `Address: ${C.bright}${address}${C.reset}`
|
|
354
|
+
: `Network: ${C.bright}Aether Chain${C.reset}`;
|
|
355
|
+
console.log(` ${C.green}★${C.reset} ${targetLabel}`);
|
|
356
|
+
console.log(` ${C.dim} RPC: ${rpc}${C.reset}`);
|
|
357
|
+
console.log();
|
|
358
|
+
|
|
359
|
+
if (apyData.error) {
|
|
360
|
+
console.log(` ${C.yellow}⚠ ${apyData.error}${C.reset}\n`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (apyData.note) {
|
|
365
|
+
console.log(` ${C.yellow}⚠ ${apyData.note}${C.reset}`);
|
|
366
|
+
console.log();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (apyData.apy_pct === null) {
|
|
370
|
+
console.log(` ${C.yellow}⚠ APY data unavailable.${C.reset}`);
|
|
371
|
+
if (apyData.method === 'inflation_model' || apyData.method === 'network_inflation_model') {
|
|
372
|
+
console.log(` ${C.dim} Using network inflation model as estimate.${C.reset}`);
|
|
373
|
+
}
|
|
374
|
+
console.log();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Main APY display
|
|
379
|
+
const apyColor = getApyColor(apyData.apy_pct);
|
|
380
|
+
console.log(` ${C.bright}${apyColor}${apyData.apy_pct.toFixed(2)}%${C.reset} ${C.dim}APY${C.reset}`);
|
|
381
|
+
console.log();
|
|
382
|
+
|
|
383
|
+
// Visual bar
|
|
384
|
+
console.log(` ${C.dim}Yield:${C.reset} ${formatApyBar(apyData.apy_pct)}`);
|
|
385
|
+
console.log();
|
|
386
|
+
|
|
387
|
+
// Stats
|
|
388
|
+
const methodLabel = apyData.method === 'reward_history'
|
|
389
|
+
? 'Reward history annualised'
|
|
390
|
+
: apyData.method === 'validator_api'
|
|
391
|
+
? 'Validator API'
|
|
392
|
+
: 'Inflation model';
|
|
393
|
+
console.log(` ${C.dim}Method:${C.reset} ${C.bright}${methodLabel}${C.reset}`);
|
|
394
|
+
|
|
395
|
+
if (apyData.epoch !== undefined && apyData.epoch !== null) {
|
|
396
|
+
console.log(` ${C.dim}Epoch:${C.reset} ${C.bright}#${apyData.epoch}${C.reset}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (apyData.epochs_used !== undefined) {
|
|
400
|
+
console.log(` ${C.dim}Epochs used:${C.reset} ${apyData.epochs_used}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (apyData.avg_yield_per_epoch_pct !== undefined) {
|
|
404
|
+
console.log(` ${C.dim}Avg/epoch:${C.reset} ${C.bright}${apyData.avg_yield_per_epoch_pct.toFixed(4)}%${C.reset}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (apyData.total_rewards_lamports !== undefined && apyData.total_staked_lamports !== undefined) {
|
|
408
|
+
const totalAeth = (Number(apyData.total_rewards_lamports) / 1e9).toFixed(4);
|
|
409
|
+
const stakedAeth = (Number(apyData.total_staked_lamports) / 1e9).toFixed(2);
|
|
410
|
+
console.log(` ${C.dim}Total rewards:${C.reset} ${C.green}${totalAeth} AETH${C.reset}`);
|
|
411
|
+
console.log(` ${C.dim}Total staked:${C.reset} ${stakedAeth} AETH`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (apyData.commission !== undefined && apyData.commission !== null) {
|
|
415
|
+
console.log(` ${C.dim}Commission:${C.reset} ${apyData.commission}%`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (apyData.stake_count !== undefined) {
|
|
419
|
+
console.log(` ${C.dim}Stake positions:${C.reset} ${apyData.stake_count}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log();
|
|
423
|
+
|
|
424
|
+
// Disclaimer
|
|
425
|
+
console.log(` ${C.dim}Note: APY is an estimate based on ${apyData.epochs_used || 0} epoch(s) of reward data.${C.reset}`);
|
|
426
|
+
console.log(` ${C.dim}Actual returns may vary. Check 'aether rewards' for precise figures.${C.reset}`);
|
|
427
|
+
console.log();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function outputJson(apyData, opts) {
|
|
431
|
+
console.log(JSON.stringify({
|
|
432
|
+
apy_pct: apyData.apy_pct,
|
|
433
|
+
apy: apyData.apy,
|
|
434
|
+
method: apyData.method,
|
|
435
|
+
epoch: apyData.epoch,
|
|
436
|
+
epochs_used: apyData.epochs_used,
|
|
437
|
+
epochs_with_rewards: apyData.epochs_with_rewards,
|
|
438
|
+
avg_yield_per_epoch_pct: apyData.avg_yield_per_epoch_pct,
|
|
439
|
+
total_rewards_lamports: apyData.total_rewards_lamports,
|
|
440
|
+
total_staked_lamports: apyData.total_staked_lamports,
|
|
441
|
+
commission: apyData.commission,
|
|
442
|
+
validator: apyData.validator,
|
|
443
|
+
address: apyData.address,
|
|
444
|
+
stake_count: apyData.stake_count,
|
|
445
|
+
note: apyData.note || null,
|
|
446
|
+
error: apyData.error || null,
|
|
447
|
+
rpc: opts.rpc,
|
|
448
|
+
cli_version: CLI_VERSION,
|
|
449
|
+
sdk_version: 'SDK via AetherClient',
|
|
450
|
+
timestamp: new Date().toISOString(),
|
|
451
|
+
}, null, 2));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Main
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
async function apyCommand() {
|
|
459
|
+
const opts = parseArgs();
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const apyData = await estimateApy(opts);
|
|
463
|
+
|
|
464
|
+
if (opts.asJson) {
|
|
465
|
+
outputJson(apyData, opts);
|
|
466
|
+
} else {
|
|
467
|
+
outputHuman(apyData, opts);
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
if (opts.asJson) {
|
|
471
|
+
console.log(JSON.stringify({
|
|
472
|
+
apy_pct: null,
|
|
473
|
+
apy: null,
|
|
474
|
+
error: err.message,
|
|
475
|
+
validator: opts.validator,
|
|
476
|
+
address: opts.address,
|
|
477
|
+
rpc: opts.rpc,
|
|
478
|
+
cli_version: CLI_VERSION,
|
|
479
|
+
timestamp: new Date().toISOString(),
|
|
480
|
+
}, null, 2));
|
|
481
|
+
} else {
|
|
482
|
+
console.log(`\n ${C.red}✗ APY calculation failed:${C.reset} ${err.message}`);
|
|
483
|
+
console.log(` ${C.dim} RPC: ${opts.rpc}${C.reset}`);
|
|
484
|
+
console.log(` ${C.dim} Is your validator running?${C.reset}\n`);
|
|
485
|
+
}
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Export for use in index.js
|
|
491
|
+
module.exports = { apyCommand };
|
|
492
|
+
|
|
493
|
+
// Run if called directly
|
|
494
|
+
if (require.main === module) {
|
|
495
|
+
apyCommand().catch(err => {
|
|
496
|
+
console.error(`\n${C.red}APY command failed:${C.reset} ${err.message}\n`);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
});
|
|
499
|
+
}
|