aether-hub 1.1.7 → 1.1.9
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/doctor.js +2 -1
- package/commands/ping.js +320 -0
- package/commands/price.js +253 -0
- package/commands/validators.js +494 -326
- package/index.js +16 -0
- package/package.json +3 -1
package/commands/doctor.js
CHANGED
|
@@ -556,7 +556,8 @@ function getFixCommand(check) {
|
|
|
556
556
|
}
|
|
557
557
|
const repoPath = path.join(workspaceRoot, 'Jelly-legs-unsteady-workshop');
|
|
558
558
|
return `cd "${repoPath}" && cargo build --bin aether-validator --release`;
|
|
559
|
-
|
|
559
|
+
}
|
|
560
|
+
|
|
560
561
|
case 'Disk':
|
|
561
562
|
// Can't auto-fix disk space, but can suggest cleanup
|
|
562
563
|
if (platform === 'win32') {
|
package/commands/ping.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli ping
|
|
4
|
+
*
|
|
5
|
+
* Quick RPC health check — measures latency, verifies connectivity,
|
|
6
|
+
* and reports node version and slot info.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* aether ping Ping default RPC (AETHER_RPC or localhost:8899)
|
|
10
|
+
* aether ping --rpc <url> Ping a specific RPC endpoint
|
|
11
|
+
* aether ping --count <n> Run <n> pings and show avg/min/max (default: 1, max 20)
|
|
12
|
+
* aether ping --json JSON output for scripting/monitoring
|
|
13
|
+
*
|
|
14
|
+
* Examples:
|
|
15
|
+
* aether ping # Single ping, default RPC
|
|
16
|
+
* aether ping --rpc https://rpc.example.com # Ping specific endpoint
|
|
17
|
+
* aether ping --count 5 --json # 5 pings, JSON output for alerting
|
|
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
|
+
};
|
|
33
|
+
|
|
34
|
+
const CLI_VERSION = '1.0.6';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function getDefaultRpc() {
|
|
41
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function httpRequest(rpcUrl, pathStr, timeoutMs = 8000) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const url = new URL(pathStr, rpcUrl);
|
|
47
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
48
|
+
const req = lib.request({
|
|
49
|
+
hostname: url.hostname,
|
|
50
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
51
|
+
path: url.pathname + url.search,
|
|
52
|
+
method: 'GET',
|
|
53
|
+
timeout: timeoutMs,
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
}, (res) => {
|
|
56
|
+
let data = '';
|
|
57
|
+
res.on('data', (chunk) => data += chunk);
|
|
58
|
+
res.on('end', () => {
|
|
59
|
+
try { resolve(JSON.parse(data)); }
|
|
60
|
+
catch { resolve({ raw: data }); }
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
req.on('error', reject);
|
|
64
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function httpPost(rpcUrl, pathStr, body, timeoutMs = 8000) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const url = new URL(pathStr, rpcUrl);
|
|
72
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
73
|
+
const bodyStr = JSON.stringify(body);
|
|
74
|
+
const req = lib.request({
|
|
75
|
+
hostname: url.hostname,
|
|
76
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
77
|
+
path: url.pathname + url.search,
|
|
78
|
+
method: 'POST',
|
|
79
|
+
timeout: timeoutMs,
|
|
80
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
|
|
81
|
+
}, (res) => {
|
|
82
|
+
let data = '';
|
|
83
|
+
res.on('data', (chunk) => data += chunk);
|
|
84
|
+
res.on('end', () => {
|
|
85
|
+
try { resolve(JSON.parse(data)); }
|
|
86
|
+
catch { resolve({ raw: data }); }
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
req.on('error', reject);
|
|
90
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
91
|
+
req.write(bodyStr);
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Single ping: measure latency to /v1/slot
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
async function pingOnce(rpcUrl) {
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
let slot = null;
|
|
103
|
+
let error = null;
|
|
104
|
+
let latencyMs = null;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Use POST to /v1/slot (some nodes only support POST)
|
|
108
|
+
const result = await httpPost(rpcUrl, '/v1/slot', {}, 8000);
|
|
109
|
+
latencyMs = Date.now() - start;
|
|
110
|
+
|
|
111
|
+
if (result && result.error) {
|
|
112
|
+
error = result.error;
|
|
113
|
+
} else {
|
|
114
|
+
slot = result.slot ?? result;
|
|
115
|
+
if (typeof slot === 'object') slot = slot.slot;
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
latencyMs = Date.now() - start;
|
|
119
|
+
error = err.message;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { latencyMs, slot, error, rpcUrl };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Multi-ping: run N pings and aggregate
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
async function pingMulti(rpcUrl, count) {
|
|
130
|
+
const results = [];
|
|
131
|
+
for (let i = 0; i < count; i++) {
|
|
132
|
+
results.push(await pingOnce(rpcUrl));
|
|
133
|
+
if (i < count - 1) await new Promise(r => setTimeout(r, 100));
|
|
134
|
+
}
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Colour helpers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
function latencyColor(ms) {
|
|
143
|
+
if (ms === null) return C.red;
|
|
144
|
+
if (ms < 50) return C.green;
|
|
145
|
+
if (ms < 200) return C.cyan;
|
|
146
|
+
if (ms < 500) return C.yellow;
|
|
147
|
+
return C.red;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function latencyLabel(ms) {
|
|
151
|
+
if (ms === null) return '✗ unreachable';
|
|
152
|
+
if (ms < 50) return `● ${ms}ms (excellent)`;
|
|
153
|
+
if (ms < 200) return `● ${ms}ms (good)`;
|
|
154
|
+
if (ms < 500) return `○ ${ms}ms (fair)`;
|
|
155
|
+
return `○ ${ms}ms (slow)`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Output formatters
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
function printResult(ping, asJson) {
|
|
163
|
+
const { latencyMs, slot, error, rpcUrl } = ping;
|
|
164
|
+
|
|
165
|
+
if (asJson) {
|
|
166
|
+
console.log(JSON.stringify({
|
|
167
|
+
rpc: rpcUrl,
|
|
168
|
+
online: error === null,
|
|
169
|
+
latency_ms: latencyMs,
|
|
170
|
+
slot,
|
|
171
|
+
error: error || null,
|
|
172
|
+
cli_version: CLI_VERSION,
|
|
173
|
+
timestamp: new Date().toISOString(),
|
|
174
|
+
}));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lc = latencyColor(latencyMs);
|
|
179
|
+
const bar = latencyMs !== null ? '█'.repeat(Math.min(10, Math.floor(latencyMs / 50))) : '▒';
|
|
180
|
+
|
|
181
|
+
console.log(` ${lc}${bar}${C.reset} ${C.bright}${latencyLabel(latencyMs)}${C.reset}`);
|
|
182
|
+
if (slot !== null) {
|
|
183
|
+
console.log(` ${C.dim} slot: ${C.reset}${C.cyan}${slot.toLocaleString()}${C.reset}`);
|
|
184
|
+
}
|
|
185
|
+
if (error) {
|
|
186
|
+
console.log(` ${C.red} ✗ ${error}${C.reset}`);
|
|
187
|
+
}
|
|
188
|
+
console.log(` ${C.dim} rpc: ${rpcUrl}${C.reset}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printAggregated(results, rpcUrl, asJson) {
|
|
192
|
+
const online = results.filter(r => r.error === null);
|
|
193
|
+
const failed = results.filter(r => r.error !== null);
|
|
194
|
+
|
|
195
|
+
if (asJson) {
|
|
196
|
+
const latencies = online.map(r => r.latencyMs).filter(Boolean);
|
|
197
|
+
const avg = latencies.length > 0 ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : null;
|
|
198
|
+
const min = latencies.length > 0 ? Math.min(...latencies) : null;
|
|
199
|
+
const max = latencies.length > 0 ? Math.max(...latencies) : null;
|
|
200
|
+
|
|
201
|
+
console.log(JSON.stringify({
|
|
202
|
+
rpc: rpcUrl,
|
|
203
|
+
count: results.length,
|
|
204
|
+
online: online.length,
|
|
205
|
+
failed: failed.length,
|
|
206
|
+
latency_ms: { avg, min, max },
|
|
207
|
+
slots: online.map(r => r.slot).filter(Boolean),
|
|
208
|
+
errors: failed.map(r => r.error),
|
|
209
|
+
cli_version: CLI_VERSION,
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
}));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`\n${C.bright}${C.cyan}── Ping Results: ${rpcUrl} ──${C.reset}\n`);
|
|
216
|
+
|
|
217
|
+
const latencies = online.map(r => r.latencyMs).filter(Boolean);
|
|
218
|
+
|
|
219
|
+
if (latencies.length > 0) {
|
|
220
|
+
const avg = Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length);
|
|
221
|
+
const min = Math.min(...latencies);
|
|
222
|
+
const max = Math.max(...latencies);
|
|
223
|
+
|
|
224
|
+
console.log(` ${C.green}✓${C.reset} ${online.length}/${results.length} successful\n`);
|
|
225
|
+
|
|
226
|
+
// Per-ping bars
|
|
227
|
+
for (let i = 0; i < online.length; i++) {
|
|
228
|
+
const r = online[i];
|
|
229
|
+
const lc = latencyColor(r.latencyMs);
|
|
230
|
+
const bar = '█'.repeat(Math.min(10, Math.floor(r.latencyMs / 50)));
|
|
231
|
+
const slotStr = r.slot !== null ? ` slot=${C.cyan}${r.slot.toLocaleString()}${C.reset}` : '';
|
|
232
|
+
console.log(` ${lc}${bar}${C.reset} ${r.latencyMs}ms${slotStr}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log();
|
|
236
|
+
console.log(` ${C.bright}Latency:${C.reset} avg=${latencyColor(avg)}${avg}ms${C.reset} min=${latencyColor(min)}${min}ms${C.reset} max=${latencyColor(max)}${max}ms${C.reset}`);
|
|
237
|
+
console.log(` ${C.dim} Packets: ${results.length} Lost: ${failed.length}${C.reset}`);
|
|
238
|
+
|
|
239
|
+
// Health assessment
|
|
240
|
+
const healthPct = (online.length / results.length) * 100;
|
|
241
|
+
if (healthPct === 100 && avg < 50) {
|
|
242
|
+
console.log(` ${C.green} Health: excellent${C.reset}`);
|
|
243
|
+
} else if (healthPct >= 80 && avg < 200) {
|
|
244
|
+
console.log(` ${C.cyan} Health: good${C.reset}`);
|
|
245
|
+
} else if (healthPct >= 60) {
|
|
246
|
+
console.log(` ${C.yellow} Health: degraded${C.reset}`);
|
|
247
|
+
} else {
|
|
248
|
+
console.log(` ${C.red} Health: poor${C.reset}`);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
console.log(` ${C.red}✗ All pings failed${C.reset}`);
|
|
252
|
+
for (const r of failed) {
|
|
253
|
+
console.log(` ${C.red}✗ ${r.error}${C.reset}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (failed.length > 0 && online.length > 0) {
|
|
258
|
+
console.log();
|
|
259
|
+
console.log(` ${C.yellow}⚠ ${failed.length} pings failed:${C.reset}`);
|
|
260
|
+
for (const r of failed) {
|
|
261
|
+
console.log(` ${C.red}✗ ${r.error}${C.reset}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
console.log();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Main
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
async function main() {
|
|
272
|
+
const args = process.argv.slice(3); // [node, index.js, ping, ...]
|
|
273
|
+
|
|
274
|
+
const rpcIndex = args.findIndex(a => a === '--rpc' || a === '-r');
|
|
275
|
+
const rpcUrl = rpcIndex !== -1 && args[rpcIndex + 1] && !args[rpcIndex + 1].startsWith('-')
|
|
276
|
+
? args[rpcIndex + 1]
|
|
277
|
+
: getDefaultRpc();
|
|
278
|
+
|
|
279
|
+
const countIndex = args.findIndex(a => a === '--count' || a === '-c');
|
|
280
|
+
const countRaw = countIndex !== -1 && args[countIndex + 1] && !args[countIndex + 1].startsWith('-')
|
|
281
|
+
? parseInt(args[countIndex + 1], 10)
|
|
282
|
+
: 1;
|
|
283
|
+
const count = Math.min(Math.max(1, countRaw || 1), 20);
|
|
284
|
+
|
|
285
|
+
const asJson = args.includes('--json') || args.includes('-j');
|
|
286
|
+
|
|
287
|
+
if (!asJson) {
|
|
288
|
+
console.log(`\n${C.bright}${C.cyan}── Aether RPC Ping ──────────────────────────────────────${C.reset}`);
|
|
289
|
+
if (count > 1) {
|
|
290
|
+
console.log(` ${C.dim}Running ${count} pings against ${rpcUrl}…${C.reset}`);
|
|
291
|
+
} else {
|
|
292
|
+
console.log(` ${C.dim}RPC: ${rpcUrl}${C.reset}`);
|
|
293
|
+
}
|
|
294
|
+
console.log();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (count === 1) {
|
|
298
|
+
const result = await pingOnce(rpcUrl);
|
|
299
|
+
printResult(result, asJson);
|
|
300
|
+
if (!asJson) console.log();
|
|
301
|
+
// Exit 1 if unreachable
|
|
302
|
+
if (result.error) process.exit(1);
|
|
303
|
+
} else {
|
|
304
|
+
const results = await pingMulti(rpcUrl, count);
|
|
305
|
+
printAggregated(results, rpcUrl, asJson);
|
|
306
|
+
// Exit 1 if all failed
|
|
307
|
+
if (results.every(r => r.error)) process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
main().catch(err => {
|
|
312
|
+
console.error(`\n${C.red}✗ Ping failed:${C.reset} ${err.message}\n`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
module.exports = { pingCommand: main };
|
|
317
|
+
|
|
318
|
+
if (require.main === module) {
|
|
319
|
+
main();
|
|
320
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli price
|
|
4
|
+
*
|
|
5
|
+
* Show real-time AETH/USD price from free public crypto APIs.
|
|
6
|
+
* Supports CoinGecko (free tier), and falls back to simulated data
|
|
7
|
+
* if no API key is available.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aether price Show current AETH/USD price
|
|
11
|
+
* aether price --pair AETH/USD Specify trading pair (default: AETH/USD)
|
|
12
|
+
* aether price --json JSON output for scripting
|
|
13
|
+
* aether price --source coingecko Fallback to CoinGecko (no API key needed)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const http = require('http');
|
|
18
|
+
|
|
19
|
+
const C = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bright: '\x1b[1m',
|
|
22
|
+
dim: '\x1b[2m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
green: '\x1b[32m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
cyan: '\x1b[36m',
|
|
27
|
+
magenta: '\x1b[35m',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const AETHER_CONTRACT = 'ATH'; // Aether token contract (hypothetical)
|
|
31
|
+
const DEFAULT_PAIR = 'AETH/USD';
|
|
32
|
+
|
|
33
|
+
function httpGet(url) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const lib = url.startsWith('https') ? https : http;
|
|
36
|
+
const parsed = new URL(url);
|
|
37
|
+
const req = lib.request({
|
|
38
|
+
hostname: parsed.hostname,
|
|
39
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
40
|
+
path: parsed.pathname + parsed.search,
|
|
41
|
+
method: 'GET',
|
|
42
|
+
timeout: 10000,
|
|
43
|
+
headers: { 'Accept': 'application/json', 'User-Agent': 'Aether-CLI/1.0' },
|
|
44
|
+
}, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', (chunk) => data += chunk);
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
try { resolve(JSON.parse(data)); }
|
|
49
|
+
catch { resolve({ _raw: data }); }
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
req.on('error', reject);
|
|
53
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
54
|
+
req.end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatPrice(num, decimals = 4) {
|
|
59
|
+
if (num === null || num === undefined || isNaN(num)) return '—';
|
|
60
|
+
return num.toFixed(decimals);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatTime(date) {
|
|
64
|
+
return date.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch AETH price from CoinGecko (free, no API key).
|
|
69
|
+
* Coins API: /coins/list → find ATH/AETH → /coins/{id}/market_chart
|
|
70
|
+
* Fallback: try known Aether token addresses on major DEXes via /simple/price
|
|
71
|
+
*/
|
|
72
|
+
async function fetchFromCoinGecko() {
|
|
73
|
+
try {
|
|
74
|
+
// Try CoinGecko simple price for AETH token
|
|
75
|
+
// We'll try a few known Aether token addresses on Ethereum mainnet as a proxy
|
|
76
|
+
const url = 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&ids=aether,ath,ath-token,aether-network';
|
|
77
|
+
const data = await httpGet(url);
|
|
78
|
+
|
|
79
|
+
if (data && !data.error) {
|
|
80
|
+
// Find first available AETH quote
|
|
81
|
+
const pairs = [
|
|
82
|
+
{ key: 'aether-network', name: 'AETH' },
|
|
83
|
+
{ key: 'aether', name: 'AETH' },
|
|
84
|
+
{ key: 'ath-token', name: 'ATH' },
|
|
85
|
+
{ key: 'ath', name: 'ATH' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const { key, name } of pairs) {
|
|
89
|
+
if (data[key] && data[key].usd !== undefined) {
|
|
90
|
+
return {
|
|
91
|
+
source: 'CoinGecko',
|
|
92
|
+
symbol: name,
|
|
93
|
+
price: data[key].usd,
|
|
94
|
+
currency: 'USD',
|
|
95
|
+
timestamp: new Date(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch price data from a public DEX aggregator or mock Aether RPC.
|
|
109
|
+
* Primary: use the Aether chain's own price oracle if available.
|
|
110
|
+
* Fallback: use CoinGecko.
|
|
111
|
+
*/
|
|
112
|
+
async function fetchAetherPrice() {
|
|
113
|
+
// Try Aether chain's built-in price oracle (if validator is running)
|
|
114
|
+
const rpcUrl = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
115
|
+
try {
|
|
116
|
+
const res = await httpGet(`${rpcUrl}/v1/price`);
|
|
117
|
+
if (res && !res.error && res.price !== undefined) {
|
|
118
|
+
return {
|
|
119
|
+
source: 'Aether Oracle',
|
|
120
|
+
symbol: 'AETH',
|
|
121
|
+
price: parseFloat(res.price),
|
|
122
|
+
currency: res.currency || 'USD',
|
|
123
|
+
timestamp: new Date(),
|
|
124
|
+
change_24h: res.change_24h || null,
|
|
125
|
+
volume_24h: res.volume_24h || null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
} catch { /* chain oracle not available */ }
|
|
129
|
+
|
|
130
|
+
// Try CoinGecko
|
|
131
|
+
const cg = await fetchFromCoinGecko();
|
|
132
|
+
if (cg) return cg;
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch 24h price change using CoinGecko market chart.
|
|
139
|
+
*/
|
|
140
|
+
async function fetchPriceChange24h(symbol) {
|
|
141
|
+
try {
|
|
142
|
+
const idMap = {
|
|
143
|
+
'AETH': 'aether-network',
|
|
144
|
+
'ATH': 'ath-token',
|
|
145
|
+
};
|
|
146
|
+
const id = idMap[symbol] || 'aether-network';
|
|
147
|
+
const url = `https://api.coingecko.com/api/v3/coins/${id}/market_chart?vs_currency=usd&days=1`;
|
|
148
|
+
const data = await httpGet(url);
|
|
149
|
+
|
|
150
|
+
if (data && data.prices && data.prices.length >= 2) {
|
|
151
|
+
const latest = data.prices[data.prices.length - 1][1];
|
|
152
|
+
const yesterday = data.prices[0][1];
|
|
153
|
+
const change = ((latest - yesterday) / yesterday) * 100;
|
|
154
|
+
const volume = data.total_volumes ? data.total_volumes[data.total_volumes.length - 1][1] : null;
|
|
155
|
+
return {
|
|
156
|
+
change_24h: change,
|
|
157
|
+
volume_24h: volume,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
} catch { /* ignore */ }
|
|
161
|
+
return { change_24h: null, volume_24h: null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function priceCommand() {
|
|
165
|
+
const args = process.argv.slice(2);
|
|
166
|
+
const asJson = args.includes('--json') || args.includes('-j');
|
|
167
|
+
const pair = args.includes('--pair')
|
|
168
|
+
? args[args.indexOf('--pair') + 1] || DEFAULT_PAIR
|
|
169
|
+
: DEFAULT_PAIR;
|
|
170
|
+
const source = args.includes('--source') ? args[args.indexOf('--source') + 1] : null;
|
|
171
|
+
|
|
172
|
+
// Parse pair
|
|
173
|
+
const [fromSymbol, toSymbol = 'USD'] = pair.split('/');
|
|
174
|
+
const symbol = fromSymbol.toUpperCase();
|
|
175
|
+
|
|
176
|
+
console.log(`\n${C.bright}${C.cyan}── Aether Price ───────────────────────────────────────${C.reset}\n`);
|
|
177
|
+
|
|
178
|
+
let priceData;
|
|
179
|
+
if (source === 'coingecko') {
|
|
180
|
+
priceData = await fetchFromCoinGecko();
|
|
181
|
+
} else {
|
|
182
|
+
priceData = await fetchAetherPrice();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!priceData) {
|
|
186
|
+
if (asJson) {
|
|
187
|
+
console.log(JSON.stringify({ error: 'Price data unavailable', symbol, pair }, null, 2));
|
|
188
|
+
} else {
|
|
189
|
+
console.log(` ${C.yellow}⚠ Price data temporarily unavailable.${C.reset}`);
|
|
190
|
+
console.log(` ${C.dim}Make sure your validator is running or check network connectivity.${C.reset}`);
|
|
191
|
+
console.log(` ${C.dim}Set AETHER_RPC env var to your validator's RPC address.${C.reset}`);
|
|
192
|
+
console.log(` ${C.dim}Fallback: aether price --source coingecko${C.reset}\n`);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Fetch 24h change if available
|
|
198
|
+
const changeData = await fetchPriceChange24h(symbol);
|
|
199
|
+
const priceInfo = { ...priceData, ...changeData };
|
|
200
|
+
|
|
201
|
+
if (asJson) {
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
symbol: priceInfo.symbol,
|
|
204
|
+
pair: `${symbol}/USD`,
|
|
205
|
+
price_usd: priceInfo.price,
|
|
206
|
+
change_24h_pct: priceInfo.change_24h !== null ? parseFloat(priceInfo.change_24h.toFixed(4)) : null,
|
|
207
|
+
volume_24h_usd: priceInfo.volume_24h,
|
|
208
|
+
source: priceInfo.source,
|
|
209
|
+
timestamp: formatTime(priceInfo.timestamp),
|
|
210
|
+
}, null, 2));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Human-readable output
|
|
215
|
+
const change = priceInfo.change_24h;
|
|
216
|
+
const changeColor = change === null ? C.dim : change >= 0 ? C.green : C.red;
|
|
217
|
+
const changeStr = change !== null
|
|
218
|
+
? `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`
|
|
219
|
+
: '—';
|
|
220
|
+
|
|
221
|
+
const arrow = change === null ? ' ' : change >= 0 ? '▲' : '▼';
|
|
222
|
+
const volumeStr = priceInfo.volume_24h !== null
|
|
223
|
+
? `$${(priceInfo.volume_24h / 1e6).toFixed(2)}M`
|
|
224
|
+
: null;
|
|
225
|
+
|
|
226
|
+
console.log(` ${C.dim}Pair:${C.reset} ${C.bright}${symbol}/USD${C.reset}`);
|
|
227
|
+
console.log(` ${C.dim}Source:${C.reset} ${C.bright}${priceInfo.source}${C.reset}`);
|
|
228
|
+
console.log(` ${C.dim}Updated:${C.reset} ${formatTime(priceInfo.timestamp)}`);
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(` ${C.bright}${C.green}$${formatPrice(priceInfo.price)}${C.reset} ${C.dim}USD${C.reset}`);
|
|
231
|
+
console.log(` ${C.dim}24h change: ${changeColor}${arrow} ${changeStr}${C.reset}`);
|
|
232
|
+
if (volumeStr) {
|
|
233
|
+
console.log(` ${C.dim}24h volume: ${volumeStr}${C.reset}`);
|
|
234
|
+
}
|
|
235
|
+
console.log();
|
|
236
|
+
|
|
237
|
+
// ASCII box
|
|
238
|
+
const barLen = 40;
|
|
239
|
+
const fillLen = change !== null ? Math.min(barLen, Math.round(Math.abs(change) / 2)) : 0;
|
|
240
|
+
const barColor = change !== null && change < 0 ? C.red : C.green;
|
|
241
|
+
const bar = barColor + '█'.repeat(fillLen) + C.dim + '░'.repeat(barLen - fillLen) + C.reset;
|
|
242
|
+
console.log(` ${C.dim}[${bar}]${C.reset}`);
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(` ${C.dim}Run with ${C.cyan}--json${C.reset}${C.dim} for scripted integrations.${C.reset}`);
|
|
245
|
+
console.log(` ${C.dim}Refreshes on each call — set up a cron job for live monitoring.${C.reset}\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
priceCommand().catch(err => {
|
|
249
|
+
console.error(`\n${C.red}Price error:${C.reset}`, err.message, '\n');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
module.exports = { priceCommand };
|