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