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.
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli broadcast
4
+ *
5
+ * Broadcast a signed transaction to the Aether network.
6
+ * Accepts a base58-encoded transaction signature or a raw JSON payload.
7
+ * Useful for submitting offline-constructed transactions.
8
+ *
9
+ * Usage:
10
+ * aether broadcast --tx <signature> Broadcast by tx signature
11
+ * aether broadcast --json <payload> Broadcast raw JSON tx payload
12
+ * aether broadcast --file <path> Read tx from a JSON file
13
+ * aether broadcast --rpc <url> Use a specific RPC endpoint
14
+ * aether broadcast --json-output Output result as JSON
15
+ *
16
+ * Examples:
17
+ * aether broadcast --tx 5abcdef... # Submit pre-signed tx
18
+ * aether broadcast --json '{"type":"Transfer",...}'
19
+ * aether broadcast --file ./unsigned_tx.json
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const https = require('https');
25
+ const http = require('http');
26
+
27
+ // ANSI colours
28
+ const C = {
29
+ reset: '\x1b[0m',
30
+ bright: '\x1b[1m',
31
+ dim: '\x1b[2m',
32
+ red: '\x1b[31m',
33
+ green: '\x1b[32m',
34
+ yellow: '\x1b[33m',
35
+ cyan: '\x1b[36m',
36
+ };
37
+
38
+ const CLI_VERSION = '1.0.0';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Config
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function getDefaultRpc() {
45
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Argument parsing
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function parseArgs() {
53
+ const args = process.argv.slice(2);
54
+ const opts = {
55
+ rpc: getDefaultRpc(),
56
+ signature: null,
57
+ jsonPayload: null,
58
+ filePath: null,
59
+ asJson: false,
60
+ };
61
+
62
+ for (let i = 0; i < args.length; i++) {
63
+ if (args[i] === '--tx' || args[i] === '-t') {
64
+ opts.signature = args[++i];
65
+ } else if (args[i] === '--json' || args[i] === '-j') {
66
+ opts.jsonPayload = args[++i];
67
+ } else if (args[i] === '--file' || args[i] === '-f') {
68
+ opts.filePath = args[++i];
69
+ } else if (args[i] === '--rpc' || args[i] === '-r') {
70
+ opts.rpc = args[++i];
71
+ } else if (args[i] === '--json-output') {
72
+ opts.asJson = true;
73
+ } else if (args[i] === '--help' || args[i] === '-h') {
74
+ showHelp();
75
+ process.exit(0);
76
+ }
77
+ }
78
+
79
+ return opts;
80
+ }
81
+
82
+ function showHelp() {
83
+ console.log(`
84
+ ${C.bright}${C.cyan}aether-cli broadcast${C.reset} - Broadcast a Signed Transaction
85
+
86
+ ${C.bright}Usage:${C.reset}
87
+ aether-cli broadcast --tx <signature> Broadcast by base58 signature
88
+ aether-cli broadcast --json <payload> Broadcast inline JSON payload
89
+ aether-cli broadcast --file <path> Read tx from a JSON file
90
+ aether-cli broadcast --rpc <url> Override default RPC
91
+ aether-cli broadcast --json-output JSON output for scripting
92
+
93
+ ${C.bright}Examples:${C.reset}
94
+ aether-cli broadcast --tx 5abcdef123456... # Submit by signature
95
+ aether-cli broadcast --json '{"type":"Transfer","data":{...}}'
96
+ aether-cli broadcast --file ./my_tx.json # Read and broadcast from file
97
+ `.trim());
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // HTTP helpers
102
+ // ---------------------------------------------------------------------------
103
+
104
+ function httpRequest(rpcUrl, pathStr, method = 'GET', body = null, timeoutMs = 15000) {
105
+ return new Promise((resolve, reject) => {
106
+ const url = new URL(pathStr, rpcUrl);
107
+ const lib = url.protocol === 'https:' ? https : http;
108
+ const bodyStr = body ? JSON.stringify(body) : null;
109
+
110
+ const reqOptions = {
111
+ hostname: url.hostname,
112
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
113
+ path: url.pathname + url.search,
114
+ method,
115
+ timeout: timeoutMs,
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {}),
119
+ },
120
+ };
121
+
122
+ const req = lib.request(reqOptions, (res) => {
123
+ let data = '';
124
+ res.on('data', (chunk) => (data += chunk));
125
+ res.on('end', () => {
126
+ try { resolve(JSON.parse(data)); }
127
+ catch { resolve({ raw: data }); }
128
+ });
129
+ });
130
+
131
+ req.on('error', reject);
132
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
133
+ if (bodyStr) req.write(bodyStr);
134
+ req.end();
135
+ });
136
+ }
137
+
138
+ function httpPost(rpcUrl, pathStr, body, timeoutMs = 15000) {
139
+ return httpRequest(rpcUrl, pathStr, 'POST', body, timeoutMs);
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Validate transaction payload
144
+ // ---------------------------------------------------------------------------
145
+
146
+ function validateTxPayload(tx) {
147
+ const errors = [];
148
+
149
+ if (!tx) {
150
+ errors.push('Transaction payload is null or empty');
151
+ return errors;
152
+ }
153
+
154
+ // Must have signer
155
+ if (!tx.signer && !tx.from && !tx.pubkey) {
156
+ errors.push('Missing signer field (signer | from | pubkey)');
157
+ }
158
+
159
+ // Must have tx_type or type
160
+ if (!tx.tx_type && !tx.type) {
161
+ errors.push('Missing tx_type or type field');
162
+ }
163
+
164
+ // Must have payload
165
+ if (!tx.payload && !tx.data) {
166
+ errors.push('Missing payload or data field');
167
+ }
168
+
169
+ return errors;
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Broadcast logic
174
+ // ---------------------------------------------------------------------------
175
+
176
+ async function broadcast({ rpc, signature, jsonPayload, filePath, asJson }) {
177
+ // Build the tx object from inputs (priority: signature > file > inline JSON)
178
+ let tx = null;
179
+
180
+ if (signature) {
181
+ // Signature-only broadcast: POST /v1/tx with { signature }
182
+ tx = { signature };
183
+ } else if (filePath) {
184
+ // Read from file
185
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
186
+ if (!fs.existsSync(absPath)) {
187
+ throw new Error(`File not found: ${absPath}`);
188
+ }
189
+ try {
190
+ tx = JSON.parse(fs.readFileSync(absPath, 'utf8'));
191
+ } catch {
192
+ throw new Error(`Invalid JSON in file: ${absPath}`);
193
+ }
194
+ } else if (jsonPayload) {
195
+ try {
196
+ tx = JSON.parse(jsonPayload);
197
+ } catch {
198
+ throw new Error('Invalid JSON payload provided with --json');
199
+ }
200
+ } else {
201
+ throw new Error('No transaction provided. Use --tx, --json, or --file');
202
+ }
203
+
204
+ // Validate the transaction has required fields
205
+ if (!signature) {
206
+ const validationErrors = validateTxPayload(tx);
207
+ if (validationErrors.length > 0) {
208
+ throw new Error('Invalid transaction payload:\n ' + validationErrors.join('\n '));
209
+ }
210
+ }
211
+
212
+ if (!asJson) {
213
+ console.log(`\n${C.bright}${C.cyan}── Broadcast Transaction ─────────────────────────────────────${C.reset}`);
214
+ console.log(` ${C.dim}RPC: ${rpc}${C.reset}`);
215
+ if (signature) {
216
+ console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${signature}${C.reset}`);
217
+ } else {
218
+ const txType = tx.tx_type || tx.type || 'Unknown';
219
+ const signer = tx.signer || tx.from || tx.pubkey || 'unknown';
220
+ console.log(` ${C.dim}Type:${C.reset} ${C.cyan}${txType}${C.reset}`);
221
+ console.log(` ${C.dim}Signer:${C.reset} ${C.cyan}${signer}${C.reset}`);
222
+ }
223
+ console.log();
224
+ }
225
+
226
+ // Submit the transaction
227
+ let result;
228
+ let latencyMs;
229
+
230
+ try {
231
+ const start = Date.now();
232
+ result = await httpPost(rpc, '/v1/tx', tx);
233
+ latencyMs = Date.now() - start;
234
+ } catch (err) {
235
+ if (asJson) {
236
+ console.log(JSON.stringify({
237
+ success: false,
238
+ error: err.message,
239
+ rpc,
240
+ cli_version: CLI_VERSION,
241
+ timestamp: new Date().toISOString(),
242
+ }, null, 2));
243
+ } else {
244
+ console.log(` ${C.red}✗ Network error:${C.reset} ${err.message}`);
245
+ console.log(` ${C.dim} Check that your RPC is accessible: ${rpc}${C.reset}`);
246
+ }
247
+ process.exit(1);
248
+ }
249
+
250
+ const success = result && !result.error && result.accepted !== false;
251
+
252
+ if (asJson) {
253
+ console.log(JSON.stringify({
254
+ success,
255
+ accepted: result?.accepted ?? null,
256
+ signature: result?.signature ?? result?.tx_signature ?? signature ?? null,
257
+ slot: result?.slot ?? null,
258
+ error: result?.error ?? null,
259
+ rpc,
260
+ latency_ms: latencyMs,
261
+ cli_version: CLI_VERSION,
262
+ timestamp: new Date().toISOString(),
263
+ }, null, 2));
264
+ return;
265
+ }
266
+
267
+ if (success) {
268
+ const sig = result?.signature ?? result?.tx_signature ?? signature ?? 'unknown';
269
+ console.log(`${C.green}✓ Transaction accepted!${C.reset}`);
270
+ console.log(` ${C.green}★${C.reset} ${C.bright}Signature:${C.reset} ${sig}`);
271
+ if (result?.slot) {
272
+ console.log(` ${C.dim} Slot:${C.reset} ${result.slot}`);
273
+ }
274
+ console.log(` ${C.dim} Latency:${C.reset} ${latencyMs}ms`);
275
+ console.log(` ${C.dim} RPC:${C.reset} ${rpc}`);
276
+ console.log();
277
+ } else {
278
+ const errMsg = result?.error || 'Transaction rejected by network';
279
+ console.log(`${C.red}✗ Transaction rejected${C.reset}`);
280
+ if (result?.error) {
281
+ console.log(` ${C.red} Error:${C.reset} ${result.error}`);
282
+ }
283
+ if (result?.logs) {
284
+ console.log(` ${C.dim} Logs:${C.reset}`);
285
+ for (const log of result.logs) {
286
+ console.log(` ${C.dim}${log}${C.reset}`);
287
+ }
288
+ }
289
+ console.log(` ${C.dim} Latency:${C.reset} ${latencyMs}ms`);
290
+ console.log(` ${C.dim} RPC:${C.reset} ${rpc}`);
291
+ console.log();
292
+ process.exit(1);
293
+ }
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Main
298
+ // ---------------------------------------------------------------------------
299
+
300
+ async function main() {
301
+ const opts = parseArgs();
302
+
303
+ try {
304
+ await broadcast(opts);
305
+ } catch (err) {
306
+ if (opts.asJson) {
307
+ console.log(JSON.stringify({
308
+ success: false,
309
+ error: err.message,
310
+ rpc: opts.rpc,
311
+ cli_version: CLI_VERSION,
312
+ timestamp: new Date().toISOString(),
313
+ }, null, 2));
314
+ } else {
315
+ console.log(`\n ${C.red}✗ ${err.message}${C.reset}\n`);
316
+ }
317
+ process.exit(1);
318
+ }
319
+ }
320
+
321
+ main();
322
+
323
+ module.exports = { broadcastCommand: main };
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli stake-positions
4
+ *
5
+ * Query and display current stake positions/delegations for a wallet.
6
+ * Shows validator, amount, status, and accumulated rewards.
7
+ *
8
+ * Usage:
9
+ * aether stake-positions --address <addr> [--json]
10
+ * aether wallet stake-positions --address <addr> [--json]
11
+ *
12
+ * Examples:
13
+ * aether stake-positions --address ATHxxx
14
+ * aether wallet stake-positions --address ATHxxx --json
15
+ */
16
+
17
+ const https = require('https');
18
+ const http = require('http');
19
+
20
+ // ANSI colours
21
+ const C = {
22
+ reset: '\x1b[0m',
23
+ bright: '\x1b[1m',
24
+ dim: '\x1b[2m',
25
+ red: '\x1b[31m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ cyan: '\x1b[36m',
29
+ magenta: '\x1b[35m',
30
+ };
31
+
32
+ const CLI_VERSION = '1.0.0';
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Config
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function getDefaultRpc() {
39
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // HTTP helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function httpRequest(rpcUrl, pathStr, timeoutMs = 8000) {
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
+ headers: { 'Content-Type': 'application/json' },
56
+ }, (res) => {
57
+ let data = '';
58
+ res.on('data', (chunk) => data += chunk);
59
+ res.on('end', () => {
60
+ try { resolve(JSON.parse(data)); }
61
+ catch { resolve(data); }
62
+ });
63
+ });
64
+ req.on('error', reject);
65
+ req.setTimeout(timeoutMs, () => {
66
+ req.destroy();
67
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
68
+ });
69
+ req.end();
70
+ });
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Argument parsing
75
+ // ---------------------------------------------------------------------------
76
+
77
+ function parseArgs() {
78
+ const args = process.argv.slice(2);
79
+ const result = { address: null, json: false };
80
+
81
+ for (let i = 0; i < args.length; i++) {
82
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
83
+ result.address = args[i + 1];
84
+ i++;
85
+ } else if (args[i] === '--json' || args[i] === '--json-output') {
86
+ result.json = true;
87
+ } else if (args[i] === '--rpc' && args[i + 1]) {
88
+ result.rpc = args[i + 1];
89
+ i++;
90
+ } else if (args[i] === '--help' || args[i] === '-h') {
91
+ result.help = true;
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Balance formatting
100
+ // ---------------------------------------------------------------------------
101
+
102
+ function formatAether(lamports) {
103
+ const aeth = (lamports || 0) / 1e9;
104
+ return aeth.toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 }) + ' AETH';
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Main
109
+ // ---------------------------------------------------------------------------
110
+
111
+ async function stakePositionsCommand() {
112
+ const opts = parseArgs();
113
+
114
+ if (opts.help) {
115
+ console.log(`
116
+ ${C.bright}${C.cyan}stake-positions${C.reset} — Query active stake delegations for a wallet
117
+
118
+ ${C.bright}USAGE${C.reset}
119
+ aether stake-positions --address <addr> [--json] [--rpc <url>]
120
+
121
+ ${C.bright}OPTIONS${C.reset}
122
+ --address <addr> Wallet address (ATH...)
123
+ --json Output raw JSON
124
+ --rpc <url> RPC endpoint (default: AETHER_RPC or localhost:8899)
125
+ --help Show this help
126
+
127
+ ${C.bright}EXAMPLES${C.reset}
128
+ aether stake-positions --address ATH3abc...
129
+ aether stake-positions --address ATH3abc... --json
130
+ `);
131
+ return;
132
+ }
133
+
134
+ if (!opts.address) {
135
+ console.log(` ${C.red}✗ Missing --address${C.reset}\n`);
136
+ console.log(` Usage: aether stake-positions --address <addr> [--json]\n`);
137
+ return;
138
+ }
139
+
140
+ const rpcUrl = opts.rpc || getDefaultRpc();
141
+ const address = opts.address;
142
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
143
+
144
+ if (!opts.json) {
145
+ console.log(`\n${C.bright}${C.cyan}── Stake Positions ──────────────────────────────────────${C.reset}\n`);
146
+ console.log(` ${C.dim}Wallet:${C.reset} ${address}`);
147
+ console.log(` ${C.dim}RPC: ${C.reset} ${rpcUrl}\n`);
148
+ }
149
+
150
+ try {
151
+ // Fetch stake accounts
152
+ const res = await httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`);
153
+
154
+ let stakeAccounts = [];
155
+ if (Array.isArray(res)) {
156
+ stakeAccounts = res;
157
+ } else if (res && typeof res === 'object') {
158
+ stakeAccounts = res.accounts || res.stake_accounts || res.data || [];
159
+ }
160
+
161
+ if (opts.json) {
162
+ const totalLamports = stakeAccounts.reduce((sum, acc) => sum + (acc.stake_lamports || acc.lamports || 0), 0);
163
+ console.log(JSON.stringify({
164
+ wallet_address: address,
165
+ stake_accounts: stakeAccounts.map(acc => ({
166
+ stake_account: acc.pubkey || acc.publicKey || acc.account || 'unknown',
167
+ validator: acc.validator || acc.delegate || acc.validator_address || 'unknown',
168
+ stake_lamports: acc.stake_lamports || acc.lamports || 0,
169
+ stake_aeth: ((acc.stake_lamports || acc.lamports || 0) / 1e9).toFixed(4),
170
+ status: acc.status || acc.state || 'active',
171
+ updated_epoch: acc.epoch || acc.last_update_epoch || null,
172
+ })),
173
+ total_staked_lamports: totalLamports,
174
+ total_staked_aeth: (totalLamports / 1e9).toFixed(4),
175
+ count: stakeAccounts.length,
176
+ }, null, 2));
177
+ return;
178
+ }
179
+
180
+ if (!stakeAccounts || stakeAccounts.length === 0) {
181
+ console.log(` ${C.yellow}? No active stake positions found.${C.reset}`);
182
+ console.log(` ${C.dim} Stake AETH with: ${C.cyan}aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
183
+ return;
184
+ }
185
+
186
+ let totalStaked = 0;
187
+ console.log(` ${C.bright}Stake Positions (${stakeAccounts.length})${C.reset}\n`);
188
+
189
+ for (const acc of stakeAccounts) {
190
+ const stakeAcct = acc.pubkey || acc.publicKey || acc.account || 'unknown';
191
+ const validator = acc.validator || acc.delegate || acc.validator_address || 'unknown';
192
+ const lamports = acc.stake_lamports || acc.lamports || 0;
193
+ const status = (acc.status || acc.state || 'active').toLowerCase();
194
+ const epoch = acc.epoch || acc.last_update_epoch || null;
195
+
196
+ totalStaked += lamports;
197
+
198
+ const statusColor = status === 'active' ? C.green : status === 'deactivating' ? C.yellow : C.dim;
199
+ const shortAcct = stakeAcct.length > 20 ? stakeAcct.slice(0, 8) + '.' + stakeAcct.slice(-8) : stakeAcct;
200
+ const shortVal = validator.length > 20 ? validator.slice(0, 8) + '.' + validator.slice(-8) : validator;
201
+ const aeth = (lamports / 1e9).toFixed(4);
202
+
203
+ console.log(` ${C.dim}┌─${C.bright}${statusColor} ${status.toUpperCase()}${C.reset}`);
204
+ console.log(` │ ${C.dim}Stake acct:${C.reset} ${shortAcct}`);
205
+ console.log(` │ ${C.dim}Validator:${C.reset} ${shortVal}`);
206
+ console.log(` │ ${C.dim}Staked:${C.reset} ${C.bright}${aeth} AETH${C.reset} (${lamports.toLocaleString()} lamports)`);
207
+ if (epoch) console.log(` │ ${C.dim}Epoch:${C.reset} ${C.bright}#${epoch}${C.reset}`);
208
+ console.log(` ${C.dim}└${C.reset}\n`);
209
+ }
210
+
211
+ console.log(` ${C.dim}────────────────────────────────────────${C.reset}`);
212
+ console.log(` ${C.bright}Total Staked:${C.reset} ${C.green}${formatAether(totalStaked)}${C.reset}\n`);
213
+
214
+ } catch (err) {
215
+ console.log(` ${C.red}? Failed to fetch stake positions:${C.reset} ${err.message}\n`);
216
+ process.exit(1);
217
+ }
218
+ }
219
+
220
+ stakePositionsCommand();