@jellylegsai/aether-cli 1.8.0
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/LICENSE +21 -0
- package/README.md +110 -0
- package/aether-cli-1.0.0.tgz +0 -0
- package/aether-cli-1.8.0.tgz +0 -0
- package/aether-hub-1.0.5.tgz +0 -0
- package/aether-hub-1.1.8.tgz +0 -0
- package/aether-hub-1.2.1.tgz +0 -0
- package/commands/account.js +280 -0
- package/commands/apy.js +499 -0
- package/commands/balance.js +241 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +387 -0
- package/commands/claim.js +490 -0
- package/commands/config.js +851 -0
- package/commands/delegations.js +582 -0
- package/commands/doctor.js +769 -0
- package/commands/emergency.js +667 -0
- package/commands/epoch.js +275 -0
- package/commands/fees.js +276 -0
- package/commands/index.js +78 -0
- package/commands/info.js +495 -0
- package/commands/init.js +816 -0
- package/commands/install.js +666 -0
- package/commands/kyc.js +272 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/multisig.js +701 -0
- package/commands/network.js +429 -0
- package/commands/nft.js +857 -0
- package/commands/ping.js +266 -0
- package/commands/price.js +253 -0
- package/commands/rewards.js +931 -0
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +656 -0
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +470 -0
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +205 -0
- package/commands/stake.js +516 -0
- package/commands/stats.js +396 -0
- package/commands/status.js +327 -0
- package/commands/supply.js +391 -0
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +346 -0
- package/commands/unstake.js +597 -0
- package/commands/validator-info.js +657 -0
- package/commands/validator-register.js +593 -0
- package/commands/validator-start.js +323 -0
- package/commands/validator-status.js +227 -0
- package/commands/validators.js +626 -0
- package/commands/wallet.js +1570 -0
- package/index.js +593 -0
- package/lib/errors.js +398 -0
- package/package.json +76 -0
- package/sdk/README.md +210 -0
- package/sdk/index.js +1639 -0
- package/sdk/package.json +34 -0
- package/sdk/rpc.js +254 -0
- package/sdk/test.js +85 -0
- package/test/doctor.test.js +76 -0
- package/validator-identity.json +4 -0
|
@@ -0,0 +1,1570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aether-cli wallet
|
|
3
|
+
*
|
|
4
|
+
* Aether wallet management:
|
|
5
|
+
* aether wallet create — Create new BIP39 wallet or import existing
|
|
6
|
+
* aether wallet list — List all wallets
|
|
7
|
+
* aether wallet import — Import wallet from mnemonic
|
|
8
|
+
* aether wallet default — Show/set default wallet
|
|
9
|
+
* aether wallet connect — Connect wallet via browser verification
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
const bip39 = require('bip39');
|
|
19
|
+
const nacl = require('tweetnacl');
|
|
20
|
+
const bs58 = require('bs58').default;
|
|
21
|
+
|
|
22
|
+
// Import SDK for blockchain RPC calls
|
|
23
|
+
const aether = require('../sdk');
|
|
24
|
+
|
|
25
|
+
// Destructure SDK functions for convenience
|
|
26
|
+
const { createClient } = aether;
|
|
27
|
+
|
|
28
|
+
// ANSI colours
|
|
29
|
+
const C = {
|
|
30
|
+
reset: '\x1b[0m',
|
|
31
|
+
bright: '\x1b[1m',
|
|
32
|
+
green: '\x1b[32m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
cyan: '\x1b[36m',
|
|
35
|
+
red: '\x1b[31m',
|
|
36
|
+
dim: '\x1b[2m',
|
|
37
|
+
magenta: '\x1b[35m',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// CLI version for session files
|
|
41
|
+
const CLI_VERSION = '1.0.3';
|
|
42
|
+
|
|
43
|
+
// Derivation path for Aether wallets
|
|
44
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Paths
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function getAetherDir() {
|
|
51
|
+
return path.join(os.homedir(), '.aether');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getWalletsDir() {
|
|
55
|
+
return path.join(getAetherDir(), 'wallets');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getSessionsDir() {
|
|
59
|
+
return path.join(getAetherDir(), 'sessions');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getConfigPath() {
|
|
63
|
+
return path.join(getAetherDir(), 'config.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ensureDirs() {
|
|
67
|
+
const wd = getWalletsDir();
|
|
68
|
+
if (!fs.existsSync(wd)) fs.mkdirSync(wd, { recursive: true });
|
|
69
|
+
const sd = getSessionsDir();
|
|
70
|
+
if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadConfig() {
|
|
74
|
+
const p = getConfigPath();
|
|
75
|
+
if (!fs.existsSync(p)) return { defaultWallet: null, version: 1 };
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
78
|
+
} catch {
|
|
79
|
+
return { defaultWallet: null, version: 1 };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function saveConfig(cfg) {
|
|
84
|
+
ensureDirs();
|
|
85
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Crypto helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Derive an Ed25519 keypair from a BIP39 seed.
|
|
94
|
+
* BIP39 seed → 64-byte seed → TweetNaCl keypair
|
|
95
|
+
*/
|
|
96
|
+
function deriveKeypair(mnemonic, derivationPath) {
|
|
97
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
98
|
+
throw new Error('Invalid mnemonic phrase.');
|
|
99
|
+
}
|
|
100
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
101
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
102
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
103
|
+
return {
|
|
104
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
105
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format Aether address: ATH + base58check of public key.
|
|
111
|
+
*/
|
|
112
|
+
function formatAddress(publicKey) {
|
|
113
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Session management helpers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
function sessionFilePath(token) {
|
|
121
|
+
return path.join(getSessionsDir(), `${token}.json`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Generate a UUID v4 session token */
|
|
125
|
+
function generateSessionToken() {
|
|
126
|
+
return crypto.randomUUID();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Save session to ~/.aether/sessions/<uuid>.json
|
|
131
|
+
* Fields: wallet_address, created_at, expires_at, verified, cli_version
|
|
132
|
+
*/
|
|
133
|
+
function saveSession(token, wallet_address, expires_in_minutes = 10) {
|
|
134
|
+
ensureDirs();
|
|
135
|
+
const now = new Date();
|
|
136
|
+
const expires_at = new Date(now.getTime() + expires_in_minutes * 60 * 1000);
|
|
137
|
+
const session = {
|
|
138
|
+
wallet_address,
|
|
139
|
+
created_at: now.toISOString(),
|
|
140
|
+
expires_at: expires_at.toISOString(),
|
|
141
|
+
verified: false,
|
|
142
|
+
cli_version: CLI_VERSION,
|
|
143
|
+
};
|
|
144
|
+
fs.writeFileSync(sessionFilePath(token), JSON.stringify(session, null, 2));
|
|
145
|
+
return session;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Load a session, or return null if missing or expired */
|
|
149
|
+
function getSession(token) {
|
|
150
|
+
const fp = sessionFilePath(token);
|
|
151
|
+
if (!fs.existsSync(fp)) return null;
|
|
152
|
+
try {
|
|
153
|
+
const session = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
154
|
+
if (new Date(session.expires_at) < new Date()) return null;
|
|
155
|
+
return session;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Mark a session as verified */
|
|
162
|
+
function markSessionVerified(token) {
|
|
163
|
+
const session = getSession(token);
|
|
164
|
+
if (!session) return false;
|
|
165
|
+
session.verified = true;
|
|
166
|
+
fs.writeFileSync(sessionFilePath(token), JSON.stringify(session, null, 2));
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Delete a session file */
|
|
171
|
+
function deleteSession(token) {
|
|
172
|
+
const fp = sessionFilePath(token);
|
|
173
|
+
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Poll ~/.aether/sessions/<token>.json every 2 seconds.
|
|
178
|
+
* Resolves when verified=true OR session expired/timeout.
|
|
179
|
+
* Returns { verified: boolean, reason?: 'expired' | 'timeout' }
|
|
180
|
+
*/
|
|
181
|
+
async function pollForVerification(token, timeout_ms = 600000) {
|
|
182
|
+
const interval_ms = 2000;
|
|
183
|
+
const max_retries = Math.floor(timeout_ms / interval_ms);
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < max_retries; i++) {
|
|
186
|
+
const session = getSession(token);
|
|
187
|
+
if (session && session.verified) {
|
|
188
|
+
return { verified: true };
|
|
189
|
+
}
|
|
190
|
+
if (!session) {
|
|
191
|
+
return { verified: false, reason: 'expired' };
|
|
192
|
+
}
|
|
193
|
+
await new Promise((res) => setTimeout(res, interval_ms));
|
|
194
|
+
}
|
|
195
|
+
return { verified: false, reason: 'timeout' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Get the site URL from env var or default */
|
|
199
|
+
function getSiteUrl() {
|
|
200
|
+
return process.env.AETHER_SITE_URL || 'https://jelly-legs-ai.github.io';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Open URL in the default browser (cross-platform) */
|
|
204
|
+
function openBrowser(url) {
|
|
205
|
+
const platform = os.platform();
|
|
206
|
+
try {
|
|
207
|
+
if (platform === 'win32') {
|
|
208
|
+
execSync(`start "" "${url}"`, { shell: 'cmd' });
|
|
209
|
+
} else if (platform === 'darwin') {
|
|
210
|
+
execSync(`open "${url}"`);
|
|
211
|
+
} else {
|
|
212
|
+
execSync(`xdg-open "${url}"`);
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Wallet file helpers
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function walletFilePath(address) {
|
|
225
|
+
return path.join(getWalletsDir(), `${address}.json`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function loadWallet(address) {
|
|
229
|
+
const fp = walletFilePath(address);
|
|
230
|
+
if (!fs.existsSync(fp)) return null;
|
|
231
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function saveWalletFile(address, publicKey) {
|
|
235
|
+
ensureDirs();
|
|
236
|
+
const data = {
|
|
237
|
+
version: 1,
|
|
238
|
+
address,
|
|
239
|
+
public_key: bs58.encode(publicKey),
|
|
240
|
+
created_at: new Date().toISOString(),
|
|
241
|
+
derivation_path: DERIVATION_PATH,
|
|
242
|
+
};
|
|
243
|
+
fs.writeFileSync(walletFilePath(address), JSON.stringify(data, null, 2));
|
|
244
|
+
return data;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Readline helpers
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
function createRl() {
|
|
252
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function question(rl, q) {
|
|
256
|
+
return new Promise((res) => rl.question(q, res));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function askMnemonic(rl, questionText) {
|
|
260
|
+
console.log(`\n${C.cyan}${questionText}${C.reset}`);
|
|
261
|
+
console.log(`${C.dim}Enter your ${C.bright}12 or 24${C.reset}${C.dim}-word mnemonic phrase, one space-separated line:${C.reset}`);
|
|
262
|
+
const raw = await question(rl, ` > ${C.reset}`);
|
|
263
|
+
return raw.trim().toLowerCase();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// CREATE WALLET
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
async function createWallet(rl) {
|
|
271
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Creation ─────────────────────────────────────${C.reset}`);
|
|
272
|
+
console.log(` ${C.green}1)${C.reset} Create new wallet — generates a fresh 12-word mnemonic`);
|
|
273
|
+
console.log(` ${C.green}2)${C.reset} Import existing — enter your own mnemonic to restore\n`);
|
|
274
|
+
|
|
275
|
+
const choice = await question(rl, ` Choose [1/2]: ${C.reset}`);
|
|
276
|
+
|
|
277
|
+
let mnemonic;
|
|
278
|
+
if (choice.trim() === '1') {
|
|
279
|
+
mnemonic = bip39.generateMnemonic(128);
|
|
280
|
+
} else if (choice.trim() === '2') {
|
|
281
|
+
mnemonic = await askMnemonic(rl, 'Importing existing wallet');
|
|
282
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
283
|
+
console.log(`\n ${C.red}✗ Invalid BIP39 mnemonic.${C.reset} Please check your word list and try again.`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
console.log(`\n ${C.red}✗ Invalid choice.${C.reset} Run \`aether wallet create\` again.`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let keyPair;
|
|
292
|
+
try {
|
|
293
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
console.log(`\n ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const address = formatAddress(keyPair.publicKey);
|
|
300
|
+
|
|
301
|
+
if (choice.trim() === '1') {
|
|
302
|
+
const words = mnemonic.split(' ');
|
|
303
|
+
console.log(`\n`);
|
|
304
|
+
console.log(`${C.red}${C.bright}╔═══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
305
|
+
console.log(`${C.red}${C.bright}║ YOUR WALLET PASSPHRASE ║${C.reset}`);
|
|
306
|
+
console.log(`${C.red}${C.bright}╚═══════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
307
|
+
console.log(`\n${C.yellow} Write these words down. They cannot be recovered.${C.reset}`);
|
|
308
|
+
console.log(`${C.yellow} No copy is stored. If you lose them, your wallet is UNRECOVERABLE.${C.reset}\n`);
|
|
309
|
+
console.log(` ${C.bright}1.${C.reset} ${words[0].padEnd(15)} ${C.bright}5.${C.reset} ${words[4].padEnd(15)} ${C.bright}9.${C.reset} ${words[8]}`);
|
|
310
|
+
console.log(` ${C.bright}2.${C.reset} ${words[1].padEnd(15)} ${C.bright}6.${C.reset} ${words[5].padEnd(15)} ${C.bright}10.${C.reset} ${words[9]}`);
|
|
311
|
+
console.log(` ${C.bright}3.${C.reset} ${words[2].padEnd(15)} ${C.bright}7.${C.reset} ${words[6].padEnd(15)} ${C.bright}11.${C.reset} ${words[10]}`);
|
|
312
|
+
console.log(` ${C.bright}4.${C.reset} ${words[3].padEnd(15)} ${C.bright}8.${C.reset} ${words[7].padEnd(15)} ${C.bright}12.${C.reset} ${words[11]}`);
|
|
313
|
+
console.log(`\n`);
|
|
314
|
+
await question(rl, ` ${C.cyan}Press Enter when you have saved your passphrase.${C.reset}\n`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const walletData = saveWalletFile(address, keyPair.publicKey);
|
|
318
|
+
const cfg = loadConfig();
|
|
319
|
+
cfg.defaultWallet = address;
|
|
320
|
+
saveConfig(cfg);
|
|
321
|
+
|
|
322
|
+
console.log(`${C.green}✓ Wallet created:${C.reset} ${C.bright}${address}${C.reset}`);
|
|
323
|
+
console.log(`${C.dim} Saved to:${C.reset} ${walletFilePath(address)}`);
|
|
324
|
+
console.log(`${C.green}✓ Set as default wallet.${C.reset}\n`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// LIST WALLETS
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
async function listWallets(rl) {
|
|
332
|
+
ensureDirs();
|
|
333
|
+
const cfg = loadConfig();
|
|
334
|
+
const defaultWallet = cfg.defaultWallet;
|
|
335
|
+
|
|
336
|
+
let files;
|
|
337
|
+
try {
|
|
338
|
+
files = fs.readdirSync(getWalletsDir()).filter((f) => f.endsWith('.json'));
|
|
339
|
+
} catch {
|
|
340
|
+
files = [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (files.length === 0) {
|
|
344
|
+
console.log(`\n ${C.dim}No wallets found. Create one with:${C.reset}`);
|
|
345
|
+
console.log(` ${C.cyan}aether wallet create${C.reset}\n`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log(`\n${C.bright}${C.cyan}── Aether Wallets ─────────────────────────────────────────${C.reset}\n`);
|
|
350
|
+
console.log(` ${C.dim}Location: ${getWalletsDir()}${C.reset}\n`);
|
|
351
|
+
|
|
352
|
+
const wallets = files
|
|
353
|
+
.map((f) => {
|
|
354
|
+
try {
|
|
355
|
+
return JSON.parse(fs.readFileSync(path.join(getWalletsDir(), f), 'utf8'));
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.filter(Boolean);
|
|
361
|
+
|
|
362
|
+
wallets.sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
363
|
+
|
|
364
|
+
for (const w of wallets) {
|
|
365
|
+
const isDefault = w.address === defaultWallet;
|
|
366
|
+
const marker = isDefault ? ` ${C.green}★ default${C.reset}` : '';
|
|
367
|
+
const date = w.created_at ? new Date(w.created_at).toLocaleDateString() : 'unknown';
|
|
368
|
+
console.log(` ${C.bright}${w.address}${C.reset}${marker}`);
|
|
369
|
+
console.log(` ${C.dim} Created: ${date} | ${w.derivation_path}${C.reset}`);
|
|
370
|
+
console.log();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (defaultWallet) {
|
|
374
|
+
console.log(` ${C.green}★${C.reset} = default wallet (used for signing transactions)\n`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// IMPORT WALLET
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
async function importWallet(rl) {
|
|
383
|
+
const mnemonic = await askMnemonic(rl, 'Importing wallet from mnemonic');
|
|
384
|
+
|
|
385
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
386
|
+
const words = mnemonic.split(/\s+/);
|
|
387
|
+
if (words.length !== 12 && words.length !== 24) {
|
|
388
|
+
console.log(`\n ${C.red}✗ Invalid word count:${C.reset} got ${words.length}, expected 12 or 24.`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
console.log(`\n ${C.red}✗ Invalid BIP39 mnemonic.${C.reset} Please check your word list and try again.`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let keyPair;
|
|
396
|
+
try {
|
|
397
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.log(`\n ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const address = formatAddress(keyPair.publicKey);
|
|
404
|
+
|
|
405
|
+
if (loadWallet(address)) {
|
|
406
|
+
console.log(`\n ${C.yellow}⚠ Wallet already exists:${C.reset} ${address}`);
|
|
407
|
+
console.log(` ${C.dim}No new file created.${C.reset}\n`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const walletData = saveWalletFile(address, keyPair.publicKey);
|
|
412
|
+
const cfg = loadConfig();
|
|
413
|
+
cfg.defaultWallet = address;
|
|
414
|
+
saveConfig(cfg);
|
|
415
|
+
|
|
416
|
+
console.log(`\n${C.green}✓ Wallet imported:${C.reset} ${C.bright}${address}${C.reset}`);
|
|
417
|
+
console.log(`${C.dim} Saved to:${C.reset} ${walletFilePath(address)}`);
|
|
418
|
+
console.log(`${C.green}✓ Set as default wallet.${C.reset}\n`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// DEFAULT WALLET
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
async function defaultWallet(rl) {
|
|
426
|
+
const cfg = loadConfig();
|
|
427
|
+
const defaultAddr = cfg.defaultWallet;
|
|
428
|
+
|
|
429
|
+
const args = process.argv.slice(4);
|
|
430
|
+
if (args.includes('--set') || args.includes('-s')) {
|
|
431
|
+
const setIdx = args.indexOf('--set') !== -1 ? args.indexOf('--set') : args.indexOf('-s');
|
|
432
|
+
const address = args[setIdx + 1];
|
|
433
|
+
if (!address) {
|
|
434
|
+
console.log(`\n ${C.red}Usage:${C.reset} aether wallet default --set <address>\n`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const w = loadWallet(address);
|
|
438
|
+
if (!w) {
|
|
439
|
+
console.log(`\n ${C.red}✗ Wallet not found:${C.reset} ${address}`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
cfg.defaultWallet = address;
|
|
443
|
+
saveConfig(cfg);
|
|
444
|
+
console.log(`\n${C.green}✓ Default wallet set to:${C.reset} ${address}\n`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.log(`\n${C.bright}${C.cyan}── Default Wallet ─────────────────────────────────────────${C.reset}\n`);
|
|
449
|
+
if (!defaultAddr) {
|
|
450
|
+
console.log(` ${C.dim}No default wallet set.${C.reset}`);
|
|
451
|
+
console.log(` ${C.dim}Usage: aether wallet default --set <address>${C.reset}\n`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const w = loadWallet(defaultAddr);
|
|
456
|
+
if (w) {
|
|
457
|
+
console.log(` ${C.green}★${C.reset} ${C.bright}${defaultAddr}${C.reset}`);
|
|
458
|
+
console.log(` ${C.dim} Created: ${new Date(w.created_at).toLocaleString()}${C.reset}`);
|
|
459
|
+
console.log(` ${C.dim} Derivation: ${w.derivation_path}${C.reset}\n`);
|
|
460
|
+
} else {
|
|
461
|
+
console.log(` ${C.yellow}⚠ Default wallet file missing, but config references:${C.reset}`);
|
|
462
|
+
console.log(` ${defaultAddr}\n`);
|
|
463
|
+
console.log(` ${C.dim}Run:${C.reset} aether wallet default --set <address> ${C.dim}to update.${C.reset}\n`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// CONNECT WALLET
|
|
469
|
+
// Generates a session token, opens browser to verify page, polls until done.
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
async function connectWallet(rl) {
|
|
473
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Connect ────────────────────────────────────────${C.reset}\n`);
|
|
474
|
+
|
|
475
|
+
// Resolve wallet address: --address flag or default
|
|
476
|
+
const args = process.argv.slice(4);
|
|
477
|
+
let address = null;
|
|
478
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
479
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
480
|
+
address = args[addrIdx + 1];
|
|
481
|
+
}
|
|
482
|
+
if (!address) {
|
|
483
|
+
const cfg = loadConfig();
|
|
484
|
+
address = cfg.defaultWallet;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!address) {
|
|
488
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
489
|
+
console.log(` ${C.dim}Usage:${C.reset} aether wallet connect --address <address>`);
|
|
490
|
+
console.log(` ${C.dim}Or set a default:${C.reset} aether wallet default --set <address>\n`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const wallet = loadWallet(address);
|
|
495
|
+
if (!wallet) {
|
|
496
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}`);
|
|
497
|
+
console.log(` ${C.dim}Check your wallets with:${C.reset} aether wallet list\n`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Generate session token and save session
|
|
502
|
+
const token = generateSessionToken();
|
|
503
|
+
saveSession(token, address, 10);
|
|
504
|
+
|
|
505
|
+
// Build verification URL
|
|
506
|
+
const siteUrl = getSiteUrl();
|
|
507
|
+
const verifyUrl = `${siteUrl}/wallet/verify?token=${token}&address=${encodeURIComponent(address)}`;
|
|
508
|
+
|
|
509
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
510
|
+
console.log(` ${C.dim} Session expires in 10 minutes${C.reset}`);
|
|
511
|
+
console.log();
|
|
512
|
+
|
|
513
|
+
// Open browser
|
|
514
|
+
const opened = openBrowser(verifyUrl);
|
|
515
|
+
if (opened) {
|
|
516
|
+
console.log(` ${C.green}✓${C.reset} Opened verification page in browser.`);
|
|
517
|
+
console.log(` ${C.dim} ${verifyUrl}${C.reset}`);
|
|
518
|
+
} else {
|
|
519
|
+
console.log(` ${C.yellow}⚠ Could not open browser automatically.${C.reset}`);
|
|
520
|
+
console.log(` ${C.cyan}Open this URL manually:${C.reset}`);
|
|
521
|
+
console.log(` ${C.dim} ${verifyUrl}${C.reset}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log();
|
|
525
|
+
console.log(` ${C.yellow}⏳ Waiting for verification...${C.reset} (Ctrl+C to cancel)`);
|
|
526
|
+
console.log(` ${C.dim} Polling every 2s, timeout after 10 minutes${C.reset}`);
|
|
527
|
+
|
|
528
|
+
// Poll for verification (blocking, async)
|
|
529
|
+
const result = await pollForVerification(token, 600000);
|
|
530
|
+
|
|
531
|
+
if (result.verified) {
|
|
532
|
+
console.log(`\n${C.green}✓ Wallet verified and connected!${C.reset}`);
|
|
533
|
+
console.log(` ${C.green}★${C.reset} ${address}`);
|
|
534
|
+
deleteSession(token);
|
|
535
|
+
console.log();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (result.reason === 'expired') {
|
|
540
|
+
console.log(`\n ${C.red}✗ Session expired.${C.reset} Please run ${C.cyan}aether wallet connect${C.reset} again.\n`);
|
|
541
|
+
} else {
|
|
542
|
+
console.log(`\n ${C.red}✗ Verification timed out (10 minutes).${C.reset} Please run ${C.cyan}aether wallet connect${C.reset} again.\n`);
|
|
543
|
+
}
|
|
544
|
+
deleteSession(token);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
// BALANCE
|
|
550
|
+
// Query chain RPC GET /v1/account/<addr> for real AETH balance using SDK
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
function getDefaultRpc() {
|
|
554
|
+
return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Format lamports as AETH string (1 AETH = 1e9 lamports)
|
|
559
|
+
*/
|
|
560
|
+
function formatAether(lamports) {
|
|
561
|
+
const aeth = lamports / 1e9;
|
|
562
|
+
if (aeth === 0) return '0 AETH';
|
|
563
|
+
// Show up to 4 decimal places, stripping trailing zeros
|
|
564
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function balanceWallet(rl) {
|
|
568
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Balance ───────────────────────────────────────${C.reset}\n`);
|
|
569
|
+
|
|
570
|
+
// Resolve wallet address: --address flag or default
|
|
571
|
+
const args = process.argv.slice(4);
|
|
572
|
+
let address = null;
|
|
573
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
574
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
575
|
+
address = args[addrIdx + 1];
|
|
576
|
+
}
|
|
577
|
+
if (!address) {
|
|
578
|
+
const cfg = loadConfig();
|
|
579
|
+
address = cfg.defaultWallet;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!address) {
|
|
583
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
584
|
+
console.log(` ${C.dim}Usage:${C.reset} aether wallet balance --address <address>`);
|
|
585
|
+
console.log(` ${C.dim}Or set a default:${C.reset} aether wallet default --set <address>\n`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const rpcUrl = getDefaultRpc();
|
|
590
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
591
|
+
console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
|
|
592
|
+
console.log();
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Use SDK for real blockchain RPC call
|
|
596
|
+
const client = new aether.AetherClient({ rpcUrl });
|
|
597
|
+
// Strip ATH prefix if present for API call
|
|
598
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
599
|
+
const account = await client.getAccountInfo(rawAddr);
|
|
600
|
+
|
|
601
|
+
if (!account || account.error) {
|
|
602
|
+
console.log(` ${C.yellow}⚠ Account not found on chain or RPC error.${C.reset}`);
|
|
603
|
+
console.log(` ${C.dim} This is normal for new wallets with 0 balance.${C.reset}`);
|
|
604
|
+
console.log(` ${C.dim} RPC response: ${JSON.stringify(account?.error || account)}${C.reset}\n`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const lamports = account.lamports || 0;
|
|
609
|
+
console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(lamports)}${C.reset}`);
|
|
610
|
+
console.log(` ${C.dim} Raw: ${lamports} lamports${C.reset}`);
|
|
611
|
+
console.log();
|
|
612
|
+
|
|
613
|
+
if (account.owner) {
|
|
614
|
+
const ownerStr = Array.isArray(account.owner)
|
|
615
|
+
? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
|
|
616
|
+
: account.owner;
|
|
617
|
+
console.log(` ${C.dim} Owner: ${ownerStr}${C.reset}`);
|
|
618
|
+
}
|
|
619
|
+
if (account.rent_epoch !== undefined) {
|
|
620
|
+
console.log(` ${C.dim} Rent epoch: ${account.rent_epoch}${C.reset}`);
|
|
621
|
+
}
|
|
622
|
+
console.log();
|
|
623
|
+
} catch (err) {
|
|
624
|
+
console.log(` ${C.red}✗ Failed to fetch balance:${C.reset} ${err.message}`);
|
|
625
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
626
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
// Transaction helpers using SDK
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Sign a transaction using the wallet's secret key.
|
|
636
|
+
* Returns a base58-encoded 64-byte signature.
|
|
637
|
+
*/
|
|
638
|
+
function signTransaction(tx, secretKey) {
|
|
639
|
+
const txBytes = Buffer.from(JSON.stringify(tx));
|
|
640
|
+
const sig = nacl.sign.detached(txBytes, secretKey);
|
|
641
|
+
return bs58.encode(sig);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Submit a transaction using SDK client.sendTransaction()
|
|
646
|
+
* @param {Object} tx - Transaction object (must have signature set)
|
|
647
|
+
* @param {string} rpcUrl - RPC endpoint
|
|
648
|
+
* @returns {Promise<Object>} Transaction result
|
|
649
|
+
*/
|
|
650
|
+
async function submitViaSDK(tx, rpcUrl) {
|
|
651
|
+
const client = new aether.AetherClient({ rpcUrl });
|
|
652
|
+
return client.sendTransaction(tx);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// STAKE
|
|
657
|
+
// Submit a Stake transaction via POST /v1/tx
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
async function stakeWallet(rl) {
|
|
661
|
+
console.log(`\n${C.bright}${C.cyan}── Stake AETH ─────────────────────────────────────────────${C.reset}\n`);
|
|
662
|
+
|
|
663
|
+
// Resolve wallet address
|
|
664
|
+
const args = process.argv.slice(4);
|
|
665
|
+
let address = null;
|
|
666
|
+
let validator = null;
|
|
667
|
+
let amountStr = null;
|
|
668
|
+
|
|
669
|
+
for (let i = 0; i < args.length; i++) {
|
|
670
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
671
|
+
address = args[i + 1];
|
|
672
|
+
}
|
|
673
|
+
if ((args[i] === '--validator' || args[i] === '-v') && args[i + 1]) {
|
|
674
|
+
validator = args[i + 1];
|
|
675
|
+
}
|
|
676
|
+
if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
|
|
677
|
+
amountStr = args[i + 1];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!address) {
|
|
682
|
+
const cfg = loadConfig();
|
|
683
|
+
address = cfg.defaultWallet;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (!address) {
|
|
687
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
688
|
+
console.log(` ${C.dim}Usage: aether stake --address <addr> --validator <val> --amount <aeth>${C.reset}\n`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const wallet = loadWallet(address);
|
|
693
|
+
if (!wallet) {
|
|
694
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Prompt for missing values interactively
|
|
699
|
+
if (!validator) {
|
|
700
|
+
console.log(` ${C.cyan}Enter validator address:${C.reset}`);
|
|
701
|
+
validator = await question(rl, ` Validator > ${C.reset}`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!amountStr) {
|
|
705
|
+
console.log(` ${C.cyan}Enter amount in AETH:${C.reset}`);
|
|
706
|
+
amountStr = await question(rl, ` Amount (AETH) > ${C.reset}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const amount = parseFloat(amountStr);
|
|
710
|
+
if (isNaN(amount) || amount <= 0) {
|
|
711
|
+
console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const lamports = Math.round(amount * 1e9);
|
|
716
|
+
|
|
717
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
718
|
+
console.log(` ${C.green}★${C.reset} Validator: ${C.bright}${validator}${C.reset}`);
|
|
719
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
|
|
720
|
+
console.log();
|
|
721
|
+
|
|
722
|
+
// Ask for mnemonic to derive signing keypair
|
|
723
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
|
|
724
|
+
const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
|
|
725
|
+
console.log();
|
|
726
|
+
|
|
727
|
+
let keyPair;
|
|
728
|
+
try {
|
|
729
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
730
|
+
} catch (e) {
|
|
731
|
+
console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
732
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Verify the derived address matches the wallet
|
|
737
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
738
|
+
if (derivedAddress !== address) {
|
|
739
|
+
console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
|
|
740
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
741
|
+
console.log(` ${C.dim} Expected: ${address}${C.reset}`);
|
|
742
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm stake? [y/N]${C.reset} > ${C.reset}`);
|
|
747
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
748
|
+
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Fetch current slot via SDK for the transaction
|
|
753
|
+
const rpcUrl = getDefaultRpc();
|
|
754
|
+
let currentSlot = 0;
|
|
755
|
+
try {
|
|
756
|
+
currentSlot = await new aether.AetherClient({ rpcUrl }).getSlot();
|
|
757
|
+
} catch (e) {
|
|
758
|
+
// Continue with slot 0 if RPC unavailable
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Build the transaction
|
|
762
|
+
const tx = {
|
|
763
|
+
signer: address.startsWith('ATH') ? address.slice(3) : address,
|
|
764
|
+
tx_type: 'Stake',
|
|
765
|
+
payload: {
|
|
766
|
+
type: 'Stake',
|
|
767
|
+
data: {
|
|
768
|
+
validator,
|
|
769
|
+
amount: lamports,
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
fee: 5000,
|
|
773
|
+
slot: currentSlot,
|
|
774
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// Sign transaction with wallet secret key
|
|
778
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
779
|
+
|
|
780
|
+
console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
// Submit via SDK (real RPC POST /v1/transaction)
|
|
784
|
+
const result = await submitViaSDK(tx, rpcUrl);
|
|
785
|
+
|
|
786
|
+
if (result.error) {
|
|
787
|
+
console.log(`\n ${C.red}✗ Transaction failed:${C.reset} ${result.error}\n`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const sig = result.signature || result.tx_signature || result.txid || result.id || JSON.stringify(result);
|
|
792
|
+
console.log(`\n${C.green}✓ Stake transaction submitted!${C.reset}`);
|
|
793
|
+
console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${sig}${C.reset}`);
|
|
794
|
+
console.log(` ${C.dim}Slot:${C.reset} ${result.slot || currentSlot}`);
|
|
795
|
+
console.log(` ${C.dim}SDK: sendTransaction()${C.reset}`);
|
|
796
|
+
console.log(` ${C.dim}Check: aether delegations list --address ${address}${C.reset}\n`);
|
|
797
|
+
} catch (err) {
|
|
798
|
+
console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
|
|
799
|
+
console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
|
|
800
|
+
process.exit(1);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ---------------------------------------------------------------------------
|
|
805
|
+
// TRANSFER
|
|
806
|
+
// Submit a Transfer transaction via POST /v1/tx
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
|
|
809
|
+
async function transferWallet(rl) {
|
|
810
|
+
console.log(`\n${C.bright}${C.cyan}── Transfer AETH ─────────────────────────────────────────${C.reset}\n`);
|
|
811
|
+
|
|
812
|
+
const args = process.argv.slice(4);
|
|
813
|
+
let address = null;
|
|
814
|
+
let recipient = null;
|
|
815
|
+
let amountStr = null;
|
|
816
|
+
|
|
817
|
+
for (let i = 0; i < args.length; i++) {
|
|
818
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
819
|
+
address = args[i + 1];
|
|
820
|
+
}
|
|
821
|
+
if ((args[i] === '--to' || args[i] === '-t') && args[i + 1]) {
|
|
822
|
+
recipient = args[i + 1];
|
|
823
|
+
}
|
|
824
|
+
if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
|
|
825
|
+
amountStr = args[i + 1];
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (!address) {
|
|
830
|
+
const cfg = loadConfig();
|
|
831
|
+
address = cfg.defaultWallet;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (!address) {
|
|
835
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
836
|
+
console.log(` ${C.dim}Usage: aether transfer --to <addr> --amount <aeth>${C.reset}\n`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const wallet = loadWallet(address);
|
|
841
|
+
if (!wallet) {
|
|
842
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Prompt for missing values interactively
|
|
847
|
+
if (!recipient) {
|
|
848
|
+
console.log(` ${C.cyan}Enter recipient address:${C.reset}`);
|
|
849
|
+
recipient = await question(rl, ` Recipient > ${C.reset}`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (!amountStr) {
|
|
853
|
+
console.log(` ${C.cyan}Enter amount in AETH:${C.reset}`);
|
|
854
|
+
amountStr = await question(rl, ` Amount (AETH) > ${C.reset}`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const amount = parseFloat(amountStr);
|
|
858
|
+
if (isNaN(amount) || amount <= 0) {
|
|
859
|
+
console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const lamports = Math.round(amount * 1e9);
|
|
864
|
+
|
|
865
|
+
console.log(` ${C.green}★${C.reset} From: ${C.bright}${address}${C.reset}`);
|
|
866
|
+
console.log(` ${C.green}★${C.reset} To: ${C.bright}${recipient}${C.reset}`);
|
|
867
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
|
|
868
|
+
console.log();
|
|
869
|
+
|
|
870
|
+
// Ask for mnemonic to derive signing keypair
|
|
871
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
|
|
872
|
+
const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
|
|
873
|
+
console.log();
|
|
874
|
+
|
|
875
|
+
let keyPair;
|
|
876
|
+
try {
|
|
877
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
878
|
+
} catch (e) {
|
|
879
|
+
console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
880
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Verify the derived address matches the wallet
|
|
885
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
886
|
+
if (derivedAddress !== address) {
|
|
887
|
+
console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
|
|
888
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
889
|
+
console.log(` ${C.dim} Expected: ${address}${C.reset}`);
|
|
890
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm transfer? [y/N]${C.reset} > ${C.reset}`);
|
|
895
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
896
|
+
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Fetch current slot via SDK
|
|
901
|
+
const rpcUrl = getDefaultRpc();
|
|
902
|
+
let currentSlot = 0;
|
|
903
|
+
try {
|
|
904
|
+
currentSlot = await new aether.AetherClient({ rpcUrl }).getSlot();
|
|
905
|
+
} catch (e) {
|
|
906
|
+
// Continue with slot 0
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Build transfer transaction
|
|
910
|
+
const tx = {
|
|
911
|
+
signer: address.startsWith('ATH') ? address.slice(3) : address,
|
|
912
|
+
tx_type: 'Transfer',
|
|
913
|
+
payload: {
|
|
914
|
+
type: 'Transfer',
|
|
915
|
+
data: {
|
|
916
|
+
recipient: recipient.startsWith('ATH') ? recipient.slice(3) : recipient,
|
|
917
|
+
amount: lamports,
|
|
918
|
+
nonce: Math.floor(Math.random() * 0xffffffff),
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
fee: 5000,
|
|
922
|
+
slot: currentSlot,
|
|
923
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// Sign transaction
|
|
927
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
928
|
+
|
|
929
|
+
console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
// Submit via SDK
|
|
933
|
+
const result = await submitViaSDK(tx, rpcUrl);
|
|
934
|
+
|
|
935
|
+
if (result.error) {
|
|
936
|
+
console.log(`\n ${C.red}✗ Transaction failed:${C.reset} ${result.error}\n`);
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const sig = result.signature || result.tx_signature || result.txid || result.id || JSON.stringify(result);
|
|
941
|
+
console.log(`\n${C.green}✓ Transfer transaction submitted!${C.reset}`);
|
|
942
|
+
console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${sig}${C.reset}`);
|
|
943
|
+
console.log(` ${C.dim}Slot:${C.reset} ${result.slot || currentSlot}`);
|
|
944
|
+
console.log(` ${C.dim}SDK: sendTransaction()${C.reset}`);
|
|
945
|
+
console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
|
|
946
|
+
} catch (err) {
|
|
947
|
+
console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
|
|
948
|
+
console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ---------------------------------------------------------------------------
|
|
954
|
+
// TX HISTORY
|
|
955
|
+
// Fetch and display recent transactions for an address using SDK
|
|
956
|
+
// ---------------------------------------------------------------------------
|
|
957
|
+
|
|
958
|
+
async function txHistory(rl) {
|
|
959
|
+
console.log(`\n${C.bright}${C.cyan}── Transaction History ────────────────────────────────────${C.reset}\n`);
|
|
960
|
+
|
|
961
|
+
const args = process.argv.slice(4);
|
|
962
|
+
let address = null;
|
|
963
|
+
let limit = 20;
|
|
964
|
+
let asJson = false;
|
|
965
|
+
|
|
966
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
967
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
968
|
+
address = args[addrIdx + 1];
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const limitIdx = args.findIndex((a) => a === '--limit' || a === '-l');
|
|
972
|
+
if (limitIdx !== -1 && args[limitIdx + 1]) {
|
|
973
|
+
limit = parseInt(args[limitIdx + 1], 10);
|
|
974
|
+
if (isNaN(limit) || limit < 1 || limit > 100) {
|
|
975
|
+
console.log(` ${C.red}✗ --limit must be between 1 and 100.${C.reset}\n`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
asJson = args.includes('--json') || args.includes('-j');
|
|
981
|
+
|
|
982
|
+
if (!address) {
|
|
983
|
+
const cfg = loadConfig();
|
|
984
|
+
address = cfg.defaultWallet;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!address) {
|
|
988
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
989
|
+
console.log(` ${C.dim}Usage: aether tx history --address <addr> [--limit 20] [--json]${C.reset}\n`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const rpcUrl = getDefaultRpc();
|
|
994
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
995
|
+
|
|
996
|
+
if (!asJson) {
|
|
997
|
+
console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
|
|
998
|
+
console.log(` ${C.dim} RPC: ${rpcUrl} Limit: ${limit}${C.reset}`);
|
|
999
|
+
console.log();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
// Create SDK client
|
|
1004
|
+
const client = createClient(rpcUrl);
|
|
1005
|
+
|
|
1006
|
+
// Fetch account info first (for context)
|
|
1007
|
+
const account = await client.getAccount(rawAddr);
|
|
1008
|
+
|
|
1009
|
+
// Fetch transactions for this address using SDK
|
|
1010
|
+
const txs = await client.getRecentTransactions(rawAddr, limit);
|
|
1011
|
+
|
|
1012
|
+
if (asJson) {
|
|
1013
|
+
const out = {
|
|
1014
|
+
address,
|
|
1015
|
+
rpc: rpcUrl,
|
|
1016
|
+
account: account && !account.error ? {
|
|
1017
|
+
lamports: account.lamports,
|
|
1018
|
+
owner: account.owner,
|
|
1019
|
+
} : null,
|
|
1020
|
+
transactions: txs && !txs.error ? (Array.isArray(txs) ? txs : txs.transactions || []) : [],
|
|
1021
|
+
fetched_at: new Date().toISOString(),
|
|
1022
|
+
};
|
|
1023
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (!account || account.error) {
|
|
1028
|
+
console.log(` ${C.yellow}⚠ Account not found on chain.${C.reset}`);
|
|
1029
|
+
} else {
|
|
1030
|
+
console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(account.lamports || 0)}${C.reset}`);
|
|
1031
|
+
if (account.owner) {
|
|
1032
|
+
const ownerStr = Array.isArray(account.owner)
|
|
1033
|
+
? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
|
|
1034
|
+
: account.owner;
|
|
1035
|
+
console.log(` ${C.dim} Owner: ${ownerStr}${C.reset}`);
|
|
1036
|
+
}
|
|
1037
|
+
console.log();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (!txs || txs.error) {
|
|
1041
|
+
console.log(` ${C.yellow}⚠ No transaction history available.${C.reset}`);
|
|
1042
|
+
console.log(` ${C.dim} RPC response: ${JSON.stringify(txs?.error || txs)}${C.reset}`);
|
|
1043
|
+
console.log(` ${C.dim} (New wallets with 0 txs will return empty results)${C.reset}\n`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const txList = Array.isArray(txs) ? txs : txs.transactions || [];
|
|
1048
|
+
console.log(` ${C.bright}Recent Transactions (${txList.length})${C.reset}\n`);
|
|
1049
|
+
|
|
1050
|
+
if (txList.length === 0) {
|
|
1051
|
+
console.log(` ${C.dim} No transactions found for this address.${C.reset}`);
|
|
1052
|
+
console.log(` ${C.dim} This is normal for new wallets.${C.reset}\n`);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const typeColors = {
|
|
1057
|
+
Transfer: C.cyan,
|
|
1058
|
+
Stake: C.green,
|
|
1059
|
+
Unstake: C.yellow,
|
|
1060
|
+
ClaimRewards: C.magenta,
|
|
1061
|
+
CreateNFT: C.red,
|
|
1062
|
+
MintNFT: C.red,
|
|
1063
|
+
TransferNFT: C.cyan,
|
|
1064
|
+
UpdateMetadata: C.yellow,
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
for (const tx of txList) {
|
|
1068
|
+
const txType = tx.tx_type || tx.type || 'Unknown';
|
|
1069
|
+
const color = typeColors[txType] || C.reset;
|
|
1070
|
+
const ts = tx.timestamp
|
|
1071
|
+
? new Date(tx.timestamp * 1000).toISOString()
|
|
1072
|
+
: 'unknown';
|
|
1073
|
+
const sig = tx.signature || tx.id || tx.tx_signature || '—';
|
|
1074
|
+
const sigShort = sig.length > 20 ? sig.slice(0, 8) + '…' + sig.slice(-8) : sig;
|
|
1075
|
+
|
|
1076
|
+
console.log(` ${C.dim}┌─ ${ts}${C.reset}`);
|
|
1077
|
+
console.log(` │ ${C.bright}${color}${txType}${C.reset} ${C.dim}sig:${C.reset} ${sigShort}`);
|
|
1078
|
+
if (tx.payload && tx.payload.data) {
|
|
1079
|
+
const d = tx.payload.data;
|
|
1080
|
+
if (d.recipient) console.log(` │ ${C.dim} → to: ${d.recipient}${C.reset}`);
|
|
1081
|
+
if (d.amount) console.log(` │ ${C.dim} amount: ${formatAether(d.amount)}${C.reset}`);
|
|
1082
|
+
if (d.validator) console.log(` │ ${C.dim} validator: ${d.validator}${C.reset}`);
|
|
1083
|
+
if (d.stake_account) console.log(` │ ${C.dim} stake_acct: ${d.stake_account}${C.reset}`);
|
|
1084
|
+
}
|
|
1085
|
+
if (tx.fee !== undefined && tx.fee > 0) {
|
|
1086
|
+
console.log(` │ ${C.dim} fee: ${tx.fee} lamports${C.reset}`);
|
|
1087
|
+
}
|
|
1088
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
1089
|
+
console.log();
|
|
1090
|
+
}
|
|
1091
|
+
console.log();
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
console.log(` ${C.red}✗ Failed to fetch transaction history:${C.reset} ${err.message}`);
|
|
1094
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
1095
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// ---------------------------------------------------------------------------
|
|
1100
|
+
// ---------------------------------------------------------------------------
|
|
1101
|
+
// EXPORT WALLET
|
|
1102
|
+
// Export wallet data for backup — public data by default, mnemonic with --mnemonic flag
|
|
1103
|
+
// ---------------------------------------------------------------------------
|
|
1104
|
+
|
|
1105
|
+
async function exportWallet(rl) {
|
|
1106
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Export ─────────────────────────────────────────${C.reset}\n`);
|
|
1107
|
+
|
|
1108
|
+
const args = process.argv.slice(4);
|
|
1109
|
+
let address = null;
|
|
1110
|
+
let asJson = false;
|
|
1111
|
+
let includeMnemonic = false;
|
|
1112
|
+
|
|
1113
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
1114
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
1115
|
+
address = args[addrIdx + 1];
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
asJson = args.includes('--json') || args.includes('-j');
|
|
1119
|
+
includeMnemonic = args.includes('--mnemonic') || args.includes('-m');
|
|
1120
|
+
|
|
1121
|
+
if (!address) {
|
|
1122
|
+
const cfg = loadConfig();
|
|
1123
|
+
address = cfg.defaultWallet;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (!address) {
|
|
1127
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
1128
|
+
console.log(` ${C.dim}Usage: aether wallet export --address <addr> [--mnemonic] [--json]${C.reset}\n`);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const walletData = loadWallet(address);
|
|
1133
|
+
if (!walletData) {
|
|
1134
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}`);
|
|
1135
|
+
console.log(` ${C.dim} Available wallets: ${C.cyan}aether wallet list${C.reset}\n`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const exportData = {
|
|
1140
|
+
version: walletData.version || 1,
|
|
1141
|
+
address: walletData.address,
|
|
1142
|
+
public_key: walletData.public_key,
|
|
1143
|
+
derivation_path: walletData.derivation_path,
|
|
1144
|
+
created_at: walletData.created_at,
|
|
1145
|
+
source: 'aether-cli',
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// Mnemonic export requires interactive confirmation for security
|
|
1149
|
+
if (includeMnemonic) {
|
|
1150
|
+
console.log(` ${C.yellow}⚠ WARNING: You are about to export your SECRET MNEMONIC.${C.reset}`);
|
|
1151
|
+
console.log(` ${C.dim} Anyone with this phrase can access your funds.${C.reset}\n`);
|
|
1152
|
+
const confirmed = await question(rl, ` Type ${C.bright}EXPORT${C.reset} to confirm: `);
|
|
1153
|
+
if (confirmed.trim().toUpperCase() !== 'EXPORT') {
|
|
1154
|
+
console.log(`\n ${C.dim}Aborted. Mnemonic not exported.${C.reset}\n`);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
console.log(` ${C.green}✓ Confirmed${C.reset}\n`);
|
|
1158
|
+
|
|
1159
|
+
// Re-derive mnemonic from keypair is NOT possible — we must ask for it
|
|
1160
|
+
console.log(` ${C.dim}The CLI cannot retrieve your mnemonic from the stored keypair.${C.reset}`);
|
|
1161
|
+
console.log(` ${C.dim}If you have a backup of your mnemonic, enter it below to include it.${C.reset}`);
|
|
1162
|
+
console.log(` ${C.dim}Otherwise, press Enter to export public data only.${C.reset}\n`);
|
|
1163
|
+
const mnemonicInput = await question(rl, ` ${C.cyan}Mnemonic (or Enter to skip):${C.reset} `);
|
|
1164
|
+
const mnemonic = mnemonicInput.trim();
|
|
1165
|
+
if (mnemonic && bip39.validateMnemonic(mnemonic)) {
|
|
1166
|
+
exportData.mnemonic = mnemonic;
|
|
1167
|
+
} else if (mnemonic) {
|
|
1168
|
+
console.log(` ${C.red}✗ Invalid mnemonic phrase. Skipping.${C.reset}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (asJson) {
|
|
1173
|
+
console.log(JSON.stringify(exportData, null, 2));
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Human-readable output
|
|
1178
|
+
console.log(` ${C.green}★${C.reset} Wallet exported successfully\n`);
|
|
1179
|
+
console.log(` ${C.dim}Address:${C.reset} ${exportData.address}`);
|
|
1180
|
+
console.log(` ${C.dim}Public key:${C.reset} ${exportData.public_key}`);
|
|
1181
|
+
console.log(` ${C.dim}Derivation path:${C.reset} ${exportData.derivation_path}`);
|
|
1182
|
+
console.log(` ${C.dim}Created:${C.reset} ${exportData.created_at ? new Date(exportData.created_at).toLocaleString() : 'unknown'}`);
|
|
1183
|
+
if (exportData.mnemonic) {
|
|
1184
|
+
console.log();
|
|
1185
|
+
console.log(` ${C.yellow}★ MNEMONIC (keep this secret!):${C.reset}`);
|
|
1186
|
+
console.log(` ${C.bright}${exportData.mnemonic}${C.reset}`);
|
|
1187
|
+
}
|
|
1188
|
+
console.log();
|
|
1189
|
+
console.log(` ${C.dim}Export saved to stdout in JSON format with:${C.reset}`);
|
|
1190
|
+
console.log(` ${C.cyan}aether wallet export --address ${address} --json${C.reset}`);
|
|
1191
|
+
console.log();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// STAKE POSITIONS
|
|
1195
|
+
// Query and display current stake positions/delegations for a wallet
|
|
1196
|
+
// ---------------------------------------------------------------------------
|
|
1197
|
+
|
|
1198
|
+
async function stakePositions(rl) {
|
|
1199
|
+
console.log(`\n${C.bright}${C.cyan}── Stake Positions ──────────────────────────────────────${C.reset}\n`);
|
|
1200
|
+
|
|
1201
|
+
const args = process.argv.slice(4);
|
|
1202
|
+
let address = null;
|
|
1203
|
+
let asJson = false;
|
|
1204
|
+
|
|
1205
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
1206
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
1207
|
+
address = args[addrIdx + 1];
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
asJson = args.includes('--json') || args.includes('-j');
|
|
1211
|
+
|
|
1212
|
+
if (!address) {
|
|
1213
|
+
const cfg = loadConfig();
|
|
1214
|
+
address = cfg.defaultWallet;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (!address) {
|
|
1218
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
1219
|
+
console.log(` ${C.dim}Usage: aether wallet stake-positions --address <addr> [--json]${C.reset}\n`);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const rpcUrl = getDefaultRpc();
|
|
1224
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
1225
|
+
|
|
1226
|
+
if (!asJson) {
|
|
1227
|
+
console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
|
|
1228
|
+
console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
|
|
1229
|
+
console.log();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
try {
|
|
1233
|
+
// Create SDK client for real blockchain calls
|
|
1234
|
+
const client = createClient(rpcUrl);
|
|
1235
|
+
|
|
1236
|
+
// Fetch stake accounts for this address using SDK (real RPC GET /v1/stake/<addr>)
|
|
1237
|
+
const stakeAccounts = await client.getStakeAccounts(rawAddr);
|
|
1238
|
+
|
|
1239
|
+
if (asJson) {
|
|
1240
|
+
const out = {
|
|
1241
|
+
address,
|
|
1242
|
+
rpc: rpcUrl,
|
|
1243
|
+
stake_accounts: stakeAccounts.map(acc => ({
|
|
1244
|
+
stake_account: acc.pubkey || acc.publicKey || acc.account || acc.address,
|
|
1245
|
+
validator: acc.validator || acc.voter || acc.vote_account,
|
|
1246
|
+
stake_lamports: acc.stake_lamports || acc.lamports || 0,
|
|
1247
|
+
stake_aeth: (acc.stake_lamports || acc.lamports || 0) / 1e9,
|
|
1248
|
+
status: acc.status || acc.state || 'unknown',
|
|
1249
|
+
activation_epoch: acc.activation_epoch,
|
|
1250
|
+
deactivation_epoch: acc.deactivation_epoch,
|
|
1251
|
+
rewards_earned: acc.rewards_earned || 0,
|
|
1252
|
+
})),
|
|
1253
|
+
total_staked_lamports: stakeAccounts.reduce((sum, acc) => sum + (acc.stake_lamports || acc.lamports || 0), 0),
|
|
1254
|
+
fetched_at: new Date().toISOString(),
|
|
1255
|
+
};
|
|
1256
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (!stakeAccounts || stakeAccounts.length === 0) {
|
|
1261
|
+
console.log(` ${C.yellow}⚠ No active stake positions found.${C.reset}`);
|
|
1262
|
+
console.log(` ${C.dim} This wallet has not delegated to any validators.${C.reset}`);
|
|
1263
|
+
console.log(` ${C.dim} Stake AETH with: ${C.cyan}aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
let totalStaked = 0;
|
|
1268
|
+
let activeCount = 0;
|
|
1269
|
+
let deactivatingCount = 0;
|
|
1270
|
+
let inactiveCount = 0;
|
|
1271
|
+
|
|
1272
|
+
console.log(` ${C.bright}Stake Positions (${stakeAccounts.length})${C.reset}\n`);
|
|
1273
|
+
|
|
1274
|
+
const statusColors = {
|
|
1275
|
+
active: C.green,
|
|
1276
|
+
activating: C.cyan,
|
|
1277
|
+
deactivating: C.yellow,
|
|
1278
|
+
inactive: C.dim,
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
for (const acc of stakeAccounts) {
|
|
1282
|
+
const stakeAcct = acc.pubkey || acc.publicKey || acc.account || 'unknown';
|
|
1283
|
+
const validator = acc.validator || acc.voter || acc.vote_account || 'unknown';
|
|
1284
|
+
const lamports = acc.stake_lamports || acc.lamports || 0;
|
|
1285
|
+
const aeth = lamports / 1e9;
|
|
1286
|
+
const status = (acc.status || acc.state || 'unknown').toLowerCase();
|
|
1287
|
+
const rewards = acc.rewards_earned || 0;
|
|
1288
|
+
|
|
1289
|
+
totalStaked += lamports;
|
|
1290
|
+
|
|
1291
|
+
if (status === 'active') activeCount++;
|
|
1292
|
+
else if (status === 'deactivating' || status === 'deactivated') deactivatingCount++;
|
|
1293
|
+
else inactiveCount++;
|
|
1294
|
+
|
|
1295
|
+
const statusColor = statusColors[status] || C.reset;
|
|
1296
|
+
const shortAcct = stakeAcct.length > 20 ? stakeAcct.slice(0, 8) + '…' + stakeAcct.slice(-8) : stakeAcct;
|
|
1297
|
+
const shortVal = validator.length > 20 ? validator.slice(0, 8) + '…' + validator.slice(-8) : validator;
|
|
1298
|
+
|
|
1299
|
+
console.log(` ${C.dim}┌─ ${C.bright}${statusColor}${status.toUpperCase()}${C.reset}`);
|
|
1300
|
+
console.log(` │ ${C.dim}Stake acct:${C.reset} ${shortAcct}`);
|
|
1301
|
+
console.log(` │ ${C.dim}Validator:${C.reset} ${shortVal}`);
|
|
1302
|
+
console.log(` │ ${C.dim}Staked:${C.reset} ${C.bright}${aeth.toFixed(4)} AETH${C.reset} (${lamports.toLocaleString()} lamports)`);
|
|
1303
|
+
if (rewards > 0) {
|
|
1304
|
+
console.log(` │ ${C.dim}Rewards:${C.reset} ${C.green}+${(rewards / 1e9).toFixed(4)} AETH${C.reset}`);
|
|
1305
|
+
}
|
|
1306
|
+
if (acc.activation_epoch !== undefined) {
|
|
1307
|
+
console.log(` │ ${C.dim}Activated:${C.reset} epoch ${acc.activation_epoch}`);
|
|
1308
|
+
}
|
|
1309
|
+
if (acc.deactivation_epoch !== undefined) {
|
|
1310
|
+
console.log(` │ ${C.dim}Deactivates:${C.reset} epoch ${acc.deactivation_epoch}`);
|
|
1311
|
+
}
|
|
1312
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
1313
|
+
console.log();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
console.log(` ${C.bright}Summary:${C.reset}`);
|
|
1317
|
+
console.log(` ${C.dim} Total staked:${C.reset} ${C.bright}${(totalStaked / 1e9).toFixed(4)} AETH${C.reset} (${totalStaked.toLocaleString()} lamports)`);
|
|
1318
|
+
console.log(` ${C.green} ● Active:${C.reset} ${activeCount} ${C.yellow}● Deactivating:${C.reset} ${deactivatingCount} ${C.dim}● Inactive:${C.reset} ${inactiveCount}`);
|
|
1319
|
+
console.log();
|
|
1320
|
+
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
console.log(` ${C.red}✗ Failed to fetch stake positions:${C.reset} ${err.message}`);
|
|
1323
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
1324
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ---------------------------------------------------------------------------
|
|
1329
|
+
// UNSTAKE
|
|
1330
|
+
// Submit an Unstake transaction via POST /v1/tx to deactivate stake
|
|
1331
|
+
// ---------------------------------------------------------------------------
|
|
1332
|
+
|
|
1333
|
+
async function unstakeWallet(rl) {
|
|
1334
|
+
console.log(`\n${C.bright}${C.cyan}── Unstake AETH ──────────────────────────────────────────${C.reset}\n`);
|
|
1335
|
+
|
|
1336
|
+
const args = process.argv.slice(4);
|
|
1337
|
+
let address = null;
|
|
1338
|
+
let stakeAccount = null;
|
|
1339
|
+
let amountStr = null;
|
|
1340
|
+
|
|
1341
|
+
for (let i = 0; i < args.length; i++) {
|
|
1342
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
1343
|
+
address = args[i + 1];
|
|
1344
|
+
}
|
|
1345
|
+
if ((args[i] === '--account' || args[i] === '-s') && args[i + 1]) {
|
|
1346
|
+
stakeAccount = args[i + 1];
|
|
1347
|
+
}
|
|
1348
|
+
if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
|
|
1349
|
+
amountStr = args[i + 1];
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (!address) {
|
|
1354
|
+
const cfg = loadConfig();
|
|
1355
|
+
address = cfg.defaultWallet;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (!address) {
|
|
1359
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
1360
|
+
console.log(` ${C.dim}Usage: aether unstake --account <stakeAcct> [--amount <aeth>] [--address <addr>]${C.reset}\n`);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const wallet = loadWallet(address);
|
|
1365
|
+
if (!wallet) {
|
|
1366
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Resolve stake account: --account flag, or query chain for first active stake
|
|
1371
|
+
if (!stakeAccount) {
|
|
1372
|
+
const rpcUrl = getDefaultRpc();
|
|
1373
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
1374
|
+
|
|
1375
|
+
let stakeAccounts = [];
|
|
1376
|
+
try {
|
|
1377
|
+
// Use SDK for real blockchain call
|
|
1378
|
+
const client = createClient(rpcUrl);
|
|
1379
|
+
stakeAccounts = await client.getStakeAccounts(rawAddr);
|
|
1380
|
+
} catch { /* no stake accounts */ }
|
|
1381
|
+
|
|
1382
|
+
if (stakeAccounts.length === 0) {
|
|
1383
|
+
console.log(` ${C.red}✗ No active stake accounts found for this wallet.${C.reset}`);
|
|
1384
|
+
console.log(` ${C.dim}Use ${C.cyan}--account <stakeAcct>${C.reset} ${C.dim}to specify a stake account.${C.reset}`);
|
|
1385
|
+
console.log(` ${C.dim}Check delegations: aether delegations list --address ${address}${C.reset}\n`);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Default to first active stake account
|
|
1390
|
+
const active = stakeAccounts.find(s => !s.deactivation_epoch && (s.status === 'active' || s.state === 'active'));
|
|
1391
|
+
stakeAccount = active
|
|
1392
|
+
? (active.pubkey || active.publicKey || active.account)
|
|
1393
|
+
: (stakeAccounts[0].pubkey || stakeAccounts[0].publicKey || stakeAccounts[0].account);
|
|
1394
|
+
|
|
1395
|
+
console.log(` ${C.cyan}Using stake account:${C.reset} ${C.bright}${stakeAccount}${C.reset}`);
|
|
1396
|
+
console.log(` ${C.dim}(override with ${C.cyan}--account <stakeAcct>${C.reset}${C.dim})${C.reset}\n`);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Resolve amount: --amount flag, or prompt if partial unstake supported
|
|
1400
|
+
// If no amount provided, unstake entire stake
|
|
1401
|
+
let lamports = null;
|
|
1402
|
+
if (amountStr) {
|
|
1403
|
+
const amount = parseFloat(amountStr);
|
|
1404
|
+
if (isNaN(amount) || amount <= 0) {
|
|
1405
|
+
console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
lamports = Math.round(amount * 1e9);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
1412
|
+
console.log(` ${C.green}★${C.reset} Stake acct: ${C.bright}${stakeAccount}${C.reset}`);
|
|
1413
|
+
if (lamports !== null) {
|
|
1414
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${(lamports / 1e9).toFixed(4)} AETH${C.reset} (${lamports} lamports)`);
|
|
1415
|
+
} else {
|
|
1416
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}FULL STAKE${C.reset}`);
|
|
1417
|
+
}
|
|
1418
|
+
console.log();
|
|
1419
|
+
|
|
1420
|
+
// Ask for mnemonic to derive signing keypair
|
|
1421
|
+
console.log(`${C.yellow} ⚠ Signing requires your wallet passphrase.${C.reset}`);
|
|
1422
|
+
const mnemonic = await askMnemonic(rl, 'Enter your 12/24-word passphrase to sign this transaction');
|
|
1423
|
+
console.log();
|
|
1424
|
+
|
|
1425
|
+
let keyPair;
|
|
1426
|
+
try {
|
|
1427
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
1428
|
+
} catch (e) {
|
|
1429
|
+
console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
1430
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Verify the derived address matches the wallet
|
|
1435
|
+
const derivedAddress = formatAddress(keyPair.publicKey);
|
|
1436
|
+
if (derivedAddress !== address) {
|
|
1437
|
+
console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
|
|
1438
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
1439
|
+
console.log(` ${C.dim} Expected: ${address}${C.reset}\n`);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm unstake? [y/N]${C.reset} > ${C.reset}`);
|
|
1444
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
1445
|
+
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Fetch current slot via SDK
|
|
1450
|
+
const rpcUrl = getDefaultRpc();
|
|
1451
|
+
let currentSlot = 0;
|
|
1452
|
+
try {
|
|
1453
|
+
currentSlot = await new aether.AetherClient({ rpcUrl }).getSlot();
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
// Continue with slot 0
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Build the unstake transaction
|
|
1459
|
+
const txData = {
|
|
1460
|
+
type: 'Unstake',
|
|
1461
|
+
data: {
|
|
1462
|
+
stake_account: stakeAccount,
|
|
1463
|
+
},
|
|
1464
|
+
};
|
|
1465
|
+
if (lamports !== null) {
|
|
1466
|
+
txData.data.amount = lamports;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const tx = {
|
|
1470
|
+
signer: address.startsWith('ATH') ? address.slice(3) : address,
|
|
1471
|
+
tx_type: 'Unstake',
|
|
1472
|
+
payload: txData,
|
|
1473
|
+
fee: 5000,
|
|
1474
|
+
slot: currentSlot,
|
|
1475
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
// Sign transaction
|
|
1479
|
+
tx.signature = signTransaction(tx, keyPair.secretKey);
|
|
1480
|
+
|
|
1481
|
+
console.log(` ${C.dim}Submitting via SDK to ${rpcUrl}...${C.reset}`);
|
|
1482
|
+
|
|
1483
|
+
try {
|
|
1484
|
+
// Submit via SDK
|
|
1485
|
+
const result = await submitViaSDK(tx, rpcUrl);
|
|
1486
|
+
|
|
1487
|
+
if (result.error) {
|
|
1488
|
+
console.log(`\n ${C.red}✗ Unstake failed:${C.reset} ${result.error}\n`);
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const sig = result.signature || result.tx_signature || result.txid || result.id || JSON.stringify(result);
|
|
1493
|
+
console.log(`\n${C.green}✓ Unstake transaction submitted!${C.reset}`);
|
|
1494
|
+
console.log(` ${C.dim}Signature:${C.reset} ${C.cyan}${sig}${C.reset}`);
|
|
1495
|
+
console.log(` ${C.dim}Slot:${C.reset} ${result.slot || currentSlot}`);
|
|
1496
|
+
console.log(` ${C.dim}SDK: sendTransaction()${C.reset}`);
|
|
1497
|
+
console.log(` ${C.dim}Stake will deactivate over the next epoch.${C.reset}`);
|
|
1498
|
+
console.log(` ${C.dim}Check status: aether delegations list --address ${address}${C.reset}\n`);
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
|
|
1501
|
+
console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// ---------------------------------------------------------------------------
|
|
1507
|
+
// Main dispatcher
|
|
1508
|
+
// ---------------------------------------------------------------------------
|
|
1509
|
+
|
|
1510
|
+
async function walletCommand() {
|
|
1511
|
+
// CLI: argv = [node, index.js, wallet, <subcmd>]
|
|
1512
|
+
let subcmd = process.argv[2];
|
|
1513
|
+
if (subcmd === 'wallet.js' || subcmd === 'wallet') {
|
|
1514
|
+
subcmd = process.argv[3];
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const rl = createRl();
|
|
1518
|
+
try {
|
|
1519
|
+
if (!subcmd || subcmd === 'create') {
|
|
1520
|
+
await createWallet(rl);
|
|
1521
|
+
} else if (subcmd === 'list') {
|
|
1522
|
+
await listWallets(rl);
|
|
1523
|
+
} else if (subcmd === 'import') {
|
|
1524
|
+
await importWallet(rl);
|
|
1525
|
+
} else if (subcmd === 'default') {
|
|
1526
|
+
await defaultWallet(rl);
|
|
1527
|
+
} else if (subcmd === 'connect') {
|
|
1528
|
+
await connectWallet(rl);
|
|
1529
|
+
} else if (subcmd === 'balance') {
|
|
1530
|
+
await balanceWallet(rl);
|
|
1531
|
+
} else if (subcmd === 'stake') {
|
|
1532
|
+
await stakeWallet(rl);
|
|
1533
|
+
} else if (subcmd === 'stake-positions') {
|
|
1534
|
+
await stakePositions(rl);
|
|
1535
|
+
} else if (subcmd === 'export') {
|
|
1536
|
+
await exportWallet(rl);
|
|
1537
|
+
} else if (subcmd === 'unstake') {
|
|
1538
|
+
await unstakeWallet(rl);
|
|
1539
|
+
} else if (subcmd === 'transfer') {
|
|
1540
|
+
await transferWallet(rl);
|
|
1541
|
+
} else if (subcmd === 'history' || subcmd === 'tx') {
|
|
1542
|
+
await txHistory(rl);
|
|
1543
|
+
} else {
|
|
1544
|
+
console.log(`\n ${C.red}Unknown wallet subcommand:${C.reset} ${subcmd}`);
|
|
1545
|
+
console.log(`\n Usage:`);
|
|
1546
|
+
console.log(` ${C.cyan}aether wallet create${C.reset} Create new or import wallet`);
|
|
1547
|
+
console.log(` ${C.cyan}aether wallet list${C.reset} List all wallets`);
|
|
1548
|
+
console.log(` ${C.cyan}aether wallet import${C.reset} Import wallet from mnemonic`);
|
|
1549
|
+
console.log(` ${C.cyan}aether wallet export${C.reset} Export wallet data (pubkey, address) — --mnemonic to include phrase`);
|
|
1550
|
+
console.log(` ${C.cyan}aether wallet default${C.reset} Show/set default wallet`);
|
|
1551
|
+
console.log(` ${C.cyan}aether wallet connect${C.reset} Connect wallet via browser verification`);
|
|
1552
|
+
console.log(` ${C.cyan}aether wallet balance${C.reset} Query chain balance for an address`);
|
|
1553
|
+
console.log(` ${C.cyan}aether wallet stake${C.reset} Stake AETH to a validator`);
|
|
1554
|
+
console.log(` ${C.cyan}aether wallet stake-positions${C.reset} Show current stake delegations and rewards`);
|
|
1555
|
+
console.log(` ${C.cyan}aether wallet unstake${C.reset} Unstake AETH — deactivate a stake account`);
|
|
1556
|
+
console.log(` ${C.cyan}aether wallet transfer${C.reset} Transfer AETH to another address`);
|
|
1557
|
+
console.log(` ${C.cyan}aether wallet history${C.reset} Show recent transactions for an address`);
|
|
1558
|
+
console.log();
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
} finally {
|
|
1562
|
+
rl.close();
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
module.exports = { walletCommand };
|
|
1567
|
+
|
|
1568
|
+
if (require.main === module) {
|
|
1569
|
+
walletCommand();
|
|
1570
|
+
}
|