@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
449 lines • 20 kB
JavaScript
/**
* 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