@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
282 lines โข 12.2 kB
JavaScript
/**
* Five CLI Deploy-and-Execute Command
*
* Combined command that deploys bytecode to Solana and then executes it.
* Perfect for testing workflows where you need real on-chain execution.
* Designed for localnet testing and development workflows.
*/
import { readFile } from 'fs/promises';
import { extname } from 'path';
import chalk from 'chalk';
import ora from 'ora';
import { FiveSDK } from '../sdk/FiveSDK.js';
import { ConfigManager } from '../config/ConfigManager.js';
import { FiveFileManager } from '../utils/FiveFileManager.js';
/**
* Five deploy-and-execute command implementation
*/
export const deployAndExecuteCommand = {
name: 'deploy-and-execute',
description: 'Deploy bytecode to Solana and execute immediately (perfect for testing)',
aliases: ['dae', 'test-onchain'],
options: [
{
flags: '-f, --function <index>',
description: 'Execute specific function by index (default: 0)',
defaultValue: 0
},
{
flags: '-p, --params <params>',
description: 'Function parameters as JSON array (e.g., "[10, 5]")',
defaultValue: '[]'
},
{
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-cu <units>',
description: 'Maximum compute units for execution (default: 1400000)',
defaultValue: 1400000
},
{
flags: '--compute-budget <units>',
description: 'Compute budget for deployment transaction',
defaultValue: 1400000
},
{
flags: '--format <format>',
description: 'Output format',
choices: ['text', 'json'],
defaultValue: 'text'
},
{
flags: '--debug',
description: 'Enable debug output',
defaultValue: false
},
{
flags: '--skip-deployment-verification',
description: 'Skip deployment verification',
defaultValue: false
},
{
flags: '--cleanup',
description: 'Clean up deployed account after execution (for testing)',
defaultValue: false
}
],
arguments: [
{
name: 'bytecode',
description: 'Five VM bytecode file (.bin, .five, or .v source)',
required: true
}
],
examples: [
{
command: 'five deploy-and-execute script.five',
description: 'Deploy and execute function 0 on configured target'
},
{
command: 'five deploy-and-execute script.five --target local -f 1 -p "[10, 5]"',
description: 'Deploy to localnet and execute function 1 with parameters [10, 5]'
},
{
command: 'five deploy-and-execute src/main.v --target local --debug',
description: 'Compile, deploy, and execute source file on localnet with debug output'
},
{
command: 'five dae script.bin --target devnet --max-cu 2000000',
description: 'Deploy and execute on devnet with higher compute limit'
}
],
handler: async (args, options, context) => {
const { logger } = context;
try {
// 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);
console.log(`${targetPrefix} Deploy-and-Execute workflow starting`);
if (context.options.verbose || options.debug) {
logger.info(`Target: ${config.target}`);
logger.info(`Network: ${config.networks[config.target].rpcUrl}`);
logger.info(`Keypair: ${config.keypairPath}`);
}
const inputFile = args[0];
// STEP 1: Load/Compile bytecode
let bytecode;
let abi = null;
if (extname(inputFile) === '.v') {
// Compile source file
console.log(chalk.blue('๐ Compiling Five source...'));
const sourceCode = await readFile(inputFile, 'utf8');
const compilationResult = await FiveSDK.compile(sourceCode, {
optimize: true,
debug: options.debug
});
if (!compilationResult.success || !compilationResult.bytecode) {
console.log(chalk.red('โ Compilation failed'));
if (compilationResult.errors) {
compilationResult.errors.forEach(error => {
console.log(chalk.red(` โข ${error.message}`));
});
}
process.exit(1);
}
bytecode = compilationResult.bytecode;
abi = compilationResult.abi;
console.log(chalk.green(`โ
Compilation successful (${bytecode.length} bytes)`));
}
else {
// Load existing bytecode file
console.log(chalk.blue('๐ Loading bytecode...'));
const fileManager = FiveFileManager.getInstance();
const loadedFile = await fileManager.loadFile(inputFile, {
validateFormat: true
});
bytecode = loadedFile.bytecode;
abi = loadedFile.abi;
console.log(chalk.green(`โ
Loaded ${loadedFile.format.toUpperCase()} file (${bytecode.length} bytes)`));
}
// Setup Solana connection and keypair
const { Connection, Keypair } = await import('@solana/web3.js');
const rpcUrl = config.networks[config.target].rpcUrl;
const connection = new Connection(rpcUrl, 'confirmed');
// Load deployer keypair
const keypairPath = config.keypairPath.startsWith('~/')
? config.keypairPath.replace('~', process.env.HOME || '')
: config.keypairPath;
const keypairData = JSON.parse(await readFile(keypairPath, 'utf8'));
const deployerKeypair = Keypair.fromSecretKey(new Uint8Array(keypairData));
if (options.debug) {
console.log(`๐ Deployer: ${deployerKeypair.publicKey.toString()}`);
}
// STEP 2: Deploy bytecode
console.log(chalk.blue('๐ Deploying to Solana...'));
const deploySpinner = ora('Deploying bytecode...').start();
const deploymentResult = await FiveSDK.deployToSolana(bytecode, connection, deployerKeypair, {
debug: options.debug,
network: config.target,
computeBudget: options.computeBudget,
maxRetries: 3
});
if (!deploymentResult.success) {
deploySpinner.fail('Deployment failed');
try {
console.log(chalk.red(`โ Deployment error: ${deploymentResult.error || 'unknown'}`));
console.log(chalk.gray('Deployment result (debug):'), JSON.stringify(deploymentResult, null, 2));
}
catch { }
process.exit(1);
}
const scriptAccount = deploymentResult.programId;
deploySpinner.succeed(`Deployed successfully: ${scriptAccount}`);
if (deploymentResult.logs && (context.options.verbose || options.debug)) {
console.log(chalk.gray('๐ Deployment logs:'));
deploymentResult.logs.forEach(log => {
console.log(chalk.gray(` ${log}`));
});
}
// STEP 3: Wait for deployment to fully propagate, then execute
console.log(chalk.blue('โฑ๏ธ Waiting for deployment to propagate...'));
await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay
console.log(chalk.blue('โก Executing deployed script...'));
// Parse function index and parameters
const functionIndex = parseInt(options.function) || 0;
let parameters = [];
try {
if (options.params && options.params !== '[]') {
parameters = JSON.parse(options.params);
}
}
catch (error) {
console.log(chalk.red(`โ Invalid parameters JSON: ${options.params}`));
process.exit(1);
}
if (options.debug) {
console.log(`๐ Executing function ${functionIndex} with parameters:`, parameters);
}
const executeSpinner = ora('Executing function...').start();
const executionResult = await FiveSDK.executeScriptAccount(scriptAccount, functionIndex, parameters, connection, deployerKeypair, {
debug: options.debug,
network: config.target,
computeBudget: options.maxCu,
maxRetries: 3
});
if (executionResult.success) {
executeSpinner.succeed('Execution completed successfully');
// Display results
displayExecutionResults(executionResult, options, targetPrefix);
}
else {
executeSpinner.fail('Execution failed');
console.log(chalk.red(`โ Execution error: ${executionResult.error}`));
if (executionResult.logs && executionResult.logs.length > 0) {
console.log(chalk.red('๐ Error logs:'));
executionResult.logs.forEach(log => {
console.log(chalk.red(` ${log}`));
});
}
process.exit(1);
}
// STEP 4: Optional cleanup (for testing)
if (options.cleanup) {
console.log(chalk.yellow('๐งน Cleanup requested (not implemented yet)'));
console.log(chalk.gray(` Script account ${scriptAccount} will remain on chain`));
}
console.log(chalk.green('๐ Deploy-and-Execute workflow completed successfully!'));
}
catch (error) {
logger.error('Deploy-and-Execute workflow failed:', error);
throw error;
}
}
};
/**
* Display execution results in a formatted way
*/
function displayExecutionResults(result, options, targetPrefix) {
if (options.format === 'json') {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(chalk.bold(`\n${targetPrefix} Execution Results:`));
if (result.transactionId) {
console.log(`Transaction: ${chalk.cyan(result.transactionId)}`);
}
if (result.computeUnitsUsed !== undefined) {
console.log(`Compute units used: ${chalk.gray(result.computeUnitsUsed.toLocaleString())}`);
}
if (result.result !== undefined) {
console.log(`Result: ${chalk.cyan(JSON.stringify(result.result))}`);
}
if (result.logs && result.logs.length > 0) {
console.log(chalk.bold('\n๐ Transaction logs:'));
result.logs.forEach((log) => {
// Filter out system logs and show only relevant ones
if (log.includes('Five') || log.includes('success') || log.includes('error') || log.includes('Program log:')) {
console.log(chalk.gray(` ${log}`));
}
});
}
}
//# sourceMappingURL=deploy-and-execute.js.map