@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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/aether-cli-1.0.0.tgz +0 -0
  4. package/aether-cli-1.8.0.tgz +0 -0
  5. package/aether-hub-1.0.5.tgz +0 -0
  6. package/aether-hub-1.1.8.tgz +0 -0
  7. package/aether-hub-1.2.1.tgz +0 -0
  8. package/commands/account.js +280 -0
  9. package/commands/apy.js +499 -0
  10. package/commands/balance.js +241 -0
  11. package/commands/blockhash.js +181 -0
  12. package/commands/broadcast.js +387 -0
  13. package/commands/claim.js +490 -0
  14. package/commands/config.js +851 -0
  15. package/commands/delegations.js +582 -0
  16. package/commands/doctor.js +769 -0
  17. package/commands/emergency.js +667 -0
  18. package/commands/epoch.js +275 -0
  19. package/commands/fees.js +276 -0
  20. package/commands/index.js +78 -0
  21. package/commands/info.js +495 -0
  22. package/commands/init.js +816 -0
  23. package/commands/install.js +666 -0
  24. package/commands/kyc.js +272 -0
  25. package/commands/logs.js +315 -0
  26. package/commands/monitor.js +431 -0
  27. package/commands/multisig.js +701 -0
  28. package/commands/network.js +429 -0
  29. package/commands/nft.js +857 -0
  30. package/commands/ping.js +266 -0
  31. package/commands/price.js +253 -0
  32. package/commands/rewards.js +931 -0
  33. package/commands/sdk-test.js +477 -0
  34. package/commands/sdk.js +656 -0
  35. package/commands/slot.js +155 -0
  36. package/commands/snapshot.js +470 -0
  37. package/commands/stake-info.js +139 -0
  38. package/commands/stake-positions.js +205 -0
  39. package/commands/stake.js +516 -0
  40. package/commands/stats.js +396 -0
  41. package/commands/status.js +327 -0
  42. package/commands/supply.js +391 -0
  43. package/commands/tps.js +238 -0
  44. package/commands/transfer.js +495 -0
  45. package/commands/tx-history.js +346 -0
  46. package/commands/unstake.js +597 -0
  47. package/commands/validator-info.js +657 -0
  48. package/commands/validator-register.js +593 -0
  49. package/commands/validator-start.js +323 -0
  50. package/commands/validator-status.js +227 -0
  51. package/commands/validators.js +626 -0
  52. package/commands/wallet.js +1570 -0
  53. package/index.js +593 -0
  54. package/lib/errors.js +398 -0
  55. package/package.json +76 -0
  56. package/sdk/README.md +210 -0
  57. package/sdk/index.js +1639 -0
  58. package/sdk/package.json +34 -0
  59. package/sdk/rpc.js +254 -0
  60. package/sdk/test.js +85 -0
  61. package/test/doctor.test.js +76 -0
  62. package/validator-identity.json +4 -0
