From 0288a26106b1339b0d556b3cb0b0093595936a01 Mon Sep 17 00:00:00 2001 From: Hugo C <911307+hugocaillard@users.noreply.github.com> Date: Wed, 13 Mar 2024 18:21:34 +0100 Subject: [PATCH] feat: call private function in simnet (#1380) * feat: call private function in simnet * refactor: remove patch * refactor: call contract function from sdk * fix: clarity values handling * test: added callPrivateFn tests * fix: improve error message * chore: update chainhook dependencies * refactor: code improvements * test: add interpreter tests * chore: bump versions * refactor: address review --- Cargo.lock | 53 ++-- components/clarinet-sdk-wasm/Cargo.toml | 2 +- components/clarinet-sdk-wasm/src/core.rs | 104 ++++--- components/clarinet-sdk-wasm/src/utils/mod.rs | 1 - components/clarinet-sdk/package-lock.json | 12 +- components/clarinet-sdk/package.json | 4 +- components/clarinet-sdk/src/index.ts | 35 ++- .../tests/fixtures/contracts/counter.clar | 13 +- .../clarinet-sdk/tests/simnet-usage.test.ts | 52 +++- components/clarity-repl/Cargo.toml | 8 +- .../src/repl}/clarity_values.rs | 13 +- .../clarity-repl/src/repl/interpreter.rs | 278 +++++++++++++++++- components/clarity-repl/src/repl/mod.rs | 1 + components/clarity-repl/src/repl/session.rs | 65 +++- 14 files changed, 535 insertions(+), 106 deletions(-) rename components/{clarinet-sdk-wasm/src/utils => clarity-repl/src/repl}/clarity_values.rs (95%) diff --git a/Cargo.lock b/Cargo.lock index 1793fdaf7..dee331b65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,7 +661,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chainhook-sdk" version = "0.12.5" -source = "git+https://github.com/hirosystems/chainhook.git?branch=chore/update-clarinet-and-clarity#ccd1be54d401cec59ac2fb33a2510a3e06b17f6d" +source = "git+https://github.com/hirosystems/chainhook.git?branch=chore/update-clarinet-and-clarity#e8e7e9e3840c9c10957168d14761abe55e0bb22c" dependencies = [ "base58 0.2.0", "base64 0.21.7", @@ -687,7 +687,7 @@ dependencies = [ "serde-hex", "serde_derive", "serde_json", - "stacks-rpc-client 2.3.1 (git+https://github.com/hirosystems/clarinet.git?branch=feat/update-stacks-core)", + "stacks-rpc-client 2.3.1 (git+https://github.com/hirosystems/clarinet.git)", "threadpool", "tokio", ] @@ -695,7 +695,7 @@ dependencies = [ [[package]] name = "chainhook-types" version = "1.3.3" -source = "git+https://github.com/hirosystems/chainhook.git?branch=chore/update-clarinet-and-clarity#ccd1be54d401cec59ac2fb33a2510a3e06b17f6d" +source = "git+https://github.com/hirosystems/chainhook.git?branch=chore/update-clarinet-and-clarity#e8e7e9e3840c9c10957168d14761abe55e0bb22c" dependencies = [ "hex 0.4.3", "schemars", @@ -898,7 +898,7 @@ dependencies = [ [[package]] name = "clarinet-sdk-wasm" -version = "2.4.0-beta2" +version = "2.4.0-beta4" dependencies = [ "clarinet-deployments", "clarinet-files", @@ -928,7 +928,7 @@ dependencies = [ [[package]] name = "clarity" version = "2.3.0" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-next#2ab366d28d473f204b67a431ba8b522c0c9a8e78" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-next#b006cd68da6124c29d48806cde6272a19efdd8ce" dependencies = [ "getrandom 0.2.8", "hashbrown 0.14.3", @@ -1041,16 +1041,17 @@ dependencies = [ [[package]] name = "clarity-repl" version = "2.3.1" -source = "git+https://github.com/hirosystems/clarinet.git?branch=feat/update-stacks-core#11f77eebff53776838c3f64308423f531b5468c3" +source = "git+https://github.com/hirosystems/clarinet.git#dbc0178a1957eb507ac1979ffefb7798ddc6614c" dependencies = [ "ansi_term", "atty", "chrono", "clarity", "getrandom 0.2.8", - "hiro-system-kit 0.1.0 (git+https://github.com/hirosystems/clarinet.git?branch=feat/update-stacks-core)", + "hiro-system-kit 0.1.0 (git+https://github.com/hirosystems/clarinet.git)", "integer-sqrt", "lazy_static", + "pox-locking", "regex", "reqwest", "serde", @@ -2264,7 +2265,7 @@ dependencies = [ [[package]] name = "hiro-system-kit" version = "0.1.0" -source = "git+https://github.com/hirosystems/clarinet.git?branch=feat/update-stacks-core#11f77eebff53776838c3f64308423f531b5468c3" +source = "git+https://github.com/hirosystems/clarinet.git#dbc0178a1957eb507ac1979ffefb7798ddc6614c" dependencies = [ "ansi_term", "atty", @@ -2630,9 +2631,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -3509,7 +3510,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "pox-locking" version = "2.4.0" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-next#2ab366d28d473f204b67a431ba8b522c0c9a8e78" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-next#b006cd68da6124c29d48806cde6272a19efdd8ce" dependencies = [ "clarity", "slog", @@ -4774,7 +4775,7 @@ dependencies = [ [[package]] name = "stacks-common" version = "0.0.2" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-next#2ab366d28d473f204b67a431ba8b522c0c9a8e78" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-next#b006cd68da6124c29d48806cde6272a19efdd8ce" dependencies = [ "chrono", "curve25519-dalek 2.0.0", @@ -4875,9 +4876,9 @@ dependencies = [ [[package]] name = "stacks-rpc-client" version = "2.3.1" -source = "git+https://github.com/hirosystems/clarinet.git?branch=feat/update-stacks-core#11f77eebff53776838c3f64308423f531b5468c3" +source = "git+https://github.com/hirosystems/clarinet.git#dbc0178a1957eb507ac1979ffefb7798ddc6614c" dependencies = [ - "clarity-repl 2.3.1 (git+https://github.com/hirosystems/clarinet.git?branch=feat/update-stacks-core)", + "clarity-repl 2.3.1 (git+https://github.com/hirosystems/clarinet.git)", "hmac 0.12.1", "libsecp256k1 0.7.1", "pbkdf2", @@ -5649,9 +5650,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5659,9 +5660,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -5686,9 +5687,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5696,9 +5697,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -5709,9 +5710,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-encoder" @@ -6063,9 +6064,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/components/clarinet-sdk-wasm/Cargo.toml b/components/clarinet-sdk-wasm/Cargo.toml index 5fa1ff4e5..f85766ba8 100644 --- a/components/clarinet-sdk-wasm/Cargo.toml +++ b/components/clarinet-sdk-wasm/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "clarinet-sdk-wasm" -version = "2.4.0-beta2" +version = "2.4.0-beta4" license = "GPL-3.0" repository = "https://github.com/hirosystems/clarinet" description = "The core lib that powers @hirosystems/clarinet-sdk" diff --git a/components/clarinet-sdk-wasm/src/core.rs b/components/clarinet-sdk-wasm/src/core.rs index 41f4e2a7a..3c3e3c0ab 100644 --- a/components/clarinet-sdk-wasm/src/core.rs +++ b/components/clarinet-sdk-wasm/src/core.rs @@ -18,9 +18,10 @@ use clarity_repl::clarity::vm::types::QualifiedContractIdentifier; use clarity_repl::clarity::{ ClarityVersion, EvaluationResult, ExecutionResult, ParsedContract, StacksEpochId, }; +use clarity_repl::repl::clarity_values::{uint8_to_string, uint8_to_value}; use clarity_repl::repl::{ - ClarityCodeSource, ClarityContract, ContractDeployer, Session, DEFAULT_CLARITY_VERSION, - DEFAULT_EPOCH, + clarity_values, ClarityCodeSource, ClarityContract, ContractDeployer, Session, + DEFAULT_CLARITY_VERSION, DEFAULT_EPOCH, }; use colored::*; use gloo_utils::format::JsValueSerdeExt; @@ -33,7 +34,6 @@ use std::{panic, path::PathBuf}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; -use crate::utils::clarity_values::{self, uint8_to_string, uint8_to_value}; use crate::utils::costs::SerializableCostsReport; use crate::utils::events::serialize_event; @@ -74,7 +74,7 @@ struct CallContractArgsJSON { #[derive(Debug, Deserialize)] #[wasm_bindgen] -pub struct CallContractArgs { +pub struct CallFnArgs { contract: String, method: String, args: Vec>, @@ -82,7 +82,7 @@ pub struct CallContractArgs { } #[wasm_bindgen] -impl CallContractArgs { +impl CallFnArgs { #[wasm_bindgen(constructor)] pub fn new( contract: String, @@ -200,6 +200,7 @@ impl TransferSTXArgs { #[serde(rename_all = "camelCase")] #[wasm_bindgen] pub struct TxArgs { + call_private_fn: Option, call_public_fn: Option, deploy_contract: Option, #[serde(rename(serialize = "transfer_stx", deserialize = "transferSTX"))] @@ -634,76 +635,82 @@ impl SDK { .ok_or(format!("contract {contract} has no function {method}")) } - fn invoke_contract_call( + fn call_contract_fn( &mut self, - call_contract_args: &CallContractArgs, - test_name: &str, - ) -> Result { - let CallContractArgs { + CallFnArgs { contract, method, args, sender, - } = call_contract_args; - - let clarity_args: Vec = args.iter().map(|a| uint8_to_string(a)).collect(); - + }: &CallFnArgs, + allow_private: bool, + ) -> Result { + let test_name = self.current_test_name.clone(); let session = self.get_session_mut(); - let (execution, _) = match session.invoke_contract_call( - contract, - method, - &clarity_args, - sender, - test_name.into(), - ) { - Ok(res) => res, - Err(diagnostics) => { + let execution = session + .call_contract_fn(contract, method, args, sender, allow_private, test_name) + .map_err(|diagnostics| { let mut message = format!( "{}: {}::{}({})", - "Contract call error", + "Call contract function error", contract, method, - clarity_args.join(", ") + args.iter() + .map(|a| uint8_to_string(a)) + .collect::>() + .join(", ") ); if let Some(diag) = diagnostics.last() { message = format!("{} -> {}", message, diag.message); } - return Err(message); - } - }; + message + })?; Ok(execution_result_to_transaction_res(&execution)) } #[wasm_bindgen(js_name=callReadOnlyFn)] - pub fn call_read_only_fn(&mut self, args: &CallContractArgs) -> Result { + pub fn call_read_only_fn(&mut self, args: &CallFnArgs) -> Result { let interface = self.get_function_interface(&args.contract, &args.method)?; if interface.access != ContractInterfaceFunctionAccess::read_only { return Err(format!("{} is not a read-only function", &args.method)); } - - self.invoke_contract_call(args, &self.current_test_name.clone()) + self.call_contract_fn(args, false) } - fn call_public_fn_private( + fn inner_call_public_fn( &mut self, - args: &CallContractArgs, + args: &CallFnArgs, advance_chain_tip: bool, ) -> Result { let interface = self.get_function_interface(&args.contract, &args.method)?; if interface.access != ContractInterfaceFunctionAccess::public { return Err(format!("{} is not a public function", &args.method)); } - let session = self.get_session_mut(); if advance_chain_tip { session.advance_chain_tip(1); } + self.call_contract_fn(args, false) + } - self.invoke_contract_call(args, &self.current_test_name.clone()) + fn inner_call_private_fn( + &mut self, + args: &CallFnArgs, + advance_chain_tip: bool, + ) -> Result { + let interface = self.get_function_interface(&args.contract, &args.method)?; + if interface.access != ContractInterfaceFunctionAccess::private { + return Err(format!("{} is not a private function", &args.method)); + } + let session = self.get_session_mut(); + if advance_chain_tip { + session.advance_chain_tip(1); + } + self.call_contract_fn(args, true) } - fn transfer_stx_private( + fn inner_transfer_stx( &mut self, args: &TransferSTXArgs, advance_chain_tip: bool, @@ -730,7 +737,7 @@ impl SDK { Ok(execution_result_to_transaction_res(&execution)) } - fn deploy_contract_private( + fn inner_deploy_contract( &mut self, args: &DeployContractArgs, advance_chain_tip: bool, @@ -775,17 +782,22 @@ impl SDK { #[wasm_bindgen(js_name=deployContract)] pub fn deploy_contract(&mut self, args: &DeployContractArgs) -> Result { - self.deploy_contract_private(args, true) + self.inner_deploy_contract(args, true) } #[wasm_bindgen(js_name = "transferSTX")] pub fn transfer_stx(&mut self, args: &TransferSTXArgs) -> Result { - self.transfer_stx_private(args, true) + self.inner_transfer_stx(args, true) } #[wasm_bindgen(js_name = "callPublicFn")] - pub fn call_public_fn(&mut self, args: &CallContractArgs) -> Result { - self.call_public_fn_private(args, true) + pub fn call_public_fn(&mut self, args: &CallFnArgs) -> Result { + self.inner_call_public_fn(args, true) + } + + #[wasm_bindgen(js_name = "callPrivateFn")] + pub fn call_private_fn(&mut self, args: &CallFnArgs) -> Result { + self.inner_call_private_fn(args, true) } #[wasm_bindgen(js_name=mineBlock)] @@ -797,12 +809,14 @@ impl SDK { .map_err(|e| format!("Failed to parse js txs: {:}", e))?; for tx in txs { - let result = if let Some(args) = tx.call_public_fn { - self.call_public_fn_private(&CallContractArgs::from_json_args(args), false) + let result = if let Some(call_public) = tx.call_public_fn { + self.inner_call_public_fn(&CallFnArgs::from_json_args(call_public), false) + } else if let Some(call_private) = tx.call_private_fn { + self.inner_call_private_fn(&CallFnArgs::from_json_args(call_private), false) } else if let Some(transfer_stx) = tx.transfer_stx { - self.transfer_stx_private(&transfer_stx, false) + self.inner_transfer_stx(&transfer_stx, false) } else if let Some(deploy_contract) = tx.deploy_contract { - self.deploy_contract_private(&deploy_contract, false) + self.inner_deploy_contract(&deploy_contract, false) } else { return Err("Invalid tx arguments".into()); }?; diff --git a/components/clarinet-sdk-wasm/src/utils/mod.rs b/components/clarinet-sdk-wasm/src/utils/mod.rs index 4dfaa7cd9..539658aaf 100644 --- a/components/clarinet-sdk-wasm/src/utils/mod.rs +++ b/components/clarinet-sdk-wasm/src/utils/mod.rs @@ -1,3 +1,2 @@ -pub mod clarity_values; pub mod costs; pub mod events; diff --git a/components/clarinet-sdk/package-lock.json b/components/clarinet-sdk/package-lock.json index 3625943d7..78c18205f 100644 --- a/components/clarinet-sdk/package-lock.json +++ b/components/clarinet-sdk/package-lock.json @@ -1,15 +1,15 @@ { "name": "@hirosystems/clarinet-sdk", - "version": "2.4.0-beta2", + "version": "2.4.0-beta4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hirosystems/clarinet-sdk", - "version": "2.4.0-beta2", + "version": "2.4.0-beta4", "license": "GPL-3.0", "dependencies": { - "@hirosystems/clarinet-sdk-wasm": "^2.4.0-beta2", + "@hirosystems/clarinet-sdk-wasm": "^2.4.0-beta3", "@stacks/encryption": "^6.12.0", "@stacks/network": "^6.11.3", "@stacks/stacking": "^6.11.4-pr.36558cf.0", @@ -381,9 +381,9 @@ } }, "node_modules/@hirosystems/clarinet-sdk-wasm": { - "version": "2.4.0-beta2", - "resolved": "https://registry.npmjs.org/@hirosystems/clarinet-sdk-wasm/-/clarinet-sdk-wasm-2.4.0-beta2.tgz", - "integrity": "sha512-gKqh5yf5IuLfAosdbLYZusP+mIu2vFac8gztM7y8JoahRdg9dru24B/ycQoQXlTZUN/YzMhr+vMV3jS4vJtZEA==" + "version": "2.4.0-beta3", + "resolved": "https://registry.npmjs.org/@hirosystems/clarinet-sdk-wasm/-/clarinet-sdk-wasm-2.4.0-beta3.tgz", + "integrity": "sha512-m4PHoE38F+YzH5WDwK5CuRs3/RZWGstIPx4bq2vX6ut1ETE2S9LkS8q91RFF4FnZHnI5f8LwxflTbaxE+RSNrA==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", diff --git a/components/clarinet-sdk/package.json b/components/clarinet-sdk/package.json index 97d55f065..ddec4c866 100644 --- a/components/clarinet-sdk/package.json +++ b/components/clarinet-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/clarinet-sdk", - "version": "2.4.0-beta2", + "version": "2.4.0-beta4", "description": "A SDK to interact with Clarity Smart Contracts", "homepage": "https://docs.hiro.so/clarinet/feature-guides/clarinet-js-sdk", "repository": { @@ -58,7 +58,7 @@ "license": "GPL-3.0", "readme": "./README.md", "dependencies": { - "@hirosystems/clarinet-sdk-wasm": "^2.4.0-beta2", + "@hirosystems/clarinet-sdk-wasm": "^2.4.0-beta3", "@stacks/encryption": "^6.12.0", "@stacks/network": "^6.11.3", "@stacks/stacking": "^6.11.4-pr.36558cf.0", diff --git a/components/clarinet-sdk/src/index.ts b/components/clarinet-sdk/src/index.ts index 15e90774d..3a5f2f62b 100644 --- a/components/clarinet-sdk/src/index.ts +++ b/components/clarinet-sdk/src/index.ts @@ -2,7 +2,7 @@ import { Cl, ClarityValue } from "@stacks/transactions"; import { SDK, TransactionRes, - CallContractArgs, + CallFnArgs, DeployContractArgs, TransferSTXArgs, ContractOptions, @@ -61,11 +61,24 @@ export type Tx = args: ClarityValue[]; sender: string; }; + callPrivateFn?: never; deployContract?: never; transferSTX?: never; } | { callPublicFn?: never; + callPrivateFn: { + contract: string; + method: string; + args: ClarityValue[]; + sender: string; + }; + deployContract?: never; + transferSTX?: never; + } + | { + callPublicFn?: never; + callPrivateFn?: never; deployContract: { name: string; content: string; @@ -76,6 +89,7 @@ export type Tx = } | { callPublicFn?: never; + callPrivateFn?: never; deployContradct?: never; transferSTX: { amount: number; recipient: string; sender: string }; }; @@ -84,6 +98,9 @@ export const tx = { callPublicFn: (contract: string, method: string, args: ClarityValue[], sender: string): Tx => ({ callPublicFn: { contract, method, args, sender }, }), + callPrivateFn: (contract: string, method: string, args: ClarityValue[], sender: string): Tx => ({ + callPrivateFn: { contract, method, args, sender }, + }), deployContract: ( name: string, content: string, @@ -106,7 +123,7 @@ export type RunSnippet = (snippet: string) => ClarityValue | string; // because the session is wrapped in a proxy the types need to be hardcoded export type Simnet = { - [K in keyof SDK]: K extends "callReadOnlyFn" | "callPublicFn" + [K in keyof SDK]: K extends "callReadOnlyFn" | "callPublicFn" | "callPrivateFn" ? CallFn : K extends "runSnippet" ? RunSnippet @@ -159,10 +176,10 @@ const getSessionProxy = () => ({ // - serialize clarity values input argument // - deserialize output into clarity values - if (prop === "callReadOnlyFn" || prop === "callPublicFn") { + if (prop === "callReadOnlyFn" || prop === "callPublicFn" || prop === "callPrivateFn") { const callFn: CallFn = (contract, method, args, sender) => { const response = session[prop]( - new CallContractArgs( + new CallFnArgs( contract, method, args.map((a) => Cl.serialize(a)), @@ -215,7 +232,15 @@ const getSessionProxy = () => ({ return { callPublicFn: { ...tx.callPublicFn, - args_maps: tx.callPublicFn.args.map((a) => Cl.serialize(a)), + args_maps: tx.callPublicFn.args.map(Cl.serialize), + }, + }; + } + if (tx.callPrivateFn) { + return { + callPrivateFn: { + ...tx.callPrivateFn, + args_maps: tx.callPrivateFn.args.map(Cl.serialize), }, }; } diff --git a/components/clarinet-sdk/tests/fixtures/contracts/counter.clar b/components/clarinet-sdk/tests/fixtures/contracts/counter.clar index 048de6e8b..5daee7327 100644 --- a/components/clarinet-sdk/tests/fixtures/contracts/counter.clar +++ b/components/clarinet-sdk/tests/fixtures/contracts/counter.clar @@ -9,15 +9,22 @@ (ok { count: (var-get count) }) ) -(define-public (increment) +(define-private (inner-increment) (begin - (print "call increment") + (print "call inner-increment") (if (is-none (map-get? participants tx-sender)) (map-insert participants tx-sender true) (map-set participants tx-sender true) ) + (var-set count (+ (var-get count) u1)) + ) +) + +(define-public (increment) + (begin + (print "call increment") (try! (stx-transfer? u1000000 tx-sender (as-contract tx-sender))) - (ok (var-set count (+ (var-get count) u1))) + (ok (inner-increment)) ) ) diff --git a/components/clarinet-sdk/tests/simnet-usage.test.ts b/components/clarinet-sdk/tests/simnet-usage.test.ts index 4ed0dd368..7e331856c 100644 --- a/components/clarinet-sdk/tests/simnet-usage.test.ts +++ b/components/clarinet-sdk/tests/simnet-usage.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { Cl } from "@stacks/transactions"; +import { Cl, cvToValue } from "@stacks/transactions"; import { describe, expect, it, beforeEach, afterEach, assert } from "vitest"; // test the built package and not the source code @@ -114,7 +114,7 @@ describe("simnet can call contracts function", () => { expect(res).toHaveProperty("events"); expect(res.result).toStrictEqual(Cl.ok(Cl.bool(true))); - expect(res.events).toHaveLength(2); + expect(res.events).toHaveLength(3); const printEvent = res.events[0]; expect(printEvent.event).toBe("print_event"); expect(printEvent.data.value).toStrictEqual(Cl.stringAscii("call increment")); @@ -156,6 +156,43 @@ describe("simnet can call contracts function", () => { expect(simnet.blockHeight).toStrictEqual(initalBH + 1); }); + it("can call private functions", () => { + const { result, events } = simnet.callPrivateFn("counter", "inner-increment", [], address1); + expect(events).toHaveLength(1); + expect(result).toStrictEqual(Cl.bool(true)); + }); + + it("can call public and private functions in the same block", () => { + const initalBH = simnet.blockHeight; + + const res = simnet.mineBlock([ + tx.callPrivateFn("counter", "inner-increment", [], address1), + tx.callPublicFn("counter", "increment", [], address1), + tx.callPrivateFn("counter", "inner-increment", [], address1), + ]); + + expect(res[0].result).toStrictEqual(Cl.bool(true)); + expect(res[1].result).toStrictEqual(Cl.ok(Cl.bool(true))); + expect(res[2].result).toStrictEqual(Cl.bool(true)); + + const counterVal = simnet.callReadOnlyFn("counter", "get-count", [], address1); + expect(counterVal.result).toStrictEqual(Cl.ok(Cl.tuple({ count: Cl.uint(3) }))); + + expect(simnet.blockHeight).toStrictEqual(initalBH + 1); + }); + + it("can not call a public function with callPrivateFn", () => { + expect(() => { + simnet.callPrivateFn("counter", "increment", [], address1); + }).toThrow("increment is not a private function"); + }); + + it("can not call a private function with callPublicFn", () => { + expect(() => { + simnet.callPublicFn("counter", "inner-increment", [], address1); + }).toThrow("increment is not a public function"); + }); + it("can get updated assets map", () => { simnet.callPublicFn("counter", "increment", [], address1); simnet.callPublicFn("counter", "increment", [], address1); @@ -206,7 +243,7 @@ describe("simnet can get contracts info and deploy contracts", () => { const counterInterface = contractInterfaces.get(`${deployerAddr}.counter`); expect(counterInterface).not.toBeNull(); - expect(counterInterface?.functions).toHaveLength(6); + expect(counterInterface?.functions).toHaveLength(7); expect(counterInterface?.variables).toHaveLength(2); expect(counterInterface?.maps).toHaveLength(1); }); @@ -225,7 +262,7 @@ describe("simnet can get contracts info and deploy contracts", () => { it("can get contract ast", () => { const counterAst = simnet.getContractAST(`${deployerAddr}.counter`); expect(counterAst).toBeDefined(); - expect(counterAst.expressions).toHaveLength(10); + expect(counterAst.expressions).toHaveLength(11); const getWithShortAddr = simnet.getContractAST("counter"); expect(getWithShortAddr).toBeDefined(); @@ -294,8 +331,15 @@ describe("simnet can get session reports", () => { it("can get line coverage", () => { simnet.callPublicFn("counter", "increment", [], address1); simnet.callPublicFn("counter", "increment", [], address1); + simnet.callPrivateFn("counter", "inner-increment", [], address1); const reports = simnet.collectReport(); + + // increment is called twice + expect(reports.coverage.includes("FNDA:2,increment")).toBe(true); + // inner-increment is called one time directly and twice by `increment` + expect(reports.coverage.includes("FNDA:3,inner-increment")).toBe(true); + expect(reports.coverage.startsWith("TN:")).toBe(true); expect(reports.coverage.endsWith("end_of_record\n")).toBe(true); }); diff --git a/components/clarity-repl/Cargo.toml b/components/clarity-repl/Cargo.toml index 8e9ae432e..81469b305 100644 --- a/components/clarity-repl/Cargo.toml +++ b/components/clarity-repl/Cargo.toml @@ -108,7 +108,13 @@ dap = [ "memchr", "log", ] -wasm = ["wasm-bindgen", "wasm-bindgen-futures", "clarity/wasm", "pox-locking/wasm"] +wasm = [ + "wasm-bindgen", + "wasm-bindgen-futures", + "clarity/wasm", + "clarity/developer-mode", + "pox-locking/wasm" +] [package.metadata.wasm-pack.profile.release.wasm-bindgen] debug-js-glue = false diff --git a/components/clarinet-sdk-wasm/src/utils/clarity_values.rs b/components/clarity-repl/src/repl/clarity_values.rs similarity index 95% rename from components/clarinet-sdk-wasm/src/utils/clarity_values.rs rename to components/clarity-repl/src/repl/clarity_values.rs index cd925ad8f..5f71abd12 100644 --- a/components/clarinet-sdk-wasm/src/utils/clarity_values.rs +++ b/components/clarity-repl/src/repl/clarity_values.rs @@ -1,9 +1,8 @@ -use clarity_repl::clarity::{ - codec::StacksMessageCodec, - util::hash, - vm::types::{CharType, SequenceData}, +use clarity::vm::{ + types::{CharType, SequenceData}, Value, }; +use clarity::{codec::StacksMessageCodec, util::hash}; pub fn to_raw_value(value: &Value) -> String { let mut bytes = vec![]; @@ -75,12 +74,12 @@ fn value_to_string(value: &Value) -> String { #[cfg(test)] mod tests { use super::value_to_string; - use clarity_repl::clarity::vm::types::{ + use clarity::vm::types::{ ASCIIData, CharType, ListData, ListTypeData, OptionalData, PrincipalData, QualifiedContractIdentifier, ResponseData, SequenceData, SequencedValue, StandardPrincipalData, TupleData, TypeSignature, UTF8Data, NONE, }; - use clarity_repl::clarity::vm::{ClarityName, Value}; + use clarity::vm::{ClarityName, Value}; use std::convert::TryFrom; #[test] @@ -150,7 +149,7 @@ mod tests { )))); assert_eq!(s, "\"Hello, \"world\"\n\""); - s = value_to_string(&UTF8Data::to_value(&"Hello, 'world'\n".as_bytes().to_vec())); + s = value_to_string(&UTF8Data::to_value(&"Hello, 'world'\n".as_bytes().to_vec()).unwrap()); assert_eq!(s, "u\"Hello, 'world'\n\""); s = value_to_string(&Value::Sequence(SequenceData::List(ListData { diff --git a/components/clarity-repl/src/repl/interpreter.rs b/components/clarity-repl/src/repl/interpreter.rs index fce4fcd70..4916b441d 100644 --- a/components/clarity-repl/src/repl/interpreter.rs +++ b/components/clarity-repl/src/repl/interpreter.rs @@ -7,6 +7,7 @@ use crate::repl::datastore::BurnDatastore; use crate::repl::datastore::Datastore; use crate::repl::Settings; use clarity::consts::CHAIN_ID_TESTNET; +use clarity::types::StacksEpochId; use clarity::vm::analysis::ContractAnalysis; use clarity::vm::ast::{build_ast_with_diagnostics, ContractAST}; #[cfg(feature = "cli")] @@ -16,7 +17,7 @@ use clarity::vm::contracts::Contract; use clarity::vm::costs::{ExecutionCost, LimitedCostTracker}; use clarity::vm::database::{ClarityDatabase, StoreType}; use clarity::vm::diagnostic::{Diagnostic, Level}; -use clarity::vm::events::*; +use clarity::vm::errors::Error; use clarity::vm::representations::SymbolicExpressionType::{Atom, List}; use clarity::vm::representations::{Span, SymbolicExpression}; use clarity::vm::types::{ @@ -24,6 +25,7 @@ use clarity::vm::types::{ }; use clarity::vm::{analysis::AnalysisDatabase, database::ClarityBackingStore}; use clarity::vm::{eval, eval_all, EvaluationResult, SnippetEvaluationResult}; +use clarity::vm::{events::*, ClarityVersion}; use clarity::vm::{ContractEvaluationResult, EvalHook}; use clarity::vm::{CostSynthesis, ExecutionResult, ParsedContract}; @@ -496,7 +498,7 @@ impl ClarityInterpreter { &mut conn, contract.epoch, ) - .expect("failed to initialize cost tracker") + .map_err(|e| format!("failed to initialize cost tracker: {e}"))? } else { LimitedCostTracker::new_free() }; @@ -725,7 +727,7 @@ impl ClarityInterpreter { &mut conn, contract.epoch, ) - .expect("failed to initialize cost tracker") + .map_err(|e| format!("failed to initialize cost tracker: {e}"))? } else { LimitedCostTracker::new_free() }; @@ -932,6 +934,132 @@ impl ClarityInterpreter { Ok(execution_result) } + pub fn call_contract_fn( + &mut self, + contract_id: &QualifiedContractIdentifier, + method: &str, + raw_args: &[Vec], + epoch: StacksEpochId, + clarity_version: ClarityVersion, + cost_track: bool, + allow_private: bool, + eval_hooks: Option>, + ) -> Result { + let mut conn = ClarityDatabase::new( + &mut self.datastore, + &self.burn_datastore, + &self.burn_datastore, + ); + let tx_sender: PrincipalData = self.tx_sender.clone().into(); + conn.begin(); + conn.set_clarity_epoch_version(epoch) + .map_err(|e| e.to_string())?; + conn.commit().map_err(|e| e.to_string())?; + let cost_tracker = if cost_track { + LimitedCostTracker::new( + false, + CHAIN_ID_TESTNET, + BLOCK_LIMIT_MAINNET.clone(), + &mut conn, + epoch, + ) + .map_err(|e| format!("failed to initialize cost tracker: {e}"))? + } else { + LimitedCostTracker::new_free() + }; + + let mut global_context = + GlobalContext::new(false, CHAIN_ID_TESTNET, conn, cost_tracker, epoch); + + if let Some(mut in_hooks) = eval_hooks { + let mut hooks: Vec<&mut dyn EvalHook> = Vec::new(); + for hook in in_hooks.drain(..) { + hooks.push(hook); + } + global_context.eval_hooks = Some(hooks); + } + + let contract_context = ContractContext::new(contract_id.clone(), clarity_version); + + global_context.begin(); + let result = global_context.execute(|g| { + let mut call_stack = CallStack::new(); + let mut env = Environment::new( + g, + &contract_context, + &mut call_stack, + Some(tx_sender.clone()), + Some(tx_sender.clone()), + None, + ); + + let mut args = vec![]; + for arg in raw_args { + let value = + Value::deserialize_read(&mut arg.as_slice(), None, false).map_err(|_| { + Error::Unchecked(clarity::vm::errors::CheckErrors::InvalidUTF8Encoding) + })?; + args.push(SymbolicExpression::atom_value(value)); + } + + match allow_private { + true => env.execute_contract_allow_private(contract_id, method, &args, false), + false => env.execute_contract(contract_id, method, &args, false), + } + }); + + let value = result.map_err(|e| { + let err = format!("Runtime error while interpreting {}: {:?}", contract_id, e); + if let Some(mut eval_hooks) = global_context.eval_hooks.take() { + for hook in eval_hooks.iter_mut() { + hook.did_complete(Err(err.clone())); + } + global_context.eval_hooks = Some(eval_hooks); + } + err + })?; + + let mut cost = None; + if cost_track { + cost = Some(CostSynthesis::from_cost_tracker(&global_context.cost_track)); + } + + let mut emitted_events = global_context + .event_batches + .iter() + .flat_map(|b| b.events.clone()) + .collect::>(); + + let eval_result = EvaluationResult::Snippet(SnippetEvaluationResult { result: value }); + global_context.commit().unwrap(); + + let (events, mut accounts_to_credit, mut accounts_to_debit) = + Self::process_events(&mut emitted_events); + + let mut execution_result = ExecutionResult { + result: eval_result, + events, + cost, + diagnostics: Vec::new(), + }; + + if let Some(mut eval_hooks) = global_context.eval_hooks { + for hook in eval_hooks.iter_mut() { + hook.did_complete(Ok(&mut execution_result)); + } + } + + for (account, token, value) in accounts_to_credit.drain(..) { + self.credit_token(account, token, value); + } + + for (account, token, value) in accounts_to_debit.drain(..) { + self.debit_token(account, token, value); + } + + Ok(execution_result) + } + fn process_events( emitted_events: &mut Vec, ) -> ( @@ -1159,7 +1287,7 @@ mod tests { }; use clarity::{ types::{chainstate::StacksAddress, Address}, - vm::{self}, + vm::{self, ClarityVersion}, }; #[test] @@ -1375,6 +1503,31 @@ mod tests { assert!(events.is_empty()); } + #[test] + fn test_call_contract_fn() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + + let contract = ClarityContract::fixture(); + let source = contract.expect_in_memory_code_source(); + let (mut ast, ..) = interpreter.build_ast(&contract); + let (annotations, _) = interpreter.collect_annotations(source); + + let (analysis, _) = interpreter + .run_analysis(&contract, &mut ast, &annotations) + .unwrap(); + + let result = interpreter.execute(&contract, &mut ast, analysis, false, None); + assert!(result.is_ok()); + let ExecutionResult { + diagnostics, + events, + .. + } = result.unwrap(); + assert!(diagnostics.is_empty()); + assert!(events.is_empty()); + } + #[test] fn test_run_both() { let mut interpreter = @@ -1633,4 +1786,121 @@ mod tests { assert!(res.diagnostics.is_empty()); } } + + #[test] + fn can_call_a_public_function() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + + let contract = ClarityContractBuilder::default() + .code_source("(define-public (public-func) (ok true))".into()) + .build(); + let source = contract.expect_in_memory_code_source(); + let (mut ast, ..) = interpreter.build_ast(&contract); + let (annotations, _) = interpreter.collect_annotations(source); + + let (analysis, _) = interpreter + .run_analysis(&contract, &mut ast, &annotations) + .unwrap(); + + let _ = interpreter.execute(&contract, &mut ast, analysis, false, None); + + let allow_private = false; + let result = interpreter.call_contract_fn( + &contract + .expect_resolved_contract_identifier(Some(&StandardPrincipalData::transient())), + "public-func", + &vec![], + StacksEpochId::Epoch24, + ClarityVersion::Clarity2, + false, + allow_private, + None, + ); + + assert!(result.is_ok()); + let ExecutionResult { result, .. } = result.unwrap(); + + assert!( + matches!(result, EvaluationResult::Snippet(SnippetEvaluationResult { result }) if result == Value::okay_true()) + ); + } + + #[test] + fn can_call_a_private_function() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + + let contract = ClarityContractBuilder::default() + .code_source("(define-private (private-func) true)".into()) + .build(); + let source = contract.expect_in_memory_code_source(); + let (mut ast, ..) = interpreter.build_ast(&contract); + let (annotations, _) = interpreter.collect_annotations(source); + + let (analysis, _) = interpreter + .run_analysis(&contract, &mut ast, &annotations) + .unwrap(); + + let _ = interpreter.execute(&contract, &mut ast, analysis, false, None); + + let allow_private = true; + let result = interpreter.call_contract_fn( + &contract + .expect_resolved_contract_identifier(Some(&StandardPrincipalData::transient())), + "private-func", + &vec![], + StacksEpochId::Epoch24, + ClarityVersion::Clarity2, + false, + allow_private, + None, + ); + + assert!(result.is_ok()); + let ExecutionResult { result, .. } = result.unwrap(); + + assert!( + matches!(result, EvaluationResult::Snippet(SnippetEvaluationResult { result }) if result == Value::Bool(true)) + ); + } + + #[test] + fn can_not_call_a_private_function_without_allow_private() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + + let contract = ClarityContractBuilder::default() + .code_source("(define-private (private-func) true)".into()) + .build(); + let source = contract.expect_in_memory_code_source(); + let (mut ast, ..) = interpreter.build_ast(&contract); + let (annotations, _) = interpreter.collect_annotations(source); + + let (analysis, _) = interpreter + .run_analysis(&contract, &mut ast, &annotations) + .unwrap(); + + let _ = interpreter.execute(&contract, &mut ast, analysis, false, None); + + let allow_private = false; + let result = interpreter.call_contract_fn( + &contract + .expect_resolved_contract_identifier(Some(&StandardPrincipalData::transient())), + "private-func", + &vec![], + StacksEpochId::Epoch24, + ClarityVersion::Clarity2, + false, + allow_private, + None, + ); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!( + err.to_string(), + "Runtime error while interpreting S1G2081040G2081040G2081040G208105NK8PE5.contract: Unchecked(NoSuchPublicFunction(\"S1G2081040G2081040G2081040G208105NK8PE5.contract\", \"private-func\"))" + ); + } } diff --git a/components/clarity-repl/src/repl/mod.rs b/components/clarity-repl/src/repl/mod.rs index c55e49fa1..823452828 100644 --- a/components/clarity-repl/src/repl/mod.rs +++ b/components/clarity-repl/src/repl/mod.rs @@ -1,4 +1,5 @@ pub mod boot; +pub mod clarity_values; pub mod datastore; pub mod diagnostic; pub mod interpreter; diff --git a/components/clarity-repl/src/repl/session.rs b/components/clarity-repl/src/repl/session.rs index 96418c205..39b3deb99 100644 --- a/components/clarity-repl/src/repl/session.rs +++ b/components/clarity-repl/src/repl/session.rs @@ -1,4 +1,5 @@ use super::boot::{STACKS_BOOT_CODE_MAINNET, STACKS_BOOT_CODE_TESTNET}; +use super::clarity_values::uint8_to_string; use super::diagnostic::output_diagnostic; use super::{ClarityCodeSource, ClarityContract, ClarityInterpreter, ContractDeployer}; use crate::analysis::coverage::TestCoverageReport; @@ -562,7 +563,7 @@ impl Session { let contract_id = if contract.starts_with('S') { contract.to_string() } else { - format!("{}.{}", initial_tx_sender, contract,) + format!("{}.{}", initial_tx_sender, contract) }; let mut hooks: Vec<&mut dyn EvalHook> = vec![]; @@ -611,6 +612,68 @@ impl Session { Ok((execution, contract_identifier)) } + pub fn call_contract_fn( + &mut self, + contract: &str, + method: &str, + args: &[Vec], + sender: &str, + allow_private: bool, + test_name: String, + ) -> Result> { + let initial_tx_sender = self.get_tx_sender(); + // Handle fully qualified contract_id and sugared syntax + let contract_id_str = if contract.starts_with('S') { + contract.to_string() + } else { + format!("{}.{}", initial_tx_sender, contract) + }; + let contract_id = QualifiedContractIdentifier::parse(&contract_id_str).unwrap(); + + let mut hooks: Vec<&mut dyn EvalHook> = vec![]; + let mut coverage = TestCoverageReport::new(test_name.clone()); + hooks.push(&mut coverage); + + let clarity_version = ClarityVersion::default_for_epoch(self.current_epoch); + + self.set_tx_sender(sender.into()); + let execution = match self.interpreter.call_contract_fn( + &contract_id, + method, + args, + self.current_epoch, + clarity_version, + true, + allow_private, + Some(hooks), + ) { + Ok(result) => result, + Err(e) => { + self.set_tx_sender(initial_tx_sender); + return Err(vec![Diagnostic { + level: Level::Error, + message: format!("Error calling contract function: {e}"), + spans: vec![], + suggestion: None, + }]); + } + }; + self.set_tx_sender(initial_tx_sender); + self.coverage_reports.push(coverage); + + if let Some(ref cost) = execution.cost { + self.costs_reports.push(CostsReport { + test_name, + contract_id: contract_id_str, + method: method.to_string(), + args: args.iter().map(|a| uint8_to_string(a)).collect(), + cost_result: cost.clone(), + }); + } + + Ok(execution) + } + pub fn eval( &mut self, snippet: String,