@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,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
|
+
}
|