aether-hub 1.2.2 → 1.2.4
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/apy.js +480 -0
- package/commands/broadcast.js +323 -0
- package/commands/stake-positions.js +220 -0
- package/commands/status.js +371 -0
- package/commands/supply.js +437 -437
- package/index.js +26 -5
- package/package.json +1 -1
package/commands/apy.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli apy
|
|
4
|
+
*
|
|
5
|
+
* Estimate APY for a validator or wallet's stake positions.
|
|
6
|
+
* Fetches recent reward history, computes average yield over the current
|
|
7
|
+
* epoch, and annualises it to an APY figure.
|
|
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
|
+
|
|
22
|
+
const http = require('http');
|
|
23
|
+
const https = require('https');
|
|
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 CLI_VERSION = '1.0.0';
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Config
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function getDefaultRpc() {
|
|
44
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDefaultConfig() {
|
|
48
|
+
const fs = require('fs');
|
|
49
|
+
const path = require('path');
|
|
50
|
+
const os = require('os');
|
|
51
|
+
const cfgPath = path.join(os.homedir(), '.aether', 'config.json');
|
|
52
|
+
if (!fs.existsSync(cfgPath)) return { defaultWallet: null };
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return { defaultWallet: null };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// HTTP helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function httpRequest(rpcUrl, pathStr, timeoutMs = 10000) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const url = new URL(pathStr, rpcUrl);
|
|
67
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
68
|
+
const req = lib.request({
|
|
69
|
+
hostname: url.hostname,
|
|
70
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
71
|
+
path: url.pathname + url.search,
|
|
72
|
+
method: 'GET',
|
|
73
|
+
timeout: timeoutMs,
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
}, (res) => {
|
|
76
|
+
let data = '';
|
|
77
|
+
res.on('data', (chunk) => data += chunk);
|
|
78
|
+
res.on('end', () => {
|
|
79
|
+
try { resolve(JSON.parse(data)); }
|
|
80
|
+
catch { resolve({ _raw: data }); }
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
req.on('error', reject);
|
|
84
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
85
|
+
req.end();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function httpPost(rpcUrl, pathStr, body, timeoutMs = 10000) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const url = new URL(pathStr, rpcUrl);
|
|
92
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
93
|
+
const bodyStr = JSON.stringify(body);
|
|
94
|
+
const req = lib.request({
|
|
95
|
+
hostname: url.hostname,
|
|
96
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
97
|
+
path: url.pathname + url.search,
|
|
98
|
+
method: 'POST',
|
|
99
|
+
timeout: timeoutMs,
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
103
|
+
},
|
|
104
|
+
}, (res) => {
|
|
105
|
+
let data = '';
|
|
106
|
+
res.on('data', (chunk) => data += chunk);
|
|
107
|
+
res.on('end', () => {
|
|
108
|
+
try { resolve(JSON.parse(data)); }
|
|
109
|
+
catch { resolve({ _raw: data }); }
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
req.on('error', reject);
|
|
113
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
114
|
+
req.write(bodyStr);
|
|
115
|
+
req.end();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Argument parsing
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function parseArgs() {
|
|
124
|
+
const args = process.argv.slice(2);
|
|
125
|
+
const opts = {
|
|
126
|
+
rpc: getDefaultRpc(),
|
|
127
|
+
validator: null,
|
|
128
|
+
address: null,
|
|
129
|
+
asJson: false,
|
|
130
|
+
epochs: 14, // default lookback for APY calc
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < args.length; i++) {
|
|
134
|
+
const arg = args[i];
|
|
135
|
+
if (arg === '--rpc' || arg === '-r') {
|
|
136
|
+
opts.rpc = args[++i];
|
|
137
|
+
} else if (arg === '--validator' || arg === '-v') {
|
|
138
|
+
opts.validator = args[++i];
|
|
139
|
+
} else if (arg === '--address' || arg === '-a') {
|
|
140
|
+
opts.address = args[++i];
|
|
141
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
142
|
+
opts.asJson = true;
|
|
143
|
+
} else if (arg === '--epochs' || arg === '-e') {
|
|
144
|
+
const v = parseInt(args[++i], 10);
|
|
145
|
+
if (!isNaN(v) && v > 0 && v <= 100) opts.epochs = v;
|
|
146
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
147
|
+
showHelp();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fall back to default wallet if no address provided
|
|
153
|
+
if (!opts.address && !opts.validator) {
|
|
154
|
+
const cfg = getDefaultConfig();
|
|
155
|
+
opts.address = cfg.defaultWallet;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return opts;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function showHelp() {
|
|
162
|
+
console.log(`
|
|
163
|
+
${C.bright}${C.cyan}aether-cli apy${C.reset} - Validator APY Estimator
|
|
164
|
+
|
|
165
|
+
${C.bright}Usage:${C.reset}
|
|
166
|
+
aether apy Network-wide average APY
|
|
167
|
+
aether apy --validator <addr> APY for a specific validator
|
|
168
|
+
aether apy --address <addr> APY for wallet's stake positions
|
|
169
|
+
aether apy --rpc <url> Override default RPC
|
|
170
|
+
aether apy --json JSON output for scripting
|
|
171
|
+
aether apy --epochs <n> Lookback epochs (default: 14, max: 100)
|
|
172
|
+
|
|
173
|
+
${C.bright}Examples:${C.reset}
|
|
174
|
+
aether apy --validator ATH3J8... Check a validator's APY
|
|
175
|
+
aether apy --address ATHabc... Check your wallet's weighted APY
|
|
176
|
+
aether apy --json Machine-readable output
|
|
177
|
+
`.trim());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// APY calculation
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Estimate APY from reward history.
|
|
186
|
+
*
|
|
187
|
+
* Strategy:
|
|
188
|
+
* 1. Fetch last N epochs of reward history for the target
|
|
189
|
+
* 2. Compute per-epoch yield: rewards_staked
|
|
190
|
+
* 3. Average the yield, then annualise: APY = ((1 + avg_yield) ^ epochs_per_year) - 1
|
|
191
|
+
*
|
|
192
|
+
* Aether epochs are ~4 hours → ~2190 epochs/year
|
|
193
|
+
*/
|
|
194
|
+
async function estimateApy({ rpc, validator, address, epochs }) {
|
|
195
|
+
const EPOCHS_PER_YEAR = 2190;
|
|
196
|
+
|
|
197
|
+
// Normalise address: strip ATH prefix for API calls
|
|
198
|
+
const rawAddr = (validator || address || '').startsWith('ATH')
|
|
199
|
+
? (validator || address || '').slice(3)
|
|
200
|
+
: (validator || address || '');
|
|
201
|
+
|
|
202
|
+
// Step 1: fetch epoch info to get current epoch
|
|
203
|
+
let currentEpoch = null;
|
|
204
|
+
let epochLength = null;
|
|
205
|
+
try {
|
|
206
|
+
const epochInfo = await httpRequest(rpc, '/v1/epoch/info');
|
|
207
|
+
if (!epochInfo.error) {
|
|
208
|
+
currentEpoch = epochInfo.epoch;
|
|
209
|
+
epochLength = epochInfo.slots_in_epoch || epochInfo.epoch_length;
|
|
210
|
+
}
|
|
211
|
+
} catch { /* use defaults */ }
|
|
212
|
+
|
|
213
|
+
// Step 2: fetch reward history
|
|
214
|
+
// Try /v1/rewards?address=<addr>&epochs=<N> first
|
|
215
|
+
let rewardHistory = [];
|
|
216
|
+
try {
|
|
217
|
+
const fromEpoch = Math.max(0, (currentEpoch || epochs) - epochs);
|
|
218
|
+
const rewardsRes = await httpRequest(
|
|
219
|
+
rpc,
|
|
220
|
+
`/v1/rewards?address=${encodeURIComponent(rawAddr)}&from_epoch=${fromEpoch}&limit=${epochs}`
|
|
221
|
+
);
|
|
222
|
+
if (!rewardsRes.error) {
|
|
223
|
+
rewardHistory = Array.isArray(rewardsRes)
|
|
224
|
+
? rewardsRes
|
|
225
|
+
: (rewardsRes.rewards || []);
|
|
226
|
+
}
|
|
227
|
+
} catch { /* try alternate endpoint */ }
|
|
228
|
+
|
|
229
|
+
// Fallback: /v1/stake?address=<addr> (contains accumulated rewards)
|
|
230
|
+
if (rewardHistory.length === 0) {
|
|
231
|
+
try {
|
|
232
|
+
const stakeRes = await httpRequest(
|
|
233
|
+
rpc,
|
|
234
|
+
`/v1/stake?address=${encodeURIComponent(rawAddr)}`
|
|
235
|
+
);
|
|
236
|
+
if (!stakeRes.error) {
|
|
237
|
+
const accounts = Array.isArray(stakeRes) ? stakeRes : (stakeRes.accounts || []);
|
|
238
|
+
for (const acc of accounts) {
|
|
239
|
+
if (acc.rewards !== undefined) {
|
|
240
|
+
rewardHistory.push({
|
|
241
|
+
epoch: acc.epoch || currentEpoch,
|
|
242
|
+
rewards: acc.rewards,
|
|
243
|
+
lamports: acc.stake_lamports || acc.lamports || 0,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch { /* no stake data */ }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Step 3: calculate APY
|
|
252
|
+
if (rewardHistory.length === 0) {
|
|
253
|
+
// No reward data — try fetching network-wide stats for a ballpark
|
|
254
|
+
try {
|
|
255
|
+
const supplyRes = await httpRequest(rpc, '/v1/supply');
|
|
256
|
+
const epochRes = await httpRequest(rpc, '/v1/epoch/info');
|
|
257
|
+
if (!supplyRes.error && !epochRes.error) {
|
|
258
|
+
// Rough estimate: total_reward_rate from inflation schedule
|
|
259
|
+
// Aether uses ~7% inflation Year 1, declining. Use 7% as starting point.
|
|
260
|
+
const inflationRate = 0.07; // 7% base, would need real data for precision
|
|
261
|
+
return {
|
|
262
|
+
apy: inflationRate,
|
|
263
|
+
apy_pct: inflationRate * 100,
|
|
264
|
+
method: 'inflation_model',
|
|
265
|
+
epoch: currentEpoch,
|
|
266
|
+
epochs_used: 0,
|
|
267
|
+
epochs_available: 0,
|
|
268
|
+
total_staked: supplyRes.total || 0,
|
|
269
|
+
validator,
|
|
270
|
+
address,
|
|
271
|
+
note: 'No reward history available. APY estimated from network inflation model.',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
} catch { /* no network data either */ }
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
apy: null,
|
|
278
|
+
apy_pct: null,
|
|
279
|
+
method: 'none',
|
|
280
|
+
epoch: currentEpoch,
|
|
281
|
+
epochs_used: 0,
|
|
282
|
+
error: 'No reward history or network data available. Ensure your validator is running and has reward data.',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Compute weighted average yield per epoch
|
|
287
|
+
let totalRewards = 0;
|
|
288
|
+
let totalStaked = 0;
|
|
289
|
+
let epochsWithRewards = 0;
|
|
290
|
+
|
|
291
|
+
for (const entry of rewardHistory) {
|
|
292
|
+
const rewards = entry.rewards || 0;
|
|
293
|
+
const staked = entry.lamports || entry.stake_lamports || 0;
|
|
294
|
+
totalRewards += rewards;
|
|
295
|
+
if (staked > 0) totalStaked += staked;
|
|
296
|
+
if (rewards > 0) epochsWithRewards++;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (totalStaked === 0 || totalRewards === 0) {
|
|
300
|
+
return {
|
|
301
|
+
apy: null,
|
|
302
|
+
apy_pct: null,
|
|
303
|
+
method: 'insufficient_data',
|
|
304
|
+
epoch: currentEpoch,
|
|
305
|
+
epochs_used: rewardHistory.length,
|
|
306
|
+
epochs_with_rewards: epochsWithRewards,
|
|
307
|
+
validator,
|
|
308
|
+
address,
|
|
309
|
+
note: 'Stake positions exist but no reward data yet. Check back after epoch ends.',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Average epoch yield = total rewards / (total staked * num epochs)
|
|
314
|
+
const avgYieldPerEpoch = totalRewards / (totalStaked * rewardHistory.length);
|
|
315
|
+
|
|
316
|
+
// Annualise: APY = (1 + yield_per_epoch) ^ epochs_per_year - 1
|
|
317
|
+
const apy = Math.pow(1 + avgYieldPerEpoch, EPOCHS_PER_YEAR) - 1;
|
|
318
|
+
const apyPct = apy * 100;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
apy,
|
|
322
|
+
apy_pct: parseFloat(apyPct.toFixed(2)),
|
|
323
|
+
method: 'reward_history',
|
|
324
|
+
epoch: currentEpoch,
|
|
325
|
+
epochs_used: rewardHistory.length,
|
|
326
|
+
epochs_with_rewards: epochsWithRewards,
|
|
327
|
+
total_rewards_lamports: totalRewards,
|
|
328
|
+
total_staked_lamports: totalStaked,
|
|
329
|
+
avg_yield_per_epoch_pct: parseFloat((avgYieldPerEpoch * 100).toFixed(4)),
|
|
330
|
+
validator,
|
|
331
|
+
address,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Format APY bar: ████░░░░░░
|
|
337
|
+
*/
|
|
338
|
+
function formatApyBar(apyPct, maxPct = 20) {
|
|
339
|
+
const totalBars = 20;
|
|
340
|
+
const filled = Math.min(totalBars, Math.round((apyPct / maxPct) * totalBars));
|
|
341
|
+
return C.green + '█'.repeat(filled) + C.dim + '░'.repeat(totalBars - filled) + C.reset;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Output formatters
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
function outputHuman(apyData, opts) {
|
|
349
|
+
const { rpc, validator, address, asJson } = opts;
|
|
350
|
+
|
|
351
|
+
console.log(`\n${C.bright}${C.cyan}── Validator APY Estimate ──────────────────────────────${C.reset}\n`);
|
|
352
|
+
|
|
353
|
+
const targetLabel = validator
|
|
354
|
+
? `Validator: ${C.bright}${validator}${C.reset}`
|
|
355
|
+
: `Address: ${C.bright}${address}${C.reset}`;
|
|
356
|
+
console.log(` ${C.green}★${C.reset} ${targetLabel}`);
|
|
357
|
+
console.log(` ${C.dim} RPC: ${rpc}${C.reset}`);
|
|
358
|
+
console.log();
|
|
359
|
+
|
|
360
|
+
if (apyData.error) {
|
|
361
|
+
console.log(` ${C.yellow}⚠ ${apyData.error}${C.reset}\n`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (apyData.note) {
|
|
366
|
+
console.log(` ${C.yellow}⚠ ${apyData.note}${C.reset}`);
|
|
367
|
+
console.log();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (apyData.apy_pct === null) {
|
|
371
|
+
console.log(` ${C.yellow}⚠ APY data unavailable.${C.reset}`);
|
|
372
|
+
if (apyData.method === 'inflation_model') {
|
|
373
|
+
console.log(` ${C.dim} Using network inflation model as estimate.${C.reset}`);
|
|
374
|
+
}
|
|
375
|
+
console.log();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Main APY display
|
|
380
|
+
const apyColor = apyData.apy_pct >= 8
|
|
381
|
+
? C.green
|
|
382
|
+
: apyData.apy_pct >= 4
|
|
383
|
+
? C.cyan
|
|
384
|
+
: apyData.apy_pct >= 2
|
|
385
|
+
? C.yellow
|
|
386
|
+
: C.red;
|
|
387
|
+
|
|
388
|
+
console.log(` ${C.bright}${apyColor}${apyData.apy_pct.toFixed(2)}%${C.reset} ${C.dim}APY${C.reset}`);
|
|
389
|
+
console.log();
|
|
390
|
+
|
|
391
|
+
// Visual bar
|
|
392
|
+
console.log(` ${C.dim}Yield:${C.reset} ${formatApyBar(apyData.apy_pct)}`);
|
|
393
|
+
console.log();
|
|
394
|
+
|
|
395
|
+
// Stats
|
|
396
|
+
console.log(` ${C.dim}Method:${C.reset} ${C.bright}${apyData.method === 'reward_history' ? 'Reward history annualised' : 'Inflation model'}${C.reset}`);
|
|
397
|
+
if (apyData.epoch !== undefined && apyData.epoch !== null) {
|
|
398
|
+
console.log(` ${C.dim}Epoch:${C.reset} ${C.bright}#${apyData.epoch}${C.reset}`);
|
|
399
|
+
}
|
|
400
|
+
console.log(` ${C.dim}Epochs used:${C.reset} ${apyData.epochs_used}`);
|
|
401
|
+
|
|
402
|
+
if (apyData.avg_yield_per_epoch_pct !== undefined) {
|
|
403
|
+
console.log(` ${C.dim}Avg/epoch:${C.reset} ${C.bright}${apyData.avg_yield_per_epoch_pct.toFixed(4)}%${C.reset}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (apyData.total_rewards_lamports !== undefined && apyData.total_staked_lamports !== undefined) {
|
|
407
|
+
const totalAeth = (apyData.total_rewards_lamports / 1e9).toFixed(4);
|
|
408
|
+
const stakedAeth = (apyData.total_staked_lamports / 1e9).toFixed(2);
|
|
409
|
+
console.log(` ${C.dim}Total rewards:${C.reset} ${C.green}${totalAeth} AETH${C.reset}`);
|
|
410
|
+
console.log(` ${C.dim}Total staked:${C.reset} ${stakedAeth} AETH`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log();
|
|
414
|
+
|
|
415
|
+
// Disclaimer
|
|
416
|
+
console.log(` ${C.dim}Note: APY is an estimate based on ${apyData.epochs_used} epoch(s) of reward data.${C.reset}`);
|
|
417
|
+
console.log(` ${C.dim}Actual returns may vary. Check aether rewards for precise figures.${C.reset}`);
|
|
418
|
+
console.log();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function outputJson(apyData, opts) {
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
apy_pct: apyData.apy_pct,
|
|
424
|
+
apy: apyData.apy,
|
|
425
|
+
method: apyData.method,
|
|
426
|
+
epoch: apyData.epoch,
|
|
427
|
+
epochs_used: apyData.epochs_used,
|
|
428
|
+
epochs_with_rewards: apyData.epochs_with_rewards,
|
|
429
|
+
avg_yield_per_epoch_pct: apyData.avg_yield_per_epoch_pct,
|
|
430
|
+
total_rewards_lamports: apyData.total_rewards_lamports,
|
|
431
|
+
total_staked_lamports: apyData.total_staked_lamports,
|
|
432
|
+
validator: apyData.validator,
|
|
433
|
+
address: apyData.address,
|
|
434
|
+
note: apyData.note || null,
|
|
435
|
+
error: apyData.error || null,
|
|
436
|
+
rpc: opts.rpc,
|
|
437
|
+
cli_version: CLI_VERSION,
|
|
438
|
+
timestamp: new Date().toISOString(),
|
|
439
|
+
}, null, 2));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Main
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
async function main() {
|
|
447
|
+
const opts = parseArgs();
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const apyData = await estimateApy(opts);
|
|
451
|
+
|
|
452
|
+
if (opts.asJson) {
|
|
453
|
+
outputJson(apyData, opts);
|
|
454
|
+
} else {
|
|
455
|
+
outputHuman(apyData, opts);
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
if (opts.asJson) {
|
|
459
|
+
console.log(JSON.stringify({
|
|
460
|
+
apy_pct: null,
|
|
461
|
+
apy: null,
|
|
462
|
+
error: err.message,
|
|
463
|
+
validator: opts.validator,
|
|
464
|
+
address: opts.address,
|
|
465
|
+
rpc: opts.rpc,
|
|
466
|
+
cli_version: CLI_VERSION,
|
|
467
|
+
timestamp: new Date().toISOString(),
|
|
468
|
+
}, null, 2));
|
|
469
|
+
} else {
|
|
470
|
+
console.log(`\n ${C.red}✗ APY calculation failed:${C.reset} ${err.message}`);
|
|
471
|
+
console.log(` ${C.dim} RPC: ${opts.rpc}${C.reset}`);
|
|
472
|
+
console.log(` ${C.dim} Is your validator running?${C.reset}\n`);
|
|
473
|
+
}
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
main();
|
|
479
|
+
|
|
480
|
+
module.exports = { apyCommand: main };
|