@@ -0,0 +1,701 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli multisig
4
+ *
5
+ * Multi-signature wallet management for Aether.
6
+ * Create 2-of-3, 3-of-5, or any M-of-N multisig wallets,
7
+ * add signers, view threshold info, and send transactions.
8
+ *
9
+ * Usage:
10
+ * aether multisig create --threshold <m> --signers <addr1,addr2,...>
11
+ * aether multisig list List all multisig wallets
12
+ * aether multisig info --address <addr> Show threshold, signers, balance
13
+ * aether multisig add-signer --address <addr> --signer <newAddr>
14
+ * aether multisig send --address <addr> --to <recipient> --amount <aeth> [--json]
15
+ *
16
+ * Requires AETHER_RPC env var or local node (default: http://127.0.0.1:8899)
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+ const readline = require('readline');
23
+ const crypto = require('crypto');
24
+ const bs58 = require('bs58').default;
25
+ const bip39 = require('bip39');
26
+ const nacl = require('tweetnacl');
27
+
28
+ // ANSI colours
29
+ const C = {
30
+ reset: '\x1b[0m',
31
+ bright: '\x1b[1m',
32
+ dim: '\x1b[2m',
33
+ red: '\x1b[31m',
34
+ green: '\x1b[32m',
35
+ yellow: '\x1b[33m',
36
+ cyan: '\x1b[36m',
37
+ magenta: '\x1b[35m',
38
+ };
39
+
40
+ const CLI_VERSION = '1.2.5';
41
+ const MULTISIG_VERSION = 1;
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Paths & config
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function getAetherDir() {
48
+ return path.join(os.homedir(), '.aether');
49
+ }
50
+
51
+ function getMultisigDir() {
52
+ return path.join(getAetherDir(), 'multisig');
53
+ }
54
+
55
+ function ensureDir(p) {
56
+ if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
57
+ }
58
+
59
+ function getMultisigFilePath(address) {
60
+ return path.join(getMultisigDir(), `${address}.json`);
61
+ }
62
+
63
+ function loadConfig() {
64
+ const p = path.join(getAetherDir(), 'config.json');
65
+ if (!fs.existsSync(p)) return { defaultWallet: null };
66
+ try {
67
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
68
+ } catch {
69
+ return { defaultWallet: null };
70
+ }
71
+ }
72
+
73
+ function loadWallet(address) {
74
+ const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
75
+ if (!fs.existsSync(fp)) return null;
76
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Crypto helpers
81
+ // ---------------------------------------------------------------------------
82
+
83
+ function deriveKeypair(mnemonic) {
84
+ if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic phrase.');
85
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
86
+ const seed32 = seedBuffer.slice(0, 32);
87
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
88
+ return { publicKey: Buffer.from(keyPair.publicKey), secretKey: Buffer.from(keyPair.secretKey) };
89
+ }
90
+
91
+ function formatAddress(publicKey) {
92
+ return 'ATH' + bs58.encode(publicKey);
93
+ }
94
+
95
+ function shortAddress(addr) {
96
+ if (!addr || addr.length < 16) return addr;
97
+ return addr.slice(0, 8) + '…' + addr.slice(-8);
98
+ }
99
+
100
+ function isValidAddress(addr) {
101
+ return addr && addr.startsWith('ATH') && addr.length >= 36;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // SDK Integration - Real blockchain RPC calls
106
+ // ---------------------------------------------------------------------------
107
+
108
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
109
+ const aether = require(sdkPath);
110
+
111
+ function getDefaultRpc() {
112
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
113
+ }
114
+
115
+ function createClient(rpcUrl) {
116
+ return new aether.AetherClient({ rpcUrl });
117
+ }
118
+
119
+ /**
120
+ * Fetch account balance via SDK (GET /v1/account/<addr>)
121
+ */
122
+ async function fetchAccountBalance(rpcUrl, address) {
123
+ const client = createClient(rpcUrl);
124
+ try {
125
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
126
+ const account = await client.getAccountInfo(rawAddr);
127
+ return account && !account.error ? account.lamports : 0;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Submit transaction via SDK (POST /v1/transaction)
135
+ */
136
+ async function submitTransaction(rpcUrl, tx) {
137
+ const client = createClient(rpcUrl);
138
+ return client.sendTransaction(tx);
139
+ }
140
+
141
+ function formatAether(lamports) {
142
+ const aeth = lamports / 1e9;
143
+ if (aeth === 0) return '0 AETH';
144
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Multisig address derivation
149
+ // Derived from threshold M and signer list — sorted lexicographically
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Derive a deterministic multisig address from signers + threshold.
154
+ * Uses SHA-512 of sorted(signers) + threshold as the seed for a keypair.
155
+ * This gives a deterministic address without requiring on-chain registration.
156
+ */
157
+ function deriveMultisigAddress(signers, threshold) {
158
+ // Sort signers lexicographically for deterministic derivation
159
+ const sortedSigners = [...signers].sort();
160
+ const data = JSON.stringify({ signers: sortedSigners, threshold, v: MULTISIG_VERSION });
161
+ const hash = crypto.createHash('sha512').update(data).digest();
162
+ // Use first 32 bytes as seed for nacl keypair
163
+ const seed32 = hash.slice(0, 32);
164
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
165
+ return {
166
+ address: formatAddress(Buffer.from(keyPair.publicKey)),
167
+ publicKey: Buffer.from(keyPair.publicKey),
168
+ };
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Multisig storage
173
+ // ---------------------------------------------------------------------------
174
+
175
+ function saveMultisig(ms) {
176
+ ensureDir(getMultisigDir());
177
+ const fp = getMultisigFilePath(ms.address);
178
+ fs.writeFileSync(fp, JSON.stringify(ms, null, 2));
179
+ }
180
+
181
+ function loadMultisig(address) {
182
+ const fp = getMultisigFilePath(address);
183
+ if (!fs.existsSync(fp)) return null;
184
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
185
+ }
186
+
187
+ function listAllMultisig() {
188
+ ensureDir(getMultisigDir());
189
+ const files = fs.readdirSync(getMultisigDir()).filter(f => f.endsWith('.json'));
190
+ const result = [];
191
+ for (const f of files) {
192
+ try {
193
+ const ms = JSON.parse(fs.readFileSync(path.join(getMultisigDir(), f), 'utf8'));
194
+ result.push(ms);
195
+ } catch {}
196
+ }
197
+ return result.sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Readline helpers
202
+ // ---------------------------------------------------------------------------
203
+
204
+ function createRl() {
205
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
206
+ }
207
+
208
+ function question(rl, q) {
209
+ return new Promise((res) => rl.question(q, res));
210
+ }
211
+
212
+ function askMnemonic(rl, label = 'wallet passphrase') {
213
+ return new Promise(async (res) => {
214
+ console.log(`\n${C.cyan}Enter your 12/24-word ${label}:${C.reset}`);
215
+ console.log(`${C.dim}One space-separated line:${C.reset}`);
216
+ const raw = await question(rl, ` > ${C.reset}`);
217
+ res(raw.trim().toLowerCase());
218
+ });
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // CREATE MULTISIG
223
+ // ---------------------------------------------------------------------------
224
+
225
+ async function createMultisig(rl, args) {
226
+ console.log(`\n${C.bright}${C.cyan}── Create Multi-Signature Wallet ─────────────────────────${C.reset}\n`);
227
+
228
+ // Parse --threshold and --signers from args
229
+ let threshold = null;
230
+ let signers = [];
231
+
232
+ for (let i = 0; i < args.length; i++) {
233
+ if ((args[i] === '--threshold' || args[i] === '-t') && args[i + 1]) {
234
+ threshold = parseInt(args[i + 1], 10);
235
+ }
236
+ if ((args[i] === '--signers' || args[i] === '-s') && args[i + 1]) {
237
+ signers = args[i + 1].split(',').map(s => s.trim()).filter(Boolean);
238
+ }
239
+ }
240
+
241
+ // Interactive prompts for missing values
242
+ if (signers.length === 0) {
243
+ console.log(` ${C.cyan}Enter signer addresses (ATH...), separated by commas.${C.reset}`);
244
+ console.log(` ${C.dim}Example: ATHabc...,ATHdef...,ATHghi...${C.reset}`);
245
+ const rawSigners = await question(rl, ` Signers: ${C.reset}`);
246
+ signers = rawSigners.split(',').map(s => s.trim()).filter(s => s.length > 0);
247
+ }
248
+
249
+ if (signers.length < 2) {
250
+ console.log(` ${C.red}✗ A multisig wallet requires at least 2 signers.${C.reset}\n`);
251
+ return;
252
+ }
253
+
254
+ // Validate all signer addresses
255
+ const invalidSigners = signers.filter(s => !isValidAddress(s));
256
+ if (invalidSigners.length > 0) {
257
+ console.log(` ${C.red}✗ Invalid signer addresses:${C.reset} ${invalidSigners.join(', ')}`);
258
+ console.log(` ${C.dim}All signers must start with 'ATH' and be at least 36 characters.${C.reset}\n`);
259
+ return;
260
+ }
261
+
262
+ // Deduplicate
263
+ const uniqueSigners = [...new Set(signers)];
264
+ if (uniqueSigners.length !== signers.length) {
265
+ console.log(` ${C.yellow}⚠ Duplicate signers removed.${C.reset}`);
266
+ signers = uniqueSigners;
267
+ }
268
+
269
+ if (threshold === null) {
270
+ const defaultThreshold = Math.max(2, Math.ceil(signers.length / 2));
271
+ const rawThresh = await question(rl, ` ${C.cyan}Threshold (M, required signatures)${C.reset} [${defaultThreshold}]: ${C.reset}`);
272
+ threshold = rawThresh.trim() ? parseInt(rawThresh.trim(), 10) : defaultThreshold;
273
+ }
274
+
275
+ if (isNaN(threshold) || threshold < 1 || threshold > signers.length) {
276
+ console.log(` ${C.red}✗ Invalid threshold:${C.reset} ${threshold}. Must be between 1 and ${signers.length}.\n`);
277
+ return;
278
+ }
279
+
280
+ console.log(`\n ${C.green}★${C.reset} Signers (${signers.length}):`);
281
+ for (const s of signers) {
282
+ console.log(` ${C.cyan}${shortAddress(s)}${C.reset}`);
283
+ }
284
+ console.log(` ${C.green}★${C.reset} Threshold: ${C.bright}${threshold} of ${signers.length}${C.reset}`);
285
+ console.log();
286
+
287
+ const confirm = await question(rl, ` ${C.yellow}Create multisig wallet? [y/N]${C.reset} > ${C.reset}`);
288
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
289
+ console.log(` ${C.dim}Cancelled.${C.reset}\n`);
290
+ return;
291
+ }
292
+
293
+ const { address, publicKey } = deriveMultisigAddress(signers, threshold);
294
+
295
+ const ms = {
296
+ version: MULTISIG_VERSION,
297
+ address,
298
+ public_key: bs58.encode(publicKey),
299
+ threshold,
300
+ signers,
301
+ created_at: new Date().toISOString(),
302
+ derivation: 'off-chain deterministic',
303
+ description: '',
304
+ };
305
+
306
+ saveMultisig(ms);
307
+
308
+ console.log(`\n${C.green}✓ Multi-signature wallet created!${C.reset}`);
309
+ console.log(` ${C.green}★${C.reset} Address: ${C.bright}${address}${C.reset}`);
310
+ console.log(` ${C.green}★${C.reset} Threshold: ${threshold}/${signers.length}`);
311
+ console.log(` ${C.dim} Saved to: ${getMultisigFilePath(address)}${C.reset}`);
312
+ console.log(` ${C.dim} Use: aether multisig send --address ${address}${C.reset}\n`);
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // LIST MULTISIG
317
+ // ---------------------------------------------------------------------------
318
+
319
+ async function listMultisig(rl, args) {
320
+ console.log(`\n${C.bright}${C.cyan}── Multi-Signature Wallets ────────────────────────────${C.reset}\n`);
321
+
322
+ const all = listAllMultisig();
323
+
324
+ if (all.length === 0) {
325
+ console.log(` ${C.dim}No multisig wallets found.${C.reset}`);
326
+ console.log(` ${C.dim}Create one with:${C.reset} ${C.cyan}aether multisig create --threshold 2 --signers addr1,addr2,addr3${C.reset}\n`);
327
+ return;
328
+ }
329
+
330
+ const rpcUrl = getDefaultRpc();
331
+ console.log(` ${C.dim}Location: ${getMultisigDir()}${C.reset}\n`);
332
+
333
+ for (const ms of all) {
334
+ const shortAddr = shortAddress(ms.address);
335
+ console.log(` ${C.bright}${C.cyan}${ms.address}${C.reset}`);
336
+ console.log(` ${C.dim} Threshold: ${ms.threshold}/${ms.signers.length} Signers: ${ms.signers.length}${C.reset}`);
337
+ console.log(` ${C.dim} Created: ${new Date(ms.created_at).toLocaleString()}${C.reset}`);
338
+
339
+ // Fetch on-chain balance via SDK (REAL RPC GET /v1/account/<addr>)
340
+ try {
341
+ const balance = await fetchAccountBalance(rpcUrl, ms.address);
342
+ if (balance !== null) {
343
+ console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(balance)}${C.reset}`);
344
+ }
345
+ } catch {}
346
+
347
+ console.log();
348
+ }
349
+ }
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // INFO
353
+ // ---------------------------------------------------------------------------
354
+
355
+ async function infoMultisig(rl, args) {
356
+ console.log(`\n${C.bright}${C.cyan}── Multi-Signature Wallet Info ─────────────────────────${C.reset}\n`);
357
+
358
+ let address = null;
359
+ for (let i = 0; i < args.length; i++) {
360
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
361
+ address = args[i + 1];
362
+ }
363
+ }
364
+
365
+ if (!address) {
366
+ // Try default wallet
367
+ const cfg = loadConfig();
368
+ address = cfg.defaultWallet;
369
+ }
370
+
371
+ if (!address) {
372
+ console.log(` ${C.red}✗ No address specified.${C.reset}`);
373
+ console.log(` ${C.dim}Usage: aether multisig info --address <addr>${C.reset}\n`);
374
+ return;
375
+ }
376
+
377
+ const ms = loadMultisig(address);
378
+ if (!ms) {
379
+ console.log(` ${C.red}✗ Multisig wallet not found:${C.reset} ${address}`);
380
+ console.log(` ${C.dim}Check your wallets: aether multisig list${C.reset}\n`);
381
+ return;
382
+ }
383
+
384
+ const rpcUrl = getDefaultRpc();
385
+ console.log(` ${C.green}★${C.reset} Address: ${C.bright}${ms.address}${C.reset}`);
386
+ console.log(` ${C.green}★${C.reset} Threshold: ${C.bright}${ms.threshold} of ${ms.signers.length}${C.reset}`);
387
+ console.log(` ${C.dim} Public key: ${ms.public_key}${C.reset}`);
388
+ console.log(` ${C.dim} Created: ${new Date(ms.created_at).toLocaleString()}${C.reset}`);
389
+ console.log(` ${C.dim} Version: ${ms.version}${C.reset}`);
390
+ console.log();
391
+
392
+ console.log(` ${C.bright}Signers (${ms.signers.length}):${C.reset}`);
393
+ for (let i = 0; i < ms.signers.length; i++) {
394
+ const s = ms.signers[i];
395
+ const isYou = s === loadConfig().defaultWallet;
396
+ const marker = isYou ? ` ${C.green}★ you${C.reset}` : '';
397
+ console.log(` ${i + 1}. ${C.cyan}${s}${C.reset}${marker}`);
398
+ }
399
+ console.log();
400
+
401
+ // On-chain balance via SDK (REAL RPC GET /v1/account/<addr>)
402
+ try {
403
+ const balance = await fetchAccountBalance(rpcUrl, ms.address);
404
+ if (balance !== null) {
405
+ console.log(` ${C.green}✓ Balance:${C.reset} ${C.bright}${formatAether(balance)}${C.reset}`);
406
+ }
407
+ } catch (err) {
408
+ console.log(` ${C.yellow}⚠ Could not fetch balance: ${err.message}${C.reset}`);
409
+ }
410
+
411
+ console.log();
412
+ }
413
+
414
+ // ---------------------------------------------------------------------------
415
+ // ADD SIGNER
416
+ // ---------------------------------------------------------------------------
417
+
418
+ async function addSignerMultisig(rl, args) {
419
+ console.log(`\n${C.bright}${C.cyan}── Add Signer to Multi-Signature Wallet ─────────────────${C.reset}\n`);
420
+
421
+ let address = null;
422
+ let newSigner = null;
423
+
424
+ for (let i = 0; i < args.length; i++) {
425
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) address = args[i + 1];
426
+ if ((args[i] === '--signer' || args[i] === '-s') && args[i + 1]) newSigner = args[i + 1];
427
+ }
428
+
429
+ if (!address || !newSigner) {
430
+ console.log(` ${C.red}✗ Missing required arguments.${C.reset}`);
431
+ console.log(` ${C.dim}Usage: aether multisig add-signer --address <msAddr> --signer <newAddr>${C.reset}\n`);
432
+ return;
433
+ }
434
+
435
+ const ms = loadMultisig(address);
436
+ if (!ms) {
437
+ console.log(` ${C.red}✗ Multisig wallet not found:${C.reset} ${address}\n`);
438
+ return;
439
+ }
440
+
441
+ if (!isValidAddress(newSigner)) {
442
+ console.log(` ${C.red}✗ Invalid signer address:${C.reset} ${newSigner}\n`);
443
+ return;
444
+ }
445
+
446
+ if (ms.signers.includes(newSigner)) {
447
+ console.log(` ${C.yellow}⚠ Signer already in wallet:${C.reset} ${newSigner}\n`);
448
+ return;
449
+ }
450
+
451
+ console.log(` ${C.green}★${C.reset} Multisig: ${C.bright}${shortAddress(address)}${C.reset}`);
452
+ console.log(` ${C.green}★${C.reset} Adding: ${C.bright}${shortAddress(newSigner)}${C.reset}`);
453
+ console.log(` ${C.dim} Current threshold: ${ms.threshold}/${ms.signers.length}${C.reset}`);
454
+ console.log();
455
+
456
+ // Re-derive the address with the new signer appended
457
+ const newSigners = [...ms.signers, newSigner];
458
+ const { address: newAddress } = deriveMultisigAddress(newSigners, ms.threshold);
459
+
460
+ const newMs = {
461
+ ...ms,
462
+ address: newAddress, // new address due to signer change
463
+ signers: newSigners,
464
+ updated_at: new Date().toISOString(),
465
+ note: 'Address changed because signers list changed. Old address no longer valid.',
466
+ };
467
+
468
+ saveMultisig(newMs);
469
+
470
+ console.log(`${C.green}✓ Signer added.${C.reset}`);
471
+ console.log(` ${C.yellow}⚠ Important: Changing signers creates a NEW wallet address.${C.reset}`);
472
+ console.log(` ${C.dim} Old address: ${ms.address}${C.reset}`);
473
+ console.log(` ${C.dim} New address: ${newAddress}${C.reset}`);
474
+ console.log(` ${C.dim} Transfer all funds to the new address.${C.reset}`);
475
+ console.log(` ${C.dim} Saved to: ${getMultisigFilePath(newAddress)}${C.reset}\n`);
476
+ }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // SEND (multi-sig transaction)
480
+ // ---------------------------------------------------------------------------
481
+
482
+ async function sendMultisig(rl, args) {
483
+ console.log(`\n${C.bright}${C.cyan}── Multi-Signature Send ──────────────────────────────────${C.reset}\n`);
484
+
485
+ let address = null;
486
+ let recipient = null;
487
+ let amountStr = null;
488
+ let asJson = false;
489
+
490
+ for (let i = 0; i < args.length; i++) {
491
+ if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) address = args[i + 1];
492
+ else if ((args[i] === '--to' || args[i] === '-t') && args[i + 1]) recipient = args[i + 1];
493
+ else if ((args[i] === '--amount' || args[i] === '-m') && args[i + 1]) amountStr = args[i + 1];
494
+ else if (args[i] === '--json' || args[i] === '-j') asJson = true;
495
+ }
496
+
497
+ if (!address || !recipient || !amountStr) {
498
+ console.log(` ${C.red}✗ Missing required arguments.${C.reset}`);
499
+ console.log(` ${C.dim}Usage: aether multisig send --address <msAddr> --to <recipient> --amount <aeth>${C.reset}\n`);
500
+ return;
501
+ }
502
+
503
+ const ms = loadMultisig(address);
504
+ if (!ms) {
505
+ console.log(` ${C.red}✗ Multisig wallet not found:${C.reset} ${address}\n`);
506
+ return;
507
+ }
508
+
509
+ const amount = parseFloat(amountStr);
510
+ if (isNaN(amount) || amount <= 0) {
511
+ console.log(` ${C.red}✗ Invalid amount:${C.reset} ${amountStr}\n`);
512
+ return;
513
+ }
514
+
515
+ const lamports = Math.round(amount * 1e9);
516
+ const rpcUrl = getDefaultRpc();
517
+
518
+ console.log(` ${C.green}★${C.reset} Multisig: ${C.bright}${shortAddress(address)}${C.reset} (${ms.threshold}/${ms.signers.length})`);
519
+ console.log(` ${C.green}★${C.reset} To: ${C.bright}${shortAddress(recipient)}${C.reset}`);
520
+ console.log(` ${C.green}★${C.reset} Amount: ${C.bright}${amount} AETH${C.reset} (${lamports.toLocaleString()} lamports)`);
521
+ console.log();
522
+
523
+ // Fetch balance check via SDK (REAL RPC GET /v1/account/<addr>)
524
+ try {
525
+ const balance = await fetchAccountBalance(rpcUrl, address);
526
+ if (balance !== null) {
527
+ if (balance < lamports) {
528
+ console.log(` ${C.red}✗ Insufficient balance.${C.reset}`);
529
+ console.log(` ${C.dim} Have: ${formatAether(balance)} Need: ${formatAether(lamports)}${C.reset}\n`);
530
+ return;
531
+ }
532
+ console.log(` ${C.green}✓ Balance check passed:${C.reset} ${formatAether(balance)}`);
533
+ }
534
+ } catch (err) {
535
+ console.log(` ${C.yellow}⚠ Could not verify balance: ${err.message}${C.reset}`);
536
+ }
537
+ console.log();
538
+
539
+ // Collect M signatures from signers
540
+ console.log(` ${C.yellow}⚠ This is a multi-signature transaction.${C.reset}`);
541
+ console.log(` ${C.dim} Require ${ms.threshold} signature(s) from ${ms.signers.length} signer(s).${C.reset}`);
542
+ console.log(` ${C.dim} Signers:${C.reset}`);
543
+ for (const s of ms.signers) {
544
+ console.log(` ${C.cyan}${shortAddress(s)}${C.reset}`);
545
+ }
546
+ console.log();
547
+
548
+ const signatures = [];
549
+ const neededSigs = ms.threshold;
550
+
551
+ for (let i = 0; i < ms.signers.length && signatures.length < neededSigs; i++) {
552
+ const signer = ms.signers[i];
553
+ console.log(` ${C.cyan}[${signatures.length + 1}/${neededSigs}] Requesting signature from:${C.reset} ${C.bright}${shortAddress(signer)}${C.reset}`);
554
+
555
+ const isYou = loadConfig().defaultWallet === signer;
556
+
557
+ if (isYou) {
558
+ // You are a signer — get your mnemonic to sign
559
+ const mnemonic = await askMnemonic(rl, `your passphrase to sign`);
560
+ let keyPair;
561
+ try {
562
+ keyPair = deriveKeypair(mnemonic);
563
+ } catch (e) {
564
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}`);
565
+ continue;
566
+ }
567
+ const derivedAddr = formatAddress(keyPair.publicKey);
568
+ if (derivedAddr !== signer) {
569
+ console.log(` ${C.red}✗ Passphrase mismatch for signer ${shortAddress(signer)}.${C.reset}`);
570
+ continue;
571
+ }
572
+ // Sign the transaction digest
573
+ const txDigest = crypto.createHash('sha512')
574
+ .update(JSON.stringify({ to: recipient, amount: lamports, from: address, nonce: Math.floor(Math.random() * 0xffffffff) }))
575
+ .digest();
576
+ const sig = nacl.sign.detached(txDigest, keyPair.secretKey);
577
+ signatures.push({ signer, signature: bs58.encode(sig) });
578
+ console.log(` ${C.green}✓ Signed.${C.reset}`);
579
+ } else {
580
+ // Not you — simulate a signature request (in real impl, this would prompt via file/network)
581
+ console.log(` ${C.yellow}⚠ Cannot automatically collect signature for ${shortAddress(signer)}.${C.reset}`);
582
+ console.log(` ${C.dim} For remote signers, use: aether multisig sign --signer ${signer} --tx <txId>${C.reset}`);
583
+ }
584
+ console.log();
585
+ }
586
+
587
+ if (signatures.length < neededSigs) {
588
+ console.log(` ${C.red}✗ Not enough signatures.${C.reset} Have ${signatures.length}, need ${neededSigs}.`);
589
+ console.log(` ${C.dim} Transaction NOT submitted.${C.reset}\n`);
590
+ return;
591
+ }
592
+
593
+ // Build multi-sig transaction
594
+ const tx = {
595
+ type: 'MultisigSend',
596
+ from: address,
597
+ to: recipient.startsWith('ATH') ? recipient.slice(3) : recipient,
598
+ amount_lamports: lamports,
599
+ threshold: ms.threshold,
600
+ signers: ms.signers,
601
+ signatures: signatures.map(s => s.signature),
602
+ timestamp: Math.floor(Date.now() / 1000),
603
+ };
604
+
605
+ console.log(` ${C.green}✓ Collected ${signatures.length} signature(s). Submitting...${C.reset}`);
606
+
607
+ // Submit via SDK (REAL RPC POST /v1/transaction)
608
+ try {
609
+ const result = await submitTransaction(rpcUrl, tx);
610
+
611
+ if (result.error) {
612
+ console.log(`\n ${C.red}✗ Transaction failed:${C.reset} ${result.error}\n`);
613
+ process.exit(1);
614
+ }
615
+
616
+ const sig = result.signature || result.tx_signature || result.id || JSON.stringify(result);
617
+ console.log(`\n${C.green}✓ Multi-sig transaction submitted!${C.reset}`);
618
+ console.log(` ${C.dim}Signature: ${sig}${C.reset}`);
619
+ console.log(` ${C.dim}From: ${address}${C.reset}`);
620
+ console.log(` ${C.dim}To: ${recipient}${C.reset}`);
621
+ console.log(` ${C.dim}Amount: ${formatAether(lamports)}${C.reset}`);
622
+ console.log(` ${C.dim}Signers used: ${signatures.length}/${ms.signers.length}${C.reset}`);
623
+ console.log(` ${C.dim}SDK: sendTransaction()${C.reset}\n`);
624
+ } catch (err) {
625
+ console.log(` ${C.red}✗ Failed to submit transaction:${C.reset} ${err.message}`);
626
+ console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}\n`);
627
+ process.exit(1);
628
+ }
629
+ }
630
+
631
+ // ---------------------------------------------------------------------------
632
+ // Parse CLI args
633
+ // ---------------------------------------------------------------------------
634
+
635
+ function parseArgs() {
636
+ // argv = [node, index.js, multisig, <subcmd>, ...]
637
+ return process.argv.slice(3);
638
+ }
639
+
640
+ function showHelp() {
641
+ console.log(`
642
+ ${C.bright}${C.cyan}aether-cli multisig${C.reset} — Multi-Signature Wallet Management
643
+
644
+ ${C.bright}Usage:${C.reset}
645
+ aether multisig create --threshold <m> --signers <addr1,addr2,...>
646
+ aether multisig list
647
+ aether multisig info --address <addr>
648
+ aether multisig add-signer --address <msAddr> --signer <newAddr>
649
+ aether multisig send --address <msAddr> --to <recipient> --amount <aeth>
650
+
651
+ ${C.bright}Examples:${C.reset}
652
+ aether multisig create --threshold 2 --signers ATHabc,ATHdef,ATHghi
653
+ aether multisig list
654
+ aether multisig info --address ATHxxxxx
655
+ aether multisig add-signer --address ATHxxxxx --signer ATHnewww
656
+ aether multisig send --address ATHxxxxx --to ATHdest --amount 10
657
+
658
+ ${C.bright}Notes:${C.reset}
659
+ Multi-sig wallets use off-chain deterministic address derivation.
660
+ Changing signers always produces a new wallet address.
661
+ All M signers must approve a transaction before it can be broadcast.
662
+ `.trim());
663
+ }
664
+
665
+ // ---------------------------------------------------------------------------
666
+ // Main dispatcher
667
+ // ---------------------------------------------------------------------------
668
+
669
+ async function multisigCommand() {
670
+ const args = parseArgs();
671
+ const subcmd = args[0];
672
+
673
+ const rl = createRl();
674
+ try {
675
+ if (!subcmd || subcmd === 'help' || subcmd === '--help' || subcmd === '-h') {
676
+ showHelp();
677
+ } else if (subcmd === 'create') {
678
+ await createMultisig(rl, args.slice(1));
679
+ } else if (subcmd === 'list') {
680
+ await listMultisig(rl, args.slice(1));
681
+ } else if (subcmd === 'info') {
682
+ await infoMultisig(rl, args.slice(1));
683
+ } else if (subcmd === 'add-signer') {
684
+ await addSignerMultisig(rl, args.slice(1));
685
+ } else if (subcmd === 'send') {
686
+ await sendMultisig(rl, args.slice(1));
687
+ } else {
688
+ console.log(`\n ${C.red}Unknown multisig subcommand:${C.reset} ${subcmd}`);
689
+ showHelp();
690
+ process.exit(1);
691
+ }
692
+ } finally {
693
+ rl.close();
694
+ }
695
+ }
696
+
697
+ module.exports = { multisigCommand };
698
+
699
+ if (require.main === module) {
700
+ multisigCommand();
701
+ }