@jellylegsai/aether-cli 1.9.2 → 2.0.1

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.
@@ -1,391 +1,483 @@
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 path = require('path');
21
-
22
- // ANSI colours
23
- const C = {
24
- reset: '\x1b[0m',
25
- bright: '\x1b[1m',
26
- dim: '\x1b[2m',
27
- red: '\x1b[31m',
28
- green: '\x1b[32m',
29
- yellow: '\x1b[33m',
30
- cyan: '\x1b[36m',
31
- magenta: '\x1b[35m',
32
- bold: '\x1b[1m',
33
- };
34
-
35
- const CLI_VERSION = '1.0.0';
36
-
37
- // ---------------------------------------------------------------------------
38
- // SDK Import - Real blockchain RPC calls via @jellylegsai/aether-sdk
39
- // ---------------------------------------------------------------------------
40
-
41
- const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
42
- const aether = require(sdkPath);
43
-
44
- function getDefaultRpc() {
45
- return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
46
- }
47
-
48
- /** Create SDK client */
49
- function createClient(rpcUrl) {
50
- return new aether.AetherClient({ rpcUrl });
51
- }
52
-
53
- function formatAether(lamports) {
54
- const aeth = Number(lamports) / 1e9;
55
- if (aeth === 0) return '0 AETH';
56
- return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
57
- }
58
-
59
- function formatAethFull(lamports) {
60
- return (Number(lamports) / 1e9).toFixed(6) + ' AETH';
61
- }
62
-
63
- function formatLargeNum(n) {
64
- return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Core supply fetchers using SDK
69
- // ---------------------------------------------------------------------------
70
-
71
- /**
72
- * Fetch the total supply of AETH from the chain using SDK.
73
- * Makes real RPC call: GET /v1/supply
74
- */
75
- async function fetchTotalSupply(rpc) {
76
- const client = createClient(rpc);
77
- try {
78
- // Primary: SDK getSupply() GET /v1/supply
79
- const res = await client.getSupply();
80
- if (res && (res.total !== undefined || res.supply !== undefined)) {
81
- return {
82
- total: BigInt(res.total || res.supply?.total || 0),
83
- circulating: BigInt(res.circulating || res.supply?.circulating || 0),
84
- nonCirculating: BigInt(res.non_circulating || res.nonCirculating || res.supply?.non_circulating || 0),
85
- source: 'rpc_v1_supply',
86
- };
87
- }
88
- } catch { /* fall through */ }
89
-
90
- // Fallback: fetch epoch info which contains total token count
91
- try {
92
- const epochInfo = await client.getEpochInfo();
93
- if (epochInfo) {
94
- const totalStaked = BigInt(epochInfo.total_staked || 0);
95
- const rewardsPerEpoch = BigInt(epochInfo.rewards_per_epoch || '2000000000');
96
- const currentEpoch = BigInt(epochInfo.epoch || 0);
97
- // Rough estimate: total supply ~= minted so far + remaining allocation
98
- // Aether has ~500M AETH max supply, minted gradually over 100 years
99
- const maxSupply = BigInt('500000000000000000'); // 500M * 1e9
100
- const mintedPerEpoch = rewardsPerEpoch;
101
- const minted = mintedPerEpoch * currentEpoch;
102
- // Some tokens are locked/vesting; assume ~30% is non-circulating
103
- const estimatedTotal = minted < maxSupply ? minted : maxSupply;
104
- const estimatedCirculating = estimatedTotal - BigInt(BigInt(estimatedTotal) / BigInt(3));
105
- return {
106
- total: estimatedTotal,
107
- circulating: estimatedCirculating,
108
- nonCirculating: estimatedTotal - estimatedCirculating,
109
- source: 'epoch_info_estimate',
110
- };
111
- }
112
- } catch { /* fall through */ }
113
-
114
- return null;
115
- }
116
-
117
- /**
118
- * Fetch staked supply by querying stake program accounts using SDK.
119
- * Makes real RPC call: GET /v1/validators
120
- */
121
- async function fetchStakedSupply(rpc) {
122
- const client = createClient(rpc);
123
- try {
124
- // SDK getValidators() → GET /v1/validators
125
- const validators = await client.getValidators();
126
- if (validators && Array.isArray(validators)) {
127
- let total = BigInt(0);
128
- for (const v of validators) {
129
- total += BigInt(v.delegated_stake || v.stake || v.delegatedStake || 0);
130
- }
131
- return total;
132
- }
133
- } catch { /* fall through */ }
134
-
135
- try {
136
- // Last resort: epoch info staked amount via SDK
137
- const epochInfo = await client.getEpochInfo();
138
- if (epochInfo && epochInfo.total_staked) {
139
- return BigInt(epochInfo.total_staked);
140
- }
141
- } catch { /* fall through */ }
142
-
143
- return BigInt(0);
144
- }
145
-
146
- /**
147
- * Estimate burned supply by querying accounts at known burn/mint addresses using SDK.
148
- * Makes real RPC calls: GET /v1/account/<address>
149
- */
150
- async function fetchBurnedSupply(rpc) {
151
- const client = createClient(rpc);
152
- const BURN_ADDRESSES = [
153
- 'ATH1111111111111111111111111111111111111', // mint authority burn
154
- 'ATH2222222222222222222222222222222222222', // zero authority
155
- 'ATHburn000000000000000000000000000000', // burn address
156
- ];
157
-
158
- let totalBurned = BigInt(0);
159
-
160
- for (const addr of BURN_ADDRESSES) {
161
- try {
162
- const rawAddr = addr.startsWith('ATH') ? addr.slice(3) : addr;
163
- // SDK getAccountInfo() GET /v1/account/<address>
164
- const account = await client.getAccountInfo(rawAddr);
165
- if (account && account.lamports !== undefined && Number(account.lamports) > 0) {
166
- totalBurned += BigInt(account.lamports);
167
- }
168
- } catch { /* skip inaccessible addresses */ }
169
- }
170
-
171
- return totalBurned;
172
- }
173
-
174
- /**
175
- * Fetch circulating supply = total - non-circulating (locked/vesting/burned).
176
- * Non-circulating includes: burn address, escrow/staking vault, team vesting.
177
- * Uses SDK client for RPC calls.
178
- */
179
- async function fetchNonCirculatingAccounts(rpc) {
180
- const client = createClient(rpc);
181
- try {
182
- const res = await client._httpGet('/v1/supply/non-circulating');
183
- if (res && !res.error && Array.isArray(res.accounts)) {
184
- let total = BigInt(0);
185
- for (const acct of res.accounts) {
186
- total += BigInt(acct.lamports || 0);
187
- }
188
- return total;
189
- }
190
- } catch { /* fall through */ }
191
-
192
- return BigInt(0);
193
- }
194
-
195
- // ---------------------------------------------------------------------------
196
- // Render output
197
- // ---------------------------------------------------------------------------
198
-
199
- function renderSupplyTable(data) {
200
- const { total, circulating, staked, burned, nonCirculating, rpc, source } = data;
201
-
202
- const circPct = total > 0 ? ((Number(circulating) / Number(total)) * 100).toFixed(1) : '?';
203
- const stakedPct = total > 0 ? ((Number(staked) / Number(total)) * 100).toFixed(1) : '?';
204
- const burnedPct = total > 0 ? ((Number(burned) / Number(total)) * 100).toFixed(2) : '?';
205
-
206
- console.log(`\n${C.bold}${C.cyan}╔═══════════════════════════════════════════════════════╗${C.reset}`);
207
- console.log(`${C.bold}${C.cyan}║ AETHER TOKEN SUPPLY ║${C.reset}`);
208
- console.log(`${C.bold}${C.cyan}╚═══════════════════════════════════════════════════════╝${C.reset}\n`);
209
- console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
210
- console.log(` ${C.dim}Source: ${source}${C.reset}\n`);
211
-
212
- console.log(` ${C.bright}┌─ ${C.cyan}TOTAL SUPPLY${C.reset}`);
213
- console.log(` │ ${C.bold}${formatAethFull(total)}${C.reset}`);
214
- console.log(` │ ${C.dim}${formatLargeNum(Number(total))} lamports${C.reset}`);
215
- console.log(` ${C.dim}└${C.reset}`);
216
- console.log();
217
-
218
- console.log(` ${C.bright}┌─ ${C.green}CIRCULATING SUPPLY${C.reset}`);
219
- console.log(` │ ${C.green}${formatAethFull(circulating)}${C.reset}`);
220
- console.log(` │ ${C.dim}${formatLargeNum(Number(circulating))} lamports${C.reset}`);
221
- console.log(` │ ${C.green}${circPct}%${C.reset} of total supply`);
222
- console.log(` ${C.dim}└${C.reset}`);
223
- console.log();
224
-
225
- console.log(` ${C.bright}┌─ ${C.yellow}STAKED SUPPLY${C.reset}`);
226
- console.log(` │ ${C.yellow}${formatAethFull(staked)}${C.reset}`);
227
- console.log(` │ ${C.dim}${formatLargeNum(Number(staked))} lamports${C.reset}`);
228
- console.log(` │ ${C.yellow}${stakedPct}%${C.reset} of total supply`);
229
- console.log(` ${C.dim}└${C.reset}`);
230
- console.log();
231
-
232
- if (burned > 0) {
233
- console.log(` ${C.bright}┌─ ${C.red}BURNED / IRRECOVERABLE${C.reset}`);
234
- console.log(` │ ${C.red}${formatAethFull(burned)}${C.reset}`);
235
- console.log(` │ ${C.dim}${formatLargeNum(Number(burned))} lamports${C.reset}`);
236
- console.log(` │ ${C.red}${burnedPct}%${C.reset} of total supply`);
237
- console.log(` ${C.dim}└${C.reset}`);
238
- console.log();
239
- }
240
-
241
- if (nonCirculating > 0) {
242
- console.log(` ${C.bright}┌─ ${C.magenta}NON-CIRCULATING (LOCKED/ESCROW)${C.reset}`);
243
- console.log(` │ ${C.magenta}${formatAethFull(nonCirculating)}${C.reset}`);
244
- console.log(` │ ${C.dim}${formatLargeNum(Number(nonCirculating))} lamports${C.reset}`);
245
- console.log(` ${C.dim}└${C.reset}`);
246
- console.log();
247
- }
248
-
249
- // Visual bar
250
- const barLen = 40;
251
- const circBars = Math.round((Number(circulating) / Number(total)) * barLen);
252
- const stakedBars = Math.round((Number(staked) / Number(total)) * barLen);
253
- const burnedBars = Math.round((Number(burned) / Number(total)) * barLen);
254
- const nonCircBars = Math.round((Number(nonCirculating) / Number(total)) * barLen);
255
-
256
- console.log(` ${C.dim}Supply breakdown bar (per ${barLen} units):${C.reset}`);
257
- const bar = [
258
- C.green + '█'.repeat(Math.min(circBars, barLen)) + C.reset,
259
- C.yellow + '█'.repeat(Math.min(stakedBars, Math.max(0, barLen - circBars))) + C.reset,
260
- C.red + '█'.repeat(Math.min(burnedBars, Math.max(0, barLen - circBars - stakedBars))) + C.reset,
261
- ].join('');
262
- console.log(` ${bar}`);
263
- console.log(` ${C.green}■ circulating${C.reset} ${C.yellow}■ staked${C.reset} ${C.red}■ burned${C.reset}`);
264
- console.log();
265
- }
266
-
267
- /**
268
- * Compute and display supply metrics.
269
- */
270
- async function showSupply(rpc, opts) {
271
- const { asJson, verbose } = opts;
272
-
273
- console.error(`${C.dim}Fetching supply data from ${rpc}...${C.reset}`);
274
-
275
- // Fetch all supply components in parallel
276
- const [totalData, staked, burned, nonCirc] = await Promise.all([
277
- fetchTotalSupply(rpc),
278
- fetchStakedSupply(rpc),
279
- fetchBurnedSupply(rpc),
280
- fetchNonCirculatingAccounts(rpc),
281
- ]);
282
-
283
- if (!totalData) {
284
- const msg = `Failed to fetch supply data from ${rpc}. Ensure your node is running or set AETHER_RPC.`;
285
- if (asJson) {
286
- console.log(JSON.stringify({ error: msg, rpc }, null, 2));
287
- } else {
288
- console.log(`\n${C.red}✗ ${msg}${C.reset}\n`);
289
- }
290
- process.exit(1);
291
- }
292
-
293
- const { total, circulating, nonCirculating: ncFromSupply, source } = totalData;
294
- // Use chain non-circulating if available, otherwise fall back to computed value
295
- const nonCirculating = ncFromSupply > 0 ? ncFromSupply : nonCirc;
296
-
297
- if (asJson) {
298
- const out = {
299
- rpc,
300
- source,
301
- supply: {
302
- total: total.toString(),
303
- total_formatted: formatAethFull(total),
304
- circulating: circulating.toString(),
305
- circulating_formatted: formatAethFull(circulating),
306
- non_circulating: nonCirculating.toString(),
307
- non_circulating_formatted: formatAethFull(nonCirculating),
308
- staked: staked.toString(),
309
- staked_formatted: formatAethFull(staked),
310
- burned: burned.toString(),
311
- burned_formatted: formatAethFull(burned),
312
- percentages: {
313
- circulating_pct: total > 0 ? ((Number(circulating) / Number(total)) * 100).toFixed(2) : '0',
314
- staked_pct: total > 0 ? ((Number(staked) / Number(total)) * 100).toFixed(2) : '0',
315
- burned_pct: total > 0 ? ((Number(burned) / Number(total)) * 100).toFixed(4) : '0',
316
- },
317
- },
318
- fetched_at: new Date().toISOString(),
319
- };
320
- console.log(JSON.stringify(out, null, 2));
321
- return;
322
- }
323
-
324
- renderSupplyTable({
325
- total,
326
- circulating,
327
- staked,
328
- burned,
329
- nonCirculating,
330
- rpc,
331
- source,
332
- });
333
-
334
- if (verbose) {
335
- console.log(` ${C.dim}Notes:${C.reset}`);
336
- console.log(` ${C.dim} - Circulating = total - non-circulating (locked/escrow)${C.reset}`);
337
- console.log(` ${C.dim} - Staked supply reflects tokens in active stake accounts${C.reset}`);
338
- console.log(` ${C.dim} - Burned supply reflects tokens sent to irrecoverable addresses${C.reset}`);
339
- console.log(` ${C.dim} - Percentages calculated against total supply${C.reset}`);
340
- console.log(` ${C.dim} - Source: ${source}${C.reset}\n`);
341
- }
342
- }
343
-
344
- // ---------------------------------------------------------------------------
345
- // CLI arg parsing
346
- // ---------------------------------------------------------------------------
347
-
348
- function parseArgs() {
349
- return process.argv.slice(3); // [node, index.js, supply, ...]
350
- }
351
-
352
- async function main() {
353
- const args = parseArgs();
354
-
355
- let rpc = getDefaultRpc();
356
- let asJson = false;
357
- let verbose = false;
358
-
359
- for (let i = 0; i < args.length; i++) {
360
- if ((args[i] === '--rpc' || args[i] === '-r') && args[i + 1]) {
361
- rpc = args[++i];
362
- } else if (args[i] === '--json' || args[i] === '-j') {
363
- asJson = true;
364
- } else if (args[i] === '--verbose' || args[i] === '-v') {
365
- verbose = true;
366
- } else if (args[i] === '--help' || args[i] === '-h') {
367
- console.log(`
368
- ${C.cyan}Usage:${C.reset}
369
- aether supply Show Aether token supply overview
370
- aether supply --json JSON output for scripting/monitoring
371
- aether supply --rpc <url> Query a specific RPC endpoint
372
- aether supply --verbose Show detailed breakdown and notes
373
-
374
- ${C.dim}Examples:${C.reset}
375
- aether supply
376
- aether supply --json --rpc https://mainnet.aether.io
377
- AETHER_RPC=https://backup-rpc.example.com aether supply --verbose
378
- `);
379
- return;
380
- }
381
- }
382
-
383
- await showSupply(rpc, { asJson, verbose });
384
- }
385
-
386
- main().catch(err => {
387
- console.error(`${C.red}Error:${C.reset} ${err.message}\n`);
388
- process.exit(1);
389
- });
390
-
391
- module.exports = { supplyCommand: main };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli supply
4
+ *
5
+ * Query Aether token supply information from the blockchain.
6
+ * Shows total supply, circulating supply, non-circulating supply,
7
+ * and supply breakdown with visual indicators.
8
+ *
9
+ * Usage:
10
+ * aether supply Show detailed supply info
11
+ * aether supply --json JSON output for scripting
12
+ * aether supply --rpc <url> Query specific RPC endpoint
13
+ * aether supply --watch Watch mode - updates every 5 seconds
14
+ * aether supply --compare Compare with theoretical max
15
+ *
16
+ * SDK wired to: GET /v1/supply
17
+ * SDK Function: sdk.getSupply()
18
+ */
19
+
20
+ const path = require('path');
21
+
22
+ // Import SDK for real blockchain RPC calls
23
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
24
+ const aether = require(sdkPath);
25
+
26
+ // Import UI framework
27
+ const {
28
+ C,
29
+ BRANDING,
30
+ indicators,
31
+ success,
32
+ error,
33
+ warning,
34
+ info,
35
+ highlight,
36
+ dim,
37
+ startSpinner,
38
+ stopSpinner,
39
+ drawBox,
40
+ drawTable,
41
+ progressBarColored,
42
+ } = require('../lib/ui');
43
+
44
+ const CLI_VERSION = '1.0.0';
45
+ const WATCH_INTERVAL_MS = 5000;
46
+
47
+ // Supply constants
48
+ const MAX_SUPPLY_AETH = 1_000_000_000; // 1 billion AETH theoretical max
49
+
50
+ // ============================================================================
51
+ // SDK 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
+ // ============================================================================
63
+ // ASCII Art & Branding
64
+ // ============================================================================
65
+
66
+ const SUPPLY_LOGO = `
67
+ ${C.cyan} ╔══════════════════════════════════════════════════════════╗${C.reset}
68
+ ${C.cyan} ║${C.reset} ${C.bright}${C.yellow}◆${C.reset} ${C.bright}AETHER TOKEN SUPPLY${C.reset}${' '.repeat(30)}${C.dim}v${CLI_VERSION}${C.reset} ${C.cyan}║${C.reset}
69
+ ${C.cyan} ║${C.reset} ${C.dim}On-chain supply metrics and tokenomics${C.reset}${' '.repeat(20)}${C.cyan}║${C.reset}
70
+ ${C.cyan} ╚══════════════════════════════════════════════════════════╝${C.reset}`;
71
+
72
+ // ============================================================================
73
+ // Format Helpers
74
+ // ============================================================================
75
+
76
+ function formatAether(lamports) {
77
+ if (!lamports && lamports !== 0) return 'N/A';
78
+ const aeth = Number(lamports) / 1e9;
79
+ if (aeth === 0) return '0 AETH';
80
+ if (aeth >= 1_000_000) {
81
+ return aeth.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' AETH';
82
+ }
83
+ return aeth.toFixed(6).replace(/\.?0+$/, '') + ' AETH';
84
+ }
85
+
86
+ function formatNumber(num) {
87
+ if (!num && num !== 0) return 'N/A';
88
+ return Number(num).toLocaleString();
89
+ }
90
+
91
+ function formatPercentage(numerator, denominator) {
92
+ if (!denominator || denominator === 0) return 'N/A';
93
+ const pct = (Number(numerator) / Number(denominator)) * 100;
94
+ return pct.toFixed(2) + '%';
95
+ }
96
+
97
+ function formatCompact(n) {
98
+ if (!n) return 'N/A';
99
+ const num = Number(n);
100
+ if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
101
+ if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
102
+ if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
103
+ return num.toString();
104
+ }
105
+
106
+ // ============================================================================
107
+ // Argument Parsing
108
+ // ============================================================================
109
+
110
+ function parseArgs() {
111
+ const args = process.argv.slice(2);
112
+ return {
113
+ rpc: args.includes('--rpc') ? args[args.indexOf('--rpc') + 1] : getDefaultRpc(),
114
+ asJson: args.includes('--json') || args.includes('-j'),
115
+ watch: args.includes('--watch') || args.includes('-w'),
116
+ compare: args.includes('--compare') || args.includes('-c'),
117
+ help: args.includes('--help') || args.includes('-h'),
118
+ };
119
+ }
120
+
121
+ function showHelp() {
122
+ console.log(`
123
+ ${C.bright}${C.cyan}aether-cli supply${C.reset} — Token Supply Information
124
+
125
+ ${C.bright}USAGE${C.reset}
126
+ aether supply [options]
127
+
128
+ ${C.bright}OPTIONS${C.reset}
129
+ -r, --rpc <url> RPC endpoint (default: ${getDefaultRpc()})
130
+ -j, --json Output as JSON
131
+ -w, --watch Watch mode - updates every 5 seconds
132
+ -c, --compare Show comparison with theoretical max supply
133
+ -h, --help Show this help
134
+
135
+ ${C.bright}SDK METHODS USED${C.reset}
136
+ ${C.dim}client.getSupply() → GET /v1/supply${C.reset}
137
+ ${C.dim}client.getEpochInfo() → GET /v1/epoch${C.reset}
138
+
139
+ ${C.bright}EXAMPLES${C.reset}
140
+ aether supply # Detailed supply view
141
+ aether supply --json # JSON for scripting
142
+ aether supply --watch # Live updates
143
+ aether supply --compare # Compare with max supply
144
+ aether supply --rpc https://api.aether.io
145
+
146
+ ${C.bright}OUTPUT FIELDS${C.reset}
147
+ Total Supply — Total minted AETH (lamports)
148
+ Circulating — AETH in active circulation
149
+ Non-Circulating — Locked, staked, or reserved AETH
150
+ Staked — AETH delegated to validators
151
+ Inflation Rate — Current epoch inflation
152
+ `);
153
+ }
154
+
155
+ // ============================================================================
156
+ // SDK Data Fetching (REAL RPC CALLS)
157
+ // ============================================================================
158
+
159
+ async function fetchSupplyData(rpcUrl) {
160
+ const client = createClient(rpcUrl);
161
+
162
+ try {
163
+ // Parallel SDK calls for supply and epoch info
164
+ const [supplyResult, epochInfo] = await Promise.all([
165
+ client.getSupply().catch(() => null),
166
+ client.getEpochInfo().catch(() => null),
167
+ ]);
168
+
169
+ if (!supplyResult) {
170
+ throw new Error('No supply data returned from RPC');
171
+ }
172
+
173
+ // Normalize supply data from various RPC response formats
174
+ const total = supplyResult.total || supplyResult.total_supply || supplyResult.totalSupply || 0;
175
+ const circulating = supplyResult.circulating || supplyResult.circulating_supply || supplyResult.circulatingSupply || 0;
176
+ const nonCirculating = supplyResult.nonCirculating || supplyResult.non_circulating || supplyResult.nonCirculatingSupply || (total - circulating);
177
+ const staked = supplyResult.staked || supplyResult.total_staked || supplyResult.delegated || 0;
178
+ const rewards = supplyResult.rewards || supplyResult.validator_rewards || 0;
179
+
180
+ return {
181
+ total: BigInt(total),
182
+ circulating: BigInt(circulating),
183
+ nonCirculating: BigInt(nonCirculating),
184
+ staked: BigInt(staked),
185
+ rewards: BigInt(rewards),
186
+ epoch: epochInfo?.epoch || 0,
187
+ slot: epochInfo?.absoluteSlot || 0,
188
+ fetchedAt: new Date().toISOString(),
189
+ };
190
+ } catch (err) {
191
+ throw new Error(`Failed to fetch supply: ${err.message}`);
192
+ }
193
+ }
194
+
195
+ // ============================================================================
196
+ // Visual Rendering
197
+ // ============================================================================
198
+
199
+ function renderSupplyBox(data, opts = {}) {
200
+ const { total, circulating, nonCirculating, staked, epoch, slot } = data;
201
+ const { compare } = opts;
202
+
203
+ const totalAeth = Number(total) / 1e9;
204
+ const circulatingAeth = Number(circulating) / 1e9;
205
+ const nonCirculatingAeth = Number(nonCirculating) / 1e9;
206
+ const stakedAeth = Number(staked) / 1e9;
207
+
208
+ // Calculate percentages
209
+ const circulatingPct = total > 0 ? (circulatingAeth / totalAeth) * 100 : 0;
210
+ const nonCircPct = total > 0 ? (nonCirculatingAeth / totalAeth) * 100 : 0;
211
+ const stakedPct = total > 0 ? (stakedAeth / totalAeth) * 100 : 0;
212
+
213
+ // Circulation ratio
214
+ const circulationRatio = circulating > 0
215
+ ? ((Number(circulating) / Number(total)) * 100).toFixed(2)
216
+ : '0.00';
217
+
218
+ console.log(SUPPLY_LOGO);
219
+ console.log();
220
+
221
+ // Main supply box
222
+ const supplyContent = `
223
+ ${C.bright}Total Supply${C.reset}
224
+ ${C.cyan}${formatAether(total)}${C.reset} ${C.dim}(${formatNumber(total)} lamports)${C.reset}
225
+
226
+ ${C.bright}Circulating${C.reset} ${C.green}${formatAether(circulating)}${C.reset}
227
+ ${C.bright}Non-Circulating${C.reset} ${C.yellow}${formatAether(nonCirculating)}${C.reset}
228
+ ${C.bright}Staked${C.reset} ${C.magenta}${formatAether(staked)}${C.reset}
229
+
230
+ ${C.dim}Circulation Ratio: ${C.bright}${circulationRatio}%${C.reset}
231
+ ${C.dim}Current Epoch: ${C.bright}${epoch}${C.reset}
232
+ ${C.dim}Current Slot: ${C.bright}${formatNumber(slot)}${C.reset}
233
+ `.trim();
234
+
235
+ console.log(drawBox(supplyContent, {
236
+ style: 'double',
237
+ title: 'SUPPLY OVERVIEW',
238
+ titleColor: C.cyan + C.bright,
239
+ borderColor: C.cyan,
240
+ width: 60,
241
+ }));
242
+
243
+ console.log();
244
+
245
+ // Distribution bars
246
+ console.log(` ${C.bright}Supply Distribution:${C.reset}\n`);
247
+
248
+ const barWidth = 40;
249
+
250
+ // Circulating bar
251
+ const circFill = Math.round((circulatingPct / 100) * barWidth);
252
+ const circBar = `${C.green}${'█'.repeat(circFill)}${C.dim}${'░'.repeat(barWidth - circFill)}${C.reset}`;
253
+ console.log(` ${C.green}●${C.reset} Circulating ${circBar} ${C.green}${circulatingPct.toFixed(1)}%${C.reset}`);
254
+
255
+ // Non-circulating bar
256
+ const ncFill = Math.round((nonCircPct / 100) * barWidth);
257
+ const ncBar = `${C.yellow}${'█'.repeat(ncFill)}${C.dim}${'░'.repeat(barWidth - ncFill)}${C.reset}`;
258
+ console.log(` ${C.yellow}●${C.reset} Non-Circulating ${ncBar} ${C.yellow}${nonCircPct.toFixed(1)}%${C.reset}`);
259
+
260
+ // Staked bar (subset of circulating or total)
261
+ const stakedOfTotal = stakedAeth / totalAeth * 100;
262
+ const stakedFill = Math.round((stakedOfTotal / 100) * barWidth);
263
+ const stakedBar = `${C.magenta}${'█'.repeat(stakedFill)}${C.dim}${'░'.repeat(barWidth - stakedFill)}${C.reset}`;
264
+ console.log(` ${C.magenta}●${C.reset} Staked ${stakedBar} ${C.magenta}${stakedOfTotal.toFixed(1)}%${C.reset}`);
265
+
266
+ console.log();
267
+
268
+ // Comparison with max supply if requested
269
+ if (compare) {
270
+ const pctOfMax = (totalAeth / MAX_SUPPLY_AETH) * 100;
271
+ const remaining = MAX_SUPPLY_AETH - totalAeth;
272
+
273
+ console.log(` ${C.bright}Comparison with Theoretical Max:${C.reset}\n`);
274
+ console.log(` ${C.dim}Max Supply:${C.reset} ${C.bright}${formatCompact(MAX_SUPPLY_AETH * 1e9)}${C.reset}`);
275
+ console.log(` ${C.dim}Current:${C.reset} ${C.cyan}${formatCompact(Number(total))}${C.reset}`);
276
+ console.log(` ${C.dim}Remaining:${C.reset} ${C.green}${formatCompact(remaining * 1e9)}${C.reset}`);
277
+ console.log(` ${C.dim}% of Max:${C.reset} ${C.bright}${pctOfMax.toFixed(4)}%${C.reset}`);
278
+
279
+ const maxFill = Math.round((pctOfMax / 100) * barWidth);
280
+ const maxBar = `${C.cyan}${'█'.repeat(maxFill)}${C.dim}${'░'.repeat(barWidth - maxFill)}${C.reset}`;
281
+ console.log(`\n ${C.dim}Supply Cap:${C.reset} ${maxBar} ${C.cyan}${pctOfMax.toFixed(2)}%${C.reset}`);
282
+ console.log();
283
+ }
284
+
285
+ // Tokenomics stats table
286
+ const statsRows = [
287
+ ['Metric', 'Value', 'Percentage'],
288
+ ['─'.repeat(20), '─'.repeat(25), '─'.repeat(12)],
289
+ ['Total Supply', formatAether(total), '100%'],
290
+ ['Circulating', formatAether(circulating), `${circulatingPct.toFixed(2)}%`],
291
+ ['Non-Circulating', formatAether(nonCirculating), `${nonCircPct.toFixed(2)}%`],
292
+ ['Staked', formatAether(staked), `${stakedPct.toFixed(2)}%`],
293
+ ];
294
+
295
+ console.log(` ${C.bright}Tokenomics Breakdown:${C.reset}\n`);
296
+ console.log(drawTable(['', '', ''], [
297
+ [`${C.cyan}Total Supply${C.reset}`, C.bright + formatAether(total) + C.reset, '100%'],
298
+ [`${C.green}Circulating${C.reset}`, formatAether(circulating), `${circulatingPct.toFixed(2)}%`],
299
+ [`${C.yellow}Non-Circulating${C.reset}`, formatAether(nonCirculating), `${nonCircPct.toFixed(2)}%`],
300
+ [`${C.magenta}Staked${C.reset}`, formatAether(staked), `${stakedPct.toFixed(2)}%`],
301
+ ], {
302
+ borderStyle: 'single',
303
+ headerColor: C.bright,
304
+ }));
305
+
306
+ console.log();
307
+
308
+ // Network health indicator
309
+ const healthStatus = stakedPct > 50
310
+ ? `${C.green}✓ Healthy${C.reset} - High stake ratio indicates network security`
311
+ : stakedPct > 30
312
+ ? `${C.yellow}⚠ Moderate${C.reset} - Adequate stake ratio`
313
+ : `${C.red}✗ Low${C.reset} - Low stake ratio may indicate risk`;
314
+
315
+ console.log(` ${C.bright}Network Health:${C.reset} ${healthStatus}`);
316
+ console.log();
317
+
318
+ // Footer
319
+ console.log(` ${C.dim}Data fetched: ${data.fetchedAt}${C.reset}`);
320
+ console.log(` ${C.dim}RPC: ${opts.rpc}${C.reset}`);
321
+ console.log(` ${C.dim}SDK: @jellylegsai/aether-sdk${C.reset}`);
322
+ console.log();
323
+ }
324
+
325
+ function renderJson(data, rpc) {
326
+ const output = {
327
+ total: {
328
+ lamports: data.total.toString(),
329
+ aeth: (Number(data.total) / 1e9).toFixed(9),
330
+ formatted: formatAether(data.total),
331
+ },
332
+ circulating: {
333
+ lamports: data.circulating.toString(),
334
+ aeth: (Number(data.circulating) / 1e9).toFixed(9),
335
+ formatted: formatAether(data.circulating),
336
+ percentage: ((Number(data.circulating) / Number(data.total)) * 100).toFixed(2),
337
+ },
338
+ nonCirculating: {
339
+ lamports: data.nonCirculating.toString(),
340
+ aeth: (Number(data.nonCirculating) / 1e9).toFixed(9),
341
+ formatted: formatAether(data.nonCirculating),
342
+ percentage: ((Number(data.nonCirculating) / Number(data.total)) * 100).toFixed(2),
343
+ },
344
+ staked: {
345
+ lamports: data.staked.toString(),
346
+ aeth: (Number(data.staked) / 1e9).toFixed(9),
347
+ formatted: formatAether(data.staked),
348
+ percentage: ((Number(data.staked) / Number(data.total)) * 100).toFixed(2),
349
+ },
350
+ epoch: data.epoch,
351
+ slot: data.slot,
352
+ rpc,
353
+ fetched_at: data.fetchedAt,
354
+ cli_version: CLI_VERSION,
355
+ sdk: '@jellylegsai/aether-sdk',
356
+ };
357
+ console.log(JSON.stringify(output, null, 2));
358
+ }
359
+
360
+ // ============================================================================
361
+ // Watch Mode
362
+ // ============================================================================
363
+
364
+ async function watchMode(rpc, compare) {
365
+ const clearScreen = () => {
366
+ process.stdout.write('\x1Bc');
367
+ };
368
+
369
+ let iteration = 0;
370
+ const spinnerFrames = ['◐', '◓', '◑', '◒'];
371
+
372
+ while (true) {
373
+ try {
374
+ clearScreen();
375
+ const data = await fetchSupplyData(rpc);
376
+
377
+ console.log(SUPPLY_LOGO);
378
+ console.log();
379
+ console.log(` ${C.dim}Watch mode enabled | Update ${iteration + 1} | Press Ctrl+C to exit${C.reset}`);
380
+ console.log();
381
+
382
+ // Simple inline display for watch mode
383
+ const totalAeth = Number(data.total) / 1e9;
384
+ const circAeth = Number(data.circulating) / 1e9;
385
+ const ncAeth = Number(data.nonCirculating) / 1e9;
386
+ const stakedAeth = Number(data.staked) / 1e9;
387
+
388
+ console.log(` ${C.bright}Total:${C.reset} ${C.cyan}${formatAether(data.total)}${C.reset}`);
389
+ console.log(` ${C.bright}Circulating:${C.reset} ${C.green}${formatAether(data.circulating)}${C.reset} (${((circAeth/totalAeth)*100).toFixed(2)}%)`);
390
+ console.log(` ${C.bright}Non-Circ:${C.reset} ${C.yellow}${formatAether(data.nonCirculating)}${C.reset} (${((ncAeth/totalAeth)*100).toFixed(2)}%)`);
391
+ console.log(` ${C.bright}Staked:${C.reset} ${C.magenta}${formatAether(data.staked)}${C.reset} (${((stakedAeth/totalAeth)*100).toFixed(2)}%)`);
392
+ console.log(` ${C.bright}Epoch:${C.reset} ${C.bright}${data.epoch}${C.reset}`);
393
+ console.log(` ${C.dim}Last update: ${data.fetchedAt}${C.reset}`);
394
+
395
+ if (compare) {
396
+ const pctOfMax = (totalAeth / MAX_SUPPLY_AETH) * 100;
397
+ console.log();
398
+ console.log(` ${C.dim}% of Max Supply: ${C.bright}${pctOfMax.toFixed(4)}%${C.reset}`);
399
+ }
400
+
401
+ console.log();
402
+ console.log(` ${C.dim}${spinnerFrames[iteration % 4]} Waiting ${WATCH_INTERVAL_MS/1000}s for next update...${C.reset}`);
403
+
404
+ iteration++;
405
+ await new Promise(r => setTimeout(r, WATCH_INTERVAL_MS));
406
+ } catch (err) {
407
+ console.log(`\n ${error('Watch mode error:')} ${err.message}`);
408
+ console.log(` ${dim('Retrying in 5s...')}`);
409
+ await new Promise(r => setTimeout(r, WATCH_INTERVAL_MS));
410
+ }
411
+ }
412
+ }
413
+
414
+ // ============================================================================
415
+ // Main Command
416
+ // ============================================================================
417
+
418
+ async function supplyCommand() {
419
+ const opts = parseArgs();
420
+
421
+ if (opts.help) {
422
+ showHelp();
423
+ return;
424
+ }
425
+
426
+ // Handle watch mode
427
+ if (opts.watch) {
428
+ console.log(`${info('Starting watch mode... Press Ctrl+C to exit')}`);
429
+ await watchMode(opts.rpc, opts.compare);
430
+ return;
431
+ }
432
+
433
+ if (!opts.asJson) {
434
+ startSpinner('Fetching supply data via SDK');
435
+ }
436
+
437
+ try {
438
+ const data = await fetchSupplyData(opts.rpc);
439
+
440
+ if (!opts.asJson) {
441
+ stopSpinner(true, 'Supply data retrieved');
442
+ }
443
+
444
+ if (opts.asJson) {
445
+ renderJson(data, opts.rpc);
446
+ } else {
447
+ renderSupplyBox(data, opts);
448
+ }
449
+ } catch (err) {
450
+ if (!opts.asJson) {
451
+ stopSpinner(false, 'Failed');
452
+ }
453
+
454
+ if (opts.asJson) {
455
+ console.log(JSON.stringify({
456
+ error: err.message,
457
+ rpc: opts.rpc,
458
+ timestamp: new Date().toISOString(),
459
+ }, null, 2));
460
+ } else {
461
+ console.log(`\n ${error('Supply query failed:')} ${err.message}\n`);
462
+ console.log(` ${dim('Troubleshooting:')}`);
463
+ console.log(` • Is your validator running? ${C.cyan}aether ping${C.reset}`);
464
+ console.log(` • Check RPC endpoint: ${C.dim}${opts.rpc}${C.reset}`);
465
+ console.log(` • Set custom RPC: ${C.dim}AETHER_RPC=https://your-rpc-url${C.reset}`);
466
+ console.log();
467
+ }
468
+ process.exit(1);
469
+ }
470
+ }
471
+
472
+ // ============================================================================
473
+ // Exports
474
+ // ============================================================================
475
+
476
+ module.exports = { supplyCommand };
477
+
478
+ if (require.main === module) {
479
+ supplyCommand().catch(err => {
480
+ console.error(`\n${C.red}✗ Supply command failed:${C.reset} ${err.message}\n`);
481
+ process.exit(1);
482
+ });
483
+ }