aether-hub 1.0.3 → 1.0.6
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/sdk.js +537 -381
- package/commands/wallet.js +1139 -0
- package/index.js +52 -1
- package/package.json +3 -2
|
@@ -0,0 +1,1139 @@
|
|
|
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
|
+
// ANSI colours
|
|
23
|
+
const C = {
|
|
24
|
+
reset: '\x1b[0m',
|
|
25
|
+
bright: '\x1b[1m',
|
|
26
|
+
green: '\x1b[32m',
|
|
27
|
+
yellow: '\x1b[33m',
|
|
28
|
+
cyan: '\x1b[36m',
|
|
29
|
+
red: '\x1b[31m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
magenta: '\x1b[35m',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// CLI version for session files
|
|
35
|
+
const CLI_VERSION = '1.0.3';
|
|
36
|
+
|
|
37
|
+
// Derivation path for Aether wallets
|
|
38
|
+
const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Paths
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function getAetherDir() {
|
|
45
|
+
return path.join(os.homedir(), '.aether');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getWalletsDir() {
|
|
49
|
+
return path.join(getAetherDir(), 'wallets');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSessionsDir() {
|
|
53
|
+
return path.join(getAetherDir(), 'sessions');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getConfigPath() {
|
|
57
|
+
return path.join(getAetherDir(), 'config.json');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ensureDirs() {
|
|
61
|
+
const wd = getWalletsDir();
|
|
62
|
+
if (!fs.existsSync(wd)) fs.mkdirSync(wd, { recursive: true });
|
|
63
|
+
const sd = getSessionsDir();
|
|
64
|
+
if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadConfig() {
|
|
68
|
+
const p = getConfigPath();
|
|
69
|
+
if (!fs.existsSync(p)) return { defaultWallet: null, version: 1 };
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return { defaultWallet: null, version: 1 };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function saveConfig(cfg) {
|
|
78
|
+
ensureDirs();
|
|
79
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Crypto helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Derive an Ed25519 keypair from a BIP39 seed.
|
|
88
|
+
* BIP39 seed → 64-byte seed → TweetNaCl keypair
|
|
89
|
+
*/
|
|
90
|
+
function deriveKeypair(mnemonic, derivationPath) {
|
|
91
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
92
|
+
throw new Error('Invalid mnemonic phrase.');
|
|
93
|
+
}
|
|
94
|
+
const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
95
|
+
const seed32 = seedBuffer.slice(0, 32);
|
|
96
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
97
|
+
return {
|
|
98
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
99
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Format Aether address: ATH + base58check of public key.
|
|
105
|
+
*/
|
|
106
|
+
function formatAddress(publicKey) {
|
|
107
|
+
return 'ATH' + bs58.encode(publicKey);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Session management helpers
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
function sessionFilePath(token) {
|
|
115
|
+
return path.join(getSessionsDir(), `${token}.json`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Generate a UUID v4 session token */
|
|
119
|
+
function generateSessionToken() {
|
|
120
|
+
return crypto.randomUUID();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Save session to ~/.aether/sessions/<uuid>.json
|
|
125
|
+
* Fields: wallet_address, created_at, expires_at, verified, cli_version
|
|
126
|
+
*/
|
|
127
|
+
function saveSession(token, wallet_address, expires_in_minutes = 10) {
|
|
128
|
+
ensureDirs();
|
|
129
|
+
const now = new Date();
|
|
130
|
+
const expires_at = new Date(now.getTime() + expires_in_minutes * 60 * 1000);
|
|
131
|
+
const session = {
|
|
132
|
+
wallet_address,
|
|
133
|
+
created_at: now.toISOString(),
|
|
134
|
+
expires_at: expires_at.toISOString(),
|
|
135
|
+
verified: false,
|
|
136
|
+
cli_version: CLI_VERSION,
|
|
137
|
+
};
|
|
138
|
+
fs.writeFileSync(sessionFilePath(token), JSON.stringify(session, null, 2));
|
|
139
|
+
return session;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Load a session, or return null if missing or expired */
|
|
143
|
+
function getSession(token) {
|
|
144
|
+
const fp = sessionFilePath(token);
|
|
145
|
+
if (!fs.existsSync(fp)) return null;
|
|
146
|
+
try {
|
|
147
|
+
const session = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
148
|
+
if (new Date(session.expires_at) < new Date()) return null;
|
|
149
|
+
return session;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Mark a session as verified */
|
|
156
|
+
function markSessionVerified(token) {
|
|
157
|
+
const session = getSession(token);
|
|
158
|
+
if (!session) return false;
|
|
159
|
+
session.verified = true;
|
|
160
|
+
fs.writeFileSync(sessionFilePath(token), JSON.stringify(session, null, 2));
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Delete a session file */
|
|
165
|
+
function deleteSession(token) {
|
|
166
|
+
const fp = sessionFilePath(token);
|
|
167
|
+
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Poll ~/.aether/sessions/<token>.json every 2 seconds.
|
|
172
|
+
* Resolves when verified=true OR session expired/timeout.
|
|
173
|
+
* Returns { verified: boolean, reason?: 'expired' | 'timeout' }
|
|
174
|
+
*/
|
|
175
|
+
async function pollForVerification(token, timeout_ms = 600000) {
|
|
176
|
+
const interval_ms = 2000;
|
|
177
|
+
const max_retries = Math.floor(timeout_ms / interval_ms);
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < max_retries; i++) {
|
|
180
|
+
const session = getSession(token);
|
|
181
|
+
if (session && session.verified) {
|
|
182
|
+
return { verified: true };
|
|
183
|
+
}
|
|
184
|
+
if (!session) {
|
|
185
|
+
return { verified: false, reason: 'expired' };
|
|
186
|
+
}
|
|
187
|
+
await new Promise((res) => setTimeout(res, interval_ms));
|
|
188
|
+
}
|
|
189
|
+
return { verified: false, reason: 'timeout' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Get the site URL from env var or default */
|
|
193
|
+
function getSiteUrl() {
|
|
194
|
+
return process.env.AETHER_SITE_URL || 'https://jelly-legs-ai.github.io';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Open URL in the default browser (cross-platform) */
|
|
198
|
+
function openBrowser(url) {
|
|
199
|
+
const platform = os.platform();
|
|
200
|
+
try {
|
|
201
|
+
if (platform === 'win32') {
|
|
202
|
+
execSync(`start "" "${url}"`, { shell: 'cmd' });
|
|
203
|
+
} else if (platform === 'darwin') {
|
|
204
|
+
execSync(`open "${url}"`);
|
|
205
|
+
} else {
|
|
206
|
+
execSync(`xdg-open "${url}"`);
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Wallet file helpers
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
function walletFilePath(address) {
|
|
219
|
+
return path.join(getWalletsDir(), `${address}.json`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function loadWallet(address) {
|
|
223
|
+
const fp = walletFilePath(address);
|
|
224
|
+
if (!fs.existsSync(fp)) return null;
|
|
225
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function saveWalletFile(address, publicKey) {
|
|
229
|
+
ensureDirs();
|
|
230
|
+
const data = {
|
|
231
|
+
version: 1,
|
|
232
|
+
address,
|
|
233
|
+
public_key: bs58.encode(publicKey),
|
|
234
|
+
created_at: new Date().toISOString(),
|
|
235
|
+
derivation_path: DERIVATION_PATH,
|
|
236
|
+
};
|
|
237
|
+
fs.writeFileSync(walletFilePath(address), JSON.stringify(data, null, 2));
|
|
238
|
+
return data;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Readline helpers
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
function createRl() {
|
|
246
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function question(rl, q) {
|
|
250
|
+
return new Promise((res) => rl.question(q, res));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function askMnemonic(rl, questionText) {
|
|
254
|
+
console.log(`\n${C.cyan}${questionText}${C.reset}`);
|
|
255
|
+
console.log(`${C.dim}Enter your ${C.bright}12 or 24${C.reset}${C.dim}-word mnemonic phrase, one space-separated line:${C.reset}`);
|
|
256
|
+
const raw = await question(rl, ` > ${C.reset}`);
|
|
257
|
+
return raw.trim().toLowerCase();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// CREATE WALLET
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
async function createWallet(rl) {
|
|
265
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Creation ─────────────────────────────────────${C.reset}`);
|
|
266
|
+
console.log(` ${C.green}1)${C.reset} Create new wallet — generates a fresh 12-word mnemonic`);
|
|
267
|
+
console.log(` ${C.green}2)${C.reset} Import existing — enter your own mnemonic to restore\n`);
|
|
268
|
+
|
|
269
|
+
const choice = await question(rl, ` Choose [1/2]: ${C.reset}`);
|
|
270
|
+
|
|
271
|
+
let mnemonic;
|
|
272
|
+
if (choice.trim() === '1') {
|
|
273
|
+
mnemonic = bip39.generateMnemonic(128);
|
|
274
|
+
} else if (choice.trim() === '2') {
|
|
275
|
+
mnemonic = await askMnemonic(rl, 'Importing existing wallet');
|
|
276
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
277
|
+
console.log(`\n ${C.red}✗ Invalid BIP39 mnemonic.${C.reset} Please check your word list and try again.`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
console.log(`\n ${C.red}✗ Invalid choice.${C.reset} Run \`aether wallet create\` again.`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let keyPair;
|
|
286
|
+
try {
|
|
287
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
console.log(`\n ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const address = formatAddress(keyPair.publicKey);
|
|
294
|
+
|
|
295
|
+
if (choice.trim() === '1') {
|
|
296
|
+
const words = mnemonic.split(' ');
|
|
297
|
+
console.log(`\n`);
|
|
298
|
+
console.log(`${C.red}${C.bright}╔═══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
299
|
+
console.log(`${C.red}${C.bright}║ YOUR WALLET PASSPHRASE ║${C.reset}`);
|
|
300
|
+
console.log(`${C.red}${C.bright}╚═══════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
301
|
+
console.log(`\n${C.yellow} Write these words down. They cannot be recovered.${C.reset}`);
|
|
302
|
+
console.log(`${C.yellow} No copy is stored. If you lose them, your wallet is UNRECOVERABLE.${C.reset}\n`);
|
|
303
|
+
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]}`);
|
|
304
|
+
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]}`);
|
|
305
|
+
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]}`);
|
|
306
|
+
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]}`);
|
|
307
|
+
console.log(`\n`);
|
|
308
|
+
await question(rl, ` ${C.cyan}Press Enter when you have saved your passphrase.${C.reset}\n`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const walletData = saveWalletFile(address, keyPair.publicKey);
|
|
312
|
+
const cfg = loadConfig();
|
|
313
|
+
cfg.defaultWallet = address;
|
|
314
|
+
saveConfig(cfg);
|
|
315
|
+
|
|
316
|
+
console.log(`${C.green}✓ Wallet created:${C.reset} ${C.bright}${address}${C.reset}`);
|
|
317
|
+
console.log(`${C.dim} Saved to:${C.reset} ${walletFilePath(address)}`);
|
|
318
|
+
console.log(`${C.green}✓ Set as default wallet.${C.reset}\n`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// LIST WALLETS
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
async function listWallets(rl) {
|
|
326
|
+
ensureDirs();
|
|
327
|
+
const cfg = loadConfig();
|
|
328
|
+
const defaultWallet = cfg.defaultWallet;
|
|
329
|
+
|
|
330
|
+
let files;
|
|
331
|
+
try {
|
|
332
|
+
files = fs.readdirSync(getWalletsDir()).filter((f) => f.endsWith('.json'));
|
|
333
|
+
} catch {
|
|
334
|
+
files = [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (files.length === 0) {
|
|
338
|
+
console.log(`\n ${C.dim}No wallets found. Create one with:${C.reset}`);
|
|
339
|
+
console.log(` ${C.cyan}aether wallet create${C.reset}\n`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(`\n${C.bright}${C.cyan}── Aether Wallets ─────────────────────────────────────────${C.reset}\n`);
|
|
344
|
+
console.log(` ${C.dim}Location: ${getWalletsDir()}${C.reset}\n`);
|
|
345
|
+
|
|
346
|
+
const wallets = files
|
|
347
|
+
.map((f) => {
|
|
348
|
+
try {
|
|
349
|
+
return JSON.parse(fs.readFileSync(path.join(getWalletsDir(), f), 'utf8'));
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
.filter(Boolean);
|
|
355
|
+
|
|
356
|
+
wallets.sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
357
|
+
|
|
358
|
+
for (const w of wallets) {
|
|
359
|
+
const isDefault = w.address === defaultWallet;
|
|
360
|
+
const marker = isDefault ? ` ${C.green}★ default${C.reset}` : '';
|
|
361
|
+
const date = w.created_at ? new Date(w.created_at).toLocaleDateString() : 'unknown';
|
|
362
|
+
console.log(` ${C.bright}${w.address}${C.reset}${marker}`);
|
|
363
|
+
console.log(` ${C.dim} Created: ${date} | ${w.derivation_path}${C.reset}`);
|
|
364
|
+
console.log();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (defaultWallet) {
|
|
368
|
+
console.log(` ${C.green}★${C.reset} = default wallet (used for signing transactions)\n`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// IMPORT WALLET
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
async function importWallet(rl) {
|
|
377
|
+
const mnemonic = await askMnemonic(rl, 'Importing wallet from mnemonic');
|
|
378
|
+
|
|
379
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
380
|
+
const words = mnemonic.split(/\s+/);
|
|
381
|
+
if (words.length !== 12 && words.length !== 24) {
|
|
382
|
+
console.log(`\n ${C.red}✗ Invalid word count:${C.reset} got ${words.length}, expected 12 or 24.`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
console.log(`\n ${C.red}✗ Invalid BIP39 mnemonic.${C.reset} Please check your word list and try again.`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let keyPair;
|
|
390
|
+
try {
|
|
391
|
+
keyPair = deriveKeypair(mnemonic, DERIVATION_PATH);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
console.log(`\n ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const address = formatAddress(keyPair.publicKey);
|
|
398
|
+
|
|
399
|
+
if (loadWallet(address)) {
|
|
400
|
+
console.log(`\n ${C.yellow}⚠ Wallet already exists:${C.reset} ${address}`);
|
|
401
|
+
console.log(` ${C.dim}No new file created.${C.reset}\n`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const walletData = saveWalletFile(address, keyPair.publicKey);
|
|
406
|
+
const cfg = loadConfig();
|
|
407
|
+
cfg.defaultWallet = address;
|
|
408
|
+
saveConfig(cfg);
|
|
409
|
+
|
|
410
|
+
console.log(`\n${C.green}✓ Wallet imported:${C.reset} ${C.bright}${address}${C.reset}`);
|
|
411
|
+
console.log(`${C.dim} Saved to:${C.reset} ${walletFilePath(address)}`);
|
|
412
|
+
console.log(`${C.green}✓ Set as default wallet.${C.reset}\n`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// DEFAULT WALLET
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
async function defaultWallet(rl) {
|
|
420
|
+
const cfg = loadConfig();
|
|
421
|
+
const defaultAddr = cfg.defaultWallet;
|
|
422
|
+
|
|
423
|
+
const args = process.argv.slice(4);
|
|
424
|
+
if (args.includes('--set') || args.includes('-s')) {
|
|
425
|
+
const setIdx = args.indexOf('--set') !== -1 ? args.indexOf('--set') : args.indexOf('-s');
|
|
426
|
+
const address = args[setIdx + 1];
|
|
427
|
+
if (!address) {
|
|
428
|
+
console.log(`\n ${C.red}Usage:${C.reset} aether wallet default --set <address>\n`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const w = loadWallet(address);
|
|
432
|
+
if (!w) {
|
|
433
|
+
console.log(`\n ${C.red}✗ Wallet not found:${C.reset} ${address}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
cfg.defaultWallet = address;
|
|
437
|
+
saveConfig(cfg);
|
|
438
|
+
console.log(`\n${C.green}✓ Default wallet set to:${C.reset} ${address}\n`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log(`\n${C.bright}${C.cyan}── Default Wallet ─────────────────────────────────────────${C.reset}\n`);
|
|
443
|
+
if (!defaultAddr) {
|
|
444
|
+
console.log(` ${C.dim}No default wallet set.${C.reset}`);
|
|
445
|
+
console.log(` ${C.dim}Usage: aether wallet default --set <address>${C.reset}\n`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const w = loadWallet(defaultAddr);
|
|
450
|
+
if (w) {
|
|
451
|
+
console.log(` ${C.green}★${C.reset} ${C.bright}${defaultAddr}${C.reset}`);
|
|
452
|
+
console.log(` ${C.dim} Created: ${new Date(w.created_at).toLocaleString()}${C.reset}`);
|
|
453
|
+
console.log(` ${C.dim} Derivation: ${w.derivation_path}${C.reset}\n`);
|
|
454
|
+
} else {
|
|
455
|
+
console.log(` ${C.yellow}⚠ Default wallet file missing, but config references:${C.reset}`);
|
|
456
|
+
console.log(` ${defaultAddr}\n`);
|
|
457
|
+
console.log(` ${C.dim}Run:${C.reset} aether wallet default --set <address> ${C.dim}to update.${C.reset}\n`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// CONNECT WALLET
|
|
463
|
+
// Generates a session token, opens browser to verify page, polls until done.
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
async function connectWallet(rl) {
|
|
467
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Connect ────────────────────────────────────────${C.reset}\n`);
|
|
468
|
+
|
|
469
|
+
// Resolve wallet address: --address flag or default
|
|
470
|
+
const args = process.argv.slice(4);
|
|
471
|
+
let address = null;
|
|
472
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
473
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
474
|
+
address = args[addrIdx + 1];
|
|
475
|
+
}
|
|
476
|
+
if (!address) {
|
|
477
|
+
const cfg = loadConfig();
|
|
478
|
+
address = cfg.defaultWallet;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!address) {
|
|
482
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
483
|
+
console.log(` ${C.dim}Usage:${C.reset} aether wallet connect --address <address>`);
|
|
484
|
+
console.log(` ${C.dim}Or set a default:${C.reset} aether wallet default --set <address>\n`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const wallet = loadWallet(address);
|
|
489
|
+
if (!wallet) {
|
|
490
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}`);
|
|
491
|
+
console.log(` ${C.dim}Check your wallets with:${C.reset} aether wallet list\n`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Generate session token and save session
|
|
496
|
+
const token = generateSessionToken();
|
|
497
|
+
saveSession(token, address, 10);
|
|
498
|
+
|
|
499
|
+
// Build verification URL
|
|
500
|
+
const siteUrl = getSiteUrl();
|
|
501
|
+
const verifyUrl = `${siteUrl}/wallet/verify?token=${token}&address=${encodeURIComponent(address)}`;
|
|
502
|
+
|
|
503
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
504
|
+
console.log(` ${C.dim} Session expires in 10 minutes${C.reset}`);
|
|
505
|
+
console.log();
|
|
506
|
+
|
|
507
|
+
// Open browser
|
|
508
|
+
const opened = openBrowser(verifyUrl);
|
|
509
|
+
if (opened) {
|
|
510
|
+
console.log(` ${C.green}✓${C.reset} Opened verification page in browser.`);
|
|
511
|
+
console.log(` ${C.dim} ${verifyUrl}${C.reset}`);
|
|
512
|
+
} else {
|
|
513
|
+
console.log(` ${C.yellow}⚠ Could not open browser automatically.${C.reset}`);
|
|
514
|
+
console.log(` ${C.cyan}Open this URL manually:${C.reset}`);
|
|
515
|
+
console.log(` ${C.dim} ${verifyUrl}${C.reset}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log();
|
|
519
|
+
console.log(` ${C.yellow}⏳ Waiting for verification...${C.reset} (Ctrl+C to cancel)`);
|
|
520
|
+
console.log(` ${C.dim} Polling every 2s, timeout after 10 minutes${C.reset}`);
|
|
521
|
+
|
|
522
|
+
// Poll for verification (blocking, async)
|
|
523
|
+
const result = await pollForVerification(token, 600000);
|
|
524
|
+
|
|
525
|
+
if (result.verified) {
|
|
526
|
+
console.log(`\n${C.green}✓ Wallet verified and connected!${C.reset}`);
|
|
527
|
+
console.log(` ${C.green}★${C.reset} ${address}`);
|
|
528
|
+
deleteSession(token);
|
|
529
|
+
console.log();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (result.reason === 'expired') {
|
|
534
|
+
console.log(`\n ${C.red}✗ Session expired.${C.reset} Please run ${C.cyan}aether wallet connect${C.reset} again.\n`);
|
|
535
|
+
} else {
|
|
536
|
+
console.log(`\n ${C.red}✗ Verification timed out (10 minutes).${C.reset} Please run ${C.cyan}aether wallet connect${C.reset} again.\n`);
|
|
537
|
+
}
|
|
538
|
+
deleteSession(token);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// BALANCE
|
|
544
|
+
// Query chain RPC GET /v1/account/<addr> for real AETH balance
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
function getDefaultRpc() {
|
|
548
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Make HTTP GET request to the RPC endpoint
|
|
553
|
+
*/
|
|
554
|
+
function httpRequest(rpcUrl, path) {
|
|
555
|
+
return new Promise((resolve, reject) => {
|
|
556
|
+
const url = new URL(path, rpcUrl);
|
|
557
|
+
const lib = url.protocol === 'https:' ? require('https') : require('http');
|
|
558
|
+
const req = lib.request({
|
|
559
|
+
hostname: url.hostname,
|
|
560
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
561
|
+
path: url.pathname + url.search,
|
|
562
|
+
method: 'GET',
|
|
563
|
+
headers: { 'Content-Type': 'application/json' },
|
|
564
|
+
}, (res) => {
|
|
565
|
+
let data = '';
|
|
566
|
+
res.on('data', (chunk) => data += chunk);
|
|
567
|
+
res.on('end', () => {
|
|
568
|
+
try { resolve(JSON.parse(data)); }
|
|
569
|
+
catch { resolve(data); }
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
req.on('error', reject);
|
|
573
|
+
req.end();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Format lamports as AETH string (1 AETH = 1e9 lamports)
|
|
579
|
+
*/
|
|
580
|
+
function formatAether(lamports) {
|
|
581
|
+
const aeth = lamports / 1e9;
|
|
582
|
+
if (aeth === 0) return '0 AETH';
|
|
583
|
+
// Show up to 4 decimal places, stripping trailing zeros
|
|
584
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function balanceWallet(rl) {
|
|
588
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Balance ───────────────────────────────────────${C.reset}\n`);
|
|
589
|
+
|
|
590
|
+
// Resolve wallet address: --address flag or default
|
|
591
|
+
const args = process.argv.slice(4);
|
|
592
|
+
let address = null;
|
|
593
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
594
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
595
|
+
address = args[addrIdx + 1];
|
|
596
|
+
}
|
|
597
|
+
if (!address) {
|
|
598
|
+
const cfg = loadConfig();
|
|
599
|
+
address = cfg.defaultWallet;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!address) {
|
|
603
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
604
|
+
console.log(` ${C.dim}Usage:${C.reset} aether wallet balance --address <address>`);
|
|
605
|
+
console.log(` ${C.dim}Or set a default:${C.reset} aether wallet default --set <address>\n`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const rpcUrl = getDefaultRpc();
|
|
610
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
611
|
+
console.log(` ${C.dim} RPC: ${rpcUrl}${C.reset}`);
|
|
612
|
+
console.log();
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
// Strip ATH prefix if present for API call
|
|
616
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
617
|
+
const account = await httpRequest(rpcUrl, `/v1/account/${rawAddr}`);
|
|
618
|
+
|
|
619
|
+
if (!account || account.error) {
|
|
620
|
+
console.log(` ${C.yellow}⚠ Account not found on chain or RPC error.${C.reset}`);
|
|
621
|
+
console.log(` ${C.dim} This is normal for new wallets with 0 balance.${C.reset}`);
|
|
622
|
+
console.log(` ${C.dim} RPC response: ${JSON.stringify(account?.error || account)}${C.reset}\n`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const lamports = account.lamports || 0;
|
|
627
|
+
console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(lamports)}${C.reset}`);
|
|
628
|
+
console.log(` ${C.dim} Raw: ${lamports} lamports${C.reset}`);
|
|
629
|
+
console.log();
|
|
630
|
+
|
|
631
|
+
if (account.owner) {
|
|
632
|
+
const ownerStr = Array.isArray(account.owner)
|
|
633
|
+
? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
|
|
634
|
+
: account.owner;
|
|
635
|
+
console.log(` ${C.dim} Owner: ${ownerStr}${C.reset}`);
|
|
636
|
+
}
|
|
637
|
+
if (account.rent_epoch !== undefined) {
|
|
638
|
+
console.log(` ${C.dim} Rent epoch: ${account.rent_epoch}${C.reset}`);
|
|
639
|
+
}
|
|
640
|
+
console.log();
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.log(` ${C.red}✗ Failed to fetch balance:${C.reset} ${err.message}`);
|
|
643
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
644
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
// HTTP helpers for POST requests
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
function httpPost(rpcUrl, path, body) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
const url = new URL(path, rpcUrl);
|
|
655
|
+
const lib = url.protocol === 'https:' ? require('https') : require('http');
|
|
656
|
+
const bodyStr = JSON.stringify(body);
|
|
657
|
+
const req = lib.request({
|
|
658
|
+
hostname: url.hostname,
|
|
659
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
660
|
+
path: url.pathname + url.search,
|
|
661
|
+
method: 'POST',
|
|
662
|
+
headers: {
|
|
663
|
+
'Content-Type': 'application/json',
|
|
664
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
665
|
+
},
|
|
666
|
+
}, (res) => {
|
|
667
|
+
let data = '';
|
|
668
|
+
res.on('data', (chunk) => data += chunk);
|
|
669
|
+
res.on('end', () => {
|
|
670
|
+
try { resolve(JSON.parse(data)); }
|
|
671
|
+
catch { resolve(data); }
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
req.on('error', reject);
|
|
675
|
+
req.write(bodyStr);
|
|
676
|
+
req.end();
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Sign a transaction using the wallet's secret key.
|
|
682
|
+
* Returns a base58-encoded 64-byte signature.
|
|
683
|
+
*/
|
|
684
|
+
function signTransaction(tx, secretKey) {
|
|
685
|
+
const txBytes = Buffer.from(JSON.stringify(tx));
|
|
686
|
+
const sig = nacl.sign.detached(txBytes, secretKey);
|
|
687
|
+
return bs58.encode(sig);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Compute SHA-512 hash of data (as hex string) — used for tx id
|
|
692
|
+
*/
|
|
693
|
+
function sha512hex(data) {
|
|
694
|
+
return crypto.createHash('sha512').update(data).digest('hex');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
// STAKE
|
|
699
|
+
// Submit a Stake transaction via POST /v1/tx
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
|
|
702
|
+
async function stakeWallet(rl) {
|
|
703
|
+
console.log(`\n${C.bright}${C.cyan}── Stake AETH ─────────────────────────────────────────────${C.reset}\n`);
|
|
704
|
+
|
|
705
|
+
// Resolve wallet address
|
|
706
|
+
const args = process.argv.slice(4);
|
|
707
|
+
let address = null;
|
|
708
|
+
let validator = null;
|
|
709
|
+
let amountStr = null;
|
|
710
|
+
|
|
711
|
+
for (let i = 0; i < args.length; i++) {
|
|
712
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
713
|
+
address = args[i + 1];
|
|
714
|
+
}
|
|
715
|
+
if ((args[i] === '--validator' || args[i] === '-v') && args[i + 1]) {
|
|
716
|
+
validator = args[i + 1];
|
|
717
|
+
}
|
|
718
|
+
if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
|
|
719
|
+
amountStr = args[i + 1];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!address) {
|
|
724
|
+
const cfg = loadConfig();
|
|
725
|
+
address = cfg.defaultWallet;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!address) {
|
|
729
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
730
|
+
console.log(` ${C.dim}Usage: aether stake --address <addr> --validator <val> --amount <aeth>${C.reset}\n`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const wallet = loadWallet(address);
|
|
735
|
+
if (!wallet) {
|
|
736
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Derive the full wallet object (secret key needed for signing)
|
|
741
|
+
let keyPair;
|
|
742
|
+
try {
|
|
743
|
+
// Re-derive from public key stored in wallet file
|
|
744
|
+
// The secret key isn't stored — we'd need the mnemonic to re-derive.
|
|
745
|
+
// For signing, the CLI requires the wallet to have been created/imported in this session.
|
|
746
|
+
// We use bs58 decoded publicKey + nacl key derivation from stored entropy.
|
|
747
|
+
// Since we store only publicKey, we need the secret key for signing.
|
|
748
|
+
// Workaround: accept a --sign-with <secretkeybase58> flag for now, or
|
|
749
|
+
// require the wallet to be "active" via a session.
|
|
750
|
+
// For simplicity, derive a keypair using a stored seed phrase approach.
|
|
751
|
+
// The wallet.json only has public_key. We need nacl sign keypair.
|
|
752
|
+
// Let's require the secret key be provided for stake/transfer.
|
|
753
|
+
console.log(` ${C.red}✗ Signing requires the wallet secret key.${C.reset}`);
|
|
754
|
+
console.log(` ${C.dim}The wallet must be created/imported in this session to access the secret key.${C.reset}`);
|
|
755
|
+
console.log(` ${C.dim}For staking, use the JS SDK's offline signing flow instead.${C.reset}`);
|
|
756
|
+
console.log(` ${C.dim}See: aether-cli sdk js${C.reset}\n`);
|
|
757
|
+
return;
|
|
758
|
+
} catch (e) {
|
|
759
|
+
console.log(` ${C.red}✗ Failed to load wallet keys: ${e.message}${C.reset}\n`);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Prompt for missing values interactively
|
|
764
|
+
if (!validator) {
|
|
765
|
+
console.log(` ${C.cyan}Enter validator address:${C.reset}`);
|
|
766
|
+
validator = await question(rl, ` Validator > ${C.reset}`);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (!amountStr) {
|
|
770
|
+
console.log(` ${C.cyan}Enter amount in AETH:${C.reset}`);
|
|
771
|
+
amountStr = await question(rl, ` Amount (AETH) > ${C.reset}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const amount = parseFloat(amountStr);
|
|
775
|
+
if (isNaN(amount) || amount <= 0) {
|
|
776
|
+
console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const lamports = Math.round(amount * 1e9);
|
|
781
|
+
|
|
782
|
+
console.log(` ${C.green}★${C.reset} Wallet: ${C.bright}${address}${C.reset}`);
|
|
783
|
+
console.log(` ${C.green}★${C.reset} Validator: ${C.bright}${validator}${C.reset}`);
|
|
784
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
|
|
785
|
+
console.log();
|
|
786
|
+
|
|
787
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm stake? [y/N]${C.reset} > ${C.reset}`);
|
|
788
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
789
|
+
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Build the transaction
|
|
794
|
+
const tx = {
|
|
795
|
+
signer: address.startsWith('ATH') ? address.slice(3) : address,
|
|
796
|
+
tx_type: 'Stake',
|
|
797
|
+
payload: {
|
|
798
|
+
type: 'Stake',
|
|
799
|
+
data: {
|
|
800
|
+
validator,
|
|
801
|
+
amount: lamports,
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
fee: 0,
|
|
805
|
+
slot: 0,
|
|
806
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const rpcUrl = getDefaultRpc();
|
|
810
|
+
console.log(` ${C.dim}Submitting to ${rpcUrl}...${C.reset}`);
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
const result = await httpPost(rpcUrl, '/v1/tx', tx);
|
|
814
|
+
|
|
815
|
+
if (result.error) {
|
|
816
|
+
console.log(`\n ${C.red}✗ Transaction failed:${C.reset} ${result.error}\n`);
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const sig = result.signature || result.tx_signature || result.id || JSON.stringify(result);
|
|
821
|
+
console.log(`\n${C.green}✓ Stake transaction submitted!${C.reset}`);
|
|
822
|
+
console.log(` ${C.dim}Signature:${C.reset} ${sig}`);
|
|
823
|
+
console.log(` ${C.dim}Use: aether-cli validator status${C.reset} to monitor.\n`);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
|
|
826
|
+
console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// TRANSFER
|
|
833
|
+
// Submit a Transfer transaction via POST /v1/tx
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
835
|
+
|
|
836
|
+
async function transferWallet(rl) {
|
|
837
|
+
console.log(`\n${C.bright}${C.cyan}── Transfer AETH ─────────────────────────────────────────${C.reset}\n`);
|
|
838
|
+
|
|
839
|
+
const args = process.argv.slice(4);
|
|
840
|
+
let address = null;
|
|
841
|
+
let recipient = null;
|
|
842
|
+
let amountStr = null;
|
|
843
|
+
|
|
844
|
+
for (let i = 0; i < args.length; i++) {
|
|
845
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
846
|
+
address = args[i + 1];
|
|
847
|
+
}
|
|
848
|
+
if ((args[i] === '--to' || args[i] === '-t') && args[i + 1]) {
|
|
849
|
+
recipient = args[i + 1];
|
|
850
|
+
}
|
|
851
|
+
if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) {
|
|
852
|
+
amountStr = args[i + 1];
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!address) {
|
|
857
|
+
const cfg = loadConfig();
|
|
858
|
+
address = cfg.defaultWallet;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!address) {
|
|
862
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
863
|
+
console.log(` ${C.dim}Usage: aether transfer --to <addr> --amount <aeth>${C.reset}\n`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const wallet = loadWallet(address);
|
|
868
|
+
if (!wallet) {
|
|
869
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${address}\n`);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Prompt for missing values interactively
|
|
874
|
+
if (!recipient) {
|
|
875
|
+
console.log(` ${C.cyan}Enter recipient address:${C.reset}`);
|
|
876
|
+
recipient = await question(rl, ` Recipient > ${C.reset}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (!amountStr) {
|
|
880
|
+
console.log(` ${C.cyan}Enter amount in AETH:${C.reset}`);
|
|
881
|
+
amountStr = await question(rl, ` Amount (AETH) > ${C.reset}`);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const amount = parseFloat(amountStr);
|
|
885
|
+
if (isNaN(amount) || amount <= 0) {
|
|
886
|
+
console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const lamports = Math.round(amount * 1e9);
|
|
891
|
+
|
|
892
|
+
console.log(` ${C.green}★${C.reset} From: ${C.bright}${address}${C.reset}`);
|
|
893
|
+
console.log(` ${C.green}★${C.reset} To: ${C.bright}${recipient}${C.reset}`);
|
|
894
|
+
console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports} lamports)`);
|
|
895
|
+
console.log();
|
|
896
|
+
|
|
897
|
+
const confirm = await question(rl, ` ${C.yellow}Confirm transfer? [y/N]${C.reset} > ${C.reset}`);
|
|
898
|
+
if (!confirm.trim().toLowerCase().startsWith('y')) {
|
|
899
|
+
console.log(` ${C.dim}Cancelled.${C.reset}\n`);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const tx = {
|
|
904
|
+
signer: address.startsWith('ATH') ? address.slice(3) : address,
|
|
905
|
+
tx_type: 'Transfer',
|
|
906
|
+
payload: {
|
|
907
|
+
type: 'Transfer',
|
|
908
|
+
data: {
|
|
909
|
+
recipient,
|
|
910
|
+
amount: lamports,
|
|
911
|
+
nonce: Math.floor(Math.random() * 0xffffffff),
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
fee: 0,
|
|
915
|
+
slot: 0,
|
|
916
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const rpcUrl = getDefaultRpc();
|
|
920
|
+
console.log(` ${C.dim}Submitting to ${rpcUrl}...${C.reset}`);
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
const result = await httpPost(rpcUrl, '/v1/tx', tx);
|
|
924
|
+
|
|
925
|
+
if (result.error) {
|
|
926
|
+
console.log(`\n ${C.red}✗ Transaction failed:${C.reset} ${result.error}\n`);
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const sig = result.signature || result.tx_signature || result.id || JSON.stringify(result);
|
|
931
|
+
console.log(`\n${C.green}✓ Transfer transaction submitted!${C.reset}`);
|
|
932
|
+
console.log(` ${C.dim}Signature:${C.reset} ${sig}`);
|
|
933
|
+
console.log(` ${C.dim}Check balance: aether wallet balance --address ${address}${C.reset}\n`);
|
|
934
|
+
} catch (err) {
|
|
935
|
+
console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
|
|
936
|
+
console.log(` ${C.dim}Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ---------------------------------------------------------------------------
|
|
942
|
+
// TX HISTORY
|
|
943
|
+
// Fetch and display recent transactions for an address
|
|
944
|
+
// ---------------------------------------------------------------------------
|
|
945
|
+
|
|
946
|
+
async function txHistory(rl) {
|
|
947
|
+
console.log(`\n${C.bright}${C.cyan}── Transaction History ────────────────────────────────────${C.reset}\n`);
|
|
948
|
+
|
|
949
|
+
const args = process.argv.slice(4);
|
|
950
|
+
let address = null;
|
|
951
|
+
let limit = 20;
|
|
952
|
+
let asJson = false;
|
|
953
|
+
|
|
954
|
+
const addrIdx = args.findIndex((a) => a === '--address' || a === '-a');
|
|
955
|
+
if (addrIdx !== -1 && args[addrIdx + 1]) {
|
|
956
|
+
address = args[addrIdx + 1];
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const limitIdx = args.findIndex((a) => a === '--limit' || a === '-l');
|
|
960
|
+
if (limitIdx !== -1 && args[limitIdx + 1]) {
|
|
961
|
+
limit = parseInt(args[limitIdx + 1], 10);
|
|
962
|
+
if (isNaN(limit) || limit < 1 || limit > 100) {
|
|
963
|
+
console.log(` ${C.red}✗ --limit must be between 1 and 100.${C.reset}\n`);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
asJson = args.includes('--json') || args.includes('-j');
|
|
969
|
+
|
|
970
|
+
if (!address) {
|
|
971
|
+
const cfg = loadConfig();
|
|
972
|
+
address = cfg.defaultWallet;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (!address) {
|
|
976
|
+
console.log(` ${C.red}✗ No wallet address specified and no default wallet set.${C.reset}`);
|
|
977
|
+
console.log(` ${C.dim}Usage: aether tx history --address <addr> [--limit 20] [--json]${C.reset}\n`);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const rpcUrl = getDefaultRpc();
|
|
982
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
983
|
+
|
|
984
|
+
if (!asJson) {
|
|
985
|
+
console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
|
|
986
|
+
console.log(` ${C.dim} RPC: ${rpcUrl} Limit: ${limit}${C.reset}`);
|
|
987
|
+
console.log();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
try {
|
|
991
|
+
// Fetch account info first (for context)
|
|
992
|
+
const account = await httpRequest(rpcUrl, `/v1/account/${rawAddr}`);
|
|
993
|
+
|
|
994
|
+
// Fetch transactions for this address
|
|
995
|
+
const txs = await httpRequest(rpcUrl, `/v1/tx?address=${encodeURIComponent(rawAddr)}&limit=${limit}`);
|
|
996
|
+
|
|
997
|
+
if (asJson) {
|
|
998
|
+
const out = {
|
|
999
|
+
address,
|
|
1000
|
+
rpc: rpcUrl,
|
|
1001
|
+
account: account && !account.error ? {
|
|
1002
|
+
lamports: account.lamports,
|
|
1003
|
+
owner: account.owner,
|
|
1004
|
+
} : null,
|
|
1005
|
+
transactions: txs && !txs.error ? (Array.isArray(txs) ? txs : txs.transactions || []) : [],
|
|
1006
|
+
fetched_at: new Date().toISOString(),
|
|
1007
|
+
};
|
|
1008
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (!account || account.error) {
|
|
1013
|
+
console.log(` ${C.yellow}⚠ Account not found on chain.${C.reset}`);
|
|
1014
|
+
} else {
|
|
1015
|
+
console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(account.lamports || 0)}${C.reset}`);
|
|
1016
|
+
if (account.owner) {
|
|
1017
|
+
const ownerStr = Array.isArray(account.owner)
|
|
1018
|
+
? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
|
|
1019
|
+
: account.owner;
|
|
1020
|
+
console.log(` ${C.dim} Owner: ${ownerStr}${C.reset}`);
|
|
1021
|
+
}
|
|
1022
|
+
console.log();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (!txs || txs.error) {
|
|
1026
|
+
console.log(` ${C.yellow}⚠ No transaction history available.${C.reset}`);
|
|
1027
|
+
console.log(` ${C.dim} RPC response: ${JSON.stringify(txs?.error || txs)}${C.reset}`);
|
|
1028
|
+
console.log(` ${C.dim} (New wallets with 0 txs will return empty results)${C.reset}\n`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const txList = Array.isArray(txs) ? txs : txs.transactions || [];
|
|
1033
|
+
console.log(` ${C.bright}Recent Transactions (${txList.length})${C.reset}\n`);
|
|
1034
|
+
|
|
1035
|
+
if (txList.length === 0) {
|
|
1036
|
+
console.log(` ${C.dim} No transactions found for this address.${C.reset}`);
|
|
1037
|
+
console.log(` ${C.dim} This is normal for new wallets.${C.reset}\n`);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const typeColors = {
|
|
1042
|
+
Transfer: C.cyan,
|
|
1043
|
+
Stake: C.green,
|
|
1044
|
+
Unstake: C.yellow,
|
|
1045
|
+
ClaimRewards: C.magenta,
|
|
1046
|
+
CreateNFT: C.red,
|
|
1047
|
+
MintNFT: C.red,
|
|
1048
|
+
TransferNFT: C.cyan,
|
|
1049
|
+
UpdateMetadata: C.yellow,
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
for (const tx of txList) {
|
|
1053
|
+
const txType = tx.tx_type || tx.type || 'Unknown';
|
|
1054
|
+
const color = typeColors[txType] || C.reset;
|
|
1055
|
+
const ts = tx.timestamp
|
|
1056
|
+
? new Date(tx.timestamp * 1000).toISOString()
|
|
1057
|
+
: 'unknown';
|
|
1058
|
+
const sig = tx.signature || tx.id || tx.tx_signature || '—';
|
|
1059
|
+
const sigShort = sig.length > 20 ? sig.slice(0, 8) + '…' + sig.slice(-8) : sig;
|
|
1060
|
+
|
|
1061
|
+
console.log(` ${C.dim}┌─ ${ts}${C.reset}`);
|
|
1062
|
+
console.log(` │ ${C.bright}${color}${txType}${C.reset} ${C.dim}sig:${C.reset} ${sigShort}`);
|
|
1063
|
+
if (tx.payload && tx.payload.data) {
|
|
1064
|
+
const d = tx.payload.data;
|
|
1065
|
+
if (d.recipient) console.log(` │ ${C.dim} → to: ${d.recipient}${C.reset}`);
|
|
1066
|
+
if (d.amount) console.log(` │ ${C.dim} amount: ${formatAether(d.amount)}${C.reset}`);
|
|
1067
|
+
if (d.validator) console.log(` │ ${C.dim} validator: ${d.validator}${C.reset}`);
|
|
1068
|
+
if (d.stake_account) console.log(` │ ${C.dim} stake_acct: ${d.stake_account}${C.reset}`);
|
|
1069
|
+
}
|
|
1070
|
+
if (tx.fee !== undefined && tx.fee > 0) {
|
|
1071
|
+
console.log(` │ ${C.dim} fee: ${tx.fee} lamports${C.reset}`);
|
|
1072
|
+
}
|
|
1073
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
1074
|
+
console.log();
|
|
1075
|
+
}
|
|
1076
|
+
console.log();
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
console.log(` ${C.red}✗ Failed to fetch transaction history:${C.reset} ${err.message}`);
|
|
1079
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
1080
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ---------------------------------------------------------------------------
|
|
1085
|
+
// Main dispatcher
|
|
1086
|
+
// ---------------------------------------------------------------------------
|
|
1087
|
+
|
|
1088
|
+
async function walletCommand() {
|
|
1089
|
+
// CLI: argv = [node, index.js, wallet, <subcmd>]
|
|
1090
|
+
let subcmd = process.argv[2];
|
|
1091
|
+
if (subcmd === 'wallet.js' || subcmd === 'wallet') {
|
|
1092
|
+
subcmd = process.argv[3];
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const rl = createRl();
|
|
1096
|
+
try {
|
|
1097
|
+
if (!subcmd || subcmd === 'create') {
|
|
1098
|
+
await createWallet(rl);
|
|
1099
|
+
} else if (subcmd === 'list') {
|
|
1100
|
+
await listWallets(rl);
|
|
1101
|
+
} else if (subcmd === 'import') {
|
|
1102
|
+
await importWallet(rl);
|
|
1103
|
+
} else if (subcmd === 'default') {
|
|
1104
|
+
await defaultWallet(rl);
|
|
1105
|
+
} else if (subcmd === 'connect') {
|
|
1106
|
+
await connectWallet(rl);
|
|
1107
|
+
} else if (subcmd === 'balance') {
|
|
1108
|
+
await balanceWallet(rl);
|
|
1109
|
+
} else if (subcmd === 'stake') {
|
|
1110
|
+
await stakeWallet(rl);
|
|
1111
|
+
} else if (subcmd === 'transfer') {
|
|
1112
|
+
await transferWallet(rl);
|
|
1113
|
+
} else if (subcmd === 'history' || subcmd === 'tx') {
|
|
1114
|
+
await txHistory(rl);
|
|
1115
|
+
} else {
|
|
1116
|
+
console.log(`\n ${C.red}Unknown wallet subcommand:${C.reset} ${subcmd}`);
|
|
1117
|
+
console.log(`\n Usage:`);
|
|
1118
|
+
console.log(` ${C.cyan}aether wallet create${C.reset} Create new or import wallet`);
|
|
1119
|
+
console.log(` ${C.cyan}aether wallet list${C.reset} List all wallets`);
|
|
1120
|
+
console.log(` ${C.cyan}aether wallet import${C.reset} Import wallet from mnemonic`);
|
|
1121
|
+
console.log(` ${C.cyan}aether wallet default${C.reset} Show/set default wallet`);
|
|
1122
|
+
console.log(` ${C.cyan}aether wallet connect${C.reset} Connect wallet via browser verification`);
|
|
1123
|
+
console.log(` ${C.cyan}aether wallet balance${C.reset} Query chain balance for an address`);
|
|
1124
|
+
console.log(` ${C.cyan}aether wallet stake${C.reset} Stake AETH to a validator`);
|
|
1125
|
+
console.log(` ${C.cyan}aether wallet transfer${C.reset} Transfer AETH to another address`);
|
|
1126
|
+
console.log(` ${C.cyan}aether wallet history${C.reset} Show recent transactions for an address`);
|
|
1127
|
+
console.log();
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
} finally {
|
|
1131
|
+
rl.close();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
module.exports = { walletCommand };
|
|
1136
|
+
|
|
1137
|
+
if (require.main === module) {
|
|
1138
|
+
walletCommand();
|
|
1139
|
+
}
|