UNPKG

@five-vm/cli

Version:

High-performance CLI for Five VM development with WebAssembly integration

570 lines 23.7 kB
/** * Five CLI Execute Command * * Real execution command using Five VM WASM bindings for bytecode execution, * with support for partial execution, debugging, and function calls. */ import { readFile } from 'fs/promises'; import { extname } from 'path'; import chalk from 'chalk'; import ora from 'ora'; import { FiveSDK, compileAndExecuteLocally } from '../sdk/index.js'; import { ConfigManager } from '../config/ConfigManager.js'; import { FiveFileManager } from '../utils/FiveFileManager.js'; /** * Five execute command implementation */ export const executeCommand = { name: 'execute', description: 'Execute Five VM bytecode', aliases: ['exec', 'run'], options: [ { flags: '-i, --input <file>', description: 'Input data file (JSON format)', required: false }, { flags: '-a, --accounts <file>', description: 'Accounts configuration file (JSON format)', required: false }, { flags: '-f, --function <index>', description: 'Execute specific function by index', required: false }, { flags: '-p, --params <file>', description: 'Function parameters file (JSON format)', required: false }, { flags: '--max-cu <units>', description: 'Maximum compute units (default: 1000000)', defaultValue: 1000000 }, { flags: '--validate', description: 'Validate bytecode before execution', defaultValue: false }, { flags: '--partial', description: 'Enable partial execution (stops at system calls)', defaultValue: true }, { flags: '--format <format>', description: 'Output format', choices: ['text', 'json', 'table'], defaultValue: 'text' }, { flags: '--trace', description: 'Show execution trace', defaultValue: false }, { flags: '--state', description: 'Show VM state after execution', defaultValue: false }, { 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: '--local', description: 'Force local execution (overrides config)', defaultValue: false }, { flags: '--script-account <account>', description: 'Execute deployed script by account ID (on-chain execution)', required: false } ], arguments: [ { name: 'bytecode', description: 'Five VM bytecode file (.bin) or script account ID', required: false } ], examples: [ { command: 'five execute program.bin', description: 'Execute using configured target (default)' }, { command: 'five execute program.bin --local', description: 'Force local execution (overrides config)' }, { command: 'five execute program.bin --target devnet', description: 'Execute on devnet (overrides config)' }, { command: 'five execute program.bin -f 0 -p params.json', description: 'Execute function 0 with parameters' }, { command: 'five execute program.bin --validate --trace --format json', description: 'Validate and execute with JSON trace output' }, { command: 'five execute src/main.v -f 0 --local --params "[10, 5]"', description: 'Compile and execute Five source locally with parameters' }, { command: 'five execute --script-account 459SanqV8nQDDYW3gWq5JZZAPCMYs78Z5ZnrtH4eFffw -f 0', description: 'Execute deployed script by account ID on-chain' } ], handler: async (args, options, context) => { const { logger } = context; try { const inputFile = args[0]; const scriptAccount = options.scriptAccount; // Validate input - either bytecode file OR script account required if (!inputFile && !scriptAccount) { throw new Error('Either bytecode file or --script-account must be provided'); } // Apply config with CLI overrides const configManager = ConfigManager.getInstance(); const overrides = { target: options.target, network: options.network, keypair: options.keypair }; const config = await configManager.applyOverrides(overrides); // Show target context prefix const targetPrefix = ConfigManager.getTargetPrefix(config.target); // Force local execution if --local flag is used, but not if script account is specified const forceLocal = options.local || false; const executeLocally = (config.target === 'wasm' || forceLocal) && !scriptAccount; // Log execution mode only in verbose if (context.options.verbose) { if (scriptAccount) { logger.info(`${targetPrefix} Executing deployed script account on-chain`); } else if (executeLocally) { logger.info(`${ConfigManager.getTargetPrefix('wasm')} Executing Five VM bytecode locally`); } else { logger.info(`${targetPrefix} Executing Five VM bytecode`); } } // Show config details only if explicitly enabled and verbose if (config.showConfig && !executeLocally && context.options.verbose) { logger.info(`Target: ${config.target}`); logger.info(`Network: ${config.networks[config.target].rpcUrl}`); logger.info(`Keypair: ${config.keypairPath}`); } if (scriptAccount) { await executeScriptAccount(scriptAccount, options, context, config); } else if (executeLocally) { await executeLocallyWithSDK(inputFile, options, context, config); } else { await executeOnChain(inputFile, options, context, config); } } catch (error) { logger.error('Execution failed:', error); throw error; } } }; /** * Execute locally using Five SDK */ async function executeLocallyWithSDK(inputFile, options, context, config) { const { logger } = context; // Initialize for local execution if (context.options.verbose) { const spinner = ora('Preparing local execution...').start(); spinner.succeed('Five SDK ready for local execution'); } try { let result; if (extname(inputFile) === '.v') { // Compile and execute Five source file logger.info(`Compiling and executing Five source: ${inputFile}`); const sourceCode = await readFile(inputFile, 'utf8'); // For .v files, we don't have ABI yet, so we'll just use the provided function or default to 0 // The real fix here would be to compile first, extract ABI, then auto-detect the public function const functionIndex = parseFunctionIndex(options.function) || 0; const parameters = parseParameters(options.params); result = await compileAndExecuteLocally(sourceCode, functionIndex, parameters, { debug: options.trace || context.options.debug, trace: options.trace, optimize: true, computeUnitLimit: options.maxCu }); // Display compilation info with proper type guard if ('compilation' in result && result.compilation) { console.log(chalk.gray(`Compilation: ${result.compilation.success ? 'Success' : 'Failed'}`)); if ('bytecodeSize' in result && result.bytecodeSize) { console.log(chalk.gray(`Bytecode size: ${result.bytecodeSize} bytes`)); } } } else { // Execute existing bytecode file logger.info(`Executing bytecode: ${inputFile}`); // Load file using centralized manager const fileManager = FiveFileManager.getInstance(); const loadedFile = await fileManager.loadFile(inputFile, { validateFormat: true }); const bytecode = loadedFile.bytecode; const abi = loadedFile.abi; if (options.trace || context.options.debug) { console.log(`[CLI] Loaded ${loadedFile.format.toUpperCase()} file: bytecode=${bytecode.length} bytes`); if (abi && abi.functions) { console.log(`[CLI] Available functions: ${Object.keys(abi.functions).length}`); } } // FUNCTION RESOLUTION FIX: Auto-detect public function when no -f flag provided let functionIndex = parseFunctionIndex(options.function); if (functionIndex === undefined && abi && abi.functions) { // SYSTEMATIC APPROACH: Public functions always have the lowest indices (0, 1, 2...) // So we always default to function index 0, which is guaranteed to be public if any functions exist functionIndex = 0; if (context.options.verbose) { let functionName = 'function_0'; if (Array.isArray(abi.functions)) { // FIVEABI format: find function with index 0 const func0 = abi.functions.find((f) => f.index === 0); if (func0 && func0.name) functionName = func0.name; } else { // SimpleABI format: find function with index 0 const func0Entry = Object.entries(abi.functions).find(([_, f]) => f.index === 0); if (func0Entry) functionName = func0Entry[0]; } logger.info(`Auto-detected public function: ${functionName} (index 0 - first public function)`); } } else if (functionIndex === undefined) { functionIndex = 0; // No ABI available, use legacy default } // Ensure functionIndex is never undefined if (functionIndex === undefined) { functionIndex = 0; } const parameters = parseParameters(options.params); result = await FiveSDK.executeLocally(bytecode, functionIndex, parameters, { debug: options.trace || context.options.debug, trace: options.trace, computeUnitLimit: options.maxCu, abi: abi // Pass ABI for function name resolution }); } // Display execution results displayLocalExecutionResult(result, options, logger); } catch (error) { throw new Error(`Local execution failed: ${error}`); } } /** * Execute deployed script account on-chain using Five SDK */ async function executeScriptAccount(scriptAccount, options, context, config) { const { logger } = context; const { Connection, Keypair } = await import('@solana/web3.js'); const targetPrefix = ConfigManager.getTargetPrefix(config.target); logger.info(`${targetPrefix} Executing script account: ${scriptAccount}`); try { // Show configuration console.log(chalk.yellow(`\nExecution configuration:`)); console.log(chalk.gray(` Script Account: ${scriptAccount}`)); console.log(chalk.gray(` Target: ${config.target}`)); console.log(chalk.gray(` Network: ${config.networks[config.target].rpcUrl}`)); console.log(chalk.gray(` Keypair: ${config.keypairPath}`)); // Set up connection and keypair const rpcUrl = config.networks[config.target].rpcUrl; const connection = new Connection(rpcUrl, 'confirmed'); // Load keypair const keypairPath = config.keypairPath; const keypairData = JSON.parse(await readFile(keypairPath, 'utf8')); const deployerKeypair = Keypair.fromSecretKey(new Uint8Array(keypairData)); console.log(chalk.cyan(` Executor: ${deployerKeypair.publicKey.toBase58()}`)); const spinner = ora('Executing script account on-chain...').start(); try { // Get function index and parameters const functionIndex = options.function ? parseInt(options.function) : 0; const parameters = options.params ? JSON.parse(options.params) : []; // Execute script account using Five SDK const result = await FiveSDK.executeScriptAccount(scriptAccount, functionIndex, parameters, connection, deployerKeypair, { debug: options.trace || context.options.debug, network: config.target, computeBudget: options.maxCu || 1400000, maxRetries: 3 }); spinner.succeed('Script account execution completed'); // Display results displayOnChainExecutionResult(result, options, logger); } catch (error) { spinner.fail('Script account execution failed'); throw error; } } catch (error) { throw new Error(`Script account execution failed: ${error}`); } } /** * Execute on-chain using Five SDK */ async function executeOnChain(inputFile, options, context, config) { const { logger } = context; const { Connection, Keypair } = await import('@solana/web3.js'); const targetPrefix = ConfigManager.getTargetPrefix(config.target); logger.info(`${targetPrefix} On-chain execution using Five SDK`); try { // Show configuration console.log(chalk.yellow(`\nExecution configuration:`)); console.log(chalk.gray(` Target: ${config.target}`)); console.log(chalk.gray(` Network: ${config.networks[config.target].rpcUrl}`)); console.log(chalk.gray(` Keypair: ${config.keypairPath}`)); // Check if input is a script account (base58 string) or bytecode file let scriptAccount; if (inputFile.length > 30 && inputFile.length < 50 && !inputFile.includes('/') && !inputFile.includes('.')) { // Looks like a base58 script account address scriptAccount = inputFile; console.log(chalk.cyan(`Using script account: ${scriptAccount}`)); } else { // It's a file - need to deploy first or prompt user console.log(chalk.red('\nError: On-chain execution requires a deployed script account.')); console.log(chalk.yellow('To execute on-chain:')); console.log(chalk.gray('1. First deploy your script: five deploy ' + inputFile)); console.log(chalk.gray('2. Then execute with the script account: five execute <SCRIPT_ACCOUNT> --target ' + config.target)); console.log(chalk.gray('3. Or use local execution: five execute ' + inputFile + ' --local')); return; } // Setup connection const rpcUrl = config.networks[config.target].rpcUrl; const connection = new Connection(rpcUrl, 'confirmed'); // Load signer keypair const signerKeypair = await loadKeypair(config.keypairPath, logger); // Parse execution options const functionName = parseFunctionIndex(options.function) || 0; const parameters = parseParameters(options.params); const accounts = []; // No additional accounts for simple execution console.log(chalk.cyan(`\nExecuting function ${functionName} with ${parameters.length} parameters...`)); // Execute using Five SDK const spinner = ora('Executing on-chain via Five SDK...').start(); const result = await FiveSDK.executeOnSolana(scriptAccount, connection, signerKeypair, functionName, parameters, accounts, { debug: options.debug || context.options.debug || false, network: config.target, computeUnitLimit: options.maxCu, maxRetries: 3 }); if (result.success) { spinner.succeed('On-chain execution completed successfully!'); displayOnChainExecutionResult(result, options, logger); } else { spinner.fail('On-chain execution failed'); displayOnChainExecutionResult(result, options, logger); process.exit(1); } } catch (error) { logger.error('On-chain execution failed:', error); throw error; } } /** * Parse function index - handle both numeric strings and function names */ function parseFunctionIndex(functionOption) { if (!functionOption) { return undefined; } // If it's already a number, return it if (typeof functionOption === 'number') { return functionOption; } // If it's a string that looks like a number, convert it if (typeof functionOption === 'string') { const numericValue = parseInt(functionOption, 10); if (!isNaN(numericValue) && numericValue.toString() === functionOption) { return numericValue; } // Otherwise, treat it as a function name return functionOption; } return functionOption; } /** * Parse parameters from JSON string or file */ function parseParameters(paramsOption) { if (!paramsOption) { return []; } try { // Try to parse as JSON directly if (typeof paramsOption === 'string' && paramsOption.startsWith('[')) { return JSON.parse(paramsOption); } // TODO: Handle parameter files return []; } catch (error) { throw new Error(`Failed to parse parameters: ${error}`); } } /** * Display on-chain execution results */ function displayOnChainExecutionResult(result, options, logger) { if (options.format === 'json') { console.log(JSON.stringify(result, null, 2)); return; } console.log('\n' + chalk.bold('On-Chain Execution Results:')); if (result.success) { console.log(chalk.green('✓ Execution successful')); if (result.transactionId) { console.log(`Transaction: ${chalk.cyan(result.transactionId)}`); } if (result.computeUnitsUsed !== undefined) { console.log(`Compute units used: ${chalk.gray(result.computeUnitsUsed)}`); } if (result.result !== undefined) { console.log(`Result: ${chalk.cyan(result.result)}`); } if (result.logs && result.logs.length > 0) { console.log('\nTransaction logs:'); result.logs.forEach((log) => { // Filter out system logs and show only Five VM logs if (log.includes('Five') || log.includes('success') || log.includes('error')) { console.log(chalk.gray(` ${log}`)); } }); } } else { console.log(chalk.red('✗ Execution failed')); if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); } if (result.logs && result.logs.length > 0) { console.log('\nError logs:'); result.logs.forEach((log) => { console.log(chalk.red(` ${log}`)); }); } } } /** * Load Solana keypair from file */ async function loadKeypair(keypairPath, logger) { const { readFile } = await import('fs/promises'); const { Keypair } = await import('@solana/web3.js'); // 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); if (logger.debug) { 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}`); } } /** * Display local execution results */ function displayLocalExecutionResult(result, options, logger) { console.log('\n' + chalk.bold('Local Execution Results:')); if (result.success) { console.log(chalk.green('✓ Execution successful')); if (result.result !== undefined) { console.log(` Result: ${chalk.cyan(JSON.stringify(result.result))}`); } if (result.executionTime) { console.log(` Execution time: ${chalk.gray(result.executionTime + 'ms')}`); } if (result.computeUnitsUsed) { console.log(` Compute units used: ${chalk.gray(result.computeUnitsUsed)}`); } if (result.logs && result.logs.length > 0) { console.log('\n' + chalk.bold('Execution logs:')); result.logs.forEach((log) => { console.log(chalk.gray(` ${log}`)); }); } if (result.trace && options.trace) { console.log('\n' + chalk.bold('Execution trace:')); // Display trace information if available if (Array.isArray(result.trace)) { result.trace.slice(0, 10).forEach((step, i) => { console.log(chalk.gray(` ${i}: ${JSON.stringify(step)}`)); }); if (result.trace.length > 10) { console.log(chalk.gray(` ... and ${result.trace.length - 10} more steps`)); } } } } else { console.log(chalk.red('✗ Execution failed')); if (result.error) { if (typeof result.error === 'object' && result.error.message) { console.log(chalk.red(` Error: ${result.error.message}`)); if (result.error.type) { console.log(chalk.red(` Type: ${result.error.type}`)); } } else { const errorMessage = typeof result.error === 'object' ? JSON.stringify(result.error, null, 2) : result.error; console.log(chalk.red(` Error: ${errorMessage}`)); } } if (result.compilationErrors && result.compilationErrors.length > 0) { console.log(chalk.red('\n Compilation errors:')); result.compilationErrors.forEach((error) => { console.log(chalk.red(` • ${error.message || error}`)); }); } } // Display output format if (options.format === 'json') { console.log('\n' + chalk.bold('JSON Output:')); console.log(JSON.stringify(result, null, 2)); } } //# sourceMappingURL=execute.js.map