@jellylegsai/aether-cli 1.9.2 → 2.0.1

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.
@@ -0,0 +1,959 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli deploy
4
+ *
5
+ * Deploy smart contracts/programs to the Aether blockchain.
6
+ * Fully wired to @jellylegsai/aether-sdk for real blockchain RPC calls.
7
+ *
8
+ * Usage:
9
+ * aether deploy <contract.wasm> Deploy a WASM contract
10
+ * aether deploy <program.so> --type bpf Deploy a BPF program
11
+ * aether deploy <contract.wasm> --name <name> Deploy with custom name
12
+ * aether deploy <contract.wasm> --upgradeable Deploy as upgradeable contract
13
+ * aether deploy --list-templates Show available contract templates
14
+ * aether deploy --verify <address> Verify deployed contract
15
+ * aether deploy --status <address> Check deployment status
16
+ *
17
+ * SDK wired to:
18
+ * - client.sendTransaction(tx) → POST /v1/transaction (Deploy)
19
+ * - client.getAccountInfo(addr) → GET /v1/account/<addr>
20
+ * - client.getProgram(programId) → GET /v1/program/<id>
21
+ * - client.getSlot() → GET /v1/slot
22
+ * - client.getRecentBlockhash() → GET /v1/recent-blockhash
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const os = require('os');
28
+ const readline = require('readline');
29
+ const crypto = require('crypto');
30
+ const nacl = require('tweetnacl');
31
+ const bs58 = require('bs58').default;
32
+ const bip39 = require('bip39');
33
+
34
+ // Import SDK for ALL blockchain RPC calls
35
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
36
+ const aether = require(sdkPath);
37
+
38
+ // Import UI framework for consistent branding
39
+ const { BRANDING, C, indicators, success, error, warning, info, code, key, value, startSpinner, stopSpinner, formatHelp, drawBox } = require('../lib/ui');
40
+
41
+ const CLI_VERSION = '2.0.0';
42
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
43
+
44
+ // Deployment constants
45
+ const DEPLOYMENT_FEE_LAMPORTS = 50000; // 0.00005 AETH base fee
46
+ const MIN_RENT_EXEMPTION_LAMPORTS = 890880; // ~0.00089 AETH for rent exemption
47
+ const MAX_CONTRACT_SIZE = 10 * 1024 * 1024; // 10MB max
48
+
49
+ // Contract templates
50
+ const CONTRACT_TEMPLATES = {
51
+ token: {
52
+ name: 'SPL Token Contract',
53
+ description: 'Standard token contract with mint/burn/transfer',
54
+ minSize: 10000,
55
+ maxSize: 50000,
56
+ },
57
+ nft: {
58
+ name: 'NFT Collection Contract',
59
+ description: 'NFT minting and management contract',
60
+ minSize: 15000,
61
+ maxSize: 100000,
62
+ },
63
+ staking: {
64
+ name: 'Staking Pool Contract',
65
+ description: 'Stake tokens and earn rewards',
66
+ minSize: 20000,
67
+ maxSize: 150000,
68
+ },
69
+ governance: {
70
+ name: 'Governance Contract',
71
+ description: 'On-chain voting and proposals',
72
+ minSize: 25000,
73
+ maxSize: 200000,
74
+ },
75
+ multisig: {
76
+ name: 'Multi-Signature Wallet',
77
+ description: 'Requires N-of-M signatures for transactions',
78
+ minSize: 18000,
79
+ maxSize: 80000,
80
+ },
81
+ custom: {
82
+ name: 'Custom Contract',
83
+ description: 'Your own compiled WASM/BPF program',
84
+ minSize: 1000,
85
+ maxSize: MAX_CONTRACT_SIZE,
86
+ },
87
+ };
88
+
89
+ // ============================================================================
90
+ // SDK Setup
91
+ // ============================================================================
92
+
93
+ function getDefaultRpc() {
94
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
95
+ }
96
+
97
+ function createClient(rpcUrl) {
98
+ return new aether.AetherClient({ rpcUrl });
99
+ }
100
+
101
+ // ============================================================================
102
+ // Config & Wallet
103
+ // ============================================================================
104
+
105
+ function getAetherDir() {
106
+ return path.join(os.homedir(), '.aether');
107
+ }
108
+
109
+ function getConfigPath() {
110
+ return path.join(getAetherDir(), 'config.json');
111
+ }
112
+
113
+ function loadConfig() {
114
+ if (!fs.existsSync(getConfigPath())) {
115
+ return { defaultWallet: null, deployments: [] };
116
+ }
117
+ try {
118
+ return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
119
+ } catch {
120
+ return { defaultWallet: null, deployments: [] };
121
+ }
122
+ }
123
+
124
+ function saveConfig(cfg) {
125
+ if (!fs.existsSync(getAetherDir())) {
126
+ fs.mkdirSync(getAetherDir(), { recursive: true });
127
+ }
128
+ fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
129
+ }
130
+
131
+ function loadWallet(address) {
132
+ const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
133
+ if (!fs.existsSync(fp)) return null;
134
+ try {
135
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ function saveDeployment(deployment) {
142
+ const cfg = loadConfig();
143
+ if (!cfg.deployments) cfg.deployments = [];
144
+ cfg.deployments.push({
145
+ ...deployment,
146
+ deployedAt: new Date().toISOString(),
147
+ });
148
+ saveConfig(cfg);
149
+ }
150
+
151
+ // ============================================================================
152
+ // Crypto Helpers
153
+ // ============================================================================
154
+
155
+ function deriveKeypair(mnemonic) {
156
+ if (!bip39.validateMnemonic(mnemonic)) {
157
+ throw new Error('Invalid mnemonic phrase');
158
+ }
159
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
160
+ const seed32 = seedBuffer.slice(0, 32);
161
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
162
+ return {
163
+ publicKey: Buffer.from(keyPair.publicKey),
164
+ secretKey: Buffer.from(keyPair.secretKey),
165
+ };
166
+ }
167
+
168
+ function formatAddress(publicKey) {
169
+ return 'ATH' + bs58.encode(publicKey);
170
+ }
171
+
172
+ function signTransaction(tx, secretKey) {
173
+ const txBytes = Buffer.from(JSON.stringify(tx));
174
+ const sig = nacl.sign.detached(txBytes, secretKey);
175
+ return bs58.encode(sig);
176
+ }
177
+
178
+ function generateProgramId() {
179
+ const keyPair = nacl.sign.keyPair();
180
+ return bs58.encode(Buffer.from(keyPair.publicKey));
181
+ }
182
+
183
+ // ============================================================================
184
+ // Format Helpers
185
+ // ============================================================================
186
+
187
+ function formatAether(lamports) {
188
+ const aeth = Number(lamports) / 1e9;
189
+ if (aeth === 0) return '0 AETH';
190
+ return aeth.toFixed(9).replace(/\.?0+$/, '') + ' AETH';
191
+ }
192
+
193
+ function formatBytes(bytes) {
194
+ if (bytes < 1024) return bytes + ' B';
195
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
196
+ return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
197
+ }
198
+
199
+ function shortAddress(addr) {
200
+ if (!addr || addr.length < 16) return addr || 'unknown';
201
+ return addr.slice(0, 8) + '…' + addr.slice(-8);
202
+ }
203
+
204
+ function formatDuration(ms) {
205
+ if (ms < 1000) return `${ms}ms`;
206
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
207
+ return `${(ms / 60000).toFixed(1)}m`;
208
+ }
209
+
210
+ // ============================================================================
211
+ // Readline Helpers
212
+ // ============================================================================
213
+
214
+ function createRl() {
215
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
216
+ }
217
+
218
+ function question(rl, q) {
219
+ return new Promise((res) => rl.question(q, res));
220
+ }
221
+
222
+ async function askMnemonic(rl, promptText) {
223
+ console.log(`\n${C.cyan}${promptText}${C.reset}`);
224
+ console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
225
+ const raw = await question(rl, ` > ${C.reset}`);
226
+ return raw.trim().toLowerCase();
227
+ }
228
+
229
+ // ============================================================================
230
+ // Contract Validation
231
+ // ============================================================================
232
+
233
+ function validateContractFile(filePath) {
234
+ const errors = [];
235
+ const warnings = [];
236
+
237
+ // Check file exists
238
+ if (!fs.existsSync(filePath)) {
239
+ errors.push(`File not found: ${filePath}`);
240
+ return { valid: false, errors, warnings };
241
+ }
242
+
243
+ // Check it's a file
244
+ const stats = fs.statSync(filePath);
245
+ if (!stats.isFile()) {
246
+ errors.push(`Path is not a file: ${filePath}`);
247
+ return { valid: false, errors, warnings };
248
+ }
249
+
250
+ // Check size
251
+ const size = stats.size;
252
+ if (size === 0) {
253
+ errors.push('Contract file is empty');
254
+ } else if (size > MAX_CONTRACT_SIZE) {
255
+ errors.push(`Contract size ${formatBytes(size)} exceeds maximum ${formatBytes(MAX_CONTRACT_SIZE)}`);
256
+ }
257
+
258
+ // Check extension
259
+ const ext = path.extname(filePath).toLowerCase();
260
+ const validExts = ['.wasm', '.so', '.bin'];
261
+ if (!validExts.includes(ext)) {
262
+ warnings.push(`Unusual extension "${ext}". Expected: .wasm, .so, or .bin`);
263
+ }
264
+
265
+ // Try to read and validate basic structure
266
+ try {
267
+ const buffer = fs.readFileSync(filePath);
268
+
269
+ // Check WASM magic number for .wasm files
270
+ if (ext === '.wasm' || buffer.slice(0, 4).toString('hex') === '0061736d') {
271
+ const wasmMagic = buffer.slice(0, 4);
272
+ if (wasmMagic.toString('hex') !== '0061736d') {
273
+ warnings.push('File does not have standard WASM magic number');
274
+ }
275
+ }
276
+
277
+ // Calculate hash
278
+ const hash = crypto.createHash('sha256').update(buffer).digest('hex');
279
+
280
+ return {
281
+ valid: errors.length === 0,
282
+ errors,
283
+ warnings,
284
+ size,
285
+ formattedSize: formatBytes(size),
286
+ hash: hash.slice(0, 16) + '...',
287
+ fullHash: hash,
288
+ };
289
+ } catch (e) {
290
+ errors.push(`Failed to read file: ${e.message}`);
291
+ return { valid: false, errors, warnings };
292
+ }
293
+ }
294
+
295
+ // ============================================================================
296
+ // Argument Parsing
297
+ // ============================================================================
298
+
299
+ function parseArgs() {
300
+ const args = process.argv.slice(2);
301
+ const opts = {
302
+ filePath: null,
303
+ contractType: 'custom',
304
+ name: null,
305
+ upgradeable: false,
306
+ rpc: getDefaultRpc(),
307
+ json: false,
308
+ dryRun: false,
309
+ force: false,
310
+ wallet: null,
311
+ listTemplates: false,
312
+ verify: null,
313
+ status: null,
314
+ help: false,
315
+ programId: null,
316
+ rentExempt: true,
317
+ computeUnits: 200000,
318
+ };
319
+
320
+ for (let i = 0; i < args.length; i++) {
321
+ const arg = args[i];
322
+
323
+ if (arg === '--type' || arg === '-t') {
324
+ opts.contractType = (args[++i] || 'custom').toLowerCase();
325
+ } else if (arg === '--name' || arg === '-n') {
326
+ opts.name = args[++i];
327
+ } else if (arg === '--upgradeable' || arg === '-u') {
328
+ opts.upgradeable = true;
329
+ } else if (arg === '--rpc' || arg === '-r') {
330
+ opts.rpc = args[++i];
331
+ } else if (arg === '--json' || arg === '-j') {
332
+ opts.json = true;
333
+ } else if (arg === '--dry-run') {
334
+ opts.dryRun = true;
335
+ } else if (arg === '--force' || arg === '-f') {
336
+ opts.force = true;
337
+ } else if (arg === '--wallet' || arg === '-w') {
338
+ opts.wallet = args[++i];
339
+ } else if (arg === '--list-templates' || arg === '-l') {
340
+ opts.listTemplates = true;
341
+ } else if (arg === '--verify' || arg === '-v') {
342
+ opts.verify = args[++i];
343
+ } else if (arg === '--status' || arg === '-s') {
344
+ opts.status = args[++i];
345
+ } else if (arg === '--program-id' || arg === '-p') {
346
+ opts.programId = args[++i];
347
+ } else if (arg === '--no-rent-exempt') {
348
+ opts.rentExempt = false;
349
+ } else if (arg === '--compute-units' || arg === '-c') {
350
+ const cu = parseInt(args[++i], 10);
351
+ if (!isNaN(cu) && cu > 0) opts.computeUnits = cu;
352
+ } else if (arg === '--help' || arg === '-h') {
353
+ opts.help = true;
354
+ } else if (!arg.startsWith('-') && !opts.filePath) {
355
+ opts.filePath = arg;
356
+ }
357
+ }
358
+
359
+ return opts;
360
+ }
361
+
362
+ function showHelp() {
363
+ console.log(BRANDING.header(CLI_VERSION));
364
+ console.log(`
365
+ ${C.bright}${C.cyan}aether-cli deploy${C.reset} — Deploy smart contracts to Aether blockchain
366
+
367
+ ${C.bright}USAGE${C.reset}
368
+ aether deploy <contract.wasm> [options]
369
+ aether deploy --list-templates
370
+ aether deploy --verify <program-id>
371
+ aether deploy --status <program-id>
372
+
373
+ ${C.bright}COMMANDS${C.reset}
374
+ deploy <file> Deploy contract from file
375
+ --list-templates Show available contract templates
376
+ --verify <address> Verify deployed contract on-chain
377
+ --status <address> Check deployment/upgrade status
378
+
379
+ ${C.bright}OPTIONS${C.reset}
380
+ -t, --type <type> Contract type: token, nft, staking, governance, multisig, custom
381
+ -n, --name <name> Contract name for identification
382
+ -u, --upgradeable Deploy as upgradeable contract
383
+ -w, --wallet <addr> Deployer wallet address
384
+ -r, --rpc <url> RPC endpoint
385
+ -c, --compute-units Max compute units (default: 200000)
386
+ --no-rent-exempt Skip rent exemption (not recommended)
387
+ --dry-run Preview deployment without submitting
388
+ --force Skip confirmation prompts
389
+ -j, --json JSON output for scripting
390
+ -h, --help Show this help
391
+
392
+ ${C.bright}SDK METHODS USED${C.reset}
393
+ client.sendTransaction() → POST /v1/transaction
394
+ client.getAccountInfo() → GET /v1/account/<addr>
395
+ client.getSlot() → GET /v1/slot
396
+ client.getRecentBlockhash() → GET /v1/recent-blockhash
397
+
398
+ ${C.bright}EXAMPLES${C.reset}
399
+ aether deploy token_contract.wasm --name "MyToken" --type token
400
+ aether deploy program.so --type bpf --upgradeable --wallet ATHxxx...
401
+ aether deploy --list-templates
402
+ aether deploy --verify ATHProgxxx...
403
+ aether deploy --status ATHProgxxx...
404
+ `);
405
+ }
406
+
407
+ // ============================================================================
408
+ // List Templates
409
+ // ============================================================================
410
+
411
+ function listTemplates(asJson) {
412
+ if (asJson) {
413
+ console.log(JSON.stringify({
414
+ templates: Object.entries(CONTRACT_TEMPLATES).map(([id, t]) => ({
415
+ id,
416
+ name: t.name,
417
+ description: t.description,
418
+ minSize: t.minSize,
419
+ maxSize: t.maxSize,
420
+ })),
421
+ }, null, 2));
422
+ return;
423
+ }
424
+
425
+ console.log(`\n${C.bright}${C.cyan}═══ Available Contract Templates ═══${C.reset}\n`);
426
+
427
+ for (const [id, template] of Object.entries(CONTRACT_TEMPLATES)) {
428
+ const boxContent = `
429
+ ${C.bright}${template.name}${C.reset}
430
+ ${C.dim}${template.description}${C.reset}
431
+
432
+ ${C.cyan}Size:${C.reset} ${formatBytes(template.minSize)} - ${formatBytes(template.maxSize)}
433
+ ${C.cyan}Usage:${C.reset} aether deploy <file> --type ${id}`;
434
+
435
+ console.log(drawBox(boxContent, {
436
+ style: 'rounded',
437
+ width: 60,
438
+ borderColor: C.dim,
439
+ }));
440
+ console.log();
441
+ }
442
+
443
+ console.log(`${C.dim}Tip: Use --type custom for your own compiled contracts${C.reset}\n`);
444
+ }
445
+
446
+ // ============================================================================
447
+ // Verify Contract
448
+ // ============================================================================
449
+
450
+ async function verifyContract(opts) {
451
+ const programId = opts.verify;
452
+ const rpcUrl = opts.rpc;
453
+
454
+ if (!opts.json) {
455
+ console.log(`\n${C.bright}${C.cyan}═══ Verify Contract ═══${C.reset}\n`);
456
+ console.log(` ${C.dim}Program ID:${C.reset} ${programId}`);
457
+ console.log(` ${C.dim}RPC:${C.reset} ${rpcUrl}\n`);
458
+ startSpinner('Fetching on-chain data');
459
+ }
460
+
461
+ const client = createClient(rpcUrl);
462
+
463
+ try {
464
+ // Try to get program account info
465
+ const rawProgramId = programId.startsWith('ATH') ? programId.slice(3) : programId;
466
+ const account = await client.getAccountInfo(rawProgramId);
467
+
468
+ stopSpinner(true);
469
+
470
+ if (!account || account.error) {
471
+ if (opts.json) {
472
+ console.log(JSON.stringify({
473
+ programId,
474
+ verified: false,
475
+ error: 'Program not found on-chain',
476
+ }, null, 2));
477
+ } else {
478
+ console.log(`\n ${indicators.error} ${error('Program not found on-chain')}\n`);
479
+ console.log(` ${C.dim}The program ID may be incorrect or the deployment failed.${C.reset}\n`);
480
+ }
481
+ return false;
482
+ }
483
+
484
+ // Program exists - verify it's executable
485
+ const isExecutable = account.executable === true || account.owner === 'BPFLoader2111111111111111111111111111111111';
486
+ const deployTime = account.rent_epoch ? `Epoch ${account.rent_epoch}` : 'Unknown';
487
+
488
+ if (opts.json) {
489
+ console.log(JSON.stringify({
490
+ programId,
491
+ verified: true,
492
+ executable: isExecutable,
493
+ owner: account.owner,
494
+ lamports: account.lamports,
495
+ dataSize: account.data ? account.data.length : 0,
496
+ rentEpoch: account.rent_epoch,
497
+ deployTime,
498
+ }, null, 2));
499
+ } else {
500
+ console.log(`\n ${indicators.success} ${success('Contract verified on-chain')}\n`);
501
+ console.log(` ${C.bright}Program ID:${C.reset} ${C.cyan}${programId}${C.reset}`);
502
+ console.log(` ${C.bright}Status:${C.reset} ${isExecutable ? C.green + 'Executable ✓' : C.yellow + 'Not Executable'}${C.reset}`);
503
+ console.log(` ${C.bright}Balance:${C.reset} ${formatAether(account.lamports || 0)}`);
504
+ console.log(` ${C.bright}Data Size:${C.reset} ${formatBytes(account.data ? account.data.length : 0)}`);
505
+ console.log(` ${C.bright}Owner:${C.reset} ${shortAddress(account.owner)}`);
506
+ console.log(` ${C.bright}Deployed:${C.reset} ${deployTime}`);
507
+ console.log();
508
+ }
509
+
510
+ return true;
511
+ } catch (err) {
512
+ stopSpinner(false);
513
+ if (opts.json) {
514
+ console.log(JSON.stringify({
515
+ programId,
516
+ verified: false,
517
+ error: err.message,
518
+ }, null, 2));
519
+ } else {
520
+ console.log(`\n ${indicators.error} ${error(`Verification failed: ${err.message}`)}\n`);
521
+ }
522
+ return false;
523
+ }
524
+ }
525
+
526
+ // ============================================================================
527
+ // Check Deployment Status
528
+ // ============================================================================
529
+
530
+ async function checkDeploymentStatus(opts) {
531
+ const programId = opts.status;
532
+ const rpcUrl = opts.rpc;
533
+
534
+ if (!opts.json) {
535
+ console.log(`\n${C.bright}${C.cyan}═══ Deployment Status ═══${C.reset}\n`);
536
+ console.log(` ${C.dim}Program ID:${C.reset} ${programId}`);
537
+ console.log(` ${C.dim}RPC:${C.reset} ${rpcUrl}\n`);
538
+ }
539
+
540
+ const client = createClient(rpcUrl);
541
+
542
+ try {
543
+ const rawProgramId = programId.startsWith('ATH') ? programId.slice(3) : programId;
544
+
545
+ // Get multiple data points
546
+ const [account, slot, health] = await Promise.all([
547
+ client.getAccountInfo(rawProgramId).catch(() => null),
548
+ client.getSlot().catch(() => null),
549
+ client.getHealth().catch(() => 'unknown'),
550
+ ]);
551
+
552
+ const status = {
553
+ programId,
554
+ exists: !!account,
555
+ executable: account?.executable || false,
556
+ currentSlot: slot,
557
+ nodeHealth: health,
558
+ timestamp: new Date().toISOString(),
559
+ };
560
+
561
+ if (opts.json) {
562
+ console.log(JSON.stringify(status, null, 2));
563
+ } else {
564
+ console.log(` ${C.bright}Program ID:${C.reset} ${C.cyan}${programId}${C.reset}`);
565
+ console.log(` ${C.bright}Status:${C.reset} ${status.exists ? C.green + '✓ Deployed' : C.red + '✗ Not Found'}${C.reset}`);
566
+ console.log(` ${C.bright}Executable:${C.reset} ${status.executable ? C.green + 'Yes' : C.yellow + 'No'}${C.reset}`);
567
+ console.log(` ${C.bright}Current Slot:${C.reset} ${slot || 'N/A'}`);
568
+ console.log(` ${C.bright}Node Health:${C.reset} ${health}${C.reset}`);
569
+ console.log();
570
+ }
571
+
572
+ return status.exists;
573
+ } catch (err) {
574
+ if (opts.json) {
575
+ console.log(JSON.stringify({
576
+ programId,
577
+ error: err.message,
578
+ }, null, 2));
579
+ } else {
580
+ console.log(`\n ${indicators.error} ${error(`Status check failed: ${err.message}`)}\n`);
581
+ }
582
+ return false;
583
+ }
584
+ }
585
+
586
+ // ============================================================================
587
+ // Core Deployment Logic
588
+ // ============================================================================
589
+
590
+ async function deployContract(opts) {
591
+ const startTime = Date.now();
592
+ const rl = createRl();
593
+
594
+ // Header
595
+ if (!opts.json) {
596
+ console.log(BRANDING.header(CLI_VERSION));
597
+ console.log(`\n${C.bright}${C.cyan}═══ Contract Deployment ═══${C.reset}\n`);
598
+ }
599
+
600
+ // Validate contract file
601
+ if (!opts.json) {
602
+ console.log(` ${C.dim}Validating contract file...${C.reset}`);
603
+ }
604
+
605
+ const validation = validateContractFile(opts.filePath);
606
+ if (!validation.valid) {
607
+ if (opts.json) {
608
+ console.log(JSON.stringify({
609
+ success: false,
610
+ stage: 'validation',
611
+ errors: validation.errors,
612
+ warnings: validation.warnings,
613
+ }, null, 2));
614
+ } else {
615
+ console.log(`\n ${indicators.error} ${error('Validation failed')}\n`);
616
+ validation.errors.forEach(e => console.log(` ${C.red}•${C.reset} ${e}`));
617
+ if (validation.warnings.length > 0) {
618
+ console.log();
619
+ validation.warnings.forEach(w => console.log(` ${C.yellow}⚠${C.reset} ${w}`));
620
+ }
621
+ console.log();
622
+ }
623
+ rl.close();
624
+ process.exit(1);
625
+ }
626
+
627
+ if (!opts.json) {
628
+ console.log(` ${indicators.success} ${success('File validated')}`);
629
+ console.log(` ${C.dim} Size: ${validation.formattedSize}${C.reset}`);
630
+ console.log(` ${C.dim} Hash: ${validation.hash}${C.reset}\n`);
631
+ }
632
+
633
+ // Resolve wallet
634
+ let walletAddress = opts.wallet;
635
+ if (!walletAddress) {
636
+ const cfg = loadConfig();
637
+ walletAddress = cfg.defaultWallet;
638
+ }
639
+
640
+ if (!walletAddress) {
641
+ if (opts.json) {
642
+ console.log(JSON.stringify({ success: false, error: 'No wallet specified' }, null, 2));
643
+ } else {
644
+ console.log(`\n ${indicators.error} ${error('No wallet address')} — Use --wallet or set a default\n`);
645
+ }
646
+ rl.close();
647
+ process.exit(1);
648
+ }
649
+
650
+ const wallet = loadWallet(walletAddress);
651
+ if (!wallet) {
652
+ if (opts.json) {
653
+ console.log(JSON.stringify({ success: false, error: 'Wallet not found' }, null, 2));
654
+ } else {
655
+ console.log(`\n ${indicators.error} ${error(`Wallet not found: ${walletAddress}`)}\n`);
656
+ }
657
+ rl.close();
658
+ process.exit(1);
659
+ }
660
+
661
+ // Initialize SDK client
662
+ const client = createClient(opts.rpc);
663
+
664
+ // Check balance
665
+ if (!opts.json) {
666
+ console.log(` ${C.dim}Checking wallet balance...${C.reset}`);
667
+ }
668
+
669
+ let balance = 0;
670
+ try {
671
+ const rawAddr = walletAddress.startsWith('ATH') ? walletAddress.slice(3) : walletAddress;
672
+ balance = await client.getBalance(rawAddr);
673
+ } catch (e) {
674
+ // Continue with balance 0
675
+ }
676
+
677
+ const minRequired = DEPLOYMENT_FEE_LAMPORTS + (opts.rentExempt ? MIN_RENT_EXEMPTION_LAMPORTS : 0) + validation.size;
678
+
679
+ if (balance < minRequired) {
680
+ if (opts.json) {
681
+ console.log(JSON.stringify({
682
+ success: false,
683
+ error: 'Insufficient balance',
684
+ required: minRequired,
685
+ available: balance,
686
+ }, null, 2));
687
+ } else {
688
+ console.log(`\n ${indicators.error} ${error('Insufficient balance')}\n`);
689
+ console.log(` Required: ${formatAether(minRequired)}`);
690
+ console.log(` Available: ${formatAether(balance)}\n`);
691
+ }
692
+ rl.close();
693
+ process.exit(1);
694
+ }
695
+
696
+ if (!opts.json) {
697
+ console.log(` ${indicators.success} ${success('Balance sufficient')}: ${formatAether(balance)}\n`);
698
+ }
699
+
700
+ // Get network state
701
+ if (!opts.json) {
702
+ console.log(` ${C.dim}Fetching network state...${C.reset}`);
703
+ }
704
+
705
+ const [slot, blockhash, epoch] = await Promise.all([
706
+ client.getSlot().catch(() => 0),
707
+ client.getRecentBlockhash().catch(() => ({ blockhash: '11111111111111111111111111111111' })),
708
+ client.getEpochInfo().catch(() => ({ epoch: 0 })),
709
+ ]);
710
+
711
+ if (!opts.json) {
712
+ console.log(` ${indicators.success} ${success('Network ready')}`);
713
+ console.log(` ${C.dim} Slot: ${slot}${C.reset}`);
714
+ console.log(` ${C.dim} Epoch: ${epoch.epoch}${C.reset}\n`);
715
+ }
716
+
717
+ // Generate or use provided program ID
718
+ const programId = opts.programId || generateProgramId();
719
+
720
+ // Read contract bytecode
721
+ const contractBytes = fs.readFileSync(opts.filePath);
722
+ const contractBase64 = contractBytes.toString('base64');
723
+
724
+ // Deployment summary
725
+ const deploymentName = opts.name || path.basename(opts.filePath, path.extname(opts.filePath));
726
+
727
+ if (!opts.json) {
728
+ console.log(`${C.bright}${C.cyan}── Deployment Summary ──${C.reset}\n`);
729
+ console.log(` ${C.bright}Contract:${C.reset} ${C.cyan}${deploymentName}${C.reset}`);
730
+ console.log(` ${C.bright}Type:${C.reset} ${opts.contractType}`);
731
+ console.log(` ${C.bright}Size:${C.reset} ${validation.formattedSize}`);
732
+ console.log(` ${C.bright}Program ID:${C.reset} ${shortAddress(programId)}`);
733
+ console.log(` ${C.bright}Deployer:${C.reset} ${shortAddress(walletAddress)}`);
734
+ console.log(` ${C.bright}Upgradeable:${C.reset} ${opts.upgradeable ? C.green + 'Yes' : C.dim + 'No'}${C.reset}`);
735
+ console.log(` ${C.bright}Rent Exempt:${C.reset} ${opts.rentExempt ? C.green + 'Yes' : C.yellow + 'No'}${C.reset}`);
736
+ console.log(` ${C.bright}RPC:${C.reset} ${opts.rpc}`);
737
+ console.log(` ${C.bright}Fee:${C.reset} ${formatAether(DEPLOYMENT_FEE_LAMPORTS)}`);
738
+ console.log();
739
+ }
740
+
741
+ // Dry run mode
742
+ if (opts.dryRun) {
743
+ if (opts.json) {
744
+ console.log(JSON.stringify({
745
+ dryRun: true,
746
+ name: deploymentName,
747
+ type: opts.contractType,
748
+ size: validation.size,
749
+ programId,
750
+ deployer: walletAddress,
751
+ upgradeable: opts.upgradeable,
752
+ rentExempt: opts.rentExempt,
753
+ fee: DEPLOYMENT_FEE_LAMPORTS,
754
+ slot,
755
+ epoch: epoch.epoch,
756
+ }, null, 2));
757
+ } else {
758
+ console.log(` ${C.yellow}⚠ Dry run mode — No transaction submitted${C.reset}\n`);
759
+ }
760
+ rl.close();
761
+ return;
762
+ }
763
+
764
+ // Get mnemonic for signing
765
+ if (!opts.json) {
766
+ console.log(`${C.yellow} Signing requires your wallet passphrase${C.reset}\n`);
767
+ }
768
+
769
+ let keyPair;
770
+ try {
771
+ const mnemonic = await askMnemonic(rl, 'Enter passphrase to sign deployment');
772
+ keyPair = deriveKeypair(mnemonic);
773
+
774
+ // Verify address
775
+ const derivedAddr = formatAddress(keyPair.publicKey);
776
+ if (derivedAddr !== walletAddress) {
777
+ if (!opts.json) {
778
+ console.log(`\n ${indicators.error} ${error('Passphrase mismatch')}\n`);
779
+ console.log(` Expected: ${walletAddress}`);
780
+ console.log(` Derived: ${derivedAddr}\n`);
781
+ } else {
782
+ console.log(JSON.stringify({ success: false, error: 'Passphrase mismatch' }, null, 2));
783
+ }
784
+ rl.close();
785
+ process.exit(1);
786
+ }
787
+ } catch (e) {
788
+ if (!opts.json) {
789
+ console.log(`\n ${indicators.error} ${error(`Failed: ${e.message}`)}\n`);
790
+ } else {
791
+ console.log(JSON.stringify({ success: false, error: e.message }, null, 2));
792
+ }
793
+ rl.close();
794
+ process.exit(1);
795
+ }
796
+
797
+ // Confirm deployment
798
+ if (!opts.json && !opts.force) {
799
+ const confirm = await question(rl, ` ${C.yellow}Confirm deployment? [y/N]${C.reset} > `);
800
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
801
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
802
+ rl.close();
803
+ return;
804
+ }
805
+ console.log();
806
+ }
807
+
808
+ rl.close();
809
+
810
+ // Build deployment transaction
811
+ const rawWalletAddr = walletAddress.startsWith('ATH') ? walletAddress.slice(3) : walletAddress;
812
+
813
+ const tx = {
814
+ signer: rawWalletAddr,
815
+ tx_type: 'Deploy',
816
+ payload: {
817
+ type: 'Deploy',
818
+ data: {
819
+ program_id: programId,
820
+ bytecode: contractBase64,
821
+ name: deploymentName,
822
+ contract_type: opts.contractType,
823
+ upgradeable: opts.upgradeable,
824
+ rent_exempt: opts.rentExempt,
825
+ compute_units: opts.computeUnits,
826
+ bytecode_hash: validation.fullHash,
827
+ },
828
+ },
829
+ fee: DEPLOYMENT_FEE_LAMPORTS,
830
+ slot: slot,
831
+ timestamp: Math.floor(Date.now() / 1000),
832
+ recent_blockhash: blockhash.blockhash,
833
+ };
834
+
835
+ // Sign transaction
836
+ tx.signature = signTransaction(tx, keyPair.secretKey);
837
+
838
+ // Submit deployment
839
+ if (!opts.json) {
840
+ startSpinner('Submitting deployment transaction');
841
+ }
842
+
843
+ try {
844
+ const result = await client.sendTransaction(tx);
845
+
846
+ stopSpinner(true);
847
+
848
+ const deployTime = Date.now() - startTime;
849
+
850
+ // Save deployment record
851
+ saveDeployment({
852
+ programId,
853
+ name: deploymentName,
854
+ type: opts.contractType,
855
+ size: validation.size,
856
+ deployer: walletAddress,
857
+ transaction: result.signature || result.txid,
858
+ slot: result.slot || slot,
859
+ upgradeable: opts.upgradeable,
860
+ });
861
+
862
+ if (opts.json) {
863
+ console.log(JSON.stringify({
864
+ success: true,
865
+ programId,
866
+ name: deploymentName,
867
+ type: opts.contractType,
868
+ size: validation.size,
869
+ deployer: walletAddress,
870
+ signature: result.signature || result.txid,
871
+ slot: result.slot || slot,
872
+ deployTimeMs: deployTime,
873
+ rpc: opts.rpc,
874
+ }, null, 2));
875
+ } else {
876
+ console.log(`\n ${indicators.success} ${C.green}${C.bright}Contract deployed successfully!${C.reset}\n`);
877
+ console.log(` ${C.bright}Program ID:${C.reset} ${C.cyan}${programId}${C.reset}`);
878
+ console.log(` ${C.bright}Name:${C.reset} ${deploymentName}`);
879
+ console.log(` ${C.bright}Type:${C.reset} ${opts.contractType}`);
880
+ console.log(` ${C.bright}Size:${C.reset} ${validation.formattedSize}`);
881
+ console.log(` ${C.bright}Deploy Time:${C.reset} ${formatDuration(deployTime)}`);
882
+
883
+ if (result.signature || result.txid) {
884
+ console.log(` ${C.bright}Signature:${C.reset} ${shortAddress(result.signature || result.txid)}`);
885
+ }
886
+ console.log(` ${C.bright}Slot:${C.reset} ${result.slot || slot}`);
887
+ console.log();
888
+ console.log(` ${C.dim}Verify: aether deploy --verify ${programId}${C.reset}`);
889
+ console.log(` ${C.dim}Status: aether deploy --status ${programId}${C.reset}`);
890
+ console.log(` ${C.dim}Explorer: https://explorer.aether.network/program/${programId}${C.reset}\n`);
891
+ }
892
+
893
+ } catch (err) {
894
+ stopSpinner(false);
895
+
896
+ if (opts.json) {
897
+ console.log(JSON.stringify({
898
+ success: false,
899
+ stage: 'deployment',
900
+ error: err.message,
901
+ }, null, 2));
902
+ } else {
903
+ console.log(`\n ${indicators.error} ${error(`Deployment failed: ${err.message}`)}\n`);
904
+ console.log(` ${C.dim}Common causes:${C.reset}`);
905
+ console.log(` • Contract bytecode is invalid or corrupted`);
906
+ console.log(` • Insufficient balance for deployment fee`);
907
+ console.log(` • RPC node rejected the transaction`);
908
+ console.log(` • Network congestion - retry with higher fee\n`);
909
+ }
910
+ process.exit(1);
911
+ }
912
+ }
913
+
914
+ // ============================================================================
915
+ // Main Entry Point
916
+ // ============================================================================
917
+
918
+ async function deployCommand() {
919
+ const opts = parseArgs();
920
+
921
+ if (opts.help) {
922
+ showHelp();
923
+ return;
924
+ }
925
+
926
+ if (opts.listTemplates) {
927
+ listTemplates(opts.json);
928
+ return;
929
+ }
930
+
931
+ if (opts.verify) {
932
+ await verifyContract(opts);
933
+ return;
934
+ }
935
+
936
+ if (opts.status) {
937
+ await checkDeploymentStatus(opts);
938
+ return;
939
+ }
940
+
941
+ if (!opts.filePath) {
942
+ console.log(BRANDING.header(CLI_VERSION));
943
+ console.log(`\n ${indicators.error} ${error('No contract file specified')}\n`);
944
+ console.log(` ${C.dim}Usage: aether deploy <contract.wasm> [options]${C.reset}`);
945
+ console.log(` ${C.dim} aether deploy --help for more info${C.reset}\n`);
946
+ process.exit(1);
947
+ }
948
+
949
+ await deployContract(opts);
950
+ }
951
+
952
+ module.exports = { deployCommand };
953
+
954
+ if (require.main === module) {
955
+ deployCommand().catch(err => {
956
+ console.error(`\n${C.red}✗ Deploy command failed: ${err.message}${C.reset}\n`);
957
+ process.exit(1);
958
+ });
959
+ }