aether-hub 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/apy.js +480 -0
- package/commands/multisig.js +727 -0
- package/commands/stake-positions.js +220 -0
- package/commands/stats.js +419 -0
- package/index.js +17 -5
- package/package.json +1 -1
|
@@ -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();
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli stats
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive wallet stats dashboard:
|
|
6
|
+
* - Token balance (AETH + lamports)
|
|
7
|
+
* - Active stake positions (validator, amount, status)
|
|
8
|
+
* - Recent transactions (last 5)
|
|
9
|
+
* - Estimated rewards accrued
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* aether stats --address <addr> Full stats dashboard
|
|
13
|
+
* aether stats --address <addr> --json JSON output for scripting
|
|
14
|
+
* aether stats --address <addr> --compact One-line summary
|
|
15
|
+
*
|
|
16
|
+
* Requires AETHER_RPC env var or local node (default: http://127.0.0.1:8899)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const http = require('http');
|
|
20
|
+
const https = require('https');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const bs58 = require('bs58').default;
|
|
25
|
+
|
|
26
|
+
// ANSI colours
|
|
27
|
+
const C = {
|
|
28
|
+
reset: '\x1b[0m',
|
|
29
|
+
bright: '\x1b[1m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
red: '\x1b[31m',
|
|
32
|
+
green: '\x1b[32m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
cyan: '\x1b[36m',
|
|
35
|
+
magenta: '\x1b[35m',
|
|
36
|
+
bold: '\x1b[1m',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Paths & config
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function getAetherDir() {
|
|
44
|
+
return path.join(os.homedir(), '.aether');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getConfigPath() {
|
|
48
|
+
return path.join(getAetherDir(), 'config.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getWalletsDir() {
|
|
52
|
+
return path.join(getAetherDir(), 'wallets');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadConfig() {
|
|
56
|
+
const p = getConfigPath();
|
|
57
|
+
if (!fs.existsSync(p)) return { defaultWallet: null };
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
60
|
+
} catch {
|
|
61
|
+
return { defaultWallet: null };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadWallet(address) {
|
|
66
|
+
const fp = path.join(getWalletsDir(), `${address}.json`);
|
|
67
|
+
if (!fs.existsSync(fp)) return null;
|
|
68
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// RPC helpers
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function getDefaultRpc() {
|
|
76
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function httpRequest(rpcUrl, pathStr) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const url = new URL(pathStr, rpcUrl);
|
|
82
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
83
|
+
const req = lib.request({
|
|
84
|
+
hostname: url.hostname,
|
|
85
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
86
|
+
path: url.pathname + url.search,
|
|
87
|
+
method: 'GET',
|
|
88
|
+
timeout: 8000,
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
}, (res) => {
|
|
91
|
+
let data = '';
|
|
92
|
+
res.on('data', (chunk) => data += chunk);
|
|
93
|
+
res.on('end', () => {
|
|
94
|
+
try { resolve(JSON.parse(data)); }
|
|
95
|
+
catch { resolve({ raw: data }); }
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
req.on('error', reject);
|
|
99
|
+
req.end();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function httpPost(rpcUrl, pathStr, body) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const url = new URL(pathStr, rpcUrl);
|
|
106
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
107
|
+
const bodyStr = JSON.stringify(body);
|
|
108
|
+
const req = lib.request({
|
|
109
|
+
hostname: url.hostname,
|
|
110
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
111
|
+
path: url.pathname + url.search,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
timeout: 8000,
|
|
114
|
+
headers: {
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
117
|
+
},
|
|
118
|
+
}, (res) => {
|
|
119
|
+
let data = '';
|
|
120
|
+
res.on('data', (chunk) => data += chunk);
|
|
121
|
+
res.on('end', () => {
|
|
122
|
+
try { resolve(JSON.parse(data)); }
|
|
123
|
+
catch { resolve(data); }
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
req.on('error', reject);
|
|
127
|
+
req.write(bodyStr);
|
|
128
|
+
req.end();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Formatting helpers
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format lamports as AETH string (1 AETH = 1e9 lamports)
|
|
138
|
+
*/
|
|
139
|
+
function formatAether(lamports) {
|
|
140
|
+
if (lamports === undefined || lamports === null) return '0 AETH';
|
|
141
|
+
const aeth = lamports / 1e9;
|
|
142
|
+
if (aeth === 0) return '0 AETH';
|
|
143
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format a timestamp as relative time ("2h ago", "3d ago")
|
|
148
|
+
*/
|
|
149
|
+
function relativeTime(timestamp) {
|
|
150
|
+
if (!timestamp) return 'unknown';
|
|
151
|
+
const now = Math.floor(Date.now() / 1000);
|
|
152
|
+
const diff = now - timestamp;
|
|
153
|
+
if (diff < 60) return `${diff}s ago`;
|
|
154
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
155
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
156
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Truncate a signature or hash for display
|
|
161
|
+
*/
|
|
162
|
+
function truncate(str, chars = 8) {
|
|
163
|
+
if (!str || typeof str !== 'string') return '—';
|
|
164
|
+
if (str.length <= chars * 2 + 3) return str;
|
|
165
|
+
return str.slice(0, chars) + '…' + str.slice(-chars);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Fetch wallet stats from RPC
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
async function fetchWalletStats(address, rpcUrl) {
|
|
173
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
174
|
+
|
|
175
|
+
// Fetch account, transactions, and stake positions in parallel
|
|
176
|
+
const [account, txs, stakeAccounts] = await Promise.all([
|
|
177
|
+
httpRequest(rpcUrl, `/v1/account/${rawAddr}`).catch(() => null),
|
|
178
|
+
httpRequest(rpcUrl, `/v1/tx?address=${encodeURIComponent(rawAddr)}&limit=5`).catch(() => null),
|
|
179
|
+
httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`).catch(() => null),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
return { account, txs, stakeAccounts };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Render dashboard
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function renderDashboard(address, stats, opts) {
|
|
190
|
+
const { account, txs, stakeAccounts } = stats;
|
|
191
|
+
const { compact, asJson } = opts;
|
|
192
|
+
const rpcUrl = opts.rpcUrl;
|
|
193
|
+
|
|
194
|
+
if (asJson) {
|
|
195
|
+
const stakeList = stakeAccounts && !stakeAccounts.error
|
|
196
|
+
? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
|
|
197
|
+
: [];
|
|
198
|
+
const txList = txs && !txs.error
|
|
199
|
+
? (Array.isArray(txs) ? txs : txs.transactions || [])
|
|
200
|
+
: [];
|
|
201
|
+
|
|
202
|
+
const out = {
|
|
203
|
+
address,
|
|
204
|
+
rpc: rpcUrl,
|
|
205
|
+
balance: account && !account.error ? {
|
|
206
|
+
lamports: account.lamports || 0,
|
|
207
|
+
aeth: formatAether(account.lamports || 0),
|
|
208
|
+
} : null,
|
|
209
|
+
stake_positions: stakeList.map((sa) => ({
|
|
210
|
+
stake_account: sa.stake_account || sa.address || 'unknown',
|
|
211
|
+
validator: sa.validator || 'unknown',
|
|
212
|
+
amount: sa.amount || 0,
|
|
213
|
+
aeth: formatAether(sa.amount || 0),
|
|
214
|
+
status: sa.status || 'active',
|
|
215
|
+
created_epoch: sa.created_epoch || null,
|
|
216
|
+
})),
|
|
217
|
+
recent_txs: txList.map((tx) => ({
|
|
218
|
+
type: tx.tx_type || tx.type || 'Unknown',
|
|
219
|
+
signature: tx.signature || tx.id || tx.tx_signature || null,
|
|
220
|
+
timestamp: tx.timestamp || null,
|
|
221
|
+
relative_time: relativeTime(tx.timestamp),
|
|
222
|
+
payload: tx.payload?.data || {},
|
|
223
|
+
fee: tx.fee || 0,
|
|
224
|
+
})),
|
|
225
|
+
fetched_at: new Date().toISOString(),
|
|
226
|
+
};
|
|
227
|
+
console.log(JSON.stringify(out, null, 2));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const lamports = (account && !account.error) ? (account.lamports || 0) : null;
|
|
232
|
+
const stakeList = stakeAccounts && !stakeAccounts.error
|
|
233
|
+
? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
|
|
234
|
+
: [];
|
|
235
|
+
const txList = txs && !txs.error
|
|
236
|
+
? (Array.isArray(txs) ? txs : txs.transactions || [])
|
|
237
|
+
: [];
|
|
238
|
+
|
|
239
|
+
if (compact) {
|
|
240
|
+
// One-line summary
|
|
241
|
+
const bal = lamports !== null ? formatAether(lamports) : 'unknown';
|
|
242
|
+
const stakes = stakeList.length;
|
|
243
|
+
const recent = txList.length > 0 ? (txList[0].tx_type || txList[0].type || '?') : 'none';
|
|
244
|
+
console.log(`${C.bright}${address}${C.reset} bal:${C.green}${bal}${C.reset} stakes:${stakes} last:${recent} txs:${txList.length}`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Full dashboard
|
|
249
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Stats ─────────────────────────────────────────${C.reset}`);
|
|
250
|
+
console.log(` ${C.green}★${C.reset} ${C.bright}${address}${C.reset}`);
|
|
251
|
+
console.log(` ${C.dim}RPC: ${rpcUrl}${C.reset}`);
|
|
252
|
+
console.log();
|
|
253
|
+
|
|
254
|
+
// Balance section
|
|
255
|
+
console.log(` ${C.bright}Balance${C.reset}`);
|
|
256
|
+
if (lamports !== null) {
|
|
257
|
+
console.log(` ${C.green}${formatAether(lamports)}${C.reset} ${C.dim}(${lamports} lamports)${C.reset}`);
|
|
258
|
+
if (account.owner) {
|
|
259
|
+
const ownerStr = Array.isArray(account.owner)
|
|
260
|
+
? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
|
|
261
|
+
: account.owner;
|
|
262
|
+
console.log(` ${C.dim}Owner: ${ownerStr}${C.reset}`);
|
|
263
|
+
}
|
|
264
|
+
if (account.rent_epoch !== undefined) {
|
|
265
|
+
console.log(` ${C.dim}Rent epoch: ${account.rent_epoch}${C.reset}`);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
console.log(` ${C.yellow}⚠ Could not fetch balance (account may not exist)${C.reset}`);
|
|
269
|
+
}
|
|
270
|
+
console.log();
|
|
271
|
+
|
|
272
|
+
// Stake positions section
|
|
273
|
+
console.log(` ${C.bright}Stake Positions (${stakeList.length})${C.reset}`);
|
|
274
|
+
if (stakeList.length === 0) {
|
|
275
|
+
console.log(` ${C.dim}No active stake positions.${C.reset}`);
|
|
276
|
+
} else {
|
|
277
|
+
const statusColors = {
|
|
278
|
+
active: C.green,
|
|
279
|
+
inactive: C.yellow,
|
|
280
|
+
activating: C.yellow,
|
|
281
|
+
deactivating: C.red,
|
|
282
|
+
unknown: C.dim,
|
|
283
|
+
};
|
|
284
|
+
for (const sa of stakeList) {
|
|
285
|
+
const status = sa.status || 'unknown';
|
|
286
|
+
const color = statusColors[status] || C.dim;
|
|
287
|
+
const amount = sa.amount ? formatAether(sa.amount) : '0 AETH';
|
|
288
|
+
const validator = sa.validator || 'unknown';
|
|
289
|
+
console.log(` ${C.dim}┌─${C.reset}`);
|
|
290
|
+
console.log(` │ ${C.bright}Validator:${C.reset} ${validator}`);
|
|
291
|
+
console.log(` │ ${C.bright}Amount:${C.reset} ${color}${amount}${C.reset}`);
|
|
292
|
+
console.log(` │ ${C.bright}Status:${C.reset} ${color}${status}${C.reset}`);
|
|
293
|
+
if (sa.stake_account) {
|
|
294
|
+
console.log(` │ ${C.bright}Stake acct:${C.reset} ${truncate(sa.stake_account)}`);
|
|
295
|
+
}
|
|
296
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
console.log();
|
|
300
|
+
|
|
301
|
+
// Recent transactions section
|
|
302
|
+
console.log(` ${C.bright}Recent Transactions (${txList.length})${C.reset}`);
|
|
303
|
+
if (txList.length === 0) {
|
|
304
|
+
console.log(` ${C.dim}No transactions yet.${C.reset}`);
|
|
305
|
+
} else {
|
|
306
|
+
const typeColors = {
|
|
307
|
+
Transfer: C.cyan,
|
|
308
|
+
Stake: C.green,
|
|
309
|
+
Unstake: C.yellow,
|
|
310
|
+
ClaimRewards: C.magenta,
|
|
311
|
+
CreateNFT: C.red,
|
|
312
|
+
MintNFT: C.red,
|
|
313
|
+
TransferNFT: C.cyan,
|
|
314
|
+
UpdateMetadata: C.yellow,
|
|
315
|
+
Unknown: C.dim,
|
|
316
|
+
};
|
|
317
|
+
for (const tx of txList) {
|
|
318
|
+
const txType = tx.tx_type || tx.type || 'Unknown';
|
|
319
|
+
const color = typeColors[txType] || C.dim;
|
|
320
|
+
const sig = tx.signature || tx.id || tx.tx_signature || '—';
|
|
321
|
+
const ts = tx.timestamp ? relativeTime(tx.timestamp) : 'unknown';
|
|
322
|
+
|
|
323
|
+
console.log(` ${C.dim}┌─ ${ts}${C.reset} ${C.bright}${color}${txType}${C.reset} sig:${truncate(sig)}`);
|
|
324
|
+
if (tx.payload && tx.payload.data) {
|
|
325
|
+
const d = tx.payload.data;
|
|
326
|
+
if (d.recipient) console.log(` │ ${C.dim}→ to: ${d.recipient}${C.reset}`);
|
|
327
|
+
if (d.amount) console.log(` │ ${C.dim}amount: ${formatAether(d.amount)}${C.reset}`);
|
|
328
|
+
if (d.validator) console.log(` │ ${C.dim}validator: ${d.validator}${C.reset}`);
|
|
329
|
+
}
|
|
330
|
+
if (tx.fee !== undefined && tx.fee > 0) {
|
|
331
|
+
console.log(` │ ${C.dim}fee: ${tx.fee} lamports${C.reset}`);
|
|
332
|
+
}
|
|
333
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
console.log();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Main command
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
function statsCommand() {
|
|
344
|
+
const args = process.argv.slice(3);
|
|
345
|
+
|
|
346
|
+
// Parse flags
|
|
347
|
+
let address = null;
|
|
348
|
+
let compact = false;
|
|
349
|
+
let asJson = false;
|
|
350
|
+
|
|
351
|
+
for (let i = 0; i < args.length; i++) {
|
|
352
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
353
|
+
address = args[i + 1];
|
|
354
|
+
i++;
|
|
355
|
+
} else if (args[i] === '--compact' || args[i] === '-c') {
|
|
356
|
+
compact = true;
|
|
357
|
+
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
358
|
+
asJson = true;
|
|
359
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
360
|
+
console.log(`
|
|
361
|
+
${C.bright}Aether Wallet Stats${C.reset}
|
|
362
|
+
${C.dim}Comprehensive wallet overview: balance, stakes, recent transactions.${C.reset}
|
|
363
|
+
|
|
364
|
+
${C.bright}Usage:${C.reset}
|
|
365
|
+
aether stats --address <addr> Full dashboard
|
|
366
|
+
aether stats --address <addr> --json JSON output
|
|
367
|
+
aether stats --address <addr> --compact One-line summary
|
|
368
|
+
|
|
369
|
+
${C.bright}Options:${C.reset}
|
|
370
|
+
-a, --address <addr> Wallet address (or set default)
|
|
371
|
+
-j, --json JSON output
|
|
372
|
+
-c, --compact One-line summary
|
|
373
|
+
-h, --help Show this help
|
|
374
|
+
`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Resolve address
|
|
380
|
+
if (!address) {
|
|
381
|
+
const cfg = loadConfig();
|
|
382
|
+
address = cfg.defaultWallet;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!address) {
|
|
386
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
387
|
+
console.log(` ${C.dim}Usage: aether stats --address <address> [--compact] [--json]${C.reset}\n`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Verify wallet file exists
|
|
392
|
+
const wallet = loadWallet(address);
|
|
393
|
+
if (!wallet) {
|
|
394
|
+
console.log(` ${C.red}✗ Wallet not found locally:${C.reset} ${address}`);
|
|
395
|
+
console.log(` ${C.dim}Check your wallets: ${C.cyan}aether wallet list${C.reset}\n`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const rpcUrl = getDefaultRpc();
|
|
400
|
+
|
|
401
|
+
if (!asJson) {
|
|
402
|
+
console.log(` ${C.dim}Fetching stats from ${rpcUrl}...${C.reset}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Fetch and render
|
|
406
|
+
(async () => {
|
|
407
|
+
try {
|
|
408
|
+
const stats = await fetchWalletStats(address, rpcUrl);
|
|
409
|
+
renderDashboard(address, stats, { compact, asJson, rpcUrl });
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.log(` ${C.red}✗ Failed to fetch wallet stats:${C.reset} ${err.message}`);
|
|
412
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
413
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
})();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = { statsCommand };
|
package/index.js
CHANGED
|
@@ -27,6 +27,8 @@ const { epochCommand } = require('./commands/epoch');
|
|
|
27
27
|
const { supplyCommand } = require('./commands/supply');
|
|
28
28
|
const { statusCommand } = require('./commands/status');
|
|
29
29
|
const { broadcastCommand } = require('./commands/broadcast');
|
|
30
|
+
const { apyCommand } = require('./commands/apy');
|
|
31
|
+
const { statsCommand } = require('./commands/stats');
|
|
30
32
|
const readline = require('readline');
|
|
31
33
|
|
|
32
34
|
// CLI version
|
|
@@ -197,11 +199,8 @@ const COMMANDS = {
|
|
|
197
199
|
'stake-positions': {
|
|
198
200
|
description: 'Show current stake positions/delegations — aether stake-positions --address <addr> [--json]',
|
|
199
201
|
handler: () => {
|
|
200
|
-
const {
|
|
201
|
-
|
|
202
|
-
process.argv = [...originalArgv.slice(0, 2), 'wallet', 'stake-positions', ...originalArgv.slice(3)];
|
|
203
|
-
walletCommand();
|
|
204
|
-
process.argv = originalArgv;
|
|
202
|
+
const { stakePositionsCommand } = require('./commands/stake-positions');
|
|
203
|
+
stakePositionsCommand();
|
|
205
204
|
},
|
|
206
205
|
},
|
|
207
206
|
unstake: {
|
|
@@ -355,6 +354,12 @@ const COMMANDS = {
|
|
|
355
354
|
validatorsListCommand();
|
|
356
355
|
},
|
|
357
356
|
},
|
|
357
|
+
stats: {
|
|
358
|
+
description: 'Wallet stats dashboard — balance, stake positions, recent txs — aether stats --address <addr> [--compact] [--json]',
|
|
359
|
+
handler: () => {
|
|
360
|
+
statsCommand();
|
|
361
|
+
},
|
|
362
|
+
},
|
|
358
363
|
price: {
|
|
359
364
|
description: 'AETH/USD price — aether price [--pair AETH/USD] [--json] [--source coingecko]',
|
|
360
365
|
handler: () => {
|
|
@@ -369,6 +374,13 @@ const COMMANDS = {
|
|
|
369
374
|
broadcastCommand();
|
|
370
375
|
},
|
|
371
376
|
},
|
|
377
|
+
apy: {
|
|
378
|
+
description: 'Validator APY estimator — aether apy [--validator <addr>] [--address <addr>] [--json] [--epochs <n>]',
|
|
379
|
+
handler: () => {
|
|
380
|
+
const { apyCommand } = require('./commands/apy');
|
|
381
|
+
apyCommand();
|
|
382
|
+
},
|
|
383
|
+
},
|
|
372
384
|
ping: {
|
|
373
385
|
description: 'Ping RPC endpoint — measure latency, check node health — aether ping [--rpc <url>] [--count <n>] [--json]',
|
|
374
386
|
handler: () => {
|