UNPKG

@five-vm/cli

Version:

High-performance CLI for Five VM development with WebAssembly integration

449 lines 20 kB
/** * Five CLI Deploy Command * * Deploy Five VM bytecode to Solana networks with automatic program creation, * upgrade management, and deployment verification. */ import { readFile } from 'fs/promises'; import chalk from 'chalk'; import ora from 'ora'; import { Connection, Keypair } from '@solana/web3.js'; import { FiveSDK } from '../sdk/FiveSDK.js'; import { ConfigManager } from '../config/ConfigManager.js'; import { FiveFileManager } from '../utils/FiveFileManager.js'; /** * Five deploy command implementation */ export const deployCommand = { name: 'deploy', description: 'Deploy bytecode to Solana', aliases: ['d'], options: [ { flags: '-t, --target <target>', description: 'Override target network (devnet, testnet, mainnet, local)', required: false }, { flags: '-n, --network <url>', description: 'Override network RPC URL', required: false }, { flags: '-k, --keypair <file>', description: 'Override keypair file path', required: false }, { flags: '--max-data-size <size>', description: 'Maximum data size for program account (bytes)', defaultValue: 1000000 }, { flags: '--compute-budget <units>', description: 'Compute budget for deployment transaction', defaultValue: 1400000 }, { flags: '--skip-verification', description: 'Skip deployment verification', defaultValue: false }, { flags: '--dry-run', description: 'Simulate deployment without executing', defaultValue: false }, { flags: '--format <format>', description: 'Output format', choices: ['text', 'json'], defaultValue: 'text' }, { flags: '--debug', description: 'Enable debug output', defaultValue: false }, { flags: '--chunk-size <bytes>', description: 'Chunk size for large program deployment (default: 750)', defaultValue: 750 }, { flags: '--progress', description: 'Show progress for large program deployments', defaultValue: false }, { flags: '--force-chunked', description: 'Force chunked deployment even for small programs', defaultValue: false }, { flags: '--optimized', description: 'Use optimized deployment instructions (50-70% fewer transactions)', defaultValue: false } ], arguments: [ { name: 'bytecode', description: 'Five VM bytecode file (.bin)', required: true } ], examples: [ { command: 'five deploy program.bin', description: 'Deploy to configured target (uses config defaults)' }, { command: 'five deploy program.bin --target mainnet', description: 'Deploy to mainnet (overrides config)' }, { command: 'five deploy program.bin --keypair deployer.json --target devnet', description: 'Deploy to devnet with specific keypair' }, { command: 'five deploy program.bin --dry-run --format json', description: 'Simulate deployment with JSON output' }, { command: 'five deploy large-program.bin --optimized --progress', description: 'Deploy large program with optimized instructions (50-70% fewer transactions)' } ], handler: async (args, options, context) => { const { logger } = context; logger.info('🔥 DEPLOY HANDLER STARTED - TRACING EXECUTION PATH'); logger.info(`🔍 Command args: ${JSON.stringify(args)}`); logger.info(`🔍 Options: ${JSON.stringify(options)}`); try { logger.info('📋 Applying configuration overrides...'); // Apply config with CLI overrides const configManager = ConfigManager.getInstance(); const overrides = { target: options.target, network: options.network, keypair: options.keypair }; logger.info(`🔧 Overrides: ${JSON.stringify(overrides)}`); const config = await configManager.applyOverrides(overrides); logger.info(`✅ Config applied: ${JSON.stringify(config, null, 2)}`); // Show target context prefix const targetPrefix = ConfigManager.getTargetPrefix(config.target); logger.info(`${targetPrefix} Deploying Five VM bytecode`); // Show config details if enabled if (config.showConfig) { logger.info(`Target: ${config.target}`); logger.info(`Network: ${config.networks[config.target].rpcUrl}`); logger.info(`Keypair: ${config.keypairPath}`); } // Load bytecode using centralized file manager const bytecodeFile = args[0]; logger.info(`📁 Loading bytecode file: ${bytecodeFile}`); const spinner = ora('Loading bytecode...').start(); const fileManager = FiveFileManager.getInstance(); const loadedFile = await fileManager.loadFile(bytecodeFile, { validateFormat: true }); spinner.succeed(`Loaded ${loadedFile.format.toUpperCase()} file: ${loadedFile.bytecode.length} bytes from ${bytecodeFile}`); logger.info(`📦 File loaded successfully: ${loadedFile.bytecode.length} bytes`); // Show additional info for .five files if (loadedFile.abi) { const functionCount = Object.keys(loadedFile.abi.functions || {}).length; logger.info(`📋 ABI functions: ${functionCount}`); if (functionCount > 0) { const functionNames = Object.keys(loadedFile.abi.functions).join(', '); logger.info(`📋 Available functions: ${functionNames}`); } } // Validate bytecode using Five SDK logger.info('🔍 Starting bytecode validation...'); spinner.start('Validating bytecode...'); const validation = await FiveSDK.validateBytecode(loadedFile.bytecode, { debug: options.debug || false }); logger.info(`🔍 Validation result: ${JSON.stringify(validation)}`); if (!validation.valid) { spinner.fail('Bytecode validation failed'); logger.error(`❌ Validation failed: ${validation.errors?.join(', ')}`); throw new Error(`Invalid bytecode: ${validation.errors?.join(', ')}`); } spinner.succeed('Bytecode validation passed'); logger.info('✅ Bytecode validation passed'); // Validate bytecode size logger.info(`📏 Checking bytecode size: ${loadedFile.bytecode.length} bytes (max: ${options.maxDataSize})`); if (loadedFile.bytecode.length > options.maxDataSize) { throw new Error(`Bytecode too large: ${loadedFile.bytecode.length} bytes (max: ${options.maxDataSize})`); } // Setup deployment options logger.info('⚙️ Setting up deployment options...'); const deploymentOptions = await setupDeploymentOptions(loadedFile.bytecode, options, config, logger); logger.info(`⚙️ Deployment options: ${JSON.stringify(deploymentOptions, null, 2)}`); // Execute deployment logger.info('🎯 ABOUT TO CALL executeDeployment FUNCTION'); let result; if (options.dryRun) { logger.info('🎭 DRY RUN MODE - CALLING simulateDeployment'); result = await simulateDeployment(deploymentOptions, options, context); } else { logger.info('🚀 LIVE DEPLOYMENT MODE - CALLING executeDeployment'); logger.info('🎯 About to call executeDeployment with parameters:'); logger.info(` - deploymentOptions: ${JSON.stringify(deploymentOptions)}`); logger.info(` - options: ${JSON.stringify(options)}`); logger.info(` - config: ${JSON.stringify(config)}`); result = await executeDeployment(deploymentOptions, options, context, config); } // Display results logger.info('📊 Displaying deployment results...'); logger.info(`📊 Result: ${JSON.stringify(result, null, 2)}`); displayDeploymentResult(result, options, logger); if (!result.success) { logger.error('❌ Deployment failed - exiting with code 1'); process.exit(1); } logger.info('✅ Deploy command completed successfully'); } catch (error) { logger.error('💥 DEPLOY HANDLER ERROR CAUGHT:', error); logger.error('💥 Error details:', { name: error instanceof Error ? error.name : 'Unknown', message: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : 'No stack trace' }); throw error; } } }; /** * Setup deployment options from CLI arguments */ async function setupDeploymentOptions(bytecode, options, config, logger) { const deploymentOptions = { bytecode, network: config.target, maxDataSize: parseInt(options.maxDataSize), computeBudget: parseInt(options.computeBudget) }; return deploymentOptions; } /** * Execute actual deployment to Solana network */ async function executeDeployment(deploymentOptions, options, context, config) { const { logger } = context; logger.info('📍 INSIDE executeDeployment FUNCTION'); logger.info(`📍 Function parameters received:`); logger.info(` - deploymentOptions: ${JSON.stringify(deploymentOptions)}`); logger.info(` - options: ${JSON.stringify(options)}`); logger.info(` - config: ${JSON.stringify(config)}`); const targetPrefix = ConfigManager.getTargetPrefix(config.target); logger.info(`${targetPrefix} Deploying to ${deploymentOptions.network}...`); try { // Get network RPC endpoint from config logger.info('🌐 Getting RPC endpoint from config...'); const rpcUrl = config.networks[config.target].rpcUrl; logger.info(`🌐 RPC URL: ${rpcUrl}`); logger.info('🔗 Creating Solana connection...'); const connection = new Connection(rpcUrl, 'confirmed'); logger.info('✅ Solana connection created'); // Load deployer keypair from config logger.info('🔑 Loading deployer keypair...'); logger.info(`🔑 Keypair path: ${config.keypairPath}`); const deployerKeypair = await loadKeypair(config.keypairPath, logger); logger.info(`✅ Deployer keypair loaded: ${deployerKeypair.publicKey.toString()}`); // Deploy using Five SDK logger.info('🚀 Using Five SDK for deployment'); logger.info(` - connection: [Connection object]`); logger.info(` - deployerKeypair: ${deployerKeypair.publicKey.toString()}`); logger.info(` - deploymentOptions: ${JSON.stringify(deploymentOptions)}`); logger.info(` - options: ${JSON.stringify(options)}`); const spinner = ora('Deploying via Five SDK...').start(); logger.info('🚀 About to call FiveSDK.deployToSolana with:'); logger.info(` - bytecode type: ${typeof deploymentOptions.bytecode}`); logger.info(` - bytecode length: ${deploymentOptions.bytecode.length}`); logger.info(` - connection type: ${typeof connection}`); logger.info(` - deployerKeypair type: ${typeof deployerKeypair}`); logger.info(` - deployerKeypair.publicKey: ${deployerKeypair.publicKey.toString()}`); // Auto-detect large programs and use appropriate deployment method const bytecodeArray = new Uint8Array(deploymentOptions.bytecode); const isLargeProgram = bytecodeArray.length > 800 || options.forceChunked; if (isLargeProgram) { if (options.optimized) { logger.info(`⚡ Large program detected (${bytecodeArray.length} bytes), using OPTIMIZED chunked deployment (50-70% fewer transactions)`); spinner.text = 'Deploying large program via OPTIMIZED chunked deployment...'; } else { logger.info(`🧩 Large program detected (${bytecodeArray.length} bytes), using traditional chunked deployment`); spinner.text = 'Deploying large program via chunked deployment...'; } } let result; if (isLargeProgram) { if (options.optimized) { // Use the new optimized deployment method result = await FiveSDK.deployLargeProgramOptimizedToSolana(bytecodeArray, connection, deployerKeypair, { debug: options.debug || false, network: deploymentOptions.network, maxRetries: 3, chunkSize: options.chunkSize || 950, // Higher default for optimized method progressCallback: options.progress ? (transaction, total) => { spinner.text = `⚡ Optimized deployment: transaction ${transaction}/${total}...`; } : undefined }); } else { // Use traditional large program deployment result = await FiveSDK.deployLargeProgramToSolana(bytecodeArray, connection, deployerKeypair, { debug: options.debug || false, network: deploymentOptions.network, maxRetries: 3, chunkSize: options.chunkSize || 750, progressCallback: options.progress ? (chunk, total) => { spinner.text = `Deploying chunk ${chunk}/${total}...`; } : undefined }); } } else { // Use regular deployment for small programs result = await FiveSDK.deployToSolana(bytecodeArray, connection, deployerKeypair, { debug: options.debug || false, network: deploymentOptions.network, computeBudget: deploymentOptions.computeBudget, maxRetries: 3 }); } logger.info(`🎯 FiveSDK.deployToSolana returned:`, result); if (result.success) { if (isLargeProgram && 'chunksUsed' in result && result.chunksUsed) { const largeResult = result; // Type assertion for large deployment result if (options.optimized && 'optimizationSavings' in result) { const optimizedResult = result; const savingsPercent = Math.round(optimizedResult.optimizationSavings.transactionsSaved / (optimizedResult.optimizationSavings.transactionsSaved + optimizedResult.totalTransactions) * 100); spinner.succeed(`⚡ Optimized deployment completed! (${largeResult.chunksUsed} chunks, ${largeResult.totalTransactions} transactions, ${savingsPercent}% reduction)`); } else { spinner.succeed(`Large program deployment completed! (${largeResult.chunksUsed} chunks, ${largeResult.totalTransactions} transactions)`); } } else { spinner.succeed('Five SDK deployment completed successfully!'); } } else { spinner.fail('Five SDK deployment failed'); } logger.info(`🎉 Five SDK deployment completed with result: ${JSON.stringify(result, null, 2)}`); return result; } catch (error) { logger.error('💥 executeDeployment ERROR CAUGHT:', error); logger.error('💥 Error details:', { name: error instanceof Error ? error.name : 'Unknown', message: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : 'No stack trace' }); const errorResult = { success: false, error: error instanceof Error ? error.message : 'Unknown deployment error', logs: [] }; logger.error(`💥 Returning error result: ${JSON.stringify(errorResult, null, 2)}`); return errorResult; } } /** * Simulate deployment without executing */ async function simulateDeployment(deploymentOptions, options, context) { const { logger } = context; logger.info('Simulating deployment...'); // Simulate validation and cost calculation await new Promise(resolve => setTimeout(resolve, 1000)); const estimatedCost = Math.ceil(deploymentOptions.bytecode.length / 1000) * 1000000; // Rough estimate return { success: true, programId: 'SIMULATED_PROGRAM_ID_' + Date.now(), transactionId: 'SIMULATED_TX_' + Date.now(), deploymentCost: estimatedCost, logs: [ 'Deployment simulation completed', `Estimated cost: ${estimatedCost / 1e9} SOL`, `Bytecode size: ${deploymentOptions.bytecode.length} bytes`, `Target network: ${deploymentOptions.network}` ] }; } /** * Display deployment result in specified format */ function displayDeploymentResult(result, options, logger) { if (options.format === 'json') { console.log(JSON.stringify(result, null, 2)); return; } console.log('\n' + chalk.bold('Deployment Result:')); if (result.success) { console.log(chalk.green('✓ Success')); if (result.programId) { console.log(`Program ID: ${chalk.cyan(result.programId)}`); } if (result.transactionId) { console.log(`Transaction: ${chalk.cyan(result.transactionId)}`); } if (result.deploymentCost !== undefined) { const costSOL = result.deploymentCost / 1e9; console.log(`Cost: ${chalk.yellow(costSOL.toFixed(6))} SOL`); } if (result.logs && result.logs.length > 0) { console.log('\nLogs:'); result.logs.forEach((log) => { console.log(` ${log}`); }); } } else { console.log(chalk.red('✗ Failed')); if (result.error) { console.log('Error:', result.error); } } } /** * Get RPC URL for network */ function getNetworkRpcUrl(network) { const endpoints = { 'devnet': 'https://api.devnet.solana.com', 'testnet': 'https://api.testnet.solana.com', 'mainnet': 'https://api.mainnet-beta.solana.com', 'local': 'http://127.0.0.1:8899' }; return endpoints[network] || endpoints['devnet']; } /** * Load Solana keypair from file */ async function loadKeypair(keypairPath, logger) { // Expand tilde in path const path = keypairPath.startsWith('~/') ? keypairPath.replace('~', process.env.HOME || '') : keypairPath; try { const keypairData = await readFile(path, 'utf8'); const secretKey = Uint8Array.from(JSON.parse(keypairData)); const keypair = Keypair.fromSecretKey(secretKey); logger.debug(`Loaded keypair from: ${path}`); logger.debug(`Public key: ${keypair.publicKey.toString()}`); return keypair; } catch (error) { throw new Error(`Failed to load keypair from ${path}: ${error}`); } } //# sourceMappingURL=deploy.js.map