aether-hub 1.2.0 → 1.2.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.
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli supply
4
+ *
5
+ * Display Aether network token supply metrics:
6
+ * - Total supply of AETH (all accounts + locked/escrow)
7
+ * - Circulating supply (liquid, tradeable tokens)
8
+ * - Staked supply (locked in stake accounts)
9
+ * - Burned supply (tokens sent to burn address / invalid addresses)
10
+ *
11
+ * Usage:
12
+ * aether supply Show supply overview
13
+ * aether supply --json JSON output for scripting/monitoring
14
+ * aether supply --rpc <url> Query a specific RPC endpoint
15
+ * aether supply --verbose Show breakdown by account type
16
+ *
17
+ * Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
18
+ */
19
+
20
+ const http = require('http');
21
+ const https = require('https');
22
+
23
+ // ANSI colours
24
+ const C = {
25
+ reset: '\x1b[0m',
26
+ bright: '\x1b[1m',
27
+ dim: '\x1b[2m',
28
+ red: '\x1b[31m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ cyan: '\x1b[36m',
32
+ magenta: '\x1b[35m',
33
+ bold: '\x1b[1m',
34
+ };
35
+
36
+ const CLI_VERSION = '1.0.0';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function getDefaultRpc() {
43
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
44
+ }
45
+
46
+ function httpRequest(rpcUrl, pathStr, timeoutMs = 10000) {
47
+ return new Promise((resolve, reject) => {
48
+ const url = new URL(pathStr, rpcUrl);
49
+ const lib = url.protocol === 'https:' ? https : http;
50
+ const req = lib.request({
51
+ hostname: url.hostname,
52
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
53
+ path: url.pathname + url.search,
54
+ method: 'GET',
55
+ timeout: timeoutMs,
56
+ headers: { 'Content-Type': 'application/json' },
57
+ }, (res) => {
58
+ let data = '';
59
+ res.on('data', (chunk) => data += chunk);
60
+ res.on('end', () => {
61
+ try { resolve(JSON.parse(data)); }
62
+ catch { resolve({ raw: data }); }
63
+ });
64
+ });
65
+ req.on('error', reject);
66
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout after ' + timeoutMs + 'ms')); });
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ function httpPost(rpcUrl, pathStr, body, timeoutMs = 10000) {
72
+ return new Promise((resolve, reject) => {
73
+ const url = new URL(pathStr, rpcUrl);
74
+ const lib = url.protocol === 'https:' ? https : http;
75
+ const bodyStr = JSON.stringify(body);
76
+ const req = lib.request({
77
+ hostname: url.hostname,
78
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
79
+ path: url.pathname + url.search,
80
+ method: 'POST',
81
+ timeout: timeoutMs,
82
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
83
+ }, (res) => {
84
+ let data = '';
85
+ res.on('data', (chunk) => data += chunk);
86
+ res.on('end', () => {
87
+ try { resolve(JSON.parse(data)); }
88
+ catch { resolve({ raw: data }); }
89
+ });
90
+ });
91
+ req.on('error', reject);
92
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout after ' + timeoutMs + 'ms')); });
93
+ req.write(bodyStr);
94
+ req.end();
95
+ });
96
+ }
97
+
98
+ function formatAether(lamports) {
99
+ const aeth = Number(lamports) / 1e9;
100
+ if (aeth === 0) return '0 AETH';
101
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
102
+ }
103
+
104
+ function formatAethFull(lamports) {
105
+ return (Number(lamports) / 1e9).toFixed(6) + ' AETH';
106
+ }
107
+
108
+ function formatLargeNum(n) {
109
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Core supply fetchers
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * Fetch the total supply of AETH from the chain.
118
+ * Uses the supply inflation schedule / mint account.
119
+ */
120
+ async function fetchTotalSupply(rpc) {
121
+ try {
122
+ // Primary: dedicated supply endpoint
123
+ const res = await httpRequest(rpc, '/v1/supply');
124
+ if (res && !res.error && (res.total !== undefined || res.supply !== undefined)) {
125
+ return {
126
+ total: BigInt(res.total || res.supply?.total || 0),
127
+ circulating: BigInt(res.circulating || res.supply?.circulating || 0),
128
+ nonCirculating: BigInt(res.non_circulating || res.supply?.non_circulating || 0),
129
+ source: 'rpc_v1_supply',
130
+ };
131
+ }
132
+ } catch { /* fall through */ }
133
+
134
+ // Fallback: fetch epoch info which contains total token count
135
+ try {
136
+ const epochInfo = await httpRequest(rpc, '/v1/epoch-info');
137
+ if (epochInfo && !epochInfo.error) {
138
+ const totalStaked = BigInt(epochInfo.total_staked || 0);
139
+ const rewardsPerEpoch = BigInt(epochInfo.rewards_per_epoch || '2000000000');
140
+ const currentEpoch = BigInt(epochInfo.epoch || 0);
141
+ // Rough estimate: total supply ~= minted so far + remaining allocation
142
+ // Aether has ~500M AETH max supply, minted gradually over 100 years
143
+ const maxSupply = BigInt('500000000000000000'); // 500M * 1e9
144
+ const mintedPerEpoch = rewardsPerEpoch;
145
+ const minted = mintedPerEpoch * currentEpoch;
146
+ // Some tokens are locked/vesting; assume ~30% is non-circulating
147
+ const estimatedTotal = minted < maxSupply ? minted : maxSupply;
148
+ const estimatedCirculating = estimatedTotal - BigInt(BigInt(estimatedTotal) / BigInt(3));
149
+ return {
150
+ total: estimatedTotal,
151
+ circulating: estimatedCirculating,
152
+ nonCirculating: estimatedTotal - estimatedCirculating,
153
+ source: 'epoch_info_estimate',
154
+ };
155
+ }
156
+ } catch { /* fall through */ }
157
+
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Fetch staked supply by querying stake program accounts.
163
+ */
164
+ async function fetchStakedSupply(rpc) {
165
+ try {
166
+ // Try stake program accounts count / total
167
+ const res = await httpRequest(rpc, '/v1/stake/total');
168
+ if (res && !res.error && res.total_staked !== undefined) {
169
+ return BigInt(res.total_staked);
170
+ }
171
+ } catch { /* fall through */ }
172
+
173
+ try {
174
+ // Fallback: sum delegated stake across top validators
175
+ const validators = await httpRequest(rpc, '/v1/validators?limit=50');
176
+ if (validators && Array.isArray(validators)) {
177
+ let total = BigInt(0);
178
+ for (const v of validators) {
179
+ total += BigInt(v.delegated_stake || v.stake || 0);
180
+ }
181
+ return total;
182
+ }
183
+ } catch { /* fall through */ }
184
+
185
+ try {
186
+ // Last resort: epoch info staked amount
187
+ const epochInfo = await httpRequest(rpc, '/v1/epoch-info');
188
+ if (epochInfo && !epochInfo.error && epochInfo.total_staked) {
189
+ return BigInt(epochInfo.total_staked);
190
+ }
191
+ } catch { /* fall through */ }
192
+
193
+ return BigInt(0);
194
+ }
195
+
196
+ /**
197
+ * Estimate burned supply by querying accounts at known burn/mint addresses.
198
+ * Uses a set of common Aether burn addresses.
199
+ */
200
+ async function fetchBurnedSupply(rpc) {
201
+ const BURN_ADDRESSES = [
202
+ 'ATH1111111111111111111111111111111111111', // mint authority burn
203
+ 'ATH2222222222222222222222222222222222222', // zero authority
204
+ 'ATHburn000000000000000000000000000000', // burn address
205
+ ];
206
+
207
+ let totalBurned = BigInt(0);
208
+
209
+ for (const addr of BURN_ADDRESSES) {
210
+ try {
211
+ const rawAddr = addr.startsWith('ATH') ? addr.slice(3) : addr;
212
+ const account = await httpRequest(rpc, `/v1/account/${encodeURIComponent(rawAddr)}`);
213
+ if (account && !account.error && account.lamports !== undefined && Number(account.lamports) > 0) {
214
+ totalBurned += BigInt(account.lamports);
215
+ }
216
+ } catch { /* skip inaccessible addresses */ }
217
+ }
218
+
219
+ return totalBurned;
220
+ }
221
+
222
+ /**
223
+ * Fetch circulating supply = total - non-circulating (locked/vesting/burned).
224
+ * Non-circulating includes: burn address, escrow/staking vault, team vesting.
225
+ */
226
+ async function fetchNonCirculatingAccounts(rpc) {
227
+ try {
228
+ const res = await httpRequest(rpc, '/v1/supply/non-circulating');
229
+ if (res && !res.error && Array.isArray(res.accounts)) {
230
+ let total = BigInt(0);
231
+ for (const acct of res.accounts) {
232
+ total += BigInt(acct.lamports || 0);
233
+ }
234
+ return total;
235
+ }
236
+ } catch { /* fall through */ }
237
+
238
+ return BigInt(0);
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Render output
243
+ // ---------------------------------------------------------------------------
244
+
245
+ function renderSupplyTable(data) {
246
+ const { total, circulating, staked, burned, nonCirculating, rpc, source } = data;
247
+
248
+ const circPct = total > 0 ? ((Number(circulating) / Number(total)) * 100).toFixed(1) : '?';
249
+ const stakedPct = total > 0 ? ((Number(staked) / Number(total)) * 100).toFixed(1) : '?';
250
+ const burnedPct = total > 0 ? ((Number(burned) / Number(total)) * 100).toFixed(2) : '?';
251
+
252
+ console.log(`\n${C.bold}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
253
+ console.log(`${C.bold}${C.cyan}║ AETHER TOKEN SUPPLY ║${C.reset}`);
254
+ console.log(`${C.bold}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
255
+ console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
256
+ console.log(` ${C.dim}Source: ${source}${C.reset}\n`);
257
+
258
+ console.log(` ${C.bright}┌─ ${C.cyan}TOTAL SUPPLY${C.reset}`);
259
+ console.log(` │ ${C.bold}${formatAethFull(total)}${C.reset}`);
260
+ console.log(` │ ${C.dim}${formatLargeNum(Number(total))} lamports${C.reset}`);
261
+ console.log(` ${C.dim}└${C.reset}`);
262
+ console.log();
263
+
264
+ console.log(` ${C.bright}┌─ ${C.green}CIRCULATING SUPPLY${C.reset}`);
265
+ console.log(` │ ${C.green}${formatAethFull(circulating)}${C.reset}`);
266
+ console.log(` │ ${C.dim}${formatLargeNum(Number(circulating))} lamports${C.reset}`);
267
+ console.log(` │ ${C.green}${circPct}%${C.reset} of total supply`);
268
+ console.log(` ${C.dim}└${C.reset}`);
269
+ console.log();
270
+
271
+ console.log(` ${C.bright}┌─ ${C.yellow}STAKED SUPPLY${C.reset}`);
272
+ console.log(` │ ${C.yellow}${formatAethFull(staked)}${C.reset}`);
273
+ console.log(` │ ${C.dim}${formatLargeNum(Number(staked))} lamports${C.reset}`);
274
+ console.log(` │ ${C.yellow}${stakedPct}%${C.reset} of total supply`);
275
+ console.log(` ${C.dim}└${C.reset}`);
276
+ console.log();
277
+
278
+ if (burned > 0) {
279
+ console.log(` ${C.bright}┌─ ${C.red}BURNED / IRRECOVERABLE${C.reset}`);
280
+ console.log(` │ ${C.red}${formatAethFull(burned)}${C.reset}`);
281
+ console.log(` │ ${C.dim}${formatLargeNum(Number(burned))} lamports${C.reset}`);
282
+ console.log(` │ ${C.red}${burnedPct}%${C.reset} of total supply`);
283
+ console.log(` ${C.dim}└${C.reset}`);
284
+ console.log();
285
+ }
286
+
287
+ if (nonCirculating > 0) {
288
+ console.log(` ${C.bright}┌─ ${C.magenta}NON-CIRCULATING (LOCKED/ESCROW)${C.reset}`);
289
+ console.log(` │ ${C.magenta}${formatAethFull(nonCirculating)}${C.reset}`);
290
+ console.log(` │ ${C.dim}${formatLargeNum(Number(nonCirculating))} lamports${C.reset}`);
291
+ console.log(` ${C.dim}└${C.reset}`);
292
+ console.log();
293
+ }
294
+
295
+ // Visual bar
296
+ const barLen = 40;
297
+ const circBars = Math.round((Number(circulating) / Number(total)) * barLen);
298
+ const stakedBars = Math.round((Number(staked) / Number(total)) * barLen);
299
+ const burnedBars = Math.round((Number(burned) / Number(total)) * barLen);
300
+ const nonCircBars = Math.round((Number(nonCirculating) / Number(total)) * barLen);
301
+
302
+ console.log(` ${C.dim}Supply breakdown bar (per ${barLen} units):${C.reset}`);
303
+ const bar = [
304
+ C.green + '█'.repeat(Math.min(circBars, barLen)) + C.reset,
305
+ C.yellow + '█'.repeat(Math.min(stakedBars, Math.max(0, barLen - circBars))) + C.reset,
306
+ C.red + '█'.repeat(Math.min(burnedBars, Math.max(0, barLen - circBars - stakedBars))) + C.reset,
307
+ ].join('');
308
+ console.log(` ${bar}`);
309
+ console.log(` ${C.green}■ circulating${C.reset} ${C.yellow}■ staked${C.reset} ${C.red}■ burned${C.reset}`);
310
+ console.log();
311
+ }
312
+
313
+ /**
314
+ * Compute and display supply metrics.
315
+ */
316
+ async function showSupply(rpc, opts) {
317
+ const { asJson, verbose } = opts;
318
+
319
+ console.error(`${C.dim}Fetching supply data from ${rpc}...${C.reset}`);
320
+
321
+ // Fetch all supply components in parallel
322
+ const [totalData, staked, burned, nonCirc] = await Promise.all([
323
+ fetchTotalSupply(rpc),
324
+ fetchStakedSupply(rpc),
325
+ fetchBurnedSupply(rpc),
326
+ fetchNonCirculatingAccounts(rpc),
327
+ ]);
328
+
329
+ if (!totalData) {
330
+ const msg = `Failed to fetch supply data from ${rpc}. Ensure your node is running or set AETHER_RPC.`;
331
+ if (asJson) {
332
+ console.log(JSON.stringify({ error: msg, rpc }, null, 2));
333
+ } else {
334
+ console.log(`\n${C.red}✗ ${msg}${C.reset}\n`);
335
+ }
336
+ process.exit(1);
337
+ }
338
+
339
+ const { total, circulating, nonCirculating: ncFromSupply, source } = totalData;
340
+ // Use chain non-circulating if available, otherwise fall back to computed value
341
+ const nonCirculating = ncFromSupply > 0 ? ncFromSupply : nonCirc;
342
+
343
+ if (asJson) {
344
+ const out = {
345
+ rpc,
346
+ source,
347
+ supply: {
348
+ total: total.toString(),
349
+ total_formatted: formatAethFull(total),
350
+ circulating: circulating.toString(),
351
+ circulating_formatted: formatAethFull(circulating),
352
+ non_circulating: nonCirculating.toString(),
353
+ non_circulating_formatted: formatAethFull(nonCirculating),
354
+ staked: staked.toString(),
355
+ staked_formatted: formatAethFull(staked),
356
+ burned: burned.toString(),
357
+ burned_formatted: formatAethFull(burned),
358
+ percentages: {
359
+ circulating_pct: total > 0 ? ((Number(circulating) / Number(total)) * 100).toFixed(2) : '0',
360
+ staked_pct: total > 0 ? ((Number(staked) / Number(total)) * 100).toFixed(2) : '0',
361
+ burned_pct: total > 0 ? ((Number(burned) / Number(total)) * 100).toFixed(4) : '0',
362
+ },
363
+ },
364
+ fetched_at: new Date().toISOString(),
365
+ };
366
+ console.log(JSON.stringify(out, null, 2));
367
+ return;
368
+ }
369
+
370
+ renderSupplyTable({
371
+ total,
372
+ circulating,
373
+ staked,
374
+ burned,
375
+ nonCirculating,
376
+ rpc,
377
+ source,
378
+ });
379
+
380
+ if (verbose) {
381
+ console.log(` ${C.dim}Notes:${C.reset}`);
382
+ console.log(` ${C.dim} - Circulating = total - non-circulating (locked/escrow)${C.reset}`);
383
+ console.log(` ${C.dim} - Staked supply reflects tokens in active stake accounts${C.reset}`);
384
+ console.log(` ${C.dim} - Burned supply reflects tokens sent to irrecoverable addresses${C.reset}`);
385
+ console.log(` ${C.dim} - Percentages calculated against total supply${C.reset}`);
386
+ console.log(` ${C.dim} - Source: ${source}${C.reset}\n`);
387
+ }
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // CLI arg parsing
392
+ // ---------------------------------------------------------------------------
393
+
394
+ function parseArgs() {
395
+ return process.argv.slice(3); // [node, index.js, supply, ...]
396
+ }
397
+
398
+ async function main() {
399
+ const args = parseArgs();
400
+
401
+ let rpc = getDefaultRpc();
402
+ let asJson = false;
403
+ let verbose = false;
404
+
405
+ for (let i = 0; i < args.length; i++) {
406
+ if ((args[i] === '--rpc' || args[i] === '-r') && args[i + 1]) {
407
+ rpc = args[++i];
408
+ } else if (args[i] === '--json' || args[i] === '-j') {
409
+ asJson = true;
410
+ } else if (args[i] === '--verbose' || args[i] === '-v') {
411
+ verbose = true;
412
+ } else if (args[i] === '--help' || args[i] === '-h') {
413
+ console.log(`
414
+ ${C.cyan}Usage:${C.reset}
415
+ aether supply Show Aether token supply overview
416
+ aether supply --json JSON output for scripting/monitoring
417
+ aether supply --rpc <url> Query a specific RPC endpoint
418
+ aether supply --verbose Show detailed breakdown and notes
419
+
420
+ ${C.dim}Examples:${C.reset}
421
+ aether supply
422
+ aether supply --json --rpc https://mainnet.aether.io
423
+ AETHER_RPC=https://backup-rpc.example.com aether supply --verbose
424
+ `);
425
+ return;
426
+ }
427
+ }
428
+
429
+ await showSupply(rpc, { asJson, verbose });
430
+ }
431
+
432
+ main().catch(err => {
433
+ console.error(`${C.red}Error:${C.reset} ${err.message}\n`);
434
+ process.exit(1);
435
+ });
436
+
437
+ module.exports = { supplyCommand: main };