diff --git a/Cargo.lock b/Cargo.lock index 482b228034..756c1f1388 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,9 +81,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -547,9 +547,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "clap" -version = "4.5.0" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.0" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -569,9 +569,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -581,9 +581,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clarity" @@ -631,6 +631,7 @@ dependencies = [ name = "clarity-cli" version = "0.1.0" dependencies = [ + "clap", "clarity 0.0.1", "lazy_static", "rand 0.8.5", @@ -1397,9 +1398,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" diff --git a/contrib/clarity-cli/Cargo.toml b/contrib/clarity-cli/Cargo.toml index 1075c774d0..9fa1f0f282 100644 --- a/contrib/clarity-cli/Cargo.toml +++ b/contrib/clarity-cli/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +clap = { version = "4.5.44", features = ["derive"] } clarity = { path = "../../clarity", default-features = false } stackslib = { package = "stackslib", path = "../../stackslib", default-features = false } stacks-common = { path = "../../stacks-common", default-features = false } diff --git a/contrib/clarity-cli/README.md b/contrib/clarity-cli/README.md index 32ed0a6690..00cd9669cb 100644 --- a/contrib/clarity-cli/README.md +++ b/contrib/clarity-cli/README.md @@ -1,15 +1,358 @@ # clarity-cli -A thin wrapper executable for the Clarity CLI exposed by `blockstack_lib::clarity_cli`. It forwards argv to the library, prints JSON output, and exits with the underlying status code. +A command-line interface for developing, testing, and debugging Clarity smart contracts locally without needing a full Stacks node. + +## Build + +```bash +cargo build --release -p clarity-cli +``` + +The binary will be at `./target/release/clarity-cli`. + +## Overview + +clarity-cli provides a local VM environment for Clarity contract development. It maintains a persistent database that simulates blockchain state, allowing you to: + +- Initialize a local VM with boot contracts +- Type-check contracts before deployment +- Deploy ("launch") contracts to the local state +- Execute public functions on deployed contracts +- Evaluate Clarity expressions in various contexts + +All commands output JSON for easy parsing and integration with other tools. + +## Commands + +### `initialize` + +Create and initialize a new local VM state database with boot contracts. + +```bash +clarity-cli initialize [OPTIONS] [ALLOCATIONS_FILE] +``` + +**Arguments:** +- `DB_PATH` - Directory path for the VM state database +- `ALLOCATIONS_FILE` - Optional JSON file with initial STX allocations (or `-` for stdin) + +**Options:** +- `--testnet` - Use testnet boot code and block limits (default: mainnet) +- `--epoch ` - Stacks epoch to use (default: 3.3) + +**Example:** +```bash +# Initialize mainnet database +clarity-cli initialize ./my-db + +# Initialize testnet with allocations +clarity-cli initialize --testnet ./my-db allocations.json +``` + +**Allocations JSON format:** +```json +[ + { "principal": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM", "amount": 1000000 }, + { "principal": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.my-contract", "amount": 500000 } +] +``` + +--- + +### `generate-address` + +Generate a random Stacks address for testing. + +```bash +clarity-cli generate-address +``` + +**Example output:** +```json +{"address": "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"} +``` + +--- + +### `check` + +Type-check a Clarity contract without deploying it. -Build: ```bash -cargo build -p clarity-cli +clarity-cli check [OPTIONS] [DB_PATH] ``` -Usage: +**Arguments:** +- `CONTRACT_FILE` - Path to `.clar` file (or `-` for stdin) +- `DB_PATH` - Optional database path for resolving contract dependencies + +**Options:** +- `--contract-id ` - Contract identifier (default: transient) +- `--output-analysis` - Include contract interface analysis in output +- `--costs` - Include execution costs in output +- `--testnet` - Use testnet configuration +- `--clarity-version ` - Clarity version (e.g., `clarity1`, `clarity2`, `clarity3`, `clarity4`) +- `--epoch ` - Stacks epoch (e.g., `2.1`, `2.5`, `3.0`) + +**Example:** ```bash -./target/debug/clarity-cli --help +# Basic type check +clarity-cli check my-contract.clar + +# Check with cost analysis +clarity-cli check --costs --output-analysis my-contract.clar + +# Check against existing database (resolves contract-call? references) +clarity-cli check my-contract.clar ./my-db + +# Read from stdin +cat my-contract.clar | clarity-cli check - ``` -For advanced usage and subcommands, see the upstream Clarity CLI documentation or run with `--help`. +--- + +### `launch` + +Deploy a contract to the local VM state database. + +```bash +clarity-cli launch [OPTIONS] +``` + +**Arguments:** +- `CONTRACT_ID` - Fully qualified contract identifier (e.g., `ST1PQHQ...PGZGM.my-contract`) +- `CONTRACT_FILE` - Path to `.clar` file (or `-` for stdin) +- `DB_PATH` - Database path (must be initialized first) + +**Options:** +- `--costs` - Include execution costs in output +- `--assets` - Include asset changes in output +- `--output-analysis` - Include contract interface analysis +- `-c, --coverage ` - Output coverage data to directory +- `--clarity-version ` - Clarity version +- `--epoch ` - Stacks epoch + +**Example:** +```bash +# Deploy a contract +clarity-cli launch ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.tokens \ + tokens.clar ./my-db + +# Deploy with full output +clarity-cli launch --costs --assets --output-analysis \ + ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.tokens tokens.clar ./my-db +``` + +--- + +### `execute` + +Execute a public function on a deployed contract. + +```bash +clarity-cli execute [OPTIONS] [ARGS]... +``` + +**Arguments:** +- `CONTRACT_ID` - Contract identifier +- `FUNCTION_NAME` - Public function name to call +- `SENDER` - Sender principal address (becomes `tx-sender`) +- `DB_PATH` - Database path +- `ARGS` - Function arguments as Clarity literals + +**Options:** +- `--costs` - Include execution costs +- `--assets` - Include asset changes +- `-c, --coverage ` - Output coverage data +- `--clarity-version ` - Clarity version +- `--epoch ` - Stacks epoch + +**Example:** +```bash +# Call a function with no arguments +clarity-cli execute ST1PQHQ...PGZGM.tokens get-balance \ + ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM ./my-db + +# Call with arguments +clarity-cli execute ST1PQHQ...PGZGM.tokens transfer \ + ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM ./my-db \ + u100 \ + 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG + +# Note: Principal arguments need the Clarity quote prefix (') +``` + +--- + +### `eval` + +Evaluate a Clarity expression in read-only mode within a contract's context. Advances to a new block. + +```bash +clarity-cli eval [OPTIONS] [PROGRAM_FILE] +``` + +**Arguments:** +- `CONTRACT_ID` - Contract context for evaluation +- `DB_PATH` - Database path +- `PROGRAM_FILE` - File with Clarity code (or `-` for stdin; omit for stdin) + +**Options:** +- `--costs` - Include execution costs +- `--clarity-version ` - Clarity version +- `--epoch ` - Stacks epoch + +**Example:** +```bash +# Evaluate from file +clarity-cli eval ST1PQHQ...PGZGM.tokens ./my-db query.clar + +# Evaluate from stdin +echo "(+ 1 2)" | clarity-cli eval ST1PQHQ...PGZGM.tokens ./my-db +``` + +--- + +### `eval-at-chaintip` + +Like `eval`, but does **not** advance to a new block. Useful for repeated read-only queries. + +```bash +clarity-cli eval-at-chaintip [OPTIONS] [PROGRAM_FILE] +``` + +**Options:** Same as `eval`, plus: +- `-c, --coverage ` - Output coverage data + +--- + +### `eval-at-block` + +Evaluate at a specific historical block identified by its index block hash. + +```bash +clarity-cli eval-at-block [OPTIONS] [PROGRAM_FILE] +``` + +**Arguments:** +- `INDEX_BLOCK_HASH` - Block hash (hex string, e.g., `0x1234...`) +- `CONTRACT_ID` - Contract context +- `VM_DIR` - VM/clarity directory path +- `PROGRAM_FILE` - File with Clarity code (or `-` for stdin; omit for stdin) + +**Options:** +- `--costs` - Include execution costs +- `--clarity-version ` - Clarity version +- `--epoch ` - Stacks epoch + +--- + +### `eval-raw` + +Evaluate a Clarity expression without any contract or database context. Useful for quick calculations. + +```bash +clarity-cli eval-raw [OPTIONS] [PROGRAM_FILE] +``` + +**Arguments:** +- `PROGRAM_FILE` - File with Clarity code (or `-` for stdin; omit for stdin) + +**Options:** +- `--testnet` - Use testnet configuration +- `--clarity-version ` - Clarity version +- `--epoch ` - Stacks epoch + +**Example:** +```bash +# Quick calculation +echo "(+ 1 2)" | clarity-cli eval-raw + +# From file +clarity-cli eval-raw expression.clar +``` + +--- + +### `repl` + +Start an interactive REPL (Read-Eval-Print Loop) for Clarity expressions. + +```bash +clarity-cli repl [OPTIONS] +``` + +**Options:** +- `--testnet` - Use testnet configuration +- `--clarity-version ` - Clarity version +- `--epoch ` - Stacks epoch + +**Example:** +```bash +clarity-cli repl --clarity-version clarity3 +> (+ 1 2) +3 +> (define-data-var counter uint u0) +true +> (var-set counter u10) +true +> (var-get counter) +u10 +``` + +--- + +## Typical Workflow + +```bash +# 1. Initialize database +clarity-cli initialize ./my-db + +# 2. Check your contract +clarity-cli check my-contract.clar ./my-db + +# 3. Deploy contract +clarity-cli launch ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.my-contract \ + my-contract.clar ./my-db + +# 4. Execute functions +clarity-cli execute ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.my-contract \ + my-function ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM ./my-db u42 + +# 5. Query state +echo "(contract-call? .my-contract get-value)" | \ + clarity-cli eval-at-chaintip ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.my-contract ./my-db +``` + +## Epoch and Clarity Version + +The CLI defaults to Epoch 3.3 with Clarity 4. You can specify earlier epochs/versions for compatibility testing. + +**Valid epoch values:** `1.0`, `2.0`, `2.05`, `2.1`, `2.2`, `2.3`, `2.4`, `2.5`, `3.0`, `3.1`, `3.2`, `3.3` + +**Valid clarity version values:** `clarity1`, `clarity2`, `clarity3`, `clarity4` + +| Epoch | Default Clarity Version | +|-------|------------------------| +| 2.0 | Clarity 1 | +| 2.05 | Clarity 1 | +| 2.1 | Clarity 2 | +| 2.2 | Clarity 2 | +| 2.3 | Clarity 2 | +| 2.4 | Clarity 2 | +| 2.5 | Clarity 2 | +| 3.0 | Clarity 3 | +| 3.1 | Clarity 3 | +| 3.2 | Clarity 3 | +| 3.3 | Clarity 4 | + +See `clarity/src/vm/version.rs` for Clarity version definitions and `stacks-common/src/types/mod.rs` for epoch definitions. + +## Exit Codes + +- `0` - Success +- `1` - Error (check JSON output for details) + +## JSON Output + +All commands output JSON to stdout. Errors are also returned as JSON with an `"error"` field. Use `--costs` and `--assets` flags to include additional information in the output. diff --git a/contrib/clarity-cli/src/lib.rs b/contrib/clarity-cli/src/lib.rs index 5ac11abfdf..ae52b31de4 100644 --- a/contrib/clarity-cli/src/lib.rs +++ b/contrib/clarity-cli/src/lib.rs @@ -14,10 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#[macro_use] -extern crate serde_derive; - -use std::io::{Read, Write}; +use std::io::Write; use std::path::PathBuf; use std::{fs, io}; @@ -39,7 +36,7 @@ use clarity::vm::{ use lazy_static::lazy_static; use rand::Rng; use rusqlite::{Connection, OpenFlags}; -use serde::Serialize; +use serde::Deserialize; use serde_json::json; use stacks_common::address::c32::c32_address; use stacks_common::consts::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET}; @@ -105,27 +102,6 @@ macro_rules! panic_test { }; } -fn print_usage(invoked_by: &str) { - eprintln!( - "Usage: {invoked_by} [command] -where command is one of: - - initialize to initialize a local VM state database. - check to typecheck a potential contract definition. - launch to launch a initialize a new contract in the local state database. - eval to evaluate (in read-only mode) a program in a given contract context. - eval_at_chaintip like `eval`, but does not advance to a new block. - eval_at_block like `eval_at_chaintip`, but accepts a index-block-hash to evaluate at, - must be passed eval string via stdin. - eval_raw to typecheck and evaluate an expression without a contract or database context from stdin. - repl to typecheck and evaluate expressions in a stdin/stdout loop. - execute to execute a public function of a defined contract. - generate_address to generate a random Stacks public address for testing purposes. -" - ); - panic_test!() -} - fn friendly_expect(input: Result, msg: &str) -> A { input.unwrap_or_else(|e| { eprintln!("{msg}\nCaused by: {e}"); @@ -140,15 +116,30 @@ fn friendly_expect_opt(input: Option, msg: &str) -> A { }) } -pub const DEFAULT_CLI_EPOCH: StacksEpochId = StacksEpochId::Epoch33; +/// Represents an initial allocation entry from JSON +#[derive(Deserialize)] +pub struct InitialAllocation { + pub principal: String, + pub amount: u64, +} -struct EvalInput { - #[allow(dead_code)] - marf_kv: MarfedKV, - contract_identifier: QualifiedContractIdentifier, - content: String, +/// Parse allocation JSON string into (PrincipalData, u64) pairs +pub fn parse_allocations_json(json_content: &str) -> Result, String> { + let initial_allocations: Vec = serde_json::from_str(json_content) + .map_err(|e| format!("Failed to parse allocations JSON: {e}"))?; + + initial_allocations + .into_iter() + .map(|a| { + let principal = PrincipalData::parse(&a.principal) + .map_err(|e| format!("Failed to parse principal {}: {e}", a.principal))?; + Ok((principal, a.amount)) + }) + .collect() } +pub const DEFAULT_CLI_EPOCH: StacksEpochId = StacksEpochId::Epoch33; + fn parse( contract_identifier: &QualifiedContractIdentifier, source_code: &str, @@ -269,7 +260,7 @@ fn create_or_open_db(path: &String) -> Connection { // need to create if let Some(dirp) = PathBuf::from(path).parent() { fs::create_dir_all(dirp).unwrap_or_else(|e| { - eprintln!("Failed to create {:?}: {:?}", dirp, &e); + eprintln!("Failed to create {dirp:?}: {e:?}"); panic_test!(); }); } @@ -475,28 +466,7 @@ pub fn vm_execute( program: &str, clarity_version: ClarityVersion, ) -> Result, VmExecutionError> { - let contract_id = QualifiedContractIdentifier::transient(); - let mut contract_context = ContractContext::new(contract_id.clone(), clarity_version); - let mut marf = MemoryBackingStore::new(); - let conn = marf.as_clarity_db(); - let mut global_context = GlobalContext::new( - false, - default_chain_id(false), - conn, - LimitedCostTracker::new_free(), - StacksEpochId::latest(), - ); - global_context.execute(|g| { - let parsed = ast::build_ast( - &contract_id, - program, - &mut (), - clarity_version, - StacksEpochId::latest(), - )? - .expressions; - eval_all(&parsed, &mut contract_context, g, None) - }) + vm_execute_in_epoch(program, clarity_version, StacksEpochId::latest()) } fn save_coverage( @@ -558,7 +528,7 @@ impl CLIHeadersDB { friendly_expect( tx.commit(), - &format!("FATAL: failed to instantiate CLI DB at {:?}", &cli_db_path), + &format!("FATAL: failed to instantiate CLI DB at {cli_db_path:?}"), ); } @@ -585,7 +555,7 @@ impl CLIHeadersDB { pub fn resume(db_path: &str) -> Result { let cli_db_path = get_cli_db_path(db_path); if let Err(e) = fs::metadata(&cli_db_path) { - return Err(format!("Failed to access {:?}: {:?}", &cli_db_path, &e)); + return Err(format!("Failed to access {cli_db_path:?}: {e:?}")); } let conn = create_or_open_db(&cli_db_path); let db = CLIHeadersDB { @@ -624,9 +594,10 @@ impl CLIHeadersDB { } pub fn advance_cli_chain_tip(&mut self) -> (StacksBlockId, StacksBlockId) { + let db_path = &self.db_path; let tx = friendly_expect( self.conn.transaction(), - &format!("FATAL: failed to begin transaction on '{}'", &self.db_path), + &format!("FATAL: failed to begin transaction on '{db_path}'"), ); let parent_block_hash = get_cli_chain_tip(&tx); @@ -642,18 +613,12 @@ impl CLIHeadersDB { "INSERT INTO cli_chain_tips (block_hash) VALUES (?1)", [&next_block_hash], ), - &format!( - "FATAL: failed to store next block hash in '{}'", - &self.db_path - ), + &format!("FATAL: failed to store next block hash in '{db_path}'"), ); friendly_expect( tx.commit(), - &format!( - "FATAL: failed to commit new chain tip to '{}'", - &self.db_path - ), + &format!("FATAL: failed to commit new chain tip to '{db_path}'"), ); (parent_block_hash, next_block_hash) @@ -786,96 +751,6 @@ impl HeadersDB for CLIHeadersDB { } } -fn get_eval_input(invoked_by: &str, args: &[String]) -> EvalInput { - if args.len() < 3 || args.len() > 4 { - eprintln!( - "Usage: {invoked_by} {} [--costs] [--epoch E] [--clarity_version N] contract-identifier [program.clar] vm-state.db", - args[0] - ); - eprintln!(); - eprintln!(" If a program file name is not provided, the program is read from stdin."); - panic_test!(); - } - - let vm_filename = if args.len() == 3 { &args[2] } else { &args[3] }; - - let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&args[1]), - "Failed to parse contract identifier.", - ); - - let marf_kv = friendly_expect( - MarfedKV::open(vm_filename, None, None), - "Failed to open VM database.", - ); - - let content: String = { - if args.len() == 3 { - let mut buffer = String::new(); - friendly_expect( - io::stdin().read_to_string(&mut buffer), - "Error reading from stdin.", - ); - buffer - } else { - friendly_expect( - fs::read_to_string(&args[2]), - &format!("Error reading file: {}", args[2]), - ) - } - }; - - EvalInput { - marf_kv, - contract_identifier, - content, - } -} - -#[derive(Serialize, Deserialize)] -struct InitialAllocation { - principal: String, - amount: u64, -} - -fn consume_arg( - args: &mut Vec, - argnames: &[&str], - has_optarg: bool, -) -> Result, String> { - if let Some(ref switch) = args - .iter() - .find(|ref arg| argnames.iter().any(|ref argname| argname == arg)) - { - let idx = args - .iter() - .position(|ref arg| arg == switch) - .expect("BUG: did not find the thing that was just found"); - let argval = if has_optarg { - // following argument is the argument value - if idx + 1 < args.len() { - Some(args[idx + 1].clone()) - } else { - // invalid usage -- expected argument - return Err("Expected argument".to_string()); - } - } else { - // only care about presence of this option - Some("".to_string()) - }; - - args.remove(idx); - if has_optarg { - // also clear the argument - args.remove(idx); - } - Ok(argval) - } else { - // not found - Ok(None) - } -} - /// This function uses Clarity1 to parse the boot code. fn install_boot_code( header_db: &CLIHeadersDB, @@ -953,7 +828,7 @@ fn install_boot_code( .unwrap(); } Err(e) => { - panic!("failed to instantiate boot contract: {:?}", &e); + panic!("failed to instantiate boot contract: {e:?}"); } }; } @@ -1007,552 +882,587 @@ pub fn add_serialized_output(result: &mut serde_json::Value, value: Value) { result["output_serialized"] = serde_json::to_value(result_raw.as_str()).unwrap(); } -/// Parse --clarity_version flag. Defaults to version for epoch. -fn parse_clarity_version_flag(argv: &mut Vec, epoch: StacksEpochId) -> ClarityVersion { - if let Ok(Some(s)) = consume_arg(argv, &["--clarity_version"], true) { - friendly_expect( - s.parse::(), - &format!("Invalid clarity version: {s}"), - ) - } else { - ClarityVersion::default_for_epoch(epoch) - } -} +/// Initialize a local VM state database +pub fn execute_initialize( + db_name: &str, + mainnet: bool, + epoch: StacksEpochId, + allocations: Vec<(PrincipalData, u64)>, +) -> (i32, Option) { + debug!("Initialize {db_name}"); + + // Create database and MARF + let mut header_db = CLIHeadersDB::new(db_name, mainnet); + let mut marf_kv = friendly_expect( + MarfedKV::open(db_name, None, None), + "Failed to open VM database.", + ); -/// Parse --epoch flag. Defaults to DEFAULT_CLI_EPOCH. -fn parse_epoch_flag(argv: &mut Vec) -> StacksEpochId { - if let Ok(Some(s)) = consume_arg(argv, &["--epoch"], true) { - friendly_expect(s.parse::(), &format!("Invalid epoch: {s}")) - } else { - DEFAULT_CLI_EPOCH - } + // Install bootcode + let state = in_block(header_db, marf_kv, |header_db, mut marf| { + install_boot_code(&header_db, &mut marf, epoch); + (header_db, marf, ()) + }); + + header_db = state.0; + marf_kv = state.1; + + // Set initial balances + in_block(header_db, marf_kv, |header_db, mut kv| { + { + let mut db = kv.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); + db.begin(); + for (principal, amount) in allocations.iter() { + let balance = STXBalance::initial(*amount as u128); + let total_balance = balance.get_total_balance().unwrap(); + + let mut snapshot = db.get_stx_balance_snapshot_genesis(principal).unwrap(); + snapshot.set_balance(balance); + snapshot.save().unwrap(); + + println!("{principal} credited: {total_balance} uSTX"); + } + db.commit().unwrap(); + }; + (header_db, kv, ()) + }); + + ( + 0, + Some(json!({ + "message": "Database created.", + "network": if mainnet { "mainnet" } else { "testnet" } + })), + ) } -/// Returns (process-exit-code, Option) -pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option) { - if args.is_empty() { - print_usage(invoked_by); - return (1, None); - } +/// Generate a random Stacks address for testing purposes +pub fn execute_generate_address() -> (i32, Option) { + // Generate random 20 bytes + let random_bytes = rand::thread_rng().r#gen::<[u8; 20]>(); - match args[0].as_ref() { - "initialize" => { - let mut argv = args.to_vec(); - - let epoch = parse_epoch_flag(&mut argv); - let mainnet = !matches!(consume_arg(&mut argv, &["--testnet"], false), Ok(Some(_))); - - let (db_name, allocations) = if argv.len() == 3 { - let filename = &argv[1]; - let json_in = if filename == "-" { - let mut buffer = String::new(); - friendly_expect( - io::stdin().read_to_string(&mut buffer), - "Error reading from stdin.", - ); - buffer - } else { - friendly_expect( - fs::read_to_string(filename), - &format!("Error reading file: {filename}"), - ) - }; - let allocations: Vec = - friendly_expect(serde_json::from_str(&json_in), "Failure parsing JSON"); - - let allocations: Vec<_> = allocations - .into_iter() - .map(|a| { - ( - friendly_expect( - PrincipalData::parse(&a.principal), - "Failed to parse principal in JSON", - ), - a.amount, - ) - }) - .collect(); + // Version = 22 + let addr = friendly_expect(c32_address(22, &random_bytes), "Failed to generate address"); - (&argv[2], allocations) - } else if argv.len() == 2 { - (&argv[1], Vec::new()) - } else { - eprintln!( - "Usage: {invoked_by} {} [--testnet] [--epoch E] (initial-allocations.json) [vm-state.db]", - argv[0] - ); - eprintln!( - " initial-allocations.json is a JSON array of {{ principal: \"ST...\", amount: 100 }} like objects." - ); - eprintln!(" if the provided filename is `-`, the JSON is read from stdin."); + (0, Some(json!({ "address": format!("{addr}") }))) +} + +/// Typecheck a potential contract definition +#[allow(clippy::too_many_arguments)] +pub fn execute_check( + content: &str, + contract_id: &QualifiedContractIdentifier, + output_analysis: bool, + costs: bool, + mainnet: bool, + clarity_version: ClarityVersion, + epoch: StacksEpochId, + db_path: Option<&str>, + testnet_given: bool, +) -> (i32, Option) { + // Parse the contract + let mut ast = friendly_expect( + parse(contract_id, content, clarity_version, epoch), + "Failed to parse program", + ); + + // Run analysis (either with persisted DB or in-memory) + let contract_analysis_res = { + if let Some(vm_filename) = db_path { + // Warn if --testnet was given but we're using DB state + if testnet_given { eprintln!( - " If --testnet is given, then testnet bootcode and block-limits are used instead of mainnet." + "WARN: ignoring --testnet in favor of DB state in {vm_filename:?}. Re-instantiate the DB to change." ); - panic_test!(); - }; + } - debug!("Initialize {db_name}"); - let mut header_db = CLIHeadersDB::new(db_name, mainnet); - let mut marf_kv = friendly_expect( - MarfedKV::open(db_name, None, None), + // Use a persisted MARF + let header_db = + friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); + let marf_kv = friendly_expect( + MarfedKV::open(vm_filename, None, None), "Failed to open VM database.", ); - // install bootcode - let state = in_block(header_db, marf_kv, |header_db, mut marf| { - install_boot_code(&header_db, &mut marf, epoch); - (header_db, marf, ()) - }); - - header_db = state.0; - marf_kv = state.1; - - // set initial balances - in_block(header_db, marf_kv, |header_db, mut kv| { - { - let mut db = kv.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - db.begin(); - for (principal, amount) in allocations.iter() { - let balance = STXBalance::initial(*amount as u128); - let total_balance = balance.get_total_balance().unwrap(); - - let mut snapshot = db.get_stx_balance_snapshot_genesis(principal).unwrap(); - snapshot.set_balance(balance); - snapshot.save().unwrap(); - - println!("{principal} credited: {total_balance} uSTX"); - } - db.commit().unwrap(); - }; - (header_db, kv, ()) - }); + at_chaintip(vm_filename, marf_kv, |mut marf| { + let result = run_analysis( + contract_id, + &mut ast, + &header_db, + &mut marf, + false, + clarity_version, + epoch, + ); + (marf, result) + }) + } else { + // Use in-memory analysis + let header_db = CLIHeadersDB::new_memory(mainnet); + let mut analysis_marf = MemoryBackingStore::new(); - if mainnet { - ( - 0, - Some(json!({ - "message": "Database created.", - "network": "mainnet" - })), - ) - } else { - ( - 0, - Some(json!({ - "message": "Database created.", - "network": "testnet" - })), - ) - } + install_boot_code(&header_db, &mut analysis_marf, epoch); + run_analysis( + contract_id, + &mut ast, + &header_db, + &mut analysis_marf, + false, + clarity_version, + epoch, + ) } - "generate_address" => { - if args.len() != 1 { - eprintln!("Usage: {} {}", invoked_by, args[0]); - panic_test!(); - } - // random 20 bytes - let random_bytes = rand::thread_rng().r#gen::<[u8; 20]>(); - // version = 22 - let addr = - friendly_expect(c32_address(22, &random_bytes), "Failed to generate address"); + }; - (0, Some(json!({ "address": format!("{addr}") }))) + // Handle analysis result + let mut contract_analysis = match contract_analysis_res { + Ok(contract_analysis) => contract_analysis, + Err(boxed) => { + let (e, cost_tracker) = *boxed; + let mut result = json!({ + "message": "Checks failed.", + "error": { + "analysis": serde_json::to_value(&e.diagnostic).unwrap(), + }, + }); + add_costs(&mut result, costs, cost_tracker.get_total()); + return (1, Some(result)); } - "check" => { - if args.len() < 2 { - eprintln!( - "Usage: {invoked_by} {} [program-file.clar] [--contract_id CONTRACT_ID] [--output_analysis] [--costs] [--testnet] [--clarity_version N] [--epoch E] (vm-state.db)", - args[0] - ); - panic_test!(); - } - - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); - let contract_id = if let Ok(optarg) = consume_arg(&mut argv, &["--contract_id"], true) { - optarg - .map(|optarg_str| { - friendly_expect( - QualifiedContractIdentifier::parse(&optarg_str), - &format!("Error parsing contract identifier '{optarg_str}"), - ) - }) - .unwrap_or(QualifiedContractIdentifier::transient()) - } else { - eprintln!("Expected argument for --contract-id"); - panic_test!(); - }; + }; - let output_analysis = - if let Ok(optarg) = consume_arg(&mut argv, &["--output_analysis"], false) { - optarg.is_some() - } else { - eprintln!("BUG: failed to parse arguments for --output_analysis"); - panic_test!(); - }; + // Build success result + let mut result = json!({ + "message": "Checks passed." + }); - let costs = matches!(consume_arg(&mut argv, &["--costs"], false), Ok(Some(_))); + add_costs( + &mut result, + costs, + contract_analysis.take_contract_cost_tracker().get_total(), + ); - // NOTE: ignored if we're using a DB - let mut testnet_given = false; - let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { - testnet_given = true; - false - } else { - true - }; - - let content: String = if &argv[1] == "-" { - let mut buffer = String::new(); - friendly_expect( - io::stdin().read_to_string(&mut buffer), - "Error reading from stdin.", - ); - buffer - } else { - friendly_expect( - fs::read_to_string(&argv[1]), - &format!("Error reading file: {}", argv[1]), - ) - }; + if output_analysis { + result["analysis"] = + serde_json::to_value(build_contract_interface(&contract_analysis).unwrap()).unwrap(); + } - let mut ast = friendly_expect( - parse(&contract_id, &content, clarity_version, epoch), - "Failed to parse program", - ); + (0, Some(result)) +} - let contract_analysis_res = { - if argv.len() >= 3 { - // use a persisted marf - if testnet_given { - eprintln!( - "WARN: ignoring --testnet in favor of DB state in {:?}. Re-instantiate the DB to change.", - &argv[2] - ); - } +/// Typecheck and evaluate expressions in a stdin/stdout REPL loop +pub fn execute_repl( + mainnet: bool, + epoch: StacksEpochId, + clarity_version: ClarityVersion, +) -> (i32, Option) { + let mut marf = MemoryBackingStore::new(); + let mut vm_env = OwnedEnvironment::new_free( + mainnet, + default_chain_id(mainnet), + marf.as_clarity_db(), + epoch, + ); + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); + let mut exec_env = vm_env.get_exec_environment(None, None, &placeholder_context); + let mut analysis_marf = MemoryBackingStore::new(); - let vm_filename = &argv[2]; - let header_db = - friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); - let marf_kv = friendly_expect( - MarfedKV::open(vm_filename, None, None), - "Failed to open VM database.", - ); - - at_chaintip(&argv[2], marf_kv, |mut marf| { - let result = run_analysis( - &contract_id, - &mut ast, - &header_db, - &mut marf, - false, - clarity_version, - epoch, - ); - (marf, result) - }) - } else { - let header_db = CLIHeadersDB::new_memory(mainnet); - let mut analysis_marf = MemoryBackingStore::new(); - - install_boot_code(&header_db, &mut analysis_marf, epoch); - run_analysis( - &contract_id, - &mut ast, - &header_db, - &mut analysis_marf, - false, - clarity_version, - epoch, - ) - } - }; + let contract_id = QualifiedContractIdentifier::transient(); - let mut contract_analysis = match contract_analysis_res { - Ok(contract_analysis) => contract_analysis, - Err(boxed) => { - let (e, cost_tracker) = *boxed; - let mut result = json!({ - "message": "Checks failed.", - "error": { - "analysis": serde_json::to_value(&e.diagnostic).unwrap(), - }, - }); - add_costs(&mut result, costs, cost_tracker.get_total()); - return (1, Some(result)); - } - }; + let mut stdout = io::stdout(); - let mut result = json!({ - "message": "Checks passed." + loop { + // Read input line + let content: String = { + let mut buffer = String::new(); + stdout.write_all(b"> ").unwrap_or_else(|e| { + panic!("Failed to write stdout prompt string:\n{e}"); + }); + stdout.flush().unwrap_or_else(|e| { + panic!("Failed to flush stdout prompt string:\n{e}"); }); + match io::stdin().read_line(&mut buffer) { + Ok(_) => buffer, + Err(error) => { + eprintln!("Error reading from stdin:\n{error}"); + panic_test!(); + } + } + }; - add_costs( - &mut result, - costs, - contract_analysis.take_contract_cost_tracker().get_total(), - ); + // Parse the expression + let mut ast = match parse(&contract_id, &content, clarity_version, epoch) { + Ok(val) => val, + Err(error) => { + println!("Parse error:\n{error}"); + continue; + } + }; - if output_analysis { - result["analysis"] = - serde_json::to_value(build_contract_interface(&contract_analysis).unwrap()) - .unwrap(); + // Type-check the expression + match run_analysis_free( + &contract_id, + &mut ast, + &mut analysis_marf, + true, + clarity_version, + epoch, + ) { + Ok(_) => (), + Err(boxed) => { + let (error, _) = *boxed; + println!("Type check error:\n{error}"); + continue; } - (0, Some(result)) } - "repl" => { - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); - let mainnet = !matches!(consume_arg(&mut argv, &["--testnet"], false), Ok(Some(_))); - if argv.len() != 1 { - eprintln!( - "Usage: {} {} [--testnet] [--epoch E] [--clarity_version N]", - invoked_by, args[0] - ); - panic_test!(); + // Evaluate the expression + let eval_result = match exec_env.eval_raw(&content) { + Ok(val) => val, + Err(error) => { + println!("Execution error:\n{error}"); + continue; } + }; - let mut marf = MemoryBackingStore::new(); - let mut vm_env = OwnedEnvironment::new_free( - mainnet, - default_chain_id(mainnet), - marf.as_clarity_db(), - epoch, - ); - let placeholder_context = - ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - let mut exec_env = vm_env.get_exec_environment(None, None, &placeholder_context); - let mut analysis_marf = MemoryBackingStore::new(); + println!("{eval_result}"); + } +} - let contract_id = QualifiedContractIdentifier::transient(); +/// Typecheck and evaluate an expression without a contract or database context. +pub fn execute_eval_raw( + content: &str, + mainnet: bool, + epoch: StacksEpochId, + clarity_version: ClarityVersion, +) -> (i32, Option) { + let mut analysis_marf = MemoryBackingStore::new(); + let mut marf = MemoryBackingStore::new(); + let mut vm_env = OwnedEnvironment::new_free( + mainnet, + default_chain_id(mainnet), + marf.as_clarity_db(), + epoch, + ); - let mut stdout = io::stdout(); + let contract_id = QualifiedContractIdentifier::transient(); + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - loop { - let content: String = { - let mut buffer = String::new(); - stdout.write_all(b"> ").unwrap_or_else(|e| { - panic!("Failed to write stdout prompt string:\n{e}"); - }); - stdout.flush().unwrap_or_else(|e| { - panic!("Failed to flush stdout prompt string:\n{e}"); - }); - match io::stdin().read_line(&mut buffer) { - Ok(_) => buffer, - Err(error) => { - eprintln!("Error reading from stdin:\n{error}"); - panic_test!(); + // Parse the expression + let mut ast = friendly_expect( + parse(&contract_id, content, clarity_version, epoch), + "Failed to parse program.", + ); + + // Type-check and evaluate + match run_analysis_free( + &contract_id, + &mut ast, + &mut analysis_marf, + true, + clarity_version, + epoch, + ) { + Ok(_) => { + // Analysis passed, now evaluate + let result = vm_env + .get_exec_environment(None, None, &placeholder_context) + .eval_raw(content); + match result { + Ok(x) => ( + 0, + Some(json!({ + "output": serde_json::to_value(&x).unwrap() + })), + ), + Err(error) => ( + 1, + Some(json!({ + "error": { + "runtime": serde_json::to_value(format!("{error}")).unwrap() } + })), + ), + } + } + Err(boxed) => { + let (error, _) = *boxed; + ( + 1, + Some(json!({ + "error": { + "analysis": serde_json::to_value(format!("{error}")).unwrap() } - }; + })), + ) + } + } +} - let mut ast = match parse(&contract_id, &content, clarity_version, epoch) { - Ok(val) => val, - Err(error) => { - println!("Parse error:\n{error}"); - continue; - } - }; +/// Evaluate (in read-only mode) a program in a given contract context +/// This advances to a new block before evaluation. +pub fn execute_eval( + contract_identifier: &QualifiedContractIdentifier, + content: &str, + costs: bool, + epoch: StacksEpochId, + clarity_version: ClarityVersion, + vm_filename: &str, +) -> (i32, Option) { + // Open database + let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); + let marf_kv = friendly_expect( + MarfedKV::open(vm_filename, None, None), + "Failed to open VM database.", + ); + let mainnet = header_db.is_mainnet(); + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - match run_analysis_free( - &contract_id, - &mut ast, - &mut analysis_marf, - true, - clarity_version, - epoch, - ) { - Ok(_) => (), - Err(boxed) => { - let (error, _) = *boxed; - println!("Type check error:\n{error}"); - continue; - } - } + // Evaluate in a new block + let (_, _, result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { + let result_and_cost = + with_env_costs(mainnet, epoch, &header_db, &mut marf, None, |vm_env| { + vm_env + .get_exec_environment(None, None, &placeholder_context) + .eval_read_only(contract_identifier, content) + }); + (header_db, marf, result_and_cost) + }); + + // Return success or error with costs + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap(), + "success": true, + }); - let eval_result = match exec_env.eval_raw(&content) { - Ok(val) => val, - Err(error) => { - println!("Execution error:\n{error}"); - continue; - } - }; + add_serialized_output(&mut result_json, result); + add_costs(&mut result_json, costs, cost); - println!("{eval_result}"); - } + (0, Some(result_json)) } - "eval_raw" => { - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); - - if argv.len() != 1 { - eprintln!( - "Usage: {} {} [--epoch E] [--clarity_version N]", - invoked_by, args[0] - ); - eprintln!(" Examples:"); - eprintln!(" echo \"(+ 1 2)\" | {} {}", invoked_by, args[0]); - eprintln!(" {} {} < input.clar", invoked_by, args[0]); - panic_test!(); - } + (Err(error), cost) => { + let mut result_json = json!({ + "error": { + "runtime": serde_json::to_value(format!("{error}")).unwrap() + }, + "success": false, + }); - let content: String = { - let mut buffer = String::new(); - friendly_expect( - io::stdin().read_to_string(&mut buffer), - "Error reading from stdin.", - ); - buffer - }; + add_costs(&mut result_json, costs, cost); - let mut analysis_marf = MemoryBackingStore::new(); - let mut marf = MemoryBackingStore::new(); - let mut vm_env = OwnedEnvironment::new_free( - true, - default_chain_id(true), - marf.as_clarity_db(), - epoch, - ); + (1, Some(result_json)) + } + } +} - let contract_id = QualifiedContractIdentifier::transient(); - let placeholder_context = - ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); +/// Like eval, but does not advance to a new block +/// Evaluates at the current chaintip without advancing the block height. +pub fn execute_eval_at_chaintip( + contract_identifier: &QualifiedContractIdentifier, + content: &str, + costs: bool, + epoch: StacksEpochId, + clarity_version: ClarityVersion, + vm_filename: &str, + coverage_folder: Option, +) -> (i32, Option) { + // Open database + let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); + let marf_kv = friendly_expect( + MarfedKV::open(vm_filename, None, None), + "Failed to open VM database.", + ); - let mut ast = friendly_expect( - parse(&contract_id, &content, clarity_version, epoch), - "Failed to parse program.", - ); - match run_analysis_free( - &contract_id, - &mut ast, - &mut analysis_marf, - true, - clarity_version, - epoch, - ) { - Ok(_) => { - let result = vm_env - .get_exec_environment(None, None, &placeholder_context) - .eval_raw(&content); - match result { - Ok(x) => ( - 0, - Some(json!({ - "output": serde_json::to_value(&x).unwrap() - })), - ), - Err(error) => ( - 1, - Some(json!({ - "error": { - "runtime": serde_json::to_value(format!("{error}")).unwrap() - } - })), - ), - } - } - Err(boxed) => { - let (error, _) = *boxed; - ( - 1, - Some(json!({ - "error": { - "analysis": serde_json::to_value(format!("{error}")).unwrap() - } - })), - ) - } - } - } - "eval" => { - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); + let mainnet = header_db.is_mainnet(); + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); + let mut coverage = if coverage_folder.is_some() { + Some(CoverageReporter::new()) + } else { + None + }; - let costs = matches!(consume_arg(&mut argv, &["--costs"], false), Ok(Some(_))); + // Evaluate at chaintip (no block advance) + let result_and_cost = at_chaintip(vm_filename, marf_kv, |mut marf| { + let result_and_cost = with_env_costs( + mainnet, + epoch, + &header_db, + &mut marf, + coverage.as_mut(), + |vm_env| { + vm_env + .get_exec_environment(None, None, &placeholder_context) + .eval_read_only(contract_identifier, content) + }, + ); + let (result, cost) = result_and_cost; + + (marf, (result, cost)) + }); + + // Return success or error with costs + match result_and_cost { + (Ok(result), cost) => { + save_coverage(coverage_folder, coverage, "eval"); + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap(), + "success": true, + }); - let eval_input = get_eval_input(invoked_by, &argv); - let vm_filename = if argv.len() == 3 { &argv[2] } else { &argv[3] }; - let header_db = - friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); - let marf_kv = friendly_expect( - MarfedKV::open(vm_filename, None, None), - "Failed to open VM database.", - ); - let mainnet = header_db.is_mainnet(); - let placeholder_context = - ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - - let (_, _, result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { - let result_and_cost = - with_env_costs(mainnet, epoch, &header_db, &mut marf, None, |vm_env| { - vm_env - .get_exec_environment(None, None, &placeholder_context) - .eval_read_only(&eval_input.contract_identifier, &eval_input.content) - }); - (header_db, marf, result_and_cost) + add_serialized_output(&mut result_json, result); + add_costs(&mut result_json, costs, cost); + + (0, Some(result_json)) + } + (Err(error), cost) => { + save_coverage(coverage_folder, coverage, "eval"); + let mut result_json = json!({ + "error": { + "runtime": serde_json::to_value(format!("{error}")).unwrap() + }, + "success": false, }); - match result_and_cost { - (Ok(result), cost) => { - let mut result_json = json!({ - "output": serde_json::to_value(&result).unwrap(), - "success": true, - }); + add_costs(&mut result_json, costs, cost); + + (1, Some(result_json)) + } + } +} - add_serialized_output(&mut result_json, result); - add_costs(&mut result_json, costs, cost); +/// Like eval-at-chaintip, but accepts an index-block-hash to evaluate at +/// Evaluates at a specific block height identified by the index block hash. +/// Reads code from stdin. +pub fn execute_eval_at_block( + chain_tip: &str, + contract_identifier: &QualifiedContractIdentifier, + content: &str, + costs: bool, + epoch: StacksEpochId, + clarity_version: ClarityVersion, + vm_filename: &str, +) -> (i32, Option) { + let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); + let marf_kv = friendly_expect( + MarfedKV::open(vm_filename, None, None), + "Failed to open VM database.", + ); + let mainnet = header_db.is_mainnet(); + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - (0, Some(result_json)) - } - (Err(error), cost) => { - let mut result_json = json!({ - "error": { - "runtime": serde_json::to_value(format!("{error}")).unwrap() - }, - "success": false, - }); + // Evaluate at specific block + let result_and_cost = at_block(chain_tip, marf_kv, |mut marf| { + let result_and_cost = + with_env_costs(mainnet, epoch, &header_db, &mut marf, None, |vm_env| { + vm_env + .get_exec_environment(None, None, &placeholder_context) + .eval_read_only(contract_identifier, content) + }); + (marf, result_and_cost) + }); + + // Return success or error with costs + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap(), + "success": true, + }); - add_costs(&mut result_json, costs, cost); + add_serialized_output(&mut result_json, result); + add_costs(&mut result_json, costs, cost); - (1, Some(result_json)) - } - } + (0, Some(result_json)) } - "eval_at_chaintip" => { - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); + (Err(error), cost) => { + let mut result_json = json!({ + "error": { + "runtime": serde_json::to_value(format!("{error}")).unwrap() + }, + "success": false, + }); - let costs = matches!(consume_arg(&mut argv, &["--costs"], false), Ok(Some(_))); - let coverage_folder = consume_arg(&mut argv, &["--c"], true).unwrap_or(None); + add_costs(&mut result_json, costs, cost); - let eval_input = get_eval_input(invoked_by, &argv); - let vm_filename = if argv.len() == 3 { - &argv[2].clone() - } else { - &argv[3].clone() - }; - let header_db = - friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); - let marf_kv = friendly_expect( - MarfedKV::open(vm_filename, None, None), - "Failed to open VM database.", - ); + (1, Some(result_json)) + } + } +} - let mainnet = header_db.is_mainnet(); - let placeholder_context = - ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - let mut coverage = if coverage_folder.is_some() { - Some(CoverageReporter::new()) - } else { - None - }; - let result_and_cost = at_chaintip(vm_filename, marf_kv, |mut marf| { +/// Initialize a new contract in the local state database +/// Parses, analyzes, and initializes a contract in the database. +#[allow(clippy::too_many_arguments)] +pub fn execute_launch( + contract_identifier: &QualifiedContractIdentifier, + contract_src_file: &str, + contract_content: &str, + costs: bool, + assets: bool, + output_analysis: bool, + epoch: StacksEpochId, + clarity_version: ClarityVersion, + vm_filename: &str, + coverage_folder: Option, +) -> (i32, Option) { + // Parse the contract + let mut ast = friendly_expect( + parse( + contract_identifier, + contract_content, + clarity_version, + epoch, + ), + "Failed to parse program.", + ); + + // Register coverage if requested + if let Some(ref coverage_folder) = coverage_folder { + let mut coverage_file = PathBuf::from(coverage_folder); + coverage_file.push(format!("launch_{}", get_epoch_time_ms())); + coverage_file.set_extension("clarcovref"); + CoverageReporter::register_src_file( + contract_identifier, + contract_src_file, + &ast, + &coverage_file, + ) + .expect("Coverage reference file generation failure"); + } + + // Open database + let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); + let marf_kv = friendly_expect( + MarfedKV::open(vm_filename, None, None), + "Failed to open VM database.", + ); + let mainnet = header_db.is_mainnet(); + + let mut coverage = if coverage_folder.is_some() { + Some(CoverageReporter::new()) + } else { + None + }; + + // Run analysis and initialize contract in a new block + let (_, _, analysis_result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { + let analysis_result = run_analysis( + contract_identifier, + &mut ast, + &header_db, + &mut marf, + true, + clarity_version, + epoch, + ); + match analysis_result { + Err(e) => (header_db, marf, Err(e)), + Ok(analysis) => { let result_and_cost = with_env_costs( mainnet, epoch, @@ -1560,241 +1470,134 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { - save_coverage(coverage_folder, coverage, "eval"); - let mut result_json = json!({ - "output": serde_json::to_value(&result).unwrap(), - "success": true, - }); - - add_serialized_output(&mut result_json, result); - add_costs(&mut result_json, costs, cost); - - (0, Some(result_json)) - } - (Err(error), cost) => { - save_coverage(coverage_folder, coverage, "eval"); - let mut result_json = json!({ - "error": { - "runtime": serde_json::to_value(format!("{error}")).unwrap() - }, - "success": false, - }); - - add_costs(&mut result_json, costs, cost); - - (1, Some(result_json)) - } + (header_db, marf, Ok((analysis, (result, cost)))) } } - "eval_at_block" => { - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); - - let costs = matches!(consume_arg(&mut argv, &["--costs"], false), Ok(Some(_))); + }); - if argv.len() != 4 { - eprintln!( - "Usage: {invoked_by} {} [--costs] [--epoch E] [index-block-hash] [contract-identifier] [--clarity_version N] [vm/clarity dir]", - &argv[0] - ); - panic_test!(); - } - let chain_tip = &argv[1].clone(); - let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&argv[2]), - "Failed to parse contract identifier.", - ); - let content: String = { - let mut buffer = String::new(); - friendly_expect( - io::stdin().read_to_string(&mut buffer), - "Error reading from stdin.", - ); - buffer - }; - - let vm_filename = &argv[3]; - let header_db = - friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); - let marf_kv = friendly_expect( - MarfedKV::open(vm_filename, None, None), - "Failed to open VM database.", - ); - let mainnet = header_db.is_mainnet(); - let placeholder_context = - ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - let result_and_cost = at_block(chain_tip, marf_kv, |mut marf| { - let result_and_cost = - with_env_costs(mainnet, epoch, &header_db, &mut marf, None, |vm_env| { - vm_env - .get_exec_environment(None, None, &placeholder_context) - .eval_read_only(&contract_identifier, &content) - }); - (marf, result_and_cost) + // Return success or error with costs, assets, analysis, and events + match analysis_result_and_cost { + Ok((contract_analysis, (Ok((_x, asset_map, events)), cost))) => { + let mut result = json!({ + "message": "Contract initialized!" }); - match result_and_cost { - (Ok(result), cost) => { - let mut result_json = json!({ - "output": serde_json::to_value(&result).unwrap(), - "success": true, - }); - - add_serialized_output(&mut result_json, result); - add_costs(&mut result_json, costs, cost); - - (0, Some(result_json)) - } - (Err(error), cost) => { - let mut result_json = json!({ - "error": { - "runtime": serde_json::to_value(format!("{error}")).unwrap() - }, - "success": false, - }); + add_costs(&mut result, costs, cost); + add_assets(&mut result, assets, asset_map); - add_costs(&mut result_json, costs, cost); + save_coverage(coverage_folder, coverage, "launch"); - (1, Some(result_json)) - } + if output_analysis { + result["analysis"] = + serde_json::to_value(build_contract_interface(&contract_analysis).unwrap()) + .unwrap(); } + let events_json: Vec<_> = events + .into_iter() + .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true).unwrap()) + .collect(); + + result["events"] = serde_json::Value::Array(events_json); + (0, Some(result)) } - "launch" => { - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); - let coverage_folder = consume_arg(&mut argv, &["--c"], true).unwrap_or(None); - - let costs = matches!(consume_arg(&mut argv, &["--costs"], false), Ok(Some(_))); - let assets = matches!(consume_arg(&mut argv, &["--assets"], false), Ok(Some(_))); - let output_analysis = matches!( - consume_arg(&mut argv, &["--output_analysis"], false), - Ok(Some(_)) - ); + Err(boxed) => { + let (error, cost_tracker) = *boxed; + let mut result = json!({ + "error": { + "initialization": serde_json::to_value(format!("{error}")).unwrap() + } + }); - if argv.len() < 4 { - eprintln!( - "Usage: {invoked_by} {} [--costs] [--assets] [--output_analysis] [contract-identifier] [contract-definition.clar] [--clarity_version N] [--epoch E] [vm-state.db]", - argv[0] - ); - panic_test!(); - } + add_costs(&mut result, costs, cost_tracker.get_total()); - let vm_filename = &argv[3].clone(); - let contract_src_file = &args[2]; - let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&argv[1]), - "Failed to parse contract identifier.", - ); + (1, Some(result)) + } + Ok((_, (Err(error), ..))) => ( + 1, + Some(json!({ + "error": { + "initialization": serde_json::to_value(format!("{error}")).unwrap() + } + })), + ), + } +} - let contract_content: String = friendly_expect( - fs::read_to_string(contract_src_file), - &format!("Error reading file: {contract_src_file}"), - ); +/// Execute a public function of a defined contract +/// Executes a public function on an initialized contract. +#[allow(clippy::too_many_arguments)] +pub fn execute_execute( + vm_filename: &str, + contract_identifier: &QualifiedContractIdentifier, + tx_name: &str, + sender: PrincipalData, + arguments: &[SymbolicExpression], + costs: bool, + assets: bool, + epoch: StacksEpochId, + coverage_folder: Option, +) -> (i32, Option) { + // Open database + let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); + let marf_kv = friendly_expect( + MarfedKV::open(vm_filename, None, None), + "Failed to open VM database.", + ); + let mainnet = header_db.is_mainnet(); - let mut ast = friendly_expect( - parse( - &contract_identifier, - &contract_content, - clarity_version, - epoch, - ), - "Failed to parse program.", - ); + let mut coverage = if coverage_folder.is_some() { + Some(CoverageReporter::new()) + } else { + None + }; - if let Some(ref coverage_folder) = coverage_folder { - let mut coverage_file = PathBuf::from(coverage_folder); - coverage_file.push(format!("launch_{}", get_epoch_time_ms())); - coverage_file.set_extension("clarcovref"); - CoverageReporter::register_src_file( - &contract_identifier, - contract_src_file, - &ast, - &coverage_file, + // Execute transaction in a new block + let (_, _, result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { + let result_and_cost = with_env_costs( + mainnet, + epoch, + &header_db, + &mut marf, + coverage.as_mut(), + |vm_env| { + vm_env.execute_transaction( + sender, + None, + contract_identifier.clone(), + tx_name, + arguments, ) - .expect("Coverage reference file generation failure"); - } - - // let header_db = CLIHeadersDB::new(vm_filename, false); - - let header_db = - friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); - let marf_kv = friendly_expect( - MarfedKV::open(vm_filename, None, None), - "Failed to open VM database.", - ); - let mainnet = header_db.is_mainnet(); - - let mut coverage = if coverage_folder.is_some() { - Some(CoverageReporter::new()) - } else { - None - }; - let (_, _, analysis_result_and_cost) = - in_block(header_db, marf_kv, |header_db, mut marf| { - let analysis_result = run_analysis( - &contract_identifier, - &mut ast, - &header_db, - &mut marf, - true, - clarity_version, - epoch, - ); - match analysis_result { - Err(e) => (header_db, marf, Err(e)), - Ok(analysis) => { - let result_and_cost = with_env_costs( - mainnet, - epoch, - &header_db, - &mut marf, - coverage.as_mut(), - |vm_env| { - vm_env.initialize_versioned_contract( - contract_identifier, - clarity_version, - &contract_content, - None, - ) - }, - ); - let (result, cost) = result_and_cost; - (header_db, marf, Ok((analysis, (result, cost)))) - } - } - }); - - match analysis_result_and_cost { - Ok((contract_analysis, (Ok((_x, asset_map, events)), cost))) => { + }, + ); + let (result, cost) = result_and_cost; + (header_db, marf, (result, cost)) + }); + + // Return success or error with costs, assets, and events + match result_and_cost { + (Ok((x, asset_map, events)), cost) => { + if let Value::Response(data) = x { + save_coverage(coverage_folder, coverage, "execute"); + if data.committed { let mut result = json!({ - "message": "Contract initialized!" + "message": "Transaction executed and committed.", + "output": serde_json::to_value(&data.data).unwrap(), + "success": true, }); + add_serialized_output(&mut result, *data.data); add_costs(&mut result, costs, cost); add_assets(&mut result, assets, asset_map); - save_coverage(coverage_folder, coverage, "launch"); - - if output_analysis { - result["analysis"] = serde_json::to_value( - build_contract_interface(&contract_analysis).unwrap(), - ) - .unwrap(); - } let events_json: Vec<_> = events .into_iter() .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true).unwrap()) @@ -1802,175 +1605,39 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { - let (error, cost_tracker) = *boxed; + } else { let mut result = json!({ - "error": { - "initialization": serde_json::to_value(format!("{error}")).unwrap() - } + "message": "Aborted.", + "output": serde_json::to_value(&data.data).unwrap(), + "success": false, }); - add_costs(&mut result, costs, cost_tracker.get_total()); - - (1, Some(result)) - } - Ok((_, (Err(error), ..))) => ( - 1, - Some(json!({ - "error": { - "initialization": serde_json::to_value(format!("{error}")).unwrap() - } - })), - ), - } - } - "execute" => { - let mut argv = args.to_vec(); - let epoch = parse_epoch_flag(&mut argv); - let clarity_version = parse_clarity_version_flag(&mut argv, epoch); - let coverage_folder = consume_arg(&mut argv, &["--c"], true).unwrap_or(None); - - let costs = matches!(consume_arg(&mut argv, &["--costs"], false), Ok(Some(_))); - let assets = matches!(consume_arg(&mut argv, &["--assets"], false), Ok(Some(_))); - - if argv.len() < 5 { - eprintln!( - "Usage: {invoked_by} {} [--costs] [--assets] [--clarity_version N] [--epoch E] [vm-state.db] [contract-identifier] [public-function-name] [sender-address] [args...]", - argv[0] - ); - panic_test!(); - } - - let vm_filename = &argv[1]; - let header_db = - friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); - let marf_kv = friendly_expect( - MarfedKV::open(vm_filename, None, None), - "Failed to open VM database.", - ); - let mainnet = header_db.is_mainnet(); - let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&argv[2]), - "Failed to parse contract identifier.", - ); - - let tx_name = &argv[3]; - let sender_in = &argv[4]; + add_costs(&mut result, costs, cost); + add_serialized_output(&mut result, *data.data); + add_assets(&mut result, assets, asset_map); - let sender = { - if let Ok(sender) = PrincipalData::parse_standard_principal(sender_in) { - PrincipalData::Standard(sender) - } else { - eprintln!("Unexpected result parsing sender: {sender_in}"); - panic_test!(); + (0, Some(result)) } - }; - - let arguments: Vec<_> = argv[5..] - .iter() - .map(|argument| { - let argument_parsed = friendly_expect( - vm_execute_in_epoch(argument, clarity_version, epoch), - &format!("Error parsing argument \"{argument}\""), - ); - let argument_value = friendly_expect_opt( - argument_parsed, - &format!("Failed to parse a value from the argument: {argument}"), - ); - SymbolicExpression::atom_value(argument_value) - }) - .collect(); - - let mut coverage = if coverage_folder.is_some() { - Some(CoverageReporter::new()) } else { - None - }; - let (_, _, result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { - let result_and_cost = with_env_costs( - mainnet, - epoch, - &header_db, - &mut marf, - coverage.as_mut(), - |vm_env| { - vm_env.execute_transaction( - sender, - None, - contract_identifier, - tx_name, - &arguments, - ) + let result = json!({ + "error": { + "runtime": "Expected a ResponseType result from transaction.", + "output": serde_json::to_value(&x).unwrap() }, - ); - let (result, cost) = result_and_cost; - (header_db, marf, (result, cost)) - }); - - match result_and_cost { - (Ok((x, asset_map, events)), cost) => { - if let Value::Response(data) = x { - save_coverage(coverage_folder, coverage, "execute"); - if data.committed { - let mut result = json!({ - "message": "Transaction executed and committed.", - "output": serde_json::to_value(&data.data).unwrap(), - "success": true, - }); - - add_serialized_output(&mut result, *data.data); - add_costs(&mut result, costs, cost); - add_assets(&mut result, assets, asset_map); - - let events_json: Vec<_> = events - .into_iter() - .map(|event| { - event.json_serialize(0, &Txid([0u8; 32]), true).unwrap() - }) - .collect(); - - result["events"] = serde_json::Value::Array(events_json); - (0, Some(result)) - } else { - let mut result = json!({ - "message": "Aborted.", - "output": serde_json::to_value(&data.data).unwrap(), - "success": false, - }); - - add_costs(&mut result, costs, cost); - add_serialized_output(&mut result, *data.data); - add_assets(&mut result, assets, asset_map); - - (0, Some(result)) - } - } else { - let result = json!({ - "error": { - "runtime": "Expected a ResponseType result from transaction.", - "output": serde_json::to_value(&x).unwrap() - }, - "success": false, - }); - (1, Some(result)) - } - } - (Err(error), ..) => { - let result = json!({ - "error": { - "runtime": "Transaction execution error.", - "error": serde_json::to_value(format!("{error}")).unwrap() - }, - "success": false, - }); - (1, Some(result)) - } + "success": false, + }); + (1, Some(result)) } } - _ => { - print_usage(invoked_by); - (1, None) + (Err(error), ..) => { + let result = json!({ + "error": { + "runtime": "Transaction execution error.", + "error": serde_json::to_value(format!("{error}")).unwrap() + }, + "success": false, + }); + (1, Some(result)) } } } @@ -2000,32 +1667,36 @@ mod test { ) .unwrap(); - fs::write(&clar_name, r#" + let contract_code = r#" (unwrap-panic (if (is-eq (stx-get-balance 'S1G2081040G2081040G2081040G208105NK8PE5) u1000) (ok 1) (err 2))) (unwrap-panic (if (is-eq (stx-get-balance 'S1G2081040G2081040G2081040G208105NK8PE5.names) u2000) (ok 1) (err 2))) -"#).unwrap(); +"#; + fs::write(&clar_name, contract_code).unwrap(); - let invoked = invoke_command( - "test", - &["initialize".to_string(), json_name, db_name.clone()], - ); - let exit = invoked.0; - let result = invoked.1.unwrap(); + let json_content = fs::read_to_string(&json_name).unwrap(); + let allocations = parse_allocations_json(&json_content).unwrap(); + + let (exit, result) = execute_initialize(&db_name, true, DEFAULT_CLI_EPOCH, allocations); assert_eq!(exit, 0); - assert_eq!(result["network"], "mainnet"); + assert_eq!(result.unwrap()["network"], "mainnet"); - let invoked = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - clar_name, - db_name, - ], + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .unwrap(); + + let (exit, _result) = execute_launch( + &contract_id, + &clar_name, + contract_code, + false, + false, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, + None, ); - let exit = invoked.0; - let _ = invoked.1.unwrap(); assert_eq!(exit, 0); } @@ -2033,13 +1704,16 @@ mod test { #[test] fn test_init_mainnet() { let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); - let invoked = invoke_command("test", &["initialize".to_string(), db_name.clone()]); - let exit = invoked.0; - let result = invoked.1.unwrap(); + let (exit, result) = execute_initialize( + &db_name, + true, + DEFAULT_CLI_EPOCH, + vec![], // no allocations + ); assert_eq!(exit, 0); - assert_eq!(result["network"], "mainnet"); + assert_eq!(result.unwrap()["network"], "mainnet"); let header_db = CLIHeadersDB::new(&db_name, true); assert!(header_db.is_mainnet()); @@ -2048,20 +1722,16 @@ mod test { #[test] fn test_init_testnet() { let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); - let invoked = invoke_command( - "test", - &[ - "initialize".to_string(), - "--testnet".to_string(), - db_name.clone(), - ], - ); - let exit = invoked.0; - let result = invoked.1.unwrap(); + let (exit, result) = execute_initialize( + &db_name, + false, // testnet + DEFAULT_CLI_EPOCH, + vec![], // no allocations + ); assert_eq!(exit, 0); - assert_eq!(result["network"], "testnet"); + assert_eq!(result.unwrap()["network"], "testnet"); let header_db = CLIHeadersDB::new(&db_name, true); assert!(!header_db.is_mainnet()); @@ -2079,183 +1749,228 @@ mod test { let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); eprintln!("initialize"); - invoke_command("test", &["initialize".to_string(), db_name.clone()]); + execute_initialize(&db_name, true, DEFAULT_CLI_EPOCH, vec![]); eprintln!("check tokens"); - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - cargo_workspace_as_string("sample/contracts/tokens.clar"), - ], + let content = fs::read_to_string(cargo_workspace_as_string("sample/contracts/tokens.clar")) + .expect("Failed to read tokens.clar"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit, result) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + None, // no db_path + false, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - assert_eq!(exit, 0); - assert!(!result["message"].as_str().unwrap().is_empty()); + assert!(!result.unwrap()["message"].as_str().unwrap().is_empty()); eprintln!("check tokens (idempotency)"); - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - cargo_workspace_as_string("sample/contracts/tokens.clar"), - db_name.clone(), - ], + let content = fs::read_to_string(cargo_workspace_as_string("sample/contracts/tokens.clar")) + .expect("Failed to read tokens.clar"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit, result) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + Some(&db_name), + false, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - assert_eq!(exit, 0); - assert!(!result["message"].as_str().unwrap().is_empty()); + assert!(!result.unwrap()["message"].as_str().unwrap().is_empty()); eprintln!("launch tokens"); - let invoked = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - cargo_workspace_as_string("sample/contracts/tokens.clar"), - db_name.clone(), - ], + let file_path = cargo_workspace_as_string("sample/contracts/tokens.clar"); + let content = fs::read_to_string(&file_path).expect("Failed to read tokens.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_launch( + &contract_id, + &file_path, + &content, + false, + false, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, + None, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - assert_eq!(exit, 0); - assert!(!result["message"].as_str().unwrap().is_empty()); + assert!(!result.unwrap()["message"].as_str().unwrap().is_empty()); eprintln!("check names"); - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - cargo_workspace_as_string("sample/contracts/names.clar"), - db_name.clone(), - ], + let content = fs::read_to_string(cargo_workspace_as_string("sample/contracts/names.clar")) + .expect("Failed to read names.clar"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit, result) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + Some(&db_name), + false, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - assert_eq!(exit, 0); - assert!(!result["message"].as_str().unwrap().is_empty()); + assert!(!result.unwrap()["message"].as_str().unwrap().is_empty()); eprintln!("check names with different contract ID"); - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - cargo_workspace_as_string("sample/contracts/names.clar"), - db_name.clone(), - "--contract_id".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - ], + let content = fs::read_to_string(cargo_workspace_as_string("sample/contracts/names.clar")) + .expect("Failed to read names.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + Some(&db_name), + false, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - assert_eq!(exit, 0); - assert!(!result["message"].as_str().unwrap().is_empty()); + assert!(!result.unwrap()["message"].as_str().unwrap().is_empty()); eprintln!("check names with analysis"); - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - "--output_analysis".to_string(), - cargo_workspace_as_string("sample/contracts/names.clar"), - db_name.clone(), - ], + let content = fs::read_to_string(cargo_workspace_as_string("sample/contracts/names.clar")) + .expect("Failed to read names.clar"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit, result) = execute_check( + &content, + &contract_id, + true, + false, + true, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + Some(&db_name), + false, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert!(!result["message"].as_str().unwrap().is_empty()); assert!(result["analysis"] != json!(null)); eprintln!("check names with cost"); - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - "--costs".to_string(), - cargo_workspace_as_string("sample/contracts/names.clar"), - db_name.clone(), - ], + let content = fs::read_to_string(cargo_workspace_as_string("sample/contracts/names.clar")) + .expect("Failed to read names.clar"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit, result) = execute_check( + &content, + &contract_id, + false, + true, + true, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + Some(&db_name), + false, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert!(!result["message"].as_str().unwrap().is_empty()); assert!(result["costs"] != json!(null)); assert!(result["assets"] == json!(null)); eprintln!("launch names with costs and assets"); - let invoked = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.names".to_string(), - cargo_workspace_as_string("sample/contracts/names.clar"), - "--costs".to_string(), - "--assets".to_string(), - db_name.clone(), - ], + let file_path = cargo_workspace_as_string("sample/contracts/names.clar"); + let content = fs::read_to_string(&file_path).expect("Failed to read names.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.names") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_launch( + &contract_id, + &file_path, + &content, + true, + true, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, + None, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert!(!result["message"].as_str().unwrap().is_empty()); assert!(result["costs"] != json!(null)); assert!(result["assets"] != json!(null)); eprintln!("execute tokens"); - let invoked = invoke_command( - "test", - &[ - "execute".to_string(), - db_name.clone(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - "mint!".to_string(), - "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR".to_string(), - "(+ u900 u100)".to_string(), - ], + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .expect("Failed to parse contract ID"); + let sender = + PrincipalData::parse_standard_principal("SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR") + .map(PrincipalData::Standard) + .expect("Failed to parse sender"); + let arg_parsed = vm_execute_in_epoch( + "(+ u900 u100)", + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + ) + .expect("Failed to parse argument") + .expect("Failed to get value from argument"); + let arguments = vec![SymbolicExpression::atom_value(arg_parsed)]; + let (exit, result) = execute_execute( + &db_name, + &contract_id, + "mint!", + sender, + &arguments, + false, + false, + DEFAULT_CLI_EPOCH, + None, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert!(!result["message"].as_str().unwrap().is_empty()); assert!(result["events"].as_array().unwrap().is_empty()); assert_eq!(result["output"], json!({"UInt": 1000})); eprintln!("eval tokens"); - let invoked = invoke_command( - "test", - &[ - "eval".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - cargo_workspace_as_string("sample/contracts/tokens-mint.clar"), - db_name.clone(), - ], + let snippet = fs::read_to_string(cargo_workspace_as_string( + "sample/contracts/tokens-mint.clar", + )) + .expect("Failed to read tokens-mint.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_eval( + &contract_id, + &snippet, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert_eq!( result["output"], @@ -2270,20 +1985,23 @@ mod test { ); eprintln!("eval tokens with cost"); - let invoked = invoke_command( - "test", - &[ - "eval".to_string(), - "--costs".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - cargo_workspace_as_string("sample/contracts/tokens-mint.clar"), - db_name.clone(), - ], + let snippet = fs::read_to_string(cargo_workspace_as_string( + "sample/contracts/tokens-mint.clar", + )) + .expect("Failed to read tokens-mint.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_eval( + &contract_id, + &snippet, + true, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert_eq!( result["output"], @@ -2299,19 +2017,24 @@ mod test { assert!(result["costs"] != json!(null)); eprintln!("eval_at_chaintip tokens"); - let invoked = invoke_command( - "test", - &[ - "eval_at_chaintip".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - cargo_workspace_as_string("sample/contracts/tokens-mint.clar"), - db_name.clone(), - ], + let snippet = fs::read_to_string(cargo_workspace_as_string( + "sample/contracts/tokens-mint.clar", + )) + .expect("Failed to read tokens-mint.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_eval_at_chaintip( + &contract_id, + &snippet, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, + None, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert_eq!( result["output"], @@ -2326,20 +2049,24 @@ mod test { ); eprintln!("eval_at_chaintip tokens with cost"); - let invoked = invoke_command( - "test", - &[ - "eval_at_chaintip".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), - cargo_workspace_as_string("sample/contracts/tokens-mint.clar"), - db_name, - "--costs".to_string(), - ], + let snippet = fs::read_to_string(cargo_workspace_as_string( + "sample/contracts/tokens-mint.clar", + )) + .expect("Failed to read tokens-mint.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_eval_at_chaintip( + &contract_id, + &snippet, + true, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, + None, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); assert_eq!(exit, 0); assert_eq!( result["output"], @@ -2360,38 +2087,48 @@ mod test { let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); eprintln!("initialize"); - invoke_command("test", &["initialize".to_string(), db_name.clone()]); + execute_initialize(&db_name, true, DEFAULT_CLI_EPOCH, vec![]); eprintln!("check tokens"); - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - cargo_workspace_as_string("sample/contracts/tokens-ft.clar"), - ], + let content = + fs::read_to_string(cargo_workspace_as_string("sample/contracts/tokens-ft.clar")) + .expect("Failed to read tokens-ft.clar"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit, result) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + DEFAULT_CLI_EPOCH, + None, + false, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - assert_eq!(exit, 0); - assert!(!result["message"].as_str().unwrap().is_empty()); + assert!(!result.unwrap()["message"].as_str().unwrap().is_empty()); eprintln!("launch tokens"); - let invoked = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft".to_string(), - cargo_workspace_as_string("sample/contracts/tokens-ft.clar"), - db_name, - "--assets".to_string(), - ], + let file_path = cargo_workspace_as_string("sample/contracts/tokens-ft.clar"); + let content = fs::read_to_string(&file_path).expect("Failed to read tokens-ft.clar"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft") + .expect("Failed to parse contract ID"); + let (exit, result) = execute_launch( + &contract_id, + &file_path, + &content, + false, + true, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, + None, ); - let exit = invoked.0; - let result = invoked.1.unwrap(); - + let result = result.unwrap(); eprintln!("{}", serde_json::to_string(&result).unwrap()); assert_eq!(exit, 0); @@ -2470,19 +2207,22 @@ mod test { .unwrap(); // Act - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - clar_path, - "--clarity_version".to_string(), - "clarity3".to_string(), - ], + let content = fs::read_to_string(&clar_path).expect("Failed to read test file"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit_code, result_json) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::Clarity3, + DEFAULT_CLI_EPOCH, + None, + false, ); // Assert - let exit_code = invoked.0; - let result_json = invoked.1.unwrap(); + let result_json = result_json.unwrap(); assert_eq!( exit_code, 0, "expected check to pass under Clarity 3, got: {}", @@ -2514,19 +2254,22 @@ mod test { .unwrap(); // Act - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - clar_path, - "--clarity_version".to_string(), - "clarity2".to_string(), - ], + let content = fs::read_to_string(&clar_path).expect("Failed to read test file"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit_code, result_json) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::Clarity2, + DEFAULT_CLI_EPOCH, + None, + false, ); // Assert - let exit_code = invoked.0; - let result_json = invoked.1.unwrap(); + let result_json = result_json.unwrap(); assert_eq!( exit_code, 1, "expected check to fail under Clarity 2, got: {}", @@ -2559,19 +2302,22 @@ mod test { .unwrap(); // Act - let invoked = invoke_command( - "test", - &[ - "check".to_string(), - clar_path, - "--epoch".to_string(), - "2.1".to_string(), - ], + let content = fs::read_to_string(&clar_path).expect("Failed to read test file"); + let contract_id = QualifiedContractIdentifier::transient(); + let (exit_code, result_json) = execute_check( + &content, + &contract_id, + false, + false, + true, + ClarityVersion::Clarity2, // Epoch 2.1 defaults to Clarity2 + StacksEpochId::Epoch21, + None, + false, ); // Assert - let exit_code = invoked.0; - let result_json = invoked.1.unwrap(); + let result_json = result_json.unwrap(); assert_eq!( exit_code, 1, "expected check to fail under Clarity 2, got: {}", @@ -2585,7 +2331,7 @@ mod test { fn test_launch_clarity3_contract_passes_with_clarity3_flag() { // Arrange let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); - invoke_command("test", &["initialize".to_string(), db_name.clone()]); + execute_initialize(&db_name, true, DEFAULT_CLI_EPOCH, vec![]); let clar_path = format!( "/tmp/version-flag-launch-c3-{}.clar", @@ -2607,21 +2353,25 @@ mod test { .unwrap(); // Act - let invoked = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tenure".to_string(), - clar_path, - db_name, - "--clarity_version".to_string(), - "clarity3".to_string(), - ], + let content = fs::read_to_string(&clar_path).expect("Failed to read test file"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tenure") + .expect("Failed to parse contract ID"); + let (exit_code, result_json) = execute_launch( + &contract_id, + &clar_path, + &content, + false, + false, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::Clarity3, + &db_name, + None, ); // Assert - let exit_code = invoked.0; - let result_json = invoked.1.unwrap(); + let result_json = result_json.unwrap(); assert_eq!( exit_code, 0, "expected launch to pass under Clarity 3, got: {}", @@ -2634,7 +2384,7 @@ mod test { fn test_launch_clarity3_contract_fails_with_clarity2_flag() { // Arrange let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); - invoke_command("test", &["initialize".to_string(), db_name.clone()]); + execute_initialize(&db_name, true, DEFAULT_CLI_EPOCH, vec![]); let clar_path = format!( "/tmp/version-flag-launch-c2-{}.clar", @@ -2656,21 +2406,25 @@ mod test { .unwrap(); // Act - let invoked = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tenure".to_string(), - clar_path, - db_name, - "--clarity_version".to_string(), - "clarity2".to_string(), - ], + let content = fs::read_to_string(&clar_path).expect("Failed to read test file"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tenure") + .expect("Failed to parse contract ID"); + let (exit_code, result_json) = execute_launch( + &contract_id, + &clar_path, + &content, + false, + false, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::Clarity2, + &db_name, + None, ); // Assert - let exit_code = invoked.0; - let result_json = invoked.1.unwrap(); + let result_json = result_json.unwrap(); assert_eq!( exit_code, 1, "expected launch to fail under Clarity 2, got: {}", @@ -2683,7 +2437,7 @@ mod test { fn test_eval_clarity3_contract_passes_with_clarity3_flag() { // Arrange let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); - invoke_command("test", &["initialize".to_string(), db_name.clone()]); + execute_initialize(&db_name, true, DEFAULT_CLI_EPOCH, vec![]); // Launch minimal contract at target for eval context. let launch_src = format!( @@ -2691,14 +2445,21 @@ mod test { rand::thread_rng().r#gen::() ); fs::write(&launch_src, "(define-read-only (dummy) true)").unwrap(); - let _ = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tenure".to_string(), - launch_src, - db_name.clone(), - ], + let launch_content = fs::read_to_string(&launch_src).expect("Failed to read launch file"); + let launch_contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tenure") + .expect("Failed to parse contract ID"); + let _ = execute_launch( + &launch_contract_id, + &launch_src, + &launch_content, + false, + false, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH), + &db_name, + None, ); // Use a Clarity3-only native expression. @@ -2709,21 +2470,21 @@ mod test { fs::write(&clar_path, "(get-tenure-info? time u1)").unwrap(); // Act - let invoked = invoke_command( - "test", - &[ - "eval".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tenure".to_string(), - clar_path, - db_name, - "--clarity_version".to_string(), - "clarity3".to_string(), - ], + let snippet = fs::read_to_string(&clar_path).expect("Failed to read eval file"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tenure") + .expect("Failed to parse contract ID"); + let (exit_code, result_json) = execute_eval( + &contract_id, + &snippet, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::Clarity3, + &db_name, ); // Assert - let exit_code = invoked.0; - let result_json = invoked.1.unwrap(); + let result_json = result_json.unwrap(); assert_eq!( exit_code, 0, "expected eval to pass under Clarity 3, got: {}", @@ -2736,7 +2497,7 @@ mod test { fn test_eval_clarity3_contract_fails_with_clarity2_flag() { // Arrange let db_name = format!("/tmp/db_{}", rand::thread_rng().r#gen::()); - invoke_command("test", &["initialize".to_string(), db_name.clone()]); + execute_initialize(&db_name, true, DEFAULT_CLI_EPOCH, vec![]); // Launch minimal contract at target for eval context. let launch_src = format!( @@ -2744,16 +2505,21 @@ mod test { rand::thread_rng().r#gen::() ); fs::write(&launch_src, "(define-read-only (dummy) true)").unwrap(); - let _ = invoke_command( - "test", - &[ - "launch".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tenure".to_string(), - launch_src, - db_name.clone(), - "--clarity_version".to_string(), - "clarity2".to_string(), - ], + let launch_content = fs::read_to_string(&launch_src).expect("Failed to read launch file"); + let launch_contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tenure") + .expect("Failed to parse contract ID"); + let _ = execute_launch( + &launch_contract_id, + &launch_src, + &launch_content, + false, + false, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::Clarity2, + &db_name, + None, ); // Use a Clarity3-only native expression. @@ -2764,21 +2530,21 @@ mod test { fs::write(&clar_path, "(get-tenure-info? time u1)").unwrap(); // Act - let invoked = invoke_command( - "test", - &[ - "eval".to_string(), - "S1G2081040G2081040G2081040G208105NK8PE5.tenure".to_string(), - clar_path, - db_name, - "--clarity_version".to_string(), - "clarity2".to_string(), - ], + let snippet = fs::read_to_string(&clar_path).expect("Failed to read eval file"); + let contract_id = + QualifiedContractIdentifier::parse("S1G2081040G2081040G2081040G208105NK8PE5.tenure") + .expect("Failed to parse contract ID"); + let (exit_code, result_json) = execute_eval( + &contract_id, + &snippet, + false, + DEFAULT_CLI_EPOCH, + ClarityVersion::Clarity2, + &db_name, ); // Assert - let exit_code = invoked.0; - let result_json = invoked.1.unwrap(); + let result_json = result_json.unwrap(); assert_eq!( exit_code, 1, "expected eval to fail under Clarity 2, got: {}", diff --git a/contrib/clarity-cli/src/main.rs b/contrib/clarity-cli/src/main.rs index f6d9d480c7..d9df2a3132 100644 --- a/contrib/clarity-cli/src/main.rs +++ b/contrib/clarity-cli/src/main.rs @@ -14,24 +14,734 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -extern crate serde_json; +use std::io::Read; +use std::path::PathBuf; +use std::{fs, io, process}; -use std::{env, process}; +use clap::{Parser, Subcommand}; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use clarity::vm::{ClarityVersion, SymbolicExpression}; +use clarity_cli::{ + DEFAULT_CLI_EPOCH, execute_check, execute_eval, execute_eval_at_block, + execute_eval_at_chaintip, execute_eval_raw, execute_execute, execute_generate_address, + execute_initialize, execute_launch, execute_repl, vm_execute_in_epoch, +}; +use stacks_common::types::StacksEpochId; -use clarity_cli::invoke_command; +/// Read content from a file path or stdin if path is "-" +fn read_file_or_stdin(path: &str) -> String { + if path == "-" { + let mut buffer = String::new(); + io::stdin() + .read_to_string(&mut buffer) + .expect("Error reading from stdin"); + buffer + } else { + fs::read_to_string(path).unwrap_or_else(|e| panic!("Error reading file {path}: {e}")) + } +} + +/// Read content from an optional file path, defaulting to stdin if None or "-" +fn read_optional_file_or_stdin(path: Option<&PathBuf>) -> String { + match path { + Some(p) => read_file_or_stdin(p.to_str().expect("Invalid UTF-8 in path")), + None => { + let mut buffer = String::new(); + io::stdin() + .read_to_string(&mut buffer) + .expect("Error reading from stdin"); + buffer + } + } +} + +/// Parse epoch string to StacksEpochId +fn parse_epoch(epoch_str: Option<&String>) -> StacksEpochId { + if let Some(s) = epoch_str { + s.parse::() + .unwrap_or_else(|_| panic!("Invalid epoch: {s}")) + } else { + DEFAULT_CLI_EPOCH + } +} + +/// Parse clarity_version string. Defaults to version for epoch if not specified. +fn parse_clarity_version(cv_str: Option<&String>, epoch: StacksEpochId) -> ClarityVersion { + if let Some(s) = cv_str { + s.parse::() + .unwrap_or_else(|_| panic!("Invalid clarity version: {s}")) + } else { + ClarityVersion::default_for_epoch(epoch) + } +} + +/// Parse allocations from JSON file or stdin +fn parse_allocations(allocations_file: &Option) -> Vec<(PrincipalData, u64)> { + if let Some(filename) = allocations_file { + let json_in = read_file_or_stdin(filename.to_str().expect("Invalid UTF-8 in path")); + clarity_cli::parse_allocations_json(&json_in).unwrap_or_else(|e| panic!("{e}")) + } else { + vec![] + } +} + +#[derive(Parser)] +#[command(name = "clarity-cli")] +#[command(about = "Clarity smart contract command-line interface", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Initialize a local VM state database + #[command(name = "initialize")] + Initialize { + /// Use testnet bootcode and block-limits instead of mainnet + #[arg(long)] + testnet: bool, + + /// Stacks epoch to use + #[arg(long)] + epoch: Option, + + /// Path to VM state database + #[arg(value_name = "DB_PATH")] + db_path: PathBuf, + + /// Initial allocations JSON file (or "-" for stdin) + #[arg(value_name = "ALLOCATIONS_FILE")] + allocations_file: Option, + }, + + /// Generate a random Stacks public address + #[command(name = "generate-address")] + GenerateAddress, + + /// Typecheck a potential contract definition + #[command(name = "check")] + Check { + /// Contract source file (or "-" for stdin) + #[arg(value_name = "CONTRACT_FILE")] + contract_file: PathBuf, + + /// Contract identifier + #[arg(long)] + contract_id: Option, + + /// Output contract analysis + #[arg(long)] + output_analysis: bool, + + /// Output cost information + #[arg(long)] + costs: bool, + + /// Use testnet configuration + #[arg(long)] + testnet: bool, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Path to VM state database (optional; if omitted, uses in-memory analysis) + #[arg(value_name = "DB_PATH")] + db_path: Option, + }, + + /// Typecheck and evaluate expressions in a stdin/stdout loop + #[command(name = "repl")] + Repl { + /// Use testnet configuration + #[arg(long)] + testnet: bool, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + }, + + /// Typecheck and evaluate an expression without a contract or database context. + /// + /// Stdin examples: + /// + /// echo "(+ 1 2)" | clarity-cli eval-raw + /// + /// clarity-cli eval-raw <<< "(+ 1 2)" + #[command(name = "eval-raw")] + EvalRaw { + /// Use testnet configuration + #[arg(long)] + testnet: bool, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + + /// Program file (or "-" for stdin; if omitted, reads from stdin) + #[arg(value_name = "PROGRAM_FILE")] + program_file: Option, + }, + + /// Evaluate (in read-only mode) a program in a given contract context. + /// + /// Stdin examples: + /// + /// echo "(+ 1 2)" | clarity-cli eval ... + /// + /// clarity-cli eval ... <<< "(+ 1 2)" + #[command(name = "eval")] + Eval { + /// Output cost information + #[arg(long)] + costs: bool, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + + /// Contract identifier + #[arg(value_name = "CONTRACT_ID")] + contract_id: String, + + /// Path to VM state database + #[arg(value_name = "DB_PATH")] + db_path: PathBuf, + + /// Program file (or "-" for stdin; if omitted, reads from stdin) + #[arg(value_name = "PROGRAM_FILE")] + program_file: Option, + }, + + /// Like eval, but does not advance to a new block. + /// + /// Stdin examples: + /// + /// echo "(+ 1 2)" | clarity-cli eval-at-chaintip ... + /// + /// clarity-cli eval-at-chaintip ... <<< "(+ 1 2)" + #[command(name = "eval-at-chaintip")] + EvalAtChaintip { + /// Output cost information + #[arg(long)] + costs: bool, + + /// Coverage folder path + #[arg(short = 'c', long)] + coverage: Option, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + + /// Contract identifier + #[arg(value_name = "CONTRACT_ID")] + contract_id: String, + + /// Path to VM state database + #[arg(value_name = "DB_PATH")] + db_path: PathBuf, + + /// Program file (or "-" for stdin; if omitted, reads from stdin) + #[arg(value_name = "PROGRAM_FILE")] + program_file: Option, + }, + + /// Like eval-at-chaintip, but accepts an index-block-hash to evaluate at. + /// + /// Stdin examples: + /// + /// echo "(+ 1 2)" | clarity-cli eval-at-block ... + /// + /// clarity-cli eval-at-block ... <<< "(+ 1 2)" + #[command(name = "eval-at-block")] + EvalAtBlock { + /// Output cost information + #[arg(long)] + costs: bool, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + + /// Index block hash + #[arg(value_name = "INDEX_BLOCK_HASH")] + index_block_hash: String, + + /// Contract identifier + #[arg(value_name = "CONTRACT_ID")] + contract_id: String, + + /// Path to VM/clarity directory + #[arg(value_name = "VM_DIR")] + vm_dir: PathBuf, + + /// Program file (or "-" for stdin; if omitted, reads from stdin) + #[arg(value_name = "PROGRAM_FILE")] + program_file: Option, + }, + + /// Deploy a new contract in the local state database + #[command(name = "launch")] + Launch { + /// Output cost information + #[arg(long)] + costs: bool, + + /// Output asset changes + #[arg(long)] + assets: bool, + + /// Output contract analysis + #[arg(long)] + output_analysis: bool, + + /// Coverage folder path + #[arg(short = 'c', long)] + coverage: Option, + + /// Contract identifier + #[arg(value_name = "CONTRACT_ID")] + contract_id: String, + + /// Contract definition file (or "-" for stdin) + #[arg(value_name = "CONTRACT_FILE")] + contract_file: PathBuf, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Path to VM state database + #[arg(value_name = "DB_PATH")] + db_path: PathBuf, + }, + + /// Execute a public function of a defined contract. + /// + /// Arguments must be valid Clarity values (e.g., "u10" for uint, "'ST1..." for principal). + /// Note: Principals require the Clarity quote prefix ('). + #[command(name = "execute")] + Execute { + /// Output cost information + #[arg(long)] + costs: bool, + + /// Output asset changes + #[arg(long)] + assets: bool, + + /// Coverage folder path + #[arg(short = 'c', long)] + coverage: Option, + + /// Clarity version + #[arg(long)] + clarity_version: Option, + + /// Stacks epoch + #[arg(long)] + epoch: Option, + + /// Contract identifier + #[arg(value_name = "CONTRACT_ID")] + contract_id: String, + + /// Public function name + #[arg(value_name = "FUNCTION_NAME")] + function_name: String, + + /// Sender address + #[arg(value_name = "SENDER")] + sender: String, + + /// Path to VM state database + #[arg(value_name = "DB_PATH")] + db_path: PathBuf, + + /// Function arguments + #[arg(value_name = "ARGS")] + args: Vec, + }, +} -#[allow(clippy::indexing_slicing)] fn main() { - let argv: Vec = env::args().collect(); + let cli = Cli::parse(); + + let (exit_code, output) = match &cli.command { + Commands::GenerateAddress => execute_generate_address(), + + Commands::Initialize { + testnet, + epoch, + db_path, + allocations_file, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let mainnet = !testnet; + let allocations = parse_allocations(allocations_file); + let db_name = db_path.to_str().expect("Invalid UTF-8 in db_path"); - let result = invoke_command(&argv[0], &argv[1..]); - match result { - (exit_code, Some(output)) => { - println!("{}", &serde_json::to_string(&output).unwrap()); - process::exit(exit_code); + execute_initialize(db_name, mainnet, epoch_id, allocations) } - (exit_code, None) => { - process::exit(exit_code); + + Commands::Check { + contract_file, + contract_id, + output_analysis, + costs, + testnet, + clarity_version, + epoch, + db_path, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + let mainnet = !testnet; + + let content = read_file_or_stdin( + contract_file + .to_str() + .expect("Invalid UTF-8 in contract_file"), + ); + + let cid = if let Some(cid_str) = contract_id { + QualifiedContractIdentifier::parse(cid_str).unwrap_or_else(|e| { + panic!("Error parsing contract identifier '{cid_str}': {e}") + }) + } else { + QualifiedContractIdentifier::transient() + }; + + let db_path_str = db_path + .as_ref() + .map(|p| p.to_str().expect("Invalid UTF-8 in db_path")); + + execute_check( + &content, + &cid, + *output_analysis, + *costs, + mainnet, + clarity_ver, + epoch_id, + db_path_str, + *testnet, + ) } + + Commands::Repl { + testnet, + epoch, + clarity_version, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + let mainnet = !testnet; + execute_repl(mainnet, epoch_id, clarity_ver) + } + + Commands::EvalRaw { + testnet, + epoch, + clarity_version, + program_file, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + let mainnet = !testnet; + + let content = read_optional_file_or_stdin(program_file.as_ref()); + + execute_eval_raw(&content, mainnet, epoch_id, clarity_ver) + } + + Commands::Eval { + costs, + epoch, + clarity_version, + contract_id, + program_file, + db_path, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + + let cid = QualifiedContractIdentifier::parse(contract_id) + .unwrap_or_else(|e| panic!("Failed to parse contract identifier: {e}")); + + let content = read_optional_file_or_stdin(program_file.as_ref()); + + let db_path_str = db_path.to_str().expect("Invalid UTF-8 in db_path"); + + execute_eval(&cid, &content, *costs, epoch_id, clarity_ver, db_path_str) + } + + Commands::EvalAtChaintip { + costs, + coverage, + epoch, + clarity_version, + contract_id, + program_file, + db_path, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + + let cid = QualifiedContractIdentifier::parse(contract_id) + .unwrap_or_else(|e| panic!("Failed to parse contract identifier: {e}")); + + let content = read_optional_file_or_stdin(program_file.as_ref()); + + let db_path_str = db_path.to_str().expect("Invalid UTF-8 in db_path"); + let coverage_str = coverage + .as_ref() + .and_then(|p| p.to_str()) + .map(|s| s.to_string()); + + execute_eval_at_chaintip( + &cid, + &content, + *costs, + epoch_id, + clarity_ver, + db_path_str, + coverage_str, + ) + } + + Commands::EvalAtBlock { + costs, + epoch, + index_block_hash, + contract_id, + program_file, + clarity_version, + vm_dir, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + + let cid = QualifiedContractIdentifier::parse(contract_id) + .unwrap_or_else(|e| panic!("Failed to parse contract identifier: {e}")); + + let content = read_optional_file_or_stdin(program_file.as_ref()); + + let vm_dir_str = vm_dir.to_str().expect("Invalid UTF-8 in vm_dir"); + + execute_eval_at_block( + index_block_hash, + &cid, + &content, + *costs, + epoch_id, + clarity_ver, + vm_dir_str, + ) + } + + Commands::Launch { + costs, + assets, + output_analysis, + coverage, + contract_id, + contract_file, + clarity_version, + epoch, + db_path, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + + let cid = QualifiedContractIdentifier::parse(contract_id) + .unwrap_or_else(|e| panic!("Failed to parse contract identifier: {e}")); + + let contract_src_file = contract_file + .to_str() + .expect("Invalid UTF-8 in contract_file"); + let contract_content = read_file_or_stdin(contract_src_file); + + let db_path_str = db_path.to_str().expect("Invalid UTF-8 in db_path"); + let coverage_str = coverage + .as_ref() + .and_then(|p| p.to_str()) + .map(|s| s.to_string()); + + execute_launch( + &cid, + contract_src_file, + &contract_content, + *costs, + *assets, + *output_analysis, + epoch_id, + clarity_ver, + db_path_str, + coverage_str, + ) + } + + Commands::Execute { + costs, + assets, + coverage, + clarity_version, + epoch, + db_path, + contract_id, + function_name, + sender, + args: fn_args, + } => { + let epoch_id = parse_epoch(epoch.as_ref()); + let clarity_ver = parse_clarity_version(clarity_version.as_ref(), epoch_id); + + let cid = QualifiedContractIdentifier::parse(contract_id) + .unwrap_or_else(|e| panic!("Failed to parse contract identifier: {e}")); + + let sender_principal = PrincipalData::parse_standard_principal(sender) + .map(PrincipalData::Standard) + .unwrap_or_else(|e| panic!("Unexpected result parsing sender {sender}: {e}")); + + let arguments: Vec<_> = fn_args + .iter() + .map(|argument| { + let argument_parsed = vm_execute_in_epoch(argument, clarity_ver, epoch_id) + .unwrap_or_else(|e| panic!("Error parsing argument '{argument}': {e}")); + let argument_value = argument_parsed.unwrap_or_else(|| { + panic!("Failed to parse a value from the argument: {argument}") + }); + SymbolicExpression::atom_value(argument_value) + }) + .collect(); + + let db_path_str = db_path.to_str().expect("Invalid UTF-8 in db_path"); + let coverage_str = coverage + .as_ref() + .and_then(|p| p.to_str()) + .map(|s| s.to_string()); + + execute_execute( + db_path_str, + &cid, + function_name, + sender_principal, + &arguments, + *costs, + *assets, + epoch_id, + coverage_str, + ) + } + }; + + // Output JSON result if present + if let Some(json_output) = output { + println!("{}", serde_json::to_string(&json_output).unwrap()); + } + + process::exit(exit_code); +} + +#[cfg(test)] +mod tests { + use clap::CommandFactory; + + use super::*; + + /// Validates the clap CLI structure has no configuration errors + #[test] + fn verify_cli_structure() { + Cli::command().debug_assert(); + } + + /// Tests that variadic arguments after positional args are collected correctly. + #[test] + fn test_execute_variadic_args() { + let cli = Cli::try_parse_from([ + "clarity-cli", + "execute", + "ST1.contract", + "transfer", + "ST1SENDER", + "/tmp/db", + "u100", + "'ST1RECIPIENT", + "(list u1 u2 u3)", + ]) + .unwrap(); + match cli.command { + Commands::Execute { args, .. } => { + assert_eq!(args, vec!["u100", "'ST1RECIPIENT", "(list u1 u2 u3)"]); + } + _ => panic!("Expected Execute command"), + } + } + + /// Tests that commands with many required positional args fail appropriately + /// when args are missing. Execute has the most complex arg structure. + #[test] + fn test_execute_missing_required_args() { + assert!(Cli::try_parse_from(["clarity-cli", "execute"]).is_err()); + assert!(Cli::try_parse_from(["clarity-cli", "execute", "ST1.contract"]).is_err()); + assert!(Cli::try_parse_from(["clarity-cli", "execute", "ST1.contract", "func"]).is_err()); + assert!( + Cli::try_parse_from(["clarity-cli", "execute", "ST1.contract", "func", "SENDER"]) + .is_err() + ); + } + + /// Tests that launch (another command with multiple required args) validates correctly + #[test] + fn test_launch_missing_required_args() { + assert!(Cli::try_parse_from(["clarity-cli", "launch"]).is_err()); + assert!(Cli::try_parse_from(["clarity-cli", "launch", "ST1.contract"]).is_err()); + assert!( + Cli::try_parse_from(["clarity-cli", "launch", "ST1.contract", "file.clar"]).is_err() + ); + } + + /// Verifies unknown subcommands are rejected + #[test] + fn test_unknown_command_rejected() { + assert!(Cli::try_parse_from(["clarity-cli", "unknown-command"]).is_err()); + } + + /// Verifies running with no subcommand is rejected + #[test] + fn test_no_command_rejected() { + assert!(Cli::try_parse_from(["clarity-cli"]).is_err()); } } diff --git a/contrib/stacks-inspect/src/main.rs b/contrib/stacks-inspect/src/main.rs index 032eee9794..c66b298fb2 100644 --- a/contrib/stacks-inspect/src/main.rs +++ b/contrib/stacks-inspect/src/main.rs @@ -975,11 +975,6 @@ check if the associated microblocks can be downloaded return; } - if argv[1] == "local" { - clarity_cli::invoke_command(&format!("{} {}", argv[0], argv[1]), &argv[2..]); - return; - } - if argv[1] == "deserialize-db" { if argv.len() < 4 { eprintln!("Usage: {} clarity_sqlite_db [byte-prefix]", &argv[0]);