aether-hub 1.1.2 → 1.1.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.
- package/commands/account.js +304 -0
- package/commands/init.js +1 -1
- package/commands/validator-start.js +46 -13
- package/commands/wallet.js +140 -0
- package/index.js +18 -0
- package/package.json +2 -2
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli account
|
|
4
|
+
*
|
|
5
|
+
* Query detailed on-chain account data for any address.
|
|
6
|
+
* Shows lamports, owner, executable, rent epoch, and program-owned account keys.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* aether account --address <addr> Full account dump
|
|
10
|
+
* aether account --address <addr> --json JSON output for scripting
|
|
11
|
+
* aether account --address <addr> --data Show raw account data as base64/hex
|
|
12
|
+
*
|
|
13
|
+
* Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const https = require('https');
|
|
18
|
+
const bs58 = require('bs58').default;
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// HTTP helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
function httpRequest(rpcUrl, path) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const url = new URL(path, rpcUrl);
|
|
39
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
40
|
+
const req = lib.request({
|
|
41
|
+
hostname: url.hostname,
|
|
42
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
43
|
+
path: url.pathname + url.search,
|
|
44
|
+
method: 'GET',
|
|
45
|
+
timeout: 8000,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
}, (res) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
res.on('data', (chunk) => data += chunk);
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try { resolve(JSON.parse(data)); }
|
|
52
|
+
catch { resolve({ raw: data }); }
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getDefaultRpc() {
|
|
62
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatAether(lamports) {
|
|
66
|
+
const aeth = lamports / 1e9;
|
|
67
|
+
if (aeth === 0) return '0 AETH';
|
|
68
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Parse args
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function parseArgs() {
|
|
76
|
+
return process.argv.slice(3); // [node, index.js, account, ...]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findArg(args, ...flags) {
|
|
80
|
+
for (const flag of flags) {
|
|
81
|
+
const idx = args.indexOf(flag);
|
|
82
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('-')) {
|
|
83
|
+
return { value: args[idx + 1], idx };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseBoolArg(args, ...flags) {
|
|
90
|
+
return flags.some(f => args.includes(f));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Format helpers
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function formatPubkey(key) {
|
|
98
|
+
if (!key) return '—';
|
|
99
|
+
if (Array.isArray(key)) {
|
|
100
|
+
// base64-like or raw bytes → treat as public key bytes
|
|
101
|
+
if (key.length === 32) return 'ATH' + bs58.encode(Buffer.from(key.slice(0, 32)));
|
|
102
|
+
return 'ATH' + bs58.encode(Buffer.from(key));
|
|
103
|
+
}
|
|
104
|
+
if (typeof key === 'string') {
|
|
105
|
+
if (key.startsWith('ATH')) return key;
|
|
106
|
+
// Assume base58
|
|
107
|
+
try { return 'ATH' + bs58.encode(bs58.decode(key)); }
|
|
108
|
+
catch { return key.substring(0, 16) + '...'; }
|
|
109
|
+
}
|
|
110
|
+
return String(key).substring(0, 16);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function base64ToHex(b64) {
|
|
114
|
+
try {
|
|
115
|
+
const buf = Buffer.from(b64, 'base64');
|
|
116
|
+
return buf.toString('hex');
|
|
117
|
+
} catch {
|
|
118
|
+
return b64;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatData(data) {
|
|
123
|
+
if (!data) return null;
|
|
124
|
+
if (typeof data === 'string') return data;
|
|
125
|
+
if (Array.isArray(data)) {
|
|
126
|
+
// raw bytes → hex
|
|
127
|
+
return Buffer.from(data).toString('hex');
|
|
128
|
+
}
|
|
129
|
+
return JSON.stringify(data);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Main account query
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
async function accountCommand() {
|
|
137
|
+
const args = parseArgs();
|
|
138
|
+
|
|
139
|
+
// Parse flags
|
|
140
|
+
const addrArg = findArg(args, '--address', '-a');
|
|
141
|
+
const asJson = parseBoolArg(args, '--json', '-j');
|
|
142
|
+
const showData = parseBoolArg(args, '--data', '-d');
|
|
143
|
+
const rpcArg = findArg(args, '--rpc', '-r');
|
|
144
|
+
const rpcUrl = rpcArg ? rpcArg.value : getDefaultRpc();
|
|
145
|
+
|
|
146
|
+
if (!addrArg) {
|
|
147
|
+
console.log(`\n ${C.red}✗ Missing --address flag.${C.reset}`);
|
|
148
|
+
console.log(`\n ${C.cyan}Usage:${C.reset}`);
|
|
149
|
+
console.log(` ${C.cyan}aether account --address <addr>${C.reset} Show account details`);
|
|
150
|
+
console.log(` ${C.cyan}aether account --address <addr> --json${C.reset} JSON output`);
|
|
151
|
+
console.log(` ${C.cyan}aether account --address <addr> --data${C.reset} Show raw account data`);
|
|
152
|
+
console.log(` ${C.cyan}aether account --address <addr> --rpc <url>${C.reset} Use custom RPC\n`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const rawAddress = addrArg.value;
|
|
157
|
+
const address = rawAddress.startsWith('ATH') ? rawAddress : rawAddress;
|
|
158
|
+
const apiAddress = rawAddress.startsWith('ATH') ? rawAddress.slice(3) : rawAddress;
|
|
159
|
+
|
|
160
|
+
if (!asJson) {
|
|
161
|
+
console.log(`\n${C.bright}${C.cyan}── Account Info ──────────────────────────────────────────${C.reset}\n`);
|
|
162
|
+
console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
|
|
163
|
+
console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
|
|
164
|
+
console.log();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const account = await httpRequest(rpcUrl, `/v1/account/${apiAddress}`);
|
|
169
|
+
|
|
170
|
+
if (!account || account.error) {
|
|
171
|
+
if (asJson) {
|
|
172
|
+
console.log(JSON.stringify({ address, error: account?.error || 'Account not found' }, null, 2));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(` ${C.yellow}⚠ Account not found on chain or RPC error.${C.reset}`);
|
|
175
|
+
console.log(` ${C.dim} This is normal for addresses with no on-chain account.${C.reset}`);
|
|
176
|
+
console.log(` ${C.dim} RPC response: ${JSON.stringify(account?.error || account)}${C.reset}\n`);
|
|
177
|
+
}
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const lamports = account.lamports || account.lamports === 0 ? account.lamports : 0;
|
|
182
|
+
const owner = account.owner || null;
|
|
183
|
+
const executable = account.executable || false;
|
|
184
|
+
const rentEpoch = account.rent_epoch;
|
|
185
|
+
const data = account.data;
|
|
186
|
+
const dataLen = data ? (Array.isArray(data) ? data.length : typeof data === 'string' ? data.length : 0) : 0;
|
|
187
|
+
|
|
188
|
+
if (asJson) {
|
|
189
|
+
let ownerStr = null;
|
|
190
|
+
if (owner) {
|
|
191
|
+
if (Array.isArray(owner)) {
|
|
192
|
+
ownerStr = 'ATH' + bs58.encode(Buffer.from(owner.slice(0, 32)));
|
|
193
|
+
} else if (typeof owner === 'string') {
|
|
194
|
+
ownerStr = owner.startsWith('ATH') ? owner : 'ATH' + bs58.encode(bs58.decode(owner));
|
|
195
|
+
} else {
|
|
196
|
+
ownerStr = String(owner);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(JSON.stringify({
|
|
201
|
+
address,
|
|
202
|
+
rpc: rpcUrl,
|
|
203
|
+
lamports,
|
|
204
|
+
lamports_formatted: formatAether(lamports),
|
|
205
|
+
owner: ownerStr,
|
|
206
|
+
executable,
|
|
207
|
+
rent_epoch: rentEpoch,
|
|
208
|
+
data_size: dataLen,
|
|
209
|
+
data: showData ? formatData(data) : null,
|
|
210
|
+
fetched_at: new Date().toISOString(),
|
|
211
|
+
}, null, 2));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Human-readable output
|
|
216
|
+
console.log(` ${C.green}✓${C.reset} Found on chain`);
|
|
217
|
+
console.log(` ${C.dim}────────────────────────────────────────${C.reset}`);
|
|
218
|
+
console.log(` ${C.dim} Balance:${C.reset} ${C.bright}${formatAether(lamports)}${C.reset} (${lamports.toLocaleString()} lamports)`);
|
|
219
|
+
|
|
220
|
+
if (owner) {
|
|
221
|
+
let ownerStr;
|
|
222
|
+
if (Array.isArray(owner)) {
|
|
223
|
+
ownerStr = 'ATH' + bs58.encode(Buffer.from(owner.slice(0, 32)));
|
|
224
|
+
} else if (typeof owner === 'string') {
|
|
225
|
+
ownerStr = owner.startsWith('ATH') ? owner : 'ATH' + bs58.encode(bs58.decode(owner));
|
|
226
|
+
} else {
|
|
227
|
+
ownerStr = String(owner);
|
|
228
|
+
}
|
|
229
|
+
console.log(` ${C.dim} Owner:${C.reset} ${C.cyan}${ownerStr}${C.reset}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(` ${C.dim} Executable:${C.reset} ${C.bright}${(executable ? 'Yes' : 'No')}${C.reset}`);
|
|
233
|
+
if (rentEpoch !== undefined) {
|
|
234
|
+
console.log(` ${C.dim} Rent epoch:${C.reset} ${rentEpoch}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (dataLen > 0) {
|
|
238
|
+
console.log(` ${C.dim} Data size:${C.reset} ${C.bright}${dataLen} bytes${C.reset}`);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(` ${C.dim} Data size:${C.reset} 0 bytes`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (showData && data) {
|
|
244
|
+
console.log();
|
|
245
|
+
console.log(` ${C.dim} Raw data (hex):${C.reset}`);
|
|
246
|
+
const hex = formatData(data);
|
|
247
|
+
// Pretty-print in 32-byte chunks
|
|
248
|
+
const chunks = hex.match(/.{1,64}/g) || [];
|
|
249
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
250
|
+
const offset = (i * 32).toString(16).padStart(8, '0');
|
|
251
|
+
console.log(` ${C.dim}${offset}:${C.reset} ${chunks[i]}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log();
|
|
256
|
+
console.log(` ${C.dim}────────────────────────────────────────${C.reset}`);
|
|
257
|
+
|
|
258
|
+
// Additional context: try to determine account type
|
|
259
|
+
let accountType = 'Unknown';
|
|
260
|
+
if (owner) {
|
|
261
|
+
const ownerStr = Array.isArray(owner)
|
|
262
|
+
? bs58.encode(Buffer.from(owner.slice(0, 32)))
|
|
263
|
+
: (typeof owner === 'string' && !owner.startsWith('ATH') ? owner : '');
|
|
264
|
+
// Known program IDs (these are common Aether program identifiers)
|
|
265
|
+
const knownPrograms = {
|
|
266
|
+
'STAKE': 'Stake Program',
|
|
267
|
+
'SYSTEM': 'System Program',
|
|
268
|
+
'VOTE': 'Vote Program',
|
|
269
|
+
'TOKEN': 'Token Program',
|
|
270
|
+
'MEMO': 'Memo Program',
|
|
271
|
+
};
|
|
272
|
+
const prog = knownPrograms[ownerStr.toUpperCase()];
|
|
273
|
+
if (prog) accountType = prog;
|
|
274
|
+
else if (executable) accountType = 'Executable Program';
|
|
275
|
+
else if (dataLen === 0) accountType = 'Empty Account (no data)';
|
|
276
|
+
else accountType = 'Data Account';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(` ${C.dim} Account type:${C.reset} ${C.bright}${accountType}${C.reset}`);
|
|
280
|
+
console.log();
|
|
281
|
+
console.log(` ${C.dim} Fetch via API:${C.reset} GET ${rpcUrl}/v1/account/${apiAddress}`);
|
|
282
|
+
console.log();
|
|
283
|
+
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (asJson) {
|
|
286
|
+
console.log(JSON.stringify({ address, error: err.message }, null, 2));
|
|
287
|
+
} else {
|
|
288
|
+
console.log(` ${C.red}✗ Failed to fetch account:${C.reset} ${err.message}`);
|
|
289
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
290
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
291
|
+
}
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Export & run
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
module.exports = { accountCommand };
|
|
301
|
+
|
|
302
|
+
if (require.main === module) {
|
|
303
|
+
accountCommand();
|
|
304
|
+
}
|
package/commands/init.js
CHANGED
|
@@ -501,7 +501,7 @@ async function connectTestnet(rl, tier = 'full') {
|
|
|
501
501
|
rl.close();
|
|
502
502
|
|
|
503
503
|
const validatorStart = require('./validator-start');
|
|
504
|
-
validatorStart.validatorStart(tier);
|
|
504
|
+
validatorStart.validatorStart({ testnet: true, tier });
|
|
505
505
|
return true;
|
|
506
506
|
}
|
|
507
507
|
} catch (err) {
|
|
@@ -64,8 +64,9 @@ function findValidatorBinary() {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Parse command line args for validator-start
|
|
67
|
+
* @param {Object} overrideOptions - Options passed directly (e.g. from init.js)
|
|
67
68
|
*/
|
|
68
|
-
function parseArgs() {
|
|
69
|
+
function parseArgs(overrideOptions = {}) {
|
|
69
70
|
const args = process.argv.slice(3); // Skip 'aether-cli validator start'
|
|
70
71
|
|
|
71
72
|
const options = {
|
|
@@ -75,6 +76,7 @@ function parseArgs() {
|
|
|
75
76
|
identity: null,
|
|
76
77
|
verbose: false,
|
|
77
78
|
tier: 'full',
|
|
79
|
+
...overrideOptions, // Allow init.js to pass testnet/tier directly
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -121,7 +123,10 @@ ${colors.cyan}╚═════════════════════
|
|
|
121
123
|
`);
|
|
122
124
|
|
|
123
125
|
console.log(` ${colors.bright}Network:${colors.reset}`);
|
|
124
|
-
|
|
126
|
+
const modeStr = options.testnet
|
|
127
|
+
? colors.yellow + 'TESTNET'
|
|
128
|
+
: colors.red + 'MAINNET';
|
|
129
|
+
console.log(` Mode: ${modeStr}`);
|
|
125
130
|
console.log(` Tier: ${tierLabel}`);
|
|
126
131
|
console.log(` RPC: http://${options.rpcAddr}`);
|
|
127
132
|
console.log(` P2P: ${options.p2pAddr}`);
|
|
@@ -136,16 +141,38 @@ ${colors.cyan}╚═════════════════════
|
|
|
136
141
|
*/
|
|
137
142
|
function buildValidator() {
|
|
138
143
|
const { execSync } = require('child_process');
|
|
144
|
+
const platform = os.platform();
|
|
145
|
+
const isWindows = platform === 'win32';
|
|
139
146
|
const workspaceRoot = path.join(__dirname, '..', '..');
|
|
140
147
|
const repoPath = path.join(workspaceRoot, 'Jelly-legs-unsteady-workshop');
|
|
141
148
|
|
|
142
149
|
console.log(` ${colors.cyan}Building aether-validator...${colors.reset}`);
|
|
143
150
|
|
|
144
151
|
try {
|
|
145
|
-
|
|
152
|
+
// Use full cargo path on Windows to avoid spawnSync ENOENT
|
|
153
|
+
const cargoPaths = isWindows
|
|
154
|
+
? [
|
|
155
|
+
path.join(process.env.USERPROFILE || '', '.cargo', 'bin', 'cargo.exe'),
|
|
156
|
+
path.join(process.env.LOCALAPPDATA || '', 'Rust', 'bin', 'cargo.exe'),
|
|
157
|
+
'C:\\Users\\RM_Ga\\.cargo\\bin\\cargo.exe',
|
|
158
|
+
'cargo',
|
|
159
|
+
]
|
|
160
|
+
: ['cargo'];
|
|
161
|
+
|
|
162
|
+
let cargoCmd = 'cargo';
|
|
163
|
+
for (const cp of cargoPaths) {
|
|
164
|
+
if (cp === 'cargo' || fs.existsSync(cp)) {
|
|
165
|
+
cargoCmd = cp;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(` ${colors.cyan}Running: ${cargoCmd} build --release --package aether-validator${colors.reset}`);
|
|
171
|
+
|
|
172
|
+
// Use execSync WITHOUT shell:true — avoids Windows spawnSync cmd.exe ENOENT
|
|
173
|
+
execSync(`${cargoCmd} build --release --package aether-validator`, {
|
|
146
174
|
cwd: repoPath,
|
|
147
175
|
stdio: 'inherit',
|
|
148
|
-
shell: true,
|
|
149
176
|
});
|
|
150
177
|
|
|
151
178
|
// Re-check for binary
|
|
@@ -165,18 +192,24 @@ function buildValidator() {
|
|
|
165
192
|
|
|
166
193
|
/**
|
|
167
194
|
* Main validator start command
|
|
195
|
+
* @param {Object|null} options - { testnet?: boolean, tier?: string }
|
|
168
196
|
*/
|
|
169
|
-
function validatorStart(
|
|
170
|
-
|
|
197
|
+
function validatorStart(options = {}) {
|
|
198
|
+
// Support both old string-style (tier only) and new object-style { testnet, tier }
|
|
199
|
+
const parsedArgs = parseArgs(typeof options === 'object' ? options : { tier: options });
|
|
200
|
+
const optionsObj = typeof options === 'object' ? options : {};
|
|
171
201
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
202
|
+
// Merge: explicit options override parseArgs defaults
|
|
203
|
+
const finalOptions = {
|
|
204
|
+
...parsedArgs,
|
|
205
|
+
...optionsObj,
|
|
206
|
+
tier: optionsObj.tier || parsedArgs.tier,
|
|
207
|
+
testnet: optionsObj.testnet !== undefined ? optionsObj.testnet : parsedArgs.testnet,
|
|
208
|
+
};
|
|
176
209
|
|
|
177
210
|
let result = findValidatorBinary();
|
|
178
211
|
|
|
179
|
-
printBanner(
|
|
212
|
+
printBanner(finalOptions);
|
|
180
213
|
|
|
181
214
|
// Handle missing binary
|
|
182
215
|
if (result.type === 'missing') {
|
|
@@ -203,12 +236,12 @@ function validatorStart(overrideTier = null) {
|
|
|
203
236
|
process.exit(1);
|
|
204
237
|
}
|
|
205
238
|
result = built;
|
|
206
|
-
startValidatorProcess(result,
|
|
239
|
+
startValidatorProcess(result, finalOptions);
|
|
207
240
|
});
|
|
208
241
|
return;
|
|
209
242
|
}
|
|
210
243
|
|
|
211
|
-
startValidatorProcess(result,
|
|
244
|
+
startValidatorProcess(result, finalOptions);
|
|
212
245
|
}
|
|
213
246
|
|
|
214
247
|
/**
|
package/commands/wallet.js
CHANGED
|
@@ -1106,6 +1106,143 @@ async function txHistory(rl) {
|
|
|
1106
1106
|
}
|
|
1107
1107
|
}
|
|
1108
1108
|
|
|
1109
|
+
// ---------------------------------------------------------------------------
|
|
1110
|
+
// STAKE POSITIONS
|
|
1111
|
+
// Query and display current stake positions/delegations for a wallet
|
|
1112
|
+
// ---------------------------------------------------------------------------
|
|
1113
|
+
|
|
1114
|
+
async function stakePositions(rl) {
|
|
1115
|
+
console.log(`\n${C.bright}${C.cyan}── Stake Positions ──────────────────────────────────────${C.reset}\n`);
|
|
1116
|
+
|
|
1117
|
+
const args = process.argv.slice(4);
|
|
1118
|
+
let address = null;
|
|
1119
|
+
let asJson = false;
|
|
1120
|
+
|
|
1121
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
1122
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
1123
|
+
address = args[addrIdx + 1];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
asJson = args.includes('--json') || args.includes('-j');
|
|
1127
|
+
|
|
1128
|
+
if (!address) {
|
|
1129
|
+
const cfg = loadConfig();
|
|
1130
|
+
address = cfg.defaultWallet;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (!address) {
|
|
1134
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
1135
|
+
console.log(` ${C.dim}Usage: aether wallet stake-positions --address <addr> [--json]${C.reset}\n`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const rpcUrl = getDefaultRpc();
|
|
1140
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
1141
|
+
|
|
1142
|
+
if (!asJson) {
|
|
1143
|
+
console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
|
|
1144
|
+
console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
|
|
1145
|
+
console.log();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
try {
|
|
1149
|
+
// Fetch stake accounts for this address
|
|
1150
|
+
const res = await httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`);
|
|
1151
|
+
|
|
1152
|
+
let stakeAccounts = [];
|
|
1153
|
+
if (res && !res.error) {
|
|
1154
|
+
stakeAccounts = Array.isArray(res) ? res : (res.accounts || res.stake_accounts || []);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (asJson) {
|
|
1158
|
+
const out = {
|
|
1159
|
+
address,
|
|
1160
|
+
rpc: rpcUrl,
|
|
1161
|
+
stake_accounts: stakeAccounts.map(acc => ({
|
|
1162
|
+
stake_account: acc.pubkey || acc.publicKey || acc.account,
|
|
1163
|
+
validator: acc.validator || acc.voter || acc.vote_account,
|
|
1164
|
+
stake_lamports: acc.stake_lamports || acc.lamports || 0,
|
|
1165
|
+
stake_aeth: (acc.stake_lamports || acc.lamports || 0) / 1e9,
|
|
1166
|
+
status: acc.status || acc.state || 'unknown',
|
|
1167
|
+
activation_epoch: acc.activation_epoch,
|
|
1168
|
+
deactivation_epoch: acc.deactivation_epoch,
|
|
1169
|
+
rewards_earned: acc.rewards_earned || 0,
|
|
1170
|
+
})),
|
|
1171
|
+
total_staked_lamports: stakeAccounts.reduce((sum, acc) => sum + (acc.stake_lamports || acc.lamports || 0), 0),
|
|
1172
|
+
fetched_at: new Date().toISOString(),
|
|
1173
|
+
};
|
|
1174
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (!stakeAccounts || stakeAccounts.length === 0) {
|
|
1179
|
+
console.log(` ${C.yellow}⚠ No active stake positions found.${C.reset}`);
|
|
1180
|
+
console.log(` ${C.dim} This wallet has not delegated to any validators.${C.reset}`);
|
|
1181
|
+
console.log(` ${C.dim} Stake AETH with: ${C.cyan}aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
let totalStaked = 0;
|
|
1186
|
+
let activeCount = 0;
|
|
1187
|
+
let deactivatingCount = 0;
|
|
1188
|
+
let inactiveCount = 0;
|
|
1189
|
+
|
|
1190
|
+
console.log(` ${C.bright}Stake Positions (${stakeAccounts.length})${C.reset}\n`);
|
|
1191
|
+
|
|
1192
|
+
const statusColors = {
|
|
1193
|
+
active: C.green,
|
|
1194
|
+
activating: C.cyan,
|
|
1195
|
+
deactivating: C.yellow,
|
|
1196
|
+
inactive: C.dim,
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
for (const acc of stakeAccounts) {
|
|
1200
|
+
const stakeAcct = acc.pubkey || acc.publicKey || acc.account || 'unknown';
|
|
1201
|
+
const validator = acc.validator || acc.voter || acc.vote_account || 'unknown';
|
|
1202
|
+
const lamports = acc.stake_lamports || acc.lamports || 0;
|
|
1203
|
+
const aeth = lamports / 1e9;
|
|
1204
|
+
const status = (acc.status || acc.state || 'unknown').toLowerCase();
|
|
1205
|
+
const rewards = acc.rewards_earned || 0;
|
|
1206
|
+
|
|
1207
|
+
totalStaked += lamports;
|
|
1208
|
+
|
|
1209
|
+
if (status === 'active') activeCount++;
|
|
1210
|
+
else if (status === 'deactivating' || status === 'deactivated') deactivatingCount++;
|
|
1211
|
+
else inactiveCount++;
|
|
1212
|
+
|
|
1213
|
+
const statusColor = statusColors[status] || C.reset;
|
|
1214
|
+
const shortAcct = stakeAcct.length > 20 ? stakeAcct.slice(0, 8) + '…' + stakeAcct.slice(-8) : stakeAcct;
|
|
1215
|
+
const shortVal = validator.length > 20 ? validator.slice(0, 8) + '…' + validator.slice(-8) : validator;
|
|
1216
|
+
|
|
1217
|
+
console.log(` ${C.dim}┌─ ${C.bright}${statusColor}${status.toUpperCase()}${C.reset}`);
|
|
1218
|
+
console.log(` │ ${C.dim}Stake acct:${C.reset} ${shortAcct}`);
|
|
1219
|
+
console.log(` │ ${C.dim}Validator:${C.reset} ${shortVal}`);
|
|
1220
|
+
console.log(` │ ${C.dim}Staked:${C.reset} ${C.bright}${aeth.toFixed(4)} AETH${C.reset} (${lamports.toLocaleString()} lamports)`);
|
|
1221
|
+
if (rewards > 0) {
|
|
1222
|
+
console.log(` │ ${C.dim}Rewards:${C.reset} ${C.green}+${(rewards / 1e9).toFixed(4)} AETH${C.reset}`);
|
|
1223
|
+
}
|
|
1224
|
+
if (acc.activation_epoch !== undefined) {
|
|
1225
|
+
console.log(` │ ${C.dim}Activated:${C.reset} epoch ${acc.activation_epoch}`);
|
|
1226
|
+
}
|
|
1227
|
+
if (acc.deactivation_epoch !== undefined) {
|
|
1228
|
+
console.log(` │ ${C.dim}Deactivates:${C.reset} epoch ${acc.deactivation_epoch}`);
|
|
1229
|
+
}
|
|
1230
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
1231
|
+
console.log();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
console.log(` ${C.bright}Summary:${C.reset}`);
|
|
1235
|
+
console.log(` ${C.dim} Total staked:${C.reset} ${C.bright}${(totalStaked / 1e9).toFixed(4)} AETH${C.reset} (${totalStaked.toLocaleString()} lamports)`);
|
|
1236
|
+
console.log(` ${C.green} ● Active:${C.reset} ${activeCount} ${C.yellow}● Deactivating:${C.reset} ${deactivatingCount} ${C.dim}● Inactive:${C.reset} ${inactiveCount}`);
|
|
1237
|
+
console.log();
|
|
1238
|
+
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
console.log(` ${C.red}✗ Failed to fetch stake positions:${C.reset} ${err.message}`);
|
|
1241
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
1242
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1109
1246
|
// ---------------------------------------------------------------------------
|
|
1110
1247
|
// UNSTAKE
|
|
1111
1248
|
// Submit an Unstake transaction via POST /v1/tx to deactivate stake
|
|
@@ -1298,6 +1435,8 @@ async function walletCommand() {
|
|
|
1298
1435
|
await balanceWallet(rl);
|
|
1299
1436
|
} else if (subcmd === 'stake') {
|
|
1300
1437
|
await stakeWallet(rl);
|
|
1438
|
+
} else if (subcmd === 'stake-positions') {
|
|
1439
|
+
await stakePositions(rl);
|
|
1301
1440
|
} else if (subcmd === 'unstake') {
|
|
1302
1441
|
await unstakeWallet(rl);
|
|
1303
1442
|
} else if (subcmd === 'transfer') {
|
|
@@ -1314,6 +1453,7 @@ async function walletCommand() {
|
|
|
1314
1453
|
console.log(` ${C.cyan}aether wallet connect${C.reset} Connect wallet via browser verification`);
|
|
1315
1454
|
console.log(` ${C.cyan}aether wallet balance${C.reset} Query chain balance for an address`);
|
|
1316
1455
|
console.log(` ${C.cyan}aether wallet stake${C.reset} Stake AETH to a validator`);
|
|
1456
|
+
console.log(` ${C.cyan}aether wallet stake-positions${C.reset} Show current stake delegations and rewards`);
|
|
1317
1457
|
console.log(` ${C.cyan}aether wallet unstake${C.reset} Unstake AETH — deactivate a stake account`);
|
|
1318
1458
|
console.log(` ${C.cyan}aether wallet transfer${C.reset} Transfer AETH to another address`);
|
|
1319
1459
|
console.log(` ${C.cyan}aether wallet history${C.reset} Show recent transactions for an address`);
|
package/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const { networkCommand } = require('./commands/network');
|
|
|
19
19
|
const { validatorsListCommand } = require('./commands/validators');
|
|
20
20
|
const { delegationsCommand } = require('./commands/delegations');
|
|
21
21
|
const { rewardsCommand } = require('./commands/rewards');
|
|
22
|
+
const { accountCommand } = require('./commands/account');
|
|
22
23
|
const { emergencyCommand } = require('./commands/emergency');
|
|
23
24
|
const readline = require('readline');
|
|
24
25
|
|
|
@@ -187,6 +188,16 @@ const COMMANDS = {
|
|
|
187
188
|
process.argv = originalArgv;
|
|
188
189
|
},
|
|
189
190
|
},
|
|
191
|
+
'stake-positions': {
|
|
192
|
+
description: 'Show current stake positions/delegations — aether stake-positions --address <addr> [--json]',
|
|
193
|
+
handler: () => {
|
|
194
|
+
const { walletCommand } = require('./commands/wallet');
|
|
195
|
+
const originalArgv = process.argv;
|
|
196
|
+
process.argv = [...originalArgv.slice(0, 2), 'wallet', 'stake-positions', ...originalArgv.slice(3)];
|
|
197
|
+
walletCommand();
|
|
198
|
+
process.argv = originalArgv;
|
|
199
|
+
},
|
|
200
|
+
},
|
|
190
201
|
unstake: {
|
|
191
202
|
description: 'Unstake AETH — deactivate a stake account — aether unstake --account <stakeAcct> [--amount <aeth>]',
|
|
192
203
|
handler: () => {
|
|
@@ -283,6 +294,13 @@ const COMMANDS = {
|
|
|
283
294
|
snapshotCommand();
|
|
284
295
|
},
|
|
285
296
|
},
|
|
297
|
+
account: {
|
|
298
|
+
description: 'Query on-chain account data — aether account --address <addr> [--json] [--data] [--rpc <url>]',
|
|
299
|
+
handler: () => {
|
|
300
|
+
const { accountCommand } = require('./commands/account');
|
|
301
|
+
accountCommand();
|
|
302
|
+
},
|
|
303
|
+
},
|
|
286
304
|
validators: {
|
|
287
305
|
description: 'List active validators — aether validators list [--tier full|lite|observer] [--json]',
|
|
288
306
|
handler: () => {
|
package/package.json
CHANGED