diff --git a/sample-contracts/tokens-ft-mint.clar b/sample-contracts/tokens-ft-mint.clar new file mode 100644 index 0000000000..0e82447390 --- /dev/null +++ b/sample-contracts/tokens-ft-mint.clar @@ -0,0 +1,5 @@ +(begin + (as-contract + (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft mint! u100) + ) +) diff --git a/sample-contracts/tokens-ft.clar b/sample-contracts/tokens-ft.clar new file mode 100644 index 0000000000..140d3e7ee8 --- /dev/null +++ b/sample-contracts/tokens-ft.clar @@ -0,0 +1,16 @@ +(define-fungible-token tokens) +(define-private (get-balance (account principal)) + (ft-get-balance tokens account)) + +(define-private (token-credit! (account principal) (amount uint)) + (ft-mint? tokens amount account)) + +(define-public (token-transfer (to principal) (amount uint)) + (ft-transfer? tokens amount tx-sender to)) + +(define-public (mint! (amount uint)) + (token-credit! tx-sender amount)) + +(token-credit! tx-sender u10300) +(token-transfer 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR u10000) +(token-transfer 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G u300) diff --git a/sample-contracts/tokens-mint.clar b/sample-contracts/tokens-mint.clar index 6477f3f5d0..19a7cf727e 100644 --- a/sample-contracts/tokens-mint.clar +++ b/sample-contracts/tokens-mint.clar @@ -2,4 +2,4 @@ (as-contract (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens mint! u100) ) -) \ No newline at end of file +) diff --git a/src/clarity.rs b/src/clarity.rs index 28ecaa17c9..930b8b927b 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -28,26 +28,45 @@ use rusqlite::types::ToSql; use rusqlite::Row; use rusqlite::Transaction; use rusqlite::{Connection, OpenFlags, NO_PARAMS}; -use serde::Serialize; use address::c32::c32_address; use chainstate::stacks::index::{storage::TrieFileStorage, MarfTrieId}; use util::db::FromColumn; use util::hash::Sha512Trunc256Sum; + use util::log; +use vm::ContractName; + use vm::analysis; use vm::analysis::contract_interface_builder::build_contract_interface; -use vm::analysis::{errors::CheckResult, AnalysisDatabase, ContractAnalysis}; +use vm::analysis::{errors::CheckError, errors::CheckResult, AnalysisDatabase, ContractAnalysis}; use vm::ast::build_ast; -use vm::contexts::OwnedEnvironment; +use vm::contexts::{AssetMap, OwnedEnvironment}; +use vm::costs::ExecutionCost; use vm::costs::LimitedCostTracker; use vm::database::{ - ClarityDatabase, HeadersDB, STXBalance, SqliteConnection, NULL_BURN_STATE_DB, NULL_HEADER_DB, + BurnStateDB, ClarityDatabase, HeadersDB, STXBalance, SqliteConnection, NULL_BURN_STATE_DB, + NULL_HEADER_DB, }; use vm::errors::{Error, InterpreterResult, RuntimeErrorType}; use vm::types::{PrincipalData, QualifiedContractIdentifier}; use vm::{execute as vm_execute, SymbolicExpression, SymbolicExpressionType, Value}; +use burnchains::PoxConstants; +use burnchains::Txid; + +use chainstate::stacks::boot::{STACKS_BOOT_CODE_MAINNET, STACKS_BOOT_CODE_TESTNET}; +use util::boot::{boot_code_addr, boot_code_id}; + +use core::BLOCK_LIMIT_MAINNET; +use core::HELIUM_BLOCK_LIMIT; + +use serde::Serialize; +use serde_json::json; +use util::strings::StacksString; + +use std::convert::TryFrom; + use crate::clarity_vm::database::marf::MarfedKV; use crate::clarity_vm::database::marf::WritableMarfStore; use crate::clarity_vm::database::MemoryBackingStore; @@ -125,35 +144,108 @@ fn parse( Ok(ast.expressions) } -fn run_analysis( +trait ClarityStorage { + fn get_clarity_db<'a>( + &'a mut self, + headers_db: &'a dyn HeadersDB, + burn_db: &'a dyn BurnStateDB, + ) -> ClarityDatabase<'a>; + fn get_analysis_db<'a>(&'a mut self) -> AnalysisDatabase<'a>; +} + +impl ClarityStorage for WritableMarfStore<'_> { + fn get_clarity_db<'a>( + &'a mut self, + headers_db: &'a dyn HeadersDB, + burn_db: &'a dyn BurnStateDB, + ) -> ClarityDatabase<'a> { + self.as_clarity_db(headers_db, burn_db) + } + + fn get_analysis_db<'a>(&'a mut self) -> AnalysisDatabase<'a> { + self.as_analysis_db() + } +} + +impl ClarityStorage for MemoryBackingStore { + fn get_clarity_db<'a>( + &'a mut self, + _headers_db: &'a dyn HeadersDB, + _burn_db: &'a dyn BurnStateDB, + ) -> ClarityDatabase<'a> { + self.as_clarity_db() + } + + fn get_analysis_db<'a>(&'a mut self) -> AnalysisDatabase<'a> { + self.as_analysis_db() + } +} + +fn run_analysis_free( contract_identifier: &QualifiedContractIdentifier, expressions: &mut [SymbolicExpression], - analysis_db: &mut AnalysisDatabase, + marf_kv: &mut C, save_contract: bool, -) -> CheckResult { +) -> Result { analysis::run_analysis( contract_identifier, expressions, - analysis_db, + &mut marf_kv.get_analysis_db(), save_contract, LimitedCostTracker::new_free(), ) - .map_err(|(e, _)| e) +} + +fn run_analysis( + contract_identifier: &QualifiedContractIdentifier, + expressions: &mut [SymbolicExpression], + header_db: &CLIHeadersDB, + marf_kv: &mut C, + save_contract: bool, +) -> Result { + let mainnet = header_db.is_mainnet(); + let cost_track = LimitedCostTracker::new( + mainnet, + if mainnet { + BLOCK_LIMIT_MAINNET.clone() + } else { + HELIUM_BLOCK_LIMIT.clone() + }, + &mut marf_kv.get_clarity_db(header_db, &NULL_BURN_STATE_DB), + ) + .unwrap(); + analysis::run_analysis( + contract_identifier, + expressions, + &mut marf_kv.get_analysis_db(), + save_contract, + cost_track, + ) } fn create_or_open_db(path: &String) -> Connection { - let open_flags = match fs::metadata(path) { - Err(e) => { - if e.kind() == io::ErrorKind::NotFound { - // need to create - OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE - } else { - panic!("FATAL: could not stat {}", path); + let open_flags = if path == ":memory:" { + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE + } else { + match fs::metadata(path) { + Err(e) => { + if e.kind() == io::ErrorKind::NotFound { + // 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); + panic_test!(); + }); + } + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE + } else { + panic!("FATAL: could not stat {}", path); + } + } + Ok(_md) => { + // can just open + OpenFlags::SQLITE_OPEN_READ_WRITE } - } - Ok(_md) => { - // can just open - OpenFlags::SQLITE_OPEN_READ_WRITE } }; @@ -204,47 +296,11 @@ fn get_cli_block_height(conn: &Connection, block_id: &StacksBlockId) -> Option (StacksBlockId, StacksBlockId) { - let mut conn = create_or_open_db(path); - let tx = friendly_expect( - conn.transaction(), - &format!("FATAL: failed to begin transaction on '{}'", path), - ); - - friendly_expect(tx.execute("CREATE TABLE IF NOT EXISTS cli_chain_tips(id INTEGER PRIMARY KEY AUTOINCREMENT, block_hash TEXT UNIQUE NOT NULL);", NO_PARAMS), - &format!("FATAL: failed to create 'cli_chain_tips' table")); - - let parent_block_hash = get_cli_chain_tip(&tx); - - let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); - let next_block_hash = friendly_expect_opt( - StacksBlockId::from_bytes(&random_bytes), - "Failed to generate random block header.", - ); - - friendly_expect( - tx.execute( - "INSERT INTO cli_chain_tips (block_hash) VALUES (?1)", - &[&next_block_hash], - ), - &format!("FATAL: failed to store next block hash in '{}'", path), - ); - - friendly_expect( - tx.commit(), - &format!("FATAL: failed to commit new chain tip to '{}'", path), - ); - - (parent_block_hash, next_block_hash) -} +fn get_cli_db_path(db_path: &str) -> String { + if db_path == ":memory:" { + return db_path.to_string(); + } -// This function is pretty weird! But it helps cut down on -// repeating a lot of block initialization for the simulation commands. -fn in_block(db_path: &str, mut marf_kv: MarfedKV, f: F) -> R -where - F: FnOnce(WritableMarfStore) -> (WritableMarfStore, R), -{ - // store CLI data alongside the MARF database state let mut cli_db_path_buf = PathBuf::from(db_path); cli_db_path_buf.push("cli.sqlite"); let cli_db_path = cli_db_path_buf @@ -254,13 +310,28 @@ where db_path )) .to_string(); + cli_db_path +} +// This function is pretty weird! But it helps cut down on +// repeating a lot of block initialization for the simulation commands. +fn in_block( + mut headers_db: CLIHeadersDB, + mut marf_kv: MarfedKV, + f: F, +) -> (CLIHeadersDB, MarfedKV, R) +where + F: FnOnce(CLIHeadersDB, WritableMarfStore) -> (CLIHeadersDB, WritableMarfStore, R), +{ // need to load the last block - let (from, to) = advance_cli_chain_tip(&cli_db_path); - let marf_tx = marf_kv.begin(&from, &to); - let (marf_return, result) = f(marf_tx); - marf_return.commit_to(&to); - result + let (from, to) = headers_db.advance_cli_chain_tip(); + let (headers_return, result) = { + let marf_tx = marf_kv.begin(&from, &to); + let (headers_return, marf_return, result) = f(headers_db, marf_tx); + marf_return.commit_to(&to); + (headers_return, result) + }; + (headers_return, marf_kv, result) } // like in_block, but does _not_ advance the chain tip. Used for read-only queries against the @@ -270,16 +341,7 @@ where F: FnOnce(WritableMarfStore) -> (WritableMarfStore, R), { // store CLI data alongside the MARF database state - let mut cli_db_path_buf = PathBuf::from(db_path); - cli_db_path_buf.push("cli.sqlite"); - let cli_db_path = cli_db_path_buf - .to_str() - .expect(&format!( - "FATAL: failed to convert '{}' to a string", - db_path - )) - .to_string(); - + let cli_db_path = get_cli_db_path(db_path); let cli_db_conn = create_or_open_db(&cli_db_path); let from = get_cli_chain_tip(&cli_db_conn); let to = StacksBlockId([2u8; 32]); // 0x0202020202 ... (pattern not used anywhere else) @@ -305,30 +367,170 @@ where result } +fn with_env_costs( + mainnet: bool, + header_db: &CLIHeadersDB, + marf: &mut WritableMarfStore, + f: F, +) -> (R, ExecutionCost) +where + F: FnOnce(&mut OwnedEnvironment) -> R, +{ + let mut db = marf.as_clarity_db(header_db, &NULL_BURN_STATE_DB); + let cost_track = LimitedCostTracker::new( + mainnet, + if mainnet { + BLOCK_LIMIT_MAINNET.clone() + } else { + HELIUM_BLOCK_LIMIT.clone() + }, + &mut db, + ) + .unwrap(); + let mut vm_env = OwnedEnvironment::new_cost_limited(mainnet, db, cost_track); + let result = f(&mut vm_env); + let cost = vm_env.get_cost_total(); + (result, cost) +} + struct CLIHeadersDB { db_path: String, + conn: Connection, } impl CLIHeadersDB { - pub fn new(db_path: &str) -> CLIHeadersDB { - CLIHeadersDB { + fn instantiate(&mut self, mainnet: bool) { + let cli_db_path = self.get_cli_db_path(); + let tx = friendly_expect( + self.conn.transaction(), + &format!("FATAL: failed to begin transaction on '{}'", cli_db_path), + ); + + friendly_expect( + tx.execute( + "CREATE TABLE IF NOT EXISTS cli_chain_tips(id INTEGER PRIMARY KEY AUTOINCREMENT, block_hash TEXT UNIQUE NOT NULL);", + NO_PARAMS + ), + &format!("FATAL: failed to create 'cli_chain_tips' table"), + ); + + friendly_expect( + tx.execute( + "CREATE TABLE IF NOT EXISTS cli_config(testnet BOOLEAN NOT NULL);", + NO_PARAMS, + ), + &format!("FATAL: failed to create 'cli_config' table"), + ); + + if !mainnet { + friendly_expect( + tx.execute("INSERT INTO cli_config (testnet) VALUES (?1)", &[&true]), + &format!("FATAL: failed to set testnet flag"), + ); + } + + friendly_expect( + tx.commit(), + &format!("FATAL: failed to instantiate CLI DB at {:?}", &cli_db_path), + ); + } + + /// Create or open a new CLI DB at db_path. If it already exists, then this method is a no-op. + pub fn new(db_path: &str, mainnet: bool) -> CLIHeadersDB { + let instantiate = db_path == ":memory:" || fs::metadata(&db_path).is_err(); + + let cli_db_path = get_cli_db_path(db_path); + let conn = create_or_open_db(&cli_db_path); + let mut db = CLIHeadersDB { db_path: db_path.to_string(), + conn: conn, + }; + + if instantiate { + db.instantiate(mainnet); } + db + } + + /// Open an CLI DB at db_path. Returns Err() if it doesn't exist. + /// Normally this would be Option<..>, but since this gets used with friendly_expect, + /// using a Result<..> is necessary. + 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)); + } + let conn = create_or_open_db(&cli_db_path); + let db = CLIHeadersDB { + db_path: db_path.to_string(), + conn: conn, + }; + + Ok(db) + } + + /// Make a new CLI DB in memory. + pub fn new_memory(mainnet: bool) -> CLIHeadersDB { + let db = CLIHeadersDB::new(":memory:", mainnet); + db } - pub fn open(&self) -> Connection { - let mut cli_db_path_buf = PathBuf::from(&self.db_path); - cli_db_path_buf.push("cli.sqlite"); - let cli_db_path = cli_db_path_buf - .to_str() - .expect(&format!( - "FATAL: failed to convert '{}' to a string", + fn get_cli_db_path(&self) -> String { + get_cli_db_path(&self.db_path) + } + + pub fn conn(&self) -> &Connection { + &self.conn + } + + pub fn is_mainnet(&self) -> bool { + let mut stmt = friendly_expect( + self.conn.prepare("SELECT testnet FROM cli_config LIMIT 1"), + "FATAL: could not prepare query", + ); + let mut rows = friendly_expect(stmt.query(NO_PARAMS), "FATAL: could not fetch rows"); + let mut mainnet = true; + while let Some(row) = rows.next().expect("FATAL: could not read config row") { + let testnet: bool = row.get_unwrap("testnet"); + mainnet = !testnet; + } + mainnet + } + + pub fn advance_cli_chain_tip(&mut self) -> (StacksBlockId, StacksBlockId) { + let tx = friendly_expect( + self.conn.transaction(), + &format!("FATAL: failed to begin transaction on '{}'", &self.db_path), + ); + + let parent_block_hash = get_cli_chain_tip(&tx); + + let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); + let next_block_hash = friendly_expect_opt( + StacksBlockId::from_bytes(&random_bytes), + "Failed to generate random block header.", + ); + + friendly_expect( + tx.execute( + "INSERT INTO cli_chain_tips (block_hash) VALUES (?1)", + &[&next_block_hash], + ), + &format!( + "FATAL: failed to store next block hash in '{}'", &self.db_path - )) - .to_string(); + ), + ); - let cli_db_conn = create_or_open_db(&cli_db_path); - cli_db_conn + friendly_expect( + tx.commit(), + &format!( + "FATAL: failed to commit new chain tip to '{}'", + &self.db_path + ), + ); + + (parent_block_hash, next_block_hash) } } @@ -338,7 +540,7 @@ impl HeadersDB for CLIHeadersDB { id_bhh: &StacksBlockId, ) -> Option { // mock it - let conn = self.open(); + let conn = self.conn(); if let Some(_) = get_cli_block_height(&conn, id_bhh) { let hash_bytes = Sha512Trunc256Sum::from_data(&id_bhh.0); Some(BurnchainHeaderHash(hash_bytes.0)) @@ -348,7 +550,7 @@ impl HeadersDB for CLIHeadersDB { } fn get_vrf_seed_for_block(&self, id_bhh: &StacksBlockId) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(_) = get_cli_block_height(&conn, id_bhh) { // mock it, but make it unique let hash_bytes = Sha512Trunc256Sum::from_data(&id_bhh.0); @@ -363,7 +565,7 @@ impl HeadersDB for CLIHeadersDB { &self, id_bhh: &StacksBlockId, ) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(_) = get_cli_block_height(&conn, id_bhh) { // mock it, but make it unique let hash_bytes = Sha512Trunc256Sum::from_data(&id_bhh.0); @@ -375,7 +577,7 @@ impl HeadersDB for CLIHeadersDB { } } fn get_burn_block_time_for_block(&self, id_bhh: &StacksBlockId) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(height) = get_cli_block_height(&conn, id_bhh) { Some((height * 600 + 1231006505) as u64) } else { @@ -383,7 +585,7 @@ impl HeadersDB for CLIHeadersDB { } } fn get_burn_block_height_for_block(&self, id_bhh: &StacksBlockId) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(height) = get_cli_block_height(&conn, id_bhh) { Some(height as u32) } else { @@ -398,7 +600,7 @@ impl HeadersDB for CLIHeadersDB { fn get_eval_input(invoked_by: &str, args: &[String]) -> EvalInput { if args.len() < 3 || args.len() > 4 { eprintln!( - "Usage: {} {} [contract-identifier] (program.clar) [vm-state.db]", + "Usage: {} {} [--costs] [contract-identifier] (program.clar) [vm-state.db]", invoked_by, args[0] ); panic_test!(); @@ -483,15 +685,106 @@ fn consume_arg( } } -pub fn invoke_command(invoked_by: &str, args: &[String]) { +fn install_boot_code(header_db: &CLIHeadersDB, marf: &mut C) { + let mainnet = header_db.is_mainnet(); + let boot_code = if mainnet { + *STACKS_BOOT_CODE_MAINNET + } else { + *STACKS_BOOT_CODE_TESTNET + }; + + for (boot_code_name, boot_code_contract) in boot_code.iter() { + let contract_identifier = QualifiedContractIdentifier::new( + boot_code_addr(mainnet).into(), + ContractName::try_from(boot_code_name.to_string()).unwrap(), + ); + let contract_content = *boot_code_contract; + + debug!( + "Instantiate boot code contract '{}' ({} bytes)...", + &contract_identifier, + boot_code_contract.len() + ); + + let mut ast = friendly_expect( + parse(&contract_identifier, &contract_content), + "Failed to parse program.", + ); + + let analysis_result = run_analysis_free(&contract_identifier, &mut ast, marf, true); + match analysis_result { + Ok(_) => { + let db = marf.get_clarity_db(header_db, &NULL_BURN_STATE_DB); + let mut vm_env = OwnedEnvironment::new_free(mainnet, db); + vm_env + .initialize_contract(contract_identifier, &contract_content) + .unwrap(); + } + Err(_) => { + panic!("failed to instantiate boot contract"); + } + }; + } + + // set up PoX + let pox_contract = boot_code_id("pox", mainnet); + let sender = PrincipalData::from(pox_contract.clone()); + let pox_params = if mainnet { + PoxConstants::mainnet_default() + } else { + PoxConstants::testnet_default() + }; + + let params = vec![ + SymbolicExpression::atom_value(Value::UInt(0)), // first burnchain block height + SymbolicExpression::atom_value(Value::UInt(pox_params.prepare_length as u128)), + SymbolicExpression::atom_value(Value::UInt(pox_params.reward_cycle_length as u128)), + SymbolicExpression::atom_value(Value::UInt(pox_params.pox_rejection_fraction as u128)), + ]; + + let db = marf.get_clarity_db(header_db, &NULL_BURN_STATE_DB); + let mut vm_env = OwnedEnvironment::new_free(mainnet, db); + vm_env + .execute_transaction( + sender, + pox_contract, + "set-burnchain-parameters", + params.as_slice(), + ) + .unwrap(); +} + +pub fn add_costs(result: &mut serde_json::Value, costs: bool, runtime: ExecutionCost) { + if costs { + result["costs"] = serde_json::to_value(runtime).unwrap(); + } +} + +pub fn add_assets(result: &mut serde_json::Value, assets: bool, asset_map: AssetMap) { + if assets { + result["assets"] = asset_map.to_json(); + } +} + +/// Returns (process-exit-code, Option) +pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option) { if args.len() < 1 { - print_usage(invoked_by) + print_usage(invoked_by); + return (1, None); } match args[0].as_ref() { "initialize" => { - let (db_name, allocations) = if args.len() == 3 { - let filename = &args[1]; + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { + false + } else { + true + }; + + let (db_name, allocations) = if argv.len() == 3 { + let filename = &argv[1]; let json_in = if filename == "-" { let mut buffer = String::new(); friendly_expect( @@ -521,26 +814,38 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { }) .collect(); - (&args[2], allocations) - } else if args.len() == 2 { - (&args[1], Vec::new()) + (&argv[2], allocations) + } else if argv.len() == 2 { + (&argv[1], Vec::new()) } else { eprintln!( - "Usage: {} {} (initial-allocations.json) [vm-state.db]", - invoked_by, args[0] + "Usage: {} {} [--testnet] (initial-allocations.json) [vm-state.db]", + invoked_by, 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."); + eprintln!(" If --testnet is given, then testnet bootcode and block-limits are used instead of mainnet."); panic_test!(); }; - let marf_kv = + debug!("Initialize {}", &db_name); + let mut header_db = CLIHeadersDB::new(&db_name, mainnet); + let mut marf_kv = friendly_expect(MarfedKV::open(db_name, None), "Failed to open VM database."); - let header_db = CLIHeadersDB::new(&db_name); - in_block(db_name, marf_kv, |mut kv| { + + // install bootcode + let state = in_block(header_db, marf_kv, |header_db, mut marf| { + install_boot_code(&header_db, &mut marf); + (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.initialize(); db.begin(); for (principal, amount) in allocations.iter() { let balance = STXBalance::initial(*amount as u128); @@ -554,9 +859,26 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { } db.commit(); }; - (kv, ()) + (header_db, kv, ()) }); - println!("Database created."); + + if mainnet { + ( + 0, + Some(json!({ + "message": "Database created.", + "network": "mainnet" + })), + ) + } else { + ( + 0, + Some(json!({ + "message": "Database created.", + "network": "testnet" + })), + ) + } } "generate_address" => { // random 20 bytes @@ -564,12 +886,13 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { // version = 22 let addr = friendly_expect(c32_address(22, &random_bytes), "Failed to generate address"); - println!("{}", addr); + + (0, Some(json!({ "address": format!("{}", addr) }))) } "check" => { if args.len() < 2 { eprintln!( - "Usage: {} {} [program-file.clar] [--contract_id CONTRACT_ID] [--output_analysis] (vm-state.db)", + "Usage: {} {} [program-file.clar] [--contract_id CONTRACT_ID] [--output_analysis] [--costs] [--testnet] (vm-state.db)", invoked_by, args[0] ); panic_test!(); @@ -598,6 +921,21 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { panic_test!(); }; + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + // 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( @@ -614,52 +952,83 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { let mut ast = friendly_expect(parse(&contract_id, &content), "Failed to parse program"); - let contract_analysis = { + 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]); + } + + 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(&argv[2], None), + MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); + let result = at_chaintip(&argv[2], marf_kv, |mut marf| { - let result = { - let mut db = marf.as_analysis_db(); - run_analysis(&contract_id, &mut ast, &mut db, false) - }; + let result = + run_analysis(&contract_id, &mut ast, &header_db, &mut marf, false); (marf, result) }); result } else { + let header_db = CLIHeadersDB::new_memory(mainnet); let mut analysis_marf = MemoryBackingStore::new(); - let mut db = analysis_marf.as_analysis_db(); - run_analysis(&contract_id, &mut ast, &mut db, false) + + install_boot_code(&header_db, &mut analysis_marf); + run_analysis( + &contract_id, + &mut ast, + &header_db, + &mut analysis_marf, + false, + ) } - } - .unwrap_or_else(|e| { - println!("{}", &e.diagnostic); - panic_test!(); + }; + + let mut contract_analysis = match contract_analysis_res { + Ok(contract_analysis) => contract_analysis, + Err((e, cost_tracker)) => { + 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 result = json!({ + "message": "Checks passed." }); + add_costs( + &mut result, + costs, + contract_analysis.take_contract_cost_tracker().get_total(), + ); + if output_analysis { - println!( - "{}", - build_contract_interface(&contract_analysis).serialize() - ); - } else { - println!("Checks passed."); + result["analysis"] = + serde_json::to_value(&build_contract_interface(&contract_analysis)).unwrap(); } + (0, Some(result)) } "repl" => { + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { + false + } else { + true + }; let mut marf = MemoryBackingStore::new(); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - marf.as_clarity_db(), - LimitedCostTracker::new_free(), - ); + let mut vm_env = OwnedEnvironment::new_free(mainnet, marf.as_clarity_db()); let mut exec_env = vm_env.get_exec_environment(None); - let mut analysis_marf = MemoryBackingStore::new(); - let mut analysis_db = analysis_marf.as_analysis_db(); let contract_id = QualifiedContractIdentifier::transient(); @@ -691,9 +1060,9 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { } }; - match run_analysis(&contract_id, &mut ast, &mut analysis_db, true) { + match run_analysis_free(&contract_id, &mut ast, &mut analysis_marf, true) { Ok(_) => (), - Err(error) => { + Err((error, _)) => { println!("Type check error:\n{}", error); continue; } @@ -721,115 +1090,163 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { }; let mut analysis_marf = MemoryBackingStore::new(); - let mut analysis_db = analysis_marf.as_analysis_db(); - let mut marf = MemoryBackingStore::new(); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - marf.as_clarity_db(), - LimitedCostTracker::new_free(), - ); + let mut vm_env = OwnedEnvironment::new_free(true, marf.as_clarity_db()); let contract_id = QualifiedContractIdentifier::transient(); let mut ast = friendly_expect(parse(&contract_id, &content), "Failed to parse program."); - match run_analysis(&contract_id, &mut ast, &mut analysis_db, true) { + match run_analysis_free(&contract_id, &mut ast, &mut analysis_marf, true) { Ok(_) => { let result = vm_env.get_exec_environment(None).eval_raw(&content); match result { - Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); - } - Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); - } + 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(error) => { - eprintln!("Type check error.\n{}", error); - panic_test!(); - } + Err((error, _)) => ( + 1, + Some(json!({ + "error": { + "analysis": serde_json::to_value(&format!("{}", error)).unwrap() + } + })), + ), } } "eval" => { - let evalInput = get_eval_input(invoked_by, args); - let vm_filename = if args.len() == 3 { &args[2] } else { &args[3] }; + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + let evalInput = 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), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - let result = in_block(vm_filename, marf_kv, |mut marf| { - let result = { - let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - db, - LimitedCostTracker::new_free(), - ); + let mainnet = header_db.is_mainnet(); + + let (_, _, result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { + let result_and_cost = with_env_costs(mainnet, &header_db, &mut marf, |vm_env| { vm_env .get_exec_environment(None) .eval_read_only(&evalInput.contract_identifier, &evalInput.content) - }; - (marf, result) + }); + (header_db, marf, result_and_cost) }); - match result { - Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap() + }); + + add_costs(&mut result_json, costs, cost); + + (0, Some(result_json)) } - Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); + (Err(error), cost) => { + let mut result_json = json!({ + "error": { + "runtime": serde_json::to_value(&format!("{}", error)).unwrap() + } + }); + + add_costs(&mut result_json, costs, cost); + + (1, Some(result_json)) } } } "eval_at_chaintip" => { - let evalInput = get_eval_input(invoked_by, args); - let vm_filename = if args.len() == 3 { &args[2] } else { &args[3] }; + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + let evalInput = 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), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - let result = at_chaintip(vm_filename, marf_kv, |mut marf| { - let result = { - let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - db, - LimitedCostTracker::new_free(), - ); + let mainnet = header_db.is_mainnet(); + let result_and_cost = at_chaintip(vm_filename, marf_kv, |mut marf| { + let result_and_cost = with_env_costs(mainnet, &header_db, &mut marf, |vm_env| { vm_env .get_exec_environment(None) .eval_read_only(&evalInput.contract_identifier, &evalInput.content) - }; - (marf, result) + }); + (marf, result_and_cost) }); - match result { - Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap() + }); + + add_costs(&mut result_json, costs, cost); + + (0, Some(result_json)) } - Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); + (Err(error), cost) => { + let mut result_json = json!({ + "error": { + "runtime": serde_json::to_value(&format!("{}", error)).unwrap() + } + }); + + add_costs(&mut result_json, costs, cost); + + (1, Some(result_json)) } } } "eval_at_block" => { - if args.len() != 4 { + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + if argv.len() != 4 { eprintln!( - "Usage: {} {} [index-block-hash] [contract-identifier] [vm/clarity dir]", - invoked_by, &args[0] + "Usage: {} {} [--costs] [index-block-hash] [contract-identifier] [vm/clarity dir]", + invoked_by, &argv[0] ); panic_test!(); } - let chain_tip = &args[1]; + let chain_tip = &argv[1]; let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&args[2]), + QualifiedContractIdentifier::parse(&argv[2]), "Failed to parse contract identifier.", ); let content: String = { @@ -841,131 +1258,189 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { buffer }; - let vm_filename = &args[3]; + 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), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - let result = at_block(chain_tip, marf_kv, |mut marf| { - let result = { - let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - db, - LimitedCostTracker::new_free(), - ); + let mainnet = header_db.is_mainnet(); + let result_and_cost = at_block(chain_tip, marf_kv, |mut marf| { + let result_and_cost = with_env_costs(mainnet, &header_db, &mut marf, |vm_env| { vm_env .get_exec_environment(None) .eval_read_only(&contract_identifier, &content) - }; - (marf, result) + }); + (marf, result_and_cost) }); - match result { - Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap() + }); + + add_costs(&mut result_json, costs, cost); + + (0, Some(result_json)) } - Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); + (Err(error), cost) => { + let mut result_json = json!({ + "error": { + "runtime": serde_json::to_value(&format!("{}", error)).unwrap() + } + }); + + add_costs(&mut result_json, costs, cost); + + (1, Some(result_json)) } } } "launch" => { - if args.len() < 4 { + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + let assets = if let Ok(Some(_)) = consume_arg(&mut argv, &["--assets"], false) { + true + } else { + false + }; + let output_analysis = + if let Ok(Some(_)) = consume_arg(&mut argv, &["--output_analysis"], false) { + true + } else { + false + }; + if argv.len() < 4 { eprintln!( - "Usage: {} {} [contract-identifier] [contract-definition.clar] [vm-state.db]", - invoked_by, args[0] + "Usage: {} {} [--costs] [--assets] [--output_analysis] [contract-identifier] [contract-definition.clar] [vm-state.db]", + invoked_by, argv[0] ); panic_test!(); } - let vm_filename = &args[3]; + let vm_filename = &argv[3]; let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&args[1]), + QualifiedContractIdentifier::parse(&argv[1]), "Failed to parse contract identifier.", ); let contract_content: String = friendly_expect( - fs::read_to_string(&args[2]), - &format!("Error reading file: {}", args[2]), + fs::read_to_string(&argv[2]), + &format!("Error reading file: {}", argv[2]), ); let mut ast = friendly_expect( parse(&contract_identifier, &contract_content), "Failed to parse program.", ); + let header_db = + friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - let result = in_block(vm_filename, marf_kv, |mut marf| { - let analysis_result = { - let mut db = AnalysisDatabase::new(&mut marf); + let mainnet = header_db.is_mainnet(); + + 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); + match analysis_result { + Err(e) => (header_db, marf, Err(e)), + Ok(analysis) => { + let result_and_cost = + with_env_costs(mainnet, &header_db, &mut marf, |vm_env| { + vm_env + .initialize_contract(contract_identifier, &contract_content) + }); + (header_db, marf, Ok((analysis, result_and_cost))) + } + } + }); - run_analysis(&contract_identifier, &mut ast, &mut db, true) - }; + match analysis_result_and_cost { + Ok((contract_analysis, (Ok((_x, asset_map, events)), cost))) => { + let mut result = json!({ + "message": "Contract initialized!" + }); - match analysis_result { - Err(e) => (marf, Err(e)), - Ok(analysis) => { - let result = { - let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - db, - LimitedCostTracker::new_free(), - ); - vm_env.initialize_contract(contract_identifier, &contract_content) - }; - (marf, Ok((analysis, result))) - } - } - }); + add_costs(&mut result, costs, cost); + add_assets(&mut result, assets, asset_map); - match result { - Ok((contract_analysis, Ok(_x))) => match args.last() { - Some(s) if s == "--output_analysis" => { - println!( - "{}", - build_contract_interface(&contract_analysis).serialize() - ); - } - _ => { - println!("Contract initialized!"); + if output_analysis { + result["analysis"] = + serde_json::to_value(&build_contract_interface(&contract_analysis)) + .unwrap(); } - }, - Err(error) => { - eprintln!("Contract initialization error: \n{}", error); - panic_test!(); + let events_json: Vec<_> = events + .into_iter() + .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true)) + .collect(); + + result["events"] = serde_json::Value::Array(events_json); + (0, Some(result)) } - Ok((_, Err(error))) => { - eprintln!("Contract initialization error: \n{}", error); - panic_test!(); + Err((error, cost_tracker)) => { + let mut result = json!({ + "error": { + "initialization": serde_json::to_value(&format!("{}", error)).unwrap() + } + }); + + 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" => { - if args.len() < 5 { - eprintln!("Usage: {} {} [vm-state.db] [contract-identifier] [public-function-name] [sender-address] [args...]", invoked_by, args[0]); + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + let assets = if let Ok(Some(_)) = consume_arg(&mut argv, &["--assets"], false) { + true + } else { + false + }; + + if argv.len() < 5 { + eprintln!("Usage: {} {} [--costs] [--assets] [vm-state.db] [contract-identifier] [public-function-name] [sender-address] [args...]", invoked_by, argv[0]); panic_test!(); } - let vm_filename = &args[1]; + + 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), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - + let mainnet = header_db.is_mainnet(); let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&args[2]), + QualifiedContractIdentifier::parse(&argv[2]), "Failed to parse contract identifier.", ); - let tx_name = &args[3]; - let sender_in = &args[4]; + let tx_name = &argv[3]; + let sender_in = &argv[4]; let sender = { if let Ok(sender) = PrincipalData::parse_standard_principal(sender_in) { @@ -976,7 +1451,7 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { } }; - let arguments: Vec<_> = args[5..] + let arguments: Vec<_> = argv[5..] .iter() .map(|argument| { let argument_parsed = friendly_expect( @@ -991,44 +1466,67 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { }) .collect(); - let result = in_block(vm_filename, marf_kv, |mut marf| { - let result = { - let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - db, - LimitedCostTracker::new_free(), - ); + let (_, _, result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { + let result_and_cost = with_env_costs(mainnet, &header_db, &mut marf, |vm_env| { vm_env.execute_transaction(sender, contract_identifier, &tx_name, &arguments) - }; - (marf, result) + }); + (header_db, marf, result_and_cost) }); - match result { - Ok((x, _, events)) => { + match result_and_cost { + (Ok((x, asset_map, events)), cost) => { if let Value::Response(data) = x { if data.committed { - println!( - "Transaction executed and committed. Returned: {}\n{:?}", - data.data, events - ); + let mut result = json!({ + "message": "Transaction executed and committed.", + "output": serde_json::to_value(&data.data).unwrap(), + }); + + 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)) + .collect(); + + result["events"] = serde_json::Value::Array(events_json); + (0, Some(result)) } else { - println!("Aborted: {}", data.data); + let mut result = json!({ + "message": "Aborted.", + "output": serde_json::to_value(&data.data).unwrap(), + }); + + add_costs(&mut result, costs, cost); + + (0, Some(result)) } } else { - panic!(format!( - "Expected a ResponseType result from transaction. Found: {}", - x - )); + let result = json!({ + "error": { + "runtime": "Expected a ResponseType result from transaction.", + "output": serde_json::to_value(&x).unwrap() + } + }); + (1, Some(result)) } } - Err(error) => { - eprintln!("Transaction execution error: \n{}", error); - panic_test!(); + (Err(error), _) => { + let result = json!({ + "error": { + "runtime": "Transaction execution error.", + "error": serde_json::to_value(&format!("{}", error)).unwrap() + } + }); + (1, Some(result)) } } } - _ => print_usage(invoked_by), + _ => { + print_usage(invoked_by); + (1, None) + } } } @@ -1058,12 +1556,17 @@ mod test { (unwrap-panic (if (is-eq (stx-get-balance 'S1G2081040G2081040G2081040G208105NK8PE5.names) u2000) (ok 1) (err 2))) "#).unwrap(); - invoke_command( + let invoked = invoke_command( "test", &["initialize".to_string(), json_name.clone(), db_name.clone()], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!(result["network"], "mainnet"); - invoke_command( + let invoked = invoke_command( "test", &[ "launch".to_string(), @@ -1072,6 +1575,47 @@ mod test { db_name, ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + } + + #[test] + fn test_init_mainnet() { + let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); + let invoked = invoke_command("test", &["initialize".to_string(), db_name.clone()]); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!(result["network"], "mainnet"); + + let header_db = CLIHeadersDB::new(&db_name, true); + assert!(header_db.is_mainnet()); + } + + #[test] + fn test_init_testnet() { + let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); + let invoked = invoke_command( + "test", + &[ + "initialize".to_string(), + "--testnet".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!(result["network"], "testnet"); + + let header_db = CLIHeadersDB::new(&db_name, true); + assert!(!header_db.is_mainnet()); } #[test] @@ -1082,7 +1626,7 @@ mod test { invoke_command("test", &["initialize".to_string(), db_name.clone()]); eprintln!("check tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1090,8 +1634,14 @@ mod test { ], ); - eprintln!("check tokens"); - invoke_command( + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + + eprintln!("check tokens (idempotency)"); + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1100,8 +1650,14 @@ mod test { ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + eprintln!("launch tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "launch".to_string(), @@ -1111,8 +1667,14 @@ mod test { ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + eprintln!("check names"); - invoke_command( + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1121,8 +1683,14 @@ mod test { ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + eprintln!("check names with different contract ID"); - invoke_command( + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1133,19 +1701,72 @@ mod test { ], ); - eprintln!("launch names"); - invoke_command( + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + + eprintln!("check names with analysis"); + let invoked = invoke_command( + "test", + &[ + "check".to_string(), + "--output_analysis".to_string(), + "sample-contracts/names.clar".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["analysis"] != json!(null)); + + eprintln!("check names with cost"); + let invoked = invoke_command( + "test", + &[ + "check".to_string(), + "--costs".to_string(), + "sample-contracts/names.clar".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + 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(), "sample-contracts/names.clar".to_string(), + "--costs".to_string(), + "--assets".to_string(), db_name.clone(), ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["costs"] != json!(null)); + assert!(result["assets"] != json!(null)); + eprintln!("execute tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "execute".to_string(), @@ -1157,8 +1778,16 @@ mod test { ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["events"].as_array().unwrap().len() == 0); + assert_eq!(result["output"], json!({"UInt": 1000})); + eprintln!("eval tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "eval".to_string(), @@ -1168,8 +1797,53 @@ mod test { ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + }) + ); + + eprintln!("eval tokens with cost"); + let invoked = invoke_command( + "test", + &[ + "eval".to_string(), + "--costs".to_string(), + "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), + "sample-contracts/tokens-mint.clar".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + }) + ); + assert!(result["costs"] != json!(null)); + eprintln!("eval_at_chaintip tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "eval_at_chaintip".to_string(), @@ -1178,5 +1852,143 @@ mod test { db_name.clone(), ], ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + }) + ); + + eprintln!("eval_at_chaintip tokens with cost"); + let invoked = invoke_command( + "test", + &[ + "eval_at_chaintip".to_string(), + "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), + "sample-contracts/tokens-mint.clar".to_string(), + db_name.clone(), + "--costs".to_string(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + }) + ); + assert!(result["costs"] != json!(null)); + } + + #[test] + fn test_assets() { + let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); + + eprintln!("initialize"); + invoke_command("test", &["initialize".to_string(), db_name.clone()]); + + eprintln!("check tokens"); + let invoked = invoke_command( + "test", + &[ + "check".to_string(), + "sample-contracts/tokens-ft.clar".to_string(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + + eprintln!("launch tokens"); + let invoked = invoke_command( + "test", + &[ + "launch".to_string(), + "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft".to_string(), + "sample-contracts/tokens-ft.clar".to_string(), + db_name.clone(), + "--assets".to_string(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + eprintln!("{}", serde_json::to_string(&result).unwrap()); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!( + result["assets"]["tokens"]["S1G2081040G2081040G2081040G208105NK8PE5"] + ["S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens"] + == "10300" + ); + assert!(result["events"].as_array().unwrap().len() == 3); + assert!( + result["events"].as_array().unwrap()[0] + == json!({ + "committed": true, + "event_index": 0, + "ft_mint_event": { + "amount": "10300", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_mint_event" + }) + ); + assert!( + result["events"].as_array().unwrap()[1] + == json!({ + "committed": true, + "event_index": 0, + "ft_transfer_event": { + "amount": "10000", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", + "sender": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_transfer_event" + }) + ); + assert!( + result["events"].as_array().unwrap()[2] + == json!({ + "committed": true, + "event_index": 0, + "ft_transfer_event": { + "amount": "300", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G", + "sender": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_transfer_event" + }) + ); } } diff --git a/src/clarity_cli.rs b/src/clarity_cli.rs index a4dfcc13f6..809bd7ce2c 100644 --- a/src/clarity_cli.rs +++ b/src/clarity_cli.rs @@ -21,12 +21,23 @@ #![allow(non_upper_case_globals)] extern crate blockstack_lib; +extern crate serde_json; use blockstack_lib::{clarity, util::log}; use std::env; +use std::process; fn main() { let argv: Vec = env::args().collect(); - clarity::invoke_command(&argv[0], &argv[1..]); + let result = clarity::invoke_command(&argv[0], &argv[1..]); + match result { + (exit_code, Some(output)) => { + println!("{}", &serde_json::to_string(&output).unwrap()); + process::exit(exit_code); + } + (exit_code, None) => { + process::exit(exit_code); + } + } } diff --git a/src/clarity_vm/clarity.rs b/src/clarity_vm/clarity.rs index 0e8a9475a1..c191d1c666 100644 --- a/src/clarity_vm/clarity.rs +++ b/src/clarity_vm/clarity.rs @@ -639,6 +639,10 @@ impl<'a> ClarityBlockConnection<'a> { pub fn get_root_hash(&mut self) -> TrieHash { self.datastore.get_root_hash() } + + pub fn destruct(self) -> WritableMarfStore<'a> { + self.datastore + } } impl<'a, 'b> ClarityConnection for ClarityTransactionConnection<'a, 'b> { diff --git a/src/core/mod.rs b/src/core/mod.rs index 0ddfb4409d..c054fe84b8 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -121,6 +121,15 @@ pub const BLOCK_LIMIT_MAINNET: ExecutionCost = ExecutionCost { runtime: 5_000_000_000, }; +pub const HELIUM_BLOCK_LIMIT: ExecutionCost = ExecutionCost { + write_length: 15_0_000_000, + write_count: 5_0_000, + read_length: 1_000_000_000, + read_count: 5_0_000, + // allow much more runtime in helium blocks than mainnet + runtime: 100_000_000_000, +}; + pub const FAULT_DISABLE_MICROBLOCKS_COST_CHECK: &str = "MICROBLOCKS_DISABLE_COST_CHECK"; pub const FAULT_DISABLE_MICROBLOCKS_BYTES_CHECK: &str = "MICROBLOCKS_DISABLE_BYTES_CHECK"; diff --git a/src/vm/contexts.rs b/src/vm/contexts.rs index 8b7208a290..e047430aed 100644 --- a/src/vm/contexts.rs +++ b/src/vm/contexts.rs @@ -91,6 +91,88 @@ pub struct AssetMap { asset_map: HashMap>>, } +impl AssetMap { + pub fn to_json(&self) -> serde_json::Value { + let stx: serde_json::map::Map<_, _> = self + .stx_map + .iter() + .map(|(principal, amount)| { + ( + format!("{}", principal), + serde_json::value::Value::String(format!("{}", amount)), + ) + }) + .collect(); + + let burns: serde_json::map::Map<_, _> = self + .burn_map + .iter() + .map(|(principal, amount)| { + ( + format!("{}", principal), + serde_json::value::Value::String(format!("{}", amount)), + ) + }) + .collect(); + + let tokens: serde_json::map::Map<_, _> = self + .token_map + .iter() + .map(|(principal, token_map)| { + let token_json: serde_json::map::Map<_, _> = token_map + .iter() + .map(|(asset_id, amount)| { + ( + format!("{}", asset_id), + serde_json::value::Value::String(format!("{}", amount)), + ) + }) + .collect(); + + ( + format!("{}", principal), + serde_json::value::Value::Object(token_json), + ) + }) + .collect(); + + let assets: serde_json::map::Map<_, _> = self + .asset_map + .iter() + .map(|(principal, nft_map)| { + let nft_json: serde_json::map::Map<_, _> = nft_map + .iter() + .map(|(asset_id, nft_values)| { + let nft_array = nft_values + .iter() + .map(|nft_value| { + serde_json::value::Value::String(format!("{}", nft_value)) + }) + .collect(); + + ( + format!("{}", asset_id), + serde_json::value::Value::Array(nft_array), + ) + }) + .collect(); + + ( + format!("{}", principal), + serde_json::value::Value::Object(nft_json), + ) + }) + .collect(); + + json!({ + "stx": stx, + "burns": burns, + "tokens": tokens, + "assets": assets + }) + } +} + #[derive(Debug, Clone)] pub struct EventBatch { pub events: Vec, @@ -640,6 +722,10 @@ impl<'a> OwnedEnvironment<'a> { Ok((asset_map, event_batch)) } + pub fn get_cost_total(&self) -> ExecutionCost { + self.context.cost_track.get_total() + } + /// Destroys this environment, returning ownership of its database reference. /// If the context wasn't top-level (i.e., it had uncommitted data), return None, /// because the database is not guaranteed to be in a sane state. diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index f00b00104a..f3c1c0ae89 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -8,8 +8,8 @@ use rand::RngCore; use stacks::burnchains::bitcoin::BitcoinNetworkType; use stacks::burnchains::{MagicBytes, BLOCKSTACK_MAGIC_MAINNET}; use stacks::core::{ - BLOCK_LIMIT_MAINNET, CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, PEER_VERSION_MAINNET, - PEER_VERSION_TESTNET, + BLOCK_LIMIT_MAINNET, CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, HELIUM_BLOCK_LIMIT, + PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, }; use stacks::net::connection::ConnectionOptions; use stacks::net::{Neighbor, NeighborKey, PeerAddress}; @@ -408,15 +408,6 @@ lazy_static! { }; } -pub const HELIUM_BLOCK_LIMIT: ExecutionCost = ExecutionCost { - write_length: 15_0_000_000, - write_count: 5_0_000, - read_length: 1_000_000_000, - read_count: 5_0_000, - // allow much more runtime in helium blocks than mainnet - runtime: 100_000_000_000, -}; - impl Config { pub fn from_config_file(config_file: ConfigFile) -> Config { let default_node_config = NodeConfig::default(); diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 2a8bb52381..95f1b84cf4 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -132,7 +132,7 @@ fn set_processed_counter(blocks_processed: &BlocksProcessedCounter, value: u64) } #[cfg(not(test))] -fn set_processed_counter(_blocks_processed: &BlocksProcessedCounter, value: u64) {} +fn set_processed_counter(_blocks_processed: &BlocksProcessedCounter, _value: u64) {} /// Process artifacts from the tenure. /// At this point, we're modifying the chainstate, and merging the artifacts from the previous tenure.