diff --git a/.github/workflows/ci-clarinet-cli.yaml b/.github/workflows/ci-cli.yaml similarity index 100% rename from .github/workflows/ci-clarinet-cli.yaml rename to .github/workflows/ci-cli.yaml diff --git a/.github/workflows/ci-sdk.yaml b/.github/workflows/ci-sdk.yaml new file mode 100644 index 000000000..f0cc77c76 --- /dev/null +++ b/.github/workflows/ci-sdk.yaml @@ -0,0 +1,68 @@ +name: CI - Clarinet SDK +on: + pull_request: + branches: + - main + paths-ignore: + - "**/CHANGELOG.md" + push: + branches: + - main + paths-ignore: + - "**/CHANGELOG.md" + +# Cancel previous runs for the same workflow +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + build_and_test_sdk: + name: Build and test clarinet-sdk packages + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: | + rustup toolchain install stable --profile minimal --component rustfmt + rustup target add wasm32-unknown-unknown + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup cache keys + run: | + echo "RUST_VERSION_HASH=$(rustc --version | sha256sum | awk '{print $1}')" >> $GITHUB_ENV + echo "NODE_VERSION_HASH=$(node --version | sha256sum | awk '{print $1}')" >> $GITHUB_ENV + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/ + ~/target/release/build/ + ~/target/wasm32-unknown-unknown/build/ + key: clarinet-sdk-cargo-${{ runner.os }}-${{ env.RUST_VERSION_HASH }}-${{ hashFiles('./Cargo.lock') }} + + - name: Cache npm + uses: actions/cache@v4 + with: + path: | + ~/node_modules/ + key: clarinet-sdk-npm-${{ runner.os }}-${{ env.NODE_VERSION_HASH }}-${{ hashFiles('./package-lock.json') }} + + - name: Install wasm-pack + run: npm install -g wasm-pack + + - name: Build Wasm packages + run: npm run build:wasm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test diff --git a/Cargo.lock b/Cargo.lock index 7e377326d..3609ae566 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -920,7 +920,7 @@ dependencies = [ [[package]] name = "clarinet-sdk-wasm" -version = "2.7.0" +version = "2.8.0-beta1" dependencies = [ "clarinet-deployments", "clarinet-files", @@ -1014,6 +1014,7 @@ dependencies = [ "chrono", "clar2wasm", "clarity", + "colored", "debug_types", "futures", "getrandom 0.2.8", @@ -1036,8 +1037,6 @@ dependencies = [ "test-case", "tokio", "tokio-util", - "wasm-bindgen", - "wasm-bindgen-futures", ] [[package]] diff --git a/components/clarinet-deployments/Cargo.toml b/components/clarinet-deployments/Cargo.toml index 77d2c888a..280fb662a 100644 --- a/components/clarinet-deployments/Cargo.toml +++ b/components/clarinet-deployments/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition = "2021" [dependencies] -colored = "2.0.4" +colored = "2.1.0" serde = "1" serde_json = "1" serde_derive = "1" diff --git a/components/clarinet-deployments/src/lib.rs b/components/clarinet-deployments/src/lib.rs index 2d53d3dd3..aa3962b5b 100644 --- a/components/clarinet-deployments/src/lib.rs +++ b/components/clarinet-deployments/src/lib.rs @@ -112,7 +112,7 @@ fn update_session_with_genesis_accounts( wallet.balance.try_into().unwrap(), ); if wallet.name == "deployer" { - session.set_tx_sender(wallet.address.to_address()); + session.set_tx_sender(&wallet.address.to_string()); } } } @@ -186,11 +186,11 @@ pub fn update_session_with_deployment_plan( fn handle_stx_transfer(session: &mut Session, tx: &StxTransferSpecification) { let default_tx_sender = session.get_tx_sender(); - session.set_tx_sender(tx.expected_sender.to_string()); + session.set_tx_sender(&tx.expected_sender.to_string()); let _ = session.stx_transfer(tx.mstx_amount, &tx.recipient.to_string()); - session.set_tx_sender(default_tx_sender); + session.set_tx_sender(&default_tx_sender); } fn handle_emulated_contract_publish( @@ -201,7 +201,7 @@ fn handle_emulated_contract_publish( code_coverage_enabled: bool, ) -> Result> { let default_tx_sender = session.get_tx_sender(); - session.set_tx_sender(tx.emulated_sender.to_string()); + session.set_tx_sender(&tx.emulated_sender.to_string()); let contract = ClarityContract { code_source: ClarityCodeSource::ContractInMemory(tx.source.clone()), @@ -217,7 +217,7 @@ fn handle_emulated_contract_publish( }; let result = session.deploy_contract(&contract, None, false, test_name, contract_ast); - session.set_tx_sender(default_tx_sender); + session.set_tx_sender(&default_tx_sender); result } @@ -236,7 +236,7 @@ fn handle_emulated_contract_call( tx: &EmulatedContractCallSpecification, ) -> Result> { let default_tx_sender = session.get_tx_sender(); - session.set_tx_sender(tx.emulated_sender.to_string()); + session.set_tx_sender(&tx.emulated_sender.to_string()); let params: Vec = tx .parameters @@ -257,7 +257,7 @@ fn handle_emulated_contract_call( println!("error: {:?}", errors.first().unwrap().message); } - session.set_tx_sender(default_tx_sender); + session.set_tx_sender(&default_tx_sender); result } diff --git a/components/clarinet-files/src/wasm_fs_accessor.rs b/components/clarinet-files/src/wasm_fs_accessor.rs index c5ad445f4..5066c160f 100644 --- a/components/clarinet-files/src/wasm_fs_accessor.rs +++ b/components/clarinet-files/src/wasm_fs_accessor.rs @@ -3,7 +3,7 @@ use js_sys::{Function as JsFunction, Promise}; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value as decode_from_js, to_value as encode_to_js}; use std::collections::HashMap; -use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; #[derive(Serialize, Deserialize)] diff --git a/components/clarinet-sdk-wasm/.gitignore b/components/clarinet-sdk-wasm/.gitignore index 5fff1d9c1..30128a3ec 100644 --- a/components/clarinet-sdk-wasm/.gitignore +++ b/components/clarinet-sdk-wasm/.gitignore @@ -1 +1,3 @@ pkg +pkg-browser +pkg-node diff --git a/components/clarinet-sdk-wasm/Cargo.toml b/components/clarinet-sdk-wasm/Cargo.toml index 6168b713c..337aadbf5 100644 --- a/components/clarinet-sdk-wasm/Cargo.toml +++ b/components/clarinet-sdk-wasm/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "clarinet-sdk-wasm" -version.workspace = true +# version.workspace = true +version = "2.8.0-beta1" edition = "2021" license = "GPL-3.0" repository = "https://github.com/hirosystems/clarinet" @@ -74,3 +75,15 @@ wasm-opt = ['-Oz'] debug-js-glue = false demangle-name-section = true dwarf-debug-info = false + +# profiling +[profile.profiling] +debug = 1 + +[package.metadata.wasm-pack.profile.profiling] +wasm-opt = ['-g', '-O'] + +[package.metadata.wasm-pack.profile.profiling.wasm-bindgen] +debug-js-glue = false +demangle-name-section = true +dwarf-debug-info = false diff --git a/components/clarinet-sdk-wasm/README.md b/components/clarinet-sdk-wasm/README.md index fde4e3b86..ebe6cea7b 100644 --- a/components/clarinet-sdk-wasm/README.md +++ b/components/clarinet-sdk-wasm/README.md @@ -1,8 +1,9 @@ # Clarity SDK WASM -This package is built with wasm-pack. -It powers [@hirosystems/clarinet-sdk](https://www.npmjs.com/package/@hirosystems/clarinet-sdk). - +This component exposes Clarinet features to a JS interface through wasm-bindgen. +It's built with wasm-pack. +It powers [@hirosystems/clarinet-sdk](https://npmjs.com/package/@hirosystems/clarinet-sdk) and +[@hirosystems/clarinet-sdk-browser](https://npmjs.com/package/@hirosystems/clarinet-sdk-browser). ## Contributing @@ -10,6 +11,40 @@ It powers [@hirosystems/clarinet-sdk](https://www.npmjs.com/package/@hirosystems Install [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). +In the root directory of Clarinet, run the following command to build the packages for Node.js and the browser. +Under the hood, it will run `wasm-pack build` twice, once for each target. + +```sh +npm run build:wasm +``` + +Alternatively, it's also possible to build the packages separately. It should only be done for development purpose. + +**Build for node** + +```sh +wasm-pack build --release --scope hirosystems --out-dir pkg-node --target nodejs +``` + +**Build for the browser** + +```sh +wasm-pack build --release --scope hirosystems --out-dir pkg-browser --target web +``` + +### Release + +The package is built twice with `wasm-pack` as it can't target `node` and `web` at the same time. +The following script will build for both target, it will also rename the package name for the +browser build. + +```sh +npm run build:wasm +``` + +Once built, the packages can be released by running the following command. Note that by default we +release with the beta tag. + ```sh -wasm-pack build --release --target=nodejs --scope hirosystems +npm run publish:sdk-wasm ``` diff --git a/components/clarinet-sdk-wasm/build.mjs b/components/clarinet-sdk-wasm/build.mjs new file mode 100644 index 000000000..e06992ffb --- /dev/null +++ b/components/clarinet-sdk-wasm/build.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/node + +import { spawn } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; + +// directory of the current file +const rootDir = new URL(".", import.meta.url).pathname; + +/** + * build + */ +async function build() { + console.log("Deleting pkg-node"); + await rmIfExists(path.join(rootDir, "pkg-node")); + console.log("Deleting pkg-browser"); + await rmIfExists(path.join(rootDir, "pkg-browser")); + + await Promise.all([ + execCommand("wasm-pack", [ + "build", + "--release", + "--scope", + "hirosystems", + "--out-dir", + "pkg-node", + "--target", + "nodejs", + ]), + execCommand("wasm-pack", [ + "build", + "--release", + "--scope", + "hirosystems", + "--out-dir", + "pkg-browser", + "--target", + "web", + ]), + ]); + + await updatePackageName(); +} + +/** + * execCommand + * @param {string} command + * @param {string[]} args + * @returns + */ +export const execCommand = async (command, args) => { + console.log(`Building ${args[5]}`); + return new Promise((resolve, reject) => { + const childProcess = spawn(command, args, { + cwd: rootDir, + }); + childProcess.stdout.on("data", (data) => { + process.stdout.write(data.toString()); + }); + childProcess.stderr.on("data", (data) => { + process.stderr.write(data.toString()); + }); + childProcess.on("error", (error) => { + reject(error); + }); + childProcess.on("exit", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`āŒ Command exited with code ${code}.`)); + } + }); + }); +}; + +/** + * rmIfExists + * @param {string} dirPath + */ +async function rmIfExists(dirPath) { + try { + await fs.rm(dirPath, { recursive: true, force: true }); + } catch (error) { + if (error.code !== "ENOENT") { + throw error; + } + } +} + +/** + * updatePackageName + */ +async function updatePackageName() { + const filePath = path.join(rootDir, "pkg-browser/package.json"); + + const fileData = await fs.readFile(filePath, "utf-8"); + const updatedData = fileData.replace( + '"name": "@hirosystems/clarinet-sdk-wasm"', + '"name": "@hirosystems/clarinet-sdk-wasm-browser"', + ); + await fs.writeFile(filePath, updatedData, "utf-8"); + console.log("āœ… Package name updated successfully."); +} + +try { + await build(); + console.log("\nāœ… Project successfully built.\nšŸš€ Ready to publish."); + console.log("Run the following commands to publish"); + console.log("\n```"); + console.log("$ npm run publish:sdk-wasm"); + console.log("```\n"); +} catch (error) { + console.error("āŒ Error building:", error); + throw error; +} diff --git a/components/clarinet-sdk-wasm/src/core.rs b/components/clarinet-sdk-wasm/src/core.rs index eb452cad0..21fe2ad09 100644 --- a/components/clarinet-sdk-wasm/src/core.rs +++ b/components/clarinet-sdk-wasm/src/core.rs @@ -14,14 +14,17 @@ use clarity_repl::clarity::analysis::contract_interface_builder::{ ContractInterface, ContractInterfaceFunction, ContractInterfaceFunctionAccess, }; use clarity_repl::clarity::ast::ContractAST; -use clarity_repl::clarity::vm::types::QualifiedContractIdentifier; +use clarity_repl::clarity::chainstate::StacksAddress; +use clarity_repl::clarity::vm::types::{ + PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, +}; use clarity_repl::clarity::{ - ClarityVersion, EvaluationResult, ExecutionResult, StacksEpochId, SymbolicExpression, + Address, ClarityVersion, EvaluationResult, ExecutionResult, StacksEpochId, SymbolicExpression, }; use clarity_repl::repl::clarity_values::{uint8_to_string, uint8_to_value}; use clarity_repl::repl::session::BOOT_CONTRACTS_DATA; use clarity_repl::repl::{ - clarity_values, ClarityCodeSource, ClarityContract, ContractDeployer, Session, + clarity_values, ClarityCodeSource, ClarityContract, ContractDeployer, Session, SessionSettings, DEFAULT_CLARITY_VERSION, DEFAULT_EPOCH, }; use gloo_utils::format::JsValueSerdeExt; @@ -37,17 +40,6 @@ use wasm_bindgen::JsValue; use crate::utils::costs::SerializableCostsReport; use crate::utils::events::serialize_event; -#[wasm_bindgen(typescript_custom_section)] -const SET_EPOCH: &'static str = r#" -type EpochString = "2.0" | "2.05" | "2.1" | "2.2" | "2.3" | "2.4" | "2.5" | "3.0" -"#; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "ITextStyle")] - pub type ITextStyle; -} - #[wasm_bindgen] extern "C" { #[wasm_bindgen(typescript_type = "Map>")] @@ -56,6 +48,12 @@ extern "C" { pub type Accounts; #[wasm_bindgen(typescript_type = "EpochString")] pub type EpochString; + #[wasm_bindgen(typescript_type = "ClarityVersionString")] + pub type ClarityVersionString; + #[wasm_bindgen(typescript_type = "IContractAST")] + pub type IContractAST; + #[wasm_bindgen(typescript_type = "Map")] + pub type IContractInterfaces; } macro_rules! log { @@ -327,6 +325,30 @@ impl SDK { QualifiedContractIdentifier::parse(&contract_id).map_err(|e| e.to_string()) } + #[wasm_bindgen(js_name=getDefaultEpoch)] + pub fn get_default_epoch() -> EpochString { + EpochString { + obj: DEFAULT_EPOCH.to_string().into(), + } + } + + #[wasm_bindgen(js_name=getDefaultClarityVersionForCurrentEpoch)] + pub fn default_clarity_version_for_current_epoch(&self) -> ClarityVersionString { + let session = self.get_session(); + ClarityVersionString { + obj: ClarityVersion::default_for_epoch(session.current_epoch) + .to_string() + .into(), + } + } + + #[wasm_bindgen(js_name=initEmtpySession)] + pub async fn init_empty_session(&mut self) -> Result<(), String> { + let session = Session::new(SessionSettings::default()); + self.session = Some(session); + Ok(()) + } + #[wasm_bindgen(js_name=initSession)] pub async fn init_session(&mut self, cwd: String, manifest_path: String) -> Result<(), String> { let cwd_path = PathBuf::from(cwd); @@ -484,6 +506,11 @@ impl SDK { Ok(cache) } + #[wasm_bindgen(js_name=clearCache)] + pub fn clear_cach(&mut self) { + self.cache.clear(); + } + async fn write_deployment_plan( &self, deployment_plan: &DeploymentSpecification, @@ -578,13 +605,13 @@ impl SDK { } #[wasm_bindgen(js_name=getContractsInterfaces)] - pub fn get_contracts_interfaces(&self) -> Result { + pub fn get_contracts_interfaces(&self) -> Result { let contracts_interfaces: HashMap = self .contracts_interfaces .iter() .map(|(k, v)| (k.to_string(), v.clone())) .collect(); - Ok(encode_to_js(&contracts_interfaces)?) + Ok(encode_to_js(&contracts_interfaces)?.unchecked_into::()) } #[wasm_bindgen(js_name=getContractSource)] @@ -596,11 +623,14 @@ impl SDK { } #[wasm_bindgen(js_name=getContractAST)] - pub fn get_contract_ast(&self, contract: &str) -> Result { + pub fn get_contract_ast(&self, contract: &str) -> Result { let session = self.get_session(); let contract_id = self.desugar_contract_id(contract)?; let contract = session.contracts.get(&contract_id).ok_or("err")?; - encode_to_js(&contract.ast).map_err(|e| e.to_string()) + + Ok(encode_to_js(&contract.ast) + .map_err(|e| e.to_string())? + .unchecked_into::()) } #[wasm_bindgen(js_name=getAssetsMap)] @@ -763,7 +793,7 @@ impl SDK { ) -> Result { let session = self.get_session_mut(); let initial_tx_sender = session.get_tx_sender(); - session.set_tx_sender(args.sender.to_string()); + session.set_tx_sender(&args.sender); let execution = match session.stx_transfer(args.amount, &args.recipient) { Ok(res) => res, @@ -779,7 +809,7 @@ impl SDK { if advance_chain_tip { session.advance_chain_tip(1); } - session.set_tx_sender(initial_tx_sender); + session.set_tx_sender(&initial_tx_sender); Ok(execution_result_to_transaction_res(&execution)) } @@ -911,6 +941,43 @@ impl SDK { } } + #[wasm_bindgen(js_name=execute)] + pub fn execute(&mut self, snippet: String) -> Result { + let session = self.get_session_mut(); + match session.eval(snippet.clone(), None, false) { + Ok(res) => Ok(execution_result_to_transaction_res(&res)), + Err(diagnostics) => { + let message = diagnostics + .iter() + .map(|d| d.message.to_string()) + .collect::>() + .join("\n"); + Err(format!("error: {}", message)) + } + } + } + + #[wasm_bindgen(js_name=executeCommand)] + pub fn execute_command(&mut self, snippet: String) -> String { + let session = self.get_session_mut(); + if !snippet.starts_with("::") { + return "error: command must start with ::".to_string(); + } + session.handle_command(&snippet) + } + + #[wasm_bindgen(js_name=mintSTX)] + pub fn mint_stx(&mut self, recipient: String, amount: u64) -> Result { + let session = self.get_session_mut(); + + session.interpreter.mint_stx_balance( + PrincipalData::Standard(StandardPrincipalData::from( + StacksAddress::from_string(&recipient).unwrap(), + )), + amount, + ) + } + #[wasm_bindgen(js_name=setCurrentTestName)] pub fn set_current_test_name(&mut self, test_name: String) { self.current_test_name = test_name; diff --git a/components/clarinet-sdk-wasm/src/lib.rs b/components/clarinet-sdk-wasm/src/lib.rs index cde4e5b90..cdc0c2365 100644 --- a/components/clarinet-sdk-wasm/src/lib.rs +++ b/components/clarinet-sdk-wasm/src/lib.rs @@ -1,3 +1,4 @@ pub mod core; +mod ts_types; mod utils; diff --git a/components/clarinet-sdk-wasm/src/ts_types.rs b/components/clarinet-sdk-wasm/src/ts_types.rs new file mode 100644 index 000000000..8aab45167 --- /dev/null +++ b/components/clarinet-sdk-wasm/src/ts_types.rs @@ -0,0 +1,163 @@ +use js_sys::wasm_bindgen; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const EPOCH_STRING: &'static str = + r#"export type EpochString = "2.0" | "2.05" | "2.1" | "2.2" | "2.3" | "2.4" | "2.5" | "3.0""#; + +// CONTRACT AST + +#[wasm_bindgen(typescript_custom_section)] +const ATOM_STRING: &'static str = r#"type Atom = { + Atom: String; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const ATOM_VALUE_STRING: &'static str = r#"type AtomValue = { + AtomValue: any; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const LIST_STRING: &'static str = r#"type List = { + List: Expression[]; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const LITERAL_VALUE_STRING: &'static str = r#"type LiteralValue = { + LiteralValue: any; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const FIELD_STRING: &'static str = r#"type Field = { + Field: any; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const TRAIT_REFERENCE_STRING: &'static str = r#"type TraitReference = { + TraitReference: any; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const EXPRESSION_TYPE_STRING: &'static str = + r#"type ExpressionType = Atom | AtomValue | List | LiteralValue | Field | TraitReference;"#; + +#[wasm_bindgen(typescript_custom_section)] +const SPAN_STRING: &'static str = r#"type Span = { + start_line: number; + start_column: number; + end_line: number; + end_column: number; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const EXPRESSION_STRING: &'static str = r#"type Expression = { + expr: ExpressionType; + id: number; + span: Span; +};"#; + +// To avoid collision with the Rust type ContractAST, prefix with the conventional typescript I +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_AST_STRING: &'static str = r#"type IContractAST = { + contract_identifier: any; + pre_expressions: any[]; + expressions: Expression[]; + top_level_expression_sorting: number[]; + referenced_traits: Map; + implemented_traits: any[]; +};"#; + +// CONTRACT INTERFACE + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_FUNCTION_ACCESS_STRING: &'static str = + r#"type ContractInterfaceFunctionAccess = "private" | "public" | "read_only";"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_TUPLE_ENTRY_TYPE_STRING: &'static str = + r#"type ContractInterfaceTupleEntryType = { name: string; type: ContractInterfaceAtomType };"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_ATOM_TYPE_STRING: &'static str = r#"type ContractInterfaceAtomType = + | "none" + | "int128" + | "uint128" + | "bool" + | "principal" + | { buffer: { length: number } } + | { "string-utf8": { length: number } } + | { "string-ascii": { length: number } } + | { tuple: ContractInterfaceTupleEntryType[] } + | { optional: ContractInterfaceAtomType } + | { response: { ok: ContractInterfaceAtomType; error: ContractInterfaceAtomType } } + | { list: { type: ContractInterfaceAtomType; length: number } } + | "trait_reference";"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_FUNCTION_ARG_STRING: &'static str = + r#"type ContractInterfaceFunctionArg = { name: string; type: ContractInterfaceAtomType };"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_FUNCTION_OUTPUT_STRING: &'static str = + r#"type ContractInterfaceFunctionOutput = { type: ContractInterfaceAtomType };"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_FUNCTION_STRING: &'static str = r#"type ContractInterfaceFunction = { + name: string; + access: ContractInterfaceFunctionAccess; + args: ContractInterfaceFunctionArg[]; + outputs: ContractInterfaceFunctionOutput; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_VARIABLE_ACCESS_STRING: &'static str = + r#"type ContractInterfaceVariableAccess = "constant" | "variable";"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_VARIABLE_STRING: &'static str = r#"type ContractInterfaceVariable = { + name: string; + type: ContractInterfaceAtomType; + access: ContractInterfaceVariableAccess; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_MAP_STRING: &'static str = r#"type ContractInterfaceMap = { + name: string; + key: ContractInterfaceAtomType; + value: ContractInterfaceAtomType; +};"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_FUNGIBLE_TOKENS_STRING: &'static str = + r#"type ContractInterfaceFungibleTokens = { name: string };"#; + +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_NON_FUNGIBLE_TOKENS_STRING: &'static str = r#"type ContractInterfaceNonFungibleTokens = { name: string; type: ContractInterfaceAtomType };"#; + +#[wasm_bindgen(typescript_custom_section)] +const STACKS_EPOCH_ID_STRING: &'static str = r#"export type StacksEpochId = + | "Epoch10" + | "Epoch20" + | "Epoch2_05" + | "Epoch21" + | "Epoch22" + | "Epoch23" + | "Epoch24" + | "Epoch25" + | "Epoch30";"#; + +#[wasm_bindgen(typescript_custom_section)] +const CLARITY_VERSION_STRING: &'static str = + r#"export type ClarityVersionString = "Clarity1" | "Clarity2" | "Clarity3";"#; + +// To avoid collision with the Rust type ContractAST, prefix with the conventional typescript I +#[wasm_bindgen(typescript_custom_section)] +const CONTRACT_INTERFACE_STRING: &'static str = r#"export type IContractInterface = { + functions: ContractInterfaceFunction[]; + variables: ContractInterfaceVariable[]; + maps: ContractInterfaceMap[]; + fungible_tokens: ContractInterfaceFungibleTokens[]; + non_fungible_tokens: ContractInterfaceNonFungibleTokens[]; + epoch: StacksEpochId; + clarity_version: ClarityVersionString; +};"#; diff --git a/components/clarinet-sdk/.gitignore b/components/clarinet-sdk/.gitignore index 1eae0cf67..849ddff3b 100644 --- a/components/clarinet-sdk/.gitignore +++ b/components/clarinet-sdk/.gitignore @@ -1,2 +1 @@ dist/ -node_modules/ diff --git a/components/clarinet-sdk/README.md b/components/clarinet-sdk/README.md index 72ede9bfe..581d11d9c 100644 --- a/components/clarinet-sdk/README.md +++ b/components/clarinet-sdk/README.md @@ -1,136 +1,57 @@ -# Clarinet SDK +# Clarinet SDK Workspace -The Clarinet SDK can be used to interact with the simnet from Node.js. +This workspace regroups +`@hirosystems/clarinet-sdk` for node.js and `@hirosystems/clarinet-sdk-browser` for web browsers. +They respectively rely on `@hirosystems/clarinet-sdk-wasm` and `@hirosystems/clarinet-sdk-browser-wasm`. -Find the API references of the SDK in [our documentation](https://docs.hiro.so/clarinet/feature-guides/clarinet-js-sdk). -Learn more about unit testing Clarity smart contracts in [this guide](https://docs.hiro.so/clarinet/feature-guides/test-contract-with-clarinet-sdk). - -You can use this SDK to: -- Call public and read-only functions from smart contracts -- Get clarity maps or data-var values -- Get contract interfaces (available functions and data) -- Write unit tests for Clarity smart contracts - -## Core - -``` -npm install @hirosystems/clarinet-sdk -``` - -### Usage - -```ts -import { initSimnet } from "@hirosystems/clarinet-sdk"; -import { Cl } from "@stacks/transactions"; - -async function main() { - const simnet = await initSimnet(); - - const accounts = simnet.getAccounts(); - const address1 = accounts.get("wallet_1"); - if (!address1) throw new Error("invalid wallet name."); - - - const call = simnet.callPublicFn("counter", "add", [Cl.uint(1)], address1); - console.log(call.result); // Cl.int(Cl.ok(true)) - - const counter = simnet.getDataVar("counter", "counter"); - console.log(counter); // Cl.int(2) -} - -main(); -``` - - -By default, the SDK will look for a Clarinet.toml file in the current working directory. -It's also possible to provide the path to the manifest like so: -```ts - const simnet = await initSimnet("./path/to/Clarinet.toml"); -``` - -## Tests - -The SDK can be used to write unit tests for Clarinet projects. - -You'll need to have Node.js (>= 18) and NPM setup. If you are not sure how to set it up, [Volta](https://volta.sh/) is a nice tool to get started. - -In the terminal, run `node --version` to make sure it's available and up to date. - -> Note: A bit of boilerplate is needed to setup the testing environment. Soon it will be handled by the clarinet-cli. - -Open your terminal and go to a new or existing Clarinet project: - -```console -cd my-project -ls # you should see a Clarinet.toml file in the list -``` - -Run the following command to setup the testing framework: - -```console -npx @hirosystems/clarinet-sdk -``` - -Visit the [clarity starter project](https://github.com/hirosystems/clarity-starter/tree/170224c9dd3bde185f194a9036c5970f44c596cd) to see the testing framework in action. - - -### Type checking - -We recommend to use TypeScript to write the unit tests, but it's also possible to do it with JavaScript. To do so, rename your test files to `.test.js` instead of `.test.ts`. You can also delete the `tsconfig.json` and uninstall typescript with `npm uninstall typescript`. - -Note: If you want to write your test in JavaScript but still have a certain level of type safety and autocompletion, VSCode can help you with that. You can create a basic `jsconfig.json` file: - -```json -{ - "compilerOptions": { - "checkJs": true, - "strict": true - }, - "include": ["node_modules/@hirosystems/clarinet-sdk/vitest-helpers/src", "unit-tests"] -} -``` +Because of the way the wasm packages are build, with wasm-pack, it made sense to have two different +packages for Node.js and the browsers, but it has some caveats. Especially, some of the code is +duplicated in `./browser/src/sdkProxy.ts` and `./node/src/sdkProxy.ts`. In the future, we hope to +be able to simplify this build, it would require some breaking changes so it could be part of +Clarinet 3.x. ## Contributing The clarinet-sdk requires a few steps to be built and tested locally. -We'll look into simplifying this workflow in a future version. Clone the clarinet repo and `cd` into it: -```console + +```sh git clone git@github.com:hirosystems/clarinet.git cd clarinet ``` Open the SDK workspace in VSCode, it's especially useful to get rust-analyzer to consider the right files with the right cargo features. -```console + +```sh code components/clarinet-sdk/clarinet-sdk.code-workspace ``` The SDK mainly relies on two components: + - the Rust component: `components/clarinet-sdk-wasm` - the TS component: `components/clarinet-sdk` To work with these two packages locally, the first one needs to be built with -wasm-pack and linked with: [npm link](https://docs.npmjs.com/cli/v8/commands/npm-link). +wasm-pack (install [wasm-pack](https://rustwasm.github.io/wasm-pack/installer)). -Install [wasm-pack](https://rustwasm.github.io/wasm-pack/installer) and run: -```console -cd components/clarinet-sdk-wasm -wasm-pack build --release --target=nodejs --scope hirosystems -cd pkg -npm link +```sh +# build the wasm package +npm run build:wasm +# install dependencies and build the node package +npm install +# make sure the installation works +npm test ``` -Go to the `clarinet-sdk` directory and link the package that was just built. -It will tell npm to use it instead of the published version. You don't need to -repeat the steps everytime the `clarinet-sdk-wasm` changes, it only needs to be -rebuilt with wasm-pack and npm will use it. +### Release -Built the TS project: -```console -cd ../../clarinet-sdk -npm link @hirosystems/clarinet-sdk-wasm -``` +The Node.js and browser versions can be published with this single command. +Make sure to check the check both packages versions first. -You can now run `npm test`, it wil be using the local version of `clarinet-sdk-wasm` +```sh +# the wasm package must be published first +# $ npm run publish:sdk-wasm +npm run publish:sdk +``` diff --git a/components/clarinet-sdk/browser/README.md b/components/clarinet-sdk/browser/README.md new file mode 100644 index 000000000..f1f40e655 --- /dev/null +++ b/components/clarinet-sdk/browser/README.md @@ -0,0 +1,40 @@ +# Clarinet SDK for the Web + +The Clarinet SDK can be used to interact with the simnet from web browsers. + +If you want to use the Clarinet SDK in Node.js, try [@hirosystems/clarinet-sdk](https://www.npmjs.com/package/@hirosystems/clarinet-sdk). + +Find the API references of the SDK in [our documentation](https://docs.hiro.so/clarinet/feature-guides/clarinet-js-sdk). +Learn more about unit testing Clarity smart contracts in [this guide](https://docs.hiro.so/clarinet/feature-guides/test-contract-with-clarinet-sdk). + +You can use this SDK to: +- Interact with a clarinet project as you would with the Clarinet CLI +- Call public, read-only, and private functions from smart contracts +- Get clarity maps or data-var values +- Get contract interfaces (available functions and data) +- Write unit tests for Clarity smart contracts + +## Installation + +```sh +npm install @hirosystems/clarinet-sdk-browser +``` + +### Usage + +There are two ways to use the sdk in the browser: + +- With an empty clarinet session: +```js +const simnet = await initSimnet(); +await simnet.initEmtpySession(); +simnet.runSnippet("(+ 1 2)") +``` + +- With a clarinet project (ie: with a Clarinet.toml) +šŸ’” It requires to use a virtual file system. More documentation and examples soon. +```js +const simnet = await initSimnet(); +await simnet.initSession("/project", "Clarinet.toml") +``` + diff --git a/components/clarinet-sdk/browser/package.json b/components/clarinet-sdk/browser/package.json new file mode 100644 index 000000000..3e2a83803 --- /dev/null +++ b/components/clarinet-sdk/browser/package.json @@ -0,0 +1,34 @@ +{ + "name": "@hirosystems/clarinet-sdk-browser", + "version": "2.8.0-beta1", + "description": "A SDK to interact with Clarity Smart Contracts in the browser", + "homepage": "https://docs.hiro.so/clarinet/feature-guides/clarinet-js-sdk", + "repository": { + "type": "git", + "url": "git+https://github.com/hirosystems/clarinet.git" + }, + "files": [ + "dist" + ], + "module": "./dist/esm/browser/src/index.js", + "types": "./dist/esm/browser/src/index.d.ts", + "scripts": { + "clean": "rimraf dist", + "compile": "tsc -b ./tsconfig.json", + "build": "npm run clean; npm run compile", + "prepare": "npm run build" + }, + "keywords": [ + "stacks", + "clarity", + "clarinet", + "tests" + ], + "author": "hirosystems", + "license": "GPL-3.0", + "readme": "./README.md", + "dependencies": { + "@hirosystems/clarinet-sdk-wasm-browser": "^2.8.0-beta1", + "@stacks/transactions": "^6.13.0" + } +} diff --git a/components/clarinet-sdk/browser/src/defaultVfs.ts b/components/clarinet-sdk/browser/src/defaultVfs.ts new file mode 100644 index 000000000..7d0a8da34 --- /dev/null +++ b/components/clarinet-sdk/browser/src/defaultVfs.ts @@ -0,0 +1,63 @@ +export const defaultFileStore = new Map(); + +function fileArrayToString(bufferArray: Uint8Array) { + return Array.from(bufferArray) + .map((item) => String.fromCharCode(item)) + .join(""); +} + +function isValidReadEvent(e: any): e is { path: string } { + return typeof e?.path === "string"; +} + +function isValidReadManyEvent(e: any): e is { paths: string[] } { + return Array.isArray(e?.paths) && e.paths.every((s: unknown) => typeof s === "string"); +} + +function isValidWriteEvent(e: any): e is { path: string; content: number[] } { + return typeof e?.path === "string" && Array.isArray(e?.content); +} + +async function exists(event: unknown) { + if (!isValidReadEvent(event)) throw new Error("invalid read event"); + return defaultFileStore.has(event.path); +} + +async function readFile(event: unknown) { + if (!isValidReadEvent(event)) throw new Error("invalid read event"); + return defaultFileStore.get(event.path) ?? null; +} + +async function readFiles(event: any) { + if (!isValidReadManyEvent(event)) throw new Error("invalid read event"); + const files = event.paths.map((p) => { + try { + return defaultFileStore.get(p); + } catch (err) { + console.warn(err); + return null; + } + }); + return Object.fromEntries( + files.reduce( + (acc, f, i) => { + if (f === null || f === undefined) return acc; + return acc.concat([[event.paths[i], f]]); + }, + [] as [string, string][], + ), + ); +} + +async function writeFile(event: unknown) { + if (!isValidWriteEvent(event)) throw new Error("invalid write event"); + return defaultFileStore.set(event.path, fileArrayToString(Uint8Array.from(event.content))); +} + +export function defaultVfs(action: string, data: unknown) { + if (action === "vfs/exists") return exists(data); + if (action === "vfs/readFile") return readFile(data); + if (action === "vfs/readFiles") return readFiles(data); + if (action === "vfs/writeFile") return writeFile(data); + throw new Error("invalid vfs action"); +} diff --git a/components/clarinet-sdk/browser/src/index.ts b/components/clarinet-sdk/browser/src/index.ts new file mode 100644 index 000000000..1c53a9e4c --- /dev/null +++ b/components/clarinet-sdk/browser/src/index.ts @@ -0,0 +1,29 @@ +import init, { SDK } from "@hirosystems/clarinet-sdk-wasm-browser"; + +import { Simnet, getSessionProxy } from "./sdkProxy.js"; +import { defaultVfs } from "./defaultVfs.js"; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json +// @ts-ignore +BigInt.prototype.toJSON = function () { + return this.toString(); +}; + +export { + tx, + type ClarityEvent, + type ParsedTransactionResult, + type DeployContractOptions, + type Tx, + type TransferSTX, +} from "../../common/src/sdkProxyHelpers.js"; + +export { init, SDK, getSessionProxy, type Simnet }; +export { defaultVfs, defaultFileStore } from "./defaultVfs.js"; + +export const initSimnet = async (virtualFileSystem?: Function) => { + await init(); + + const vfs = virtualFileSystem ? virtualFileSystem : defaultVfs; + return new Proxy(new SDK(vfs), getSessionProxy()) as unknown as Simnet; +}; diff --git a/components/clarinet-sdk/browser/src/sdkProxy.ts b/components/clarinet-sdk/browser/src/sdkProxy.ts new file mode 100644 index 000000000..67bb68b05 --- /dev/null +++ b/components/clarinet-sdk/browser/src/sdkProxy.ts @@ -0,0 +1,155 @@ +import { Cl } from "@stacks/transactions"; +import { + CallFnArgs, + DeployContractArgs, + TransferSTXArgs, + ContractOptions, + type SDK, + type TransactionRes, +} from "@hirosystems/clarinet-sdk-wasm-browser"; + +import { + parseEvents, + type CallFn, + type DeployContract, + type GetDataVar, + type GetMapEntry, + type MineBlock, + type ParsedTransactionResult, + type Execute, + type TransferSTX, +} from "../../common/src/sdkProxyHelpers.js"; + +/** @deprecated use `simnet.execute(command)` instead */ +type RunSnippet = SDK["runSnippet"]; + +// 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" | "callPrivateFn" + ? CallFn + : K extends "execute" + ? Execute + : K extends "runSnippet" + ? RunSnippet + : K extends "deployContract" + ? DeployContract + : K extends "transferSTX" + ? TransferSTX + : K extends "mineBlock" + ? MineBlock + : K extends "getDataVar" + ? GetDataVar + : K extends "getMapEntry" + ? GetMapEntry + : SDK[K]; +}; + +function parseTxResponse(response: TransactionRes): ParsedTransactionResult { + return { + result: Cl.deserialize(response.result), + events: parseEvents(response.events), + }; +} + +export function getSessionProxy() { + return { + get(session: SDK, prop: keyof SDK, receiver: any) { + // some of the WASM methods are proxied here to: + // - serialize clarity values input argument + // - deserialize output into clarity values + + if (prop === "callReadOnlyFn" || prop === "callPublicFn" || prop === "callPrivateFn") { + const callFn: CallFn = (contract, method, args, sender) => { + const response = session[prop]( + new CallFnArgs( + contract, + method, + args.map((a) => Cl.serialize(a)), + sender, + ), + ); + return parseTxResponse(response); + }; + return callFn; + } + + if (prop === "execute") { + const execute: Execute = (snippet) => { + const response = session.execute(snippet); + return parseTxResponse(response); + }; + return execute; + } + + if (prop === "deployContract") { + const callDeployContract: DeployContract = (name, content, options, sender) => { + const rustOptions = options + ? new ContractOptions(options.clarityVersion) + : new ContractOptions(); + + const response = session.deployContract( + new DeployContractArgs(name, content, rustOptions, sender), + ); + return parseTxResponse(response); + }; + return callDeployContract; + } + + if (prop === "transferSTX") { + const callTransferSTX: TransferSTX = (amount, ...args) => { + const response = session.transferSTX(new TransferSTXArgs(BigInt(amount), ...args)); + return parseTxResponse(response); + }; + return callTransferSTX; + } + + if (prop === "mineBlock") { + const callMineBlock: MineBlock = (txs) => { + const serializedTxs = txs.map((tx) => { + if (tx.callPublicFn) { + return { + callPublicFn: { + ...tx.callPublicFn, + args_maps: tx.callPublicFn.args.map(Cl.serialize), + }, + }; + } + if (tx.callPrivateFn) { + return { + callPrivateFn: { + ...tx.callPrivateFn, + args_maps: tx.callPrivateFn.args.map(Cl.serialize), + }, + }; + } + return tx; + }); + + const responses: TransactionRes[] = session.mineBlock(serializedTxs); + return responses.map(parseTxResponse); + }; + return callMineBlock; + } + + if (prop === "getDataVar") { + const getDataVar: GetDataVar = (...args) => { + const response = session.getDataVar(...args); + const result = Cl.deserialize(response); + return result; + }; + return getDataVar; + } + + if (prop === "getMapEntry") { + const getMapEntry: GetMapEntry = (contract, mapName, mapKey) => { + const response = session.getMapEntry(contract, mapName, Cl.serialize(mapKey)); + const result = Cl.deserialize(response); + return result; + }; + return getMapEntry; + } + + return Reflect.get(session, prop, receiver); + }, + }; +} diff --git a/components/clarinet-sdk/browser/tsconfig.json b/components/clarinet-sdk/browser/tsconfig.json new file mode 100644 index 000000000..e8d3d769a --- /dev/null +++ b/components/clarinet-sdk/browser/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "esnext", + "outDir": "dist/esm", + "target": "esnext", + + "lib": ["es2022"], + + "moduleResolution": "Node", + "rootDir": "../", + "resolveJsonModule": false, + "allowJs": false, + + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + + /* Type Checking */ + "strict": true, + "noImplicitAny": true, + "skipLibCheck": true + }, + "include": ["./src", "../common/src"], + "exclude": ["node_modules", "dist"] +} diff --git a/components/clarinet-sdk/clarinet-sdk.code-workspace b/components/clarinet-sdk/clarinet-sdk.code-workspace index 57fa45a0f..0169bdd30 100644 --- a/components/clarinet-sdk/clarinet-sdk.code-workspace +++ b/components/clarinet-sdk/clarinet-sdk.code-workspace @@ -1,8 +1,6 @@ { "folders": [{ "path": "../../" }], "settings": { - "rust-analyzer.cargo.noDefaultFeatures": true, - "rust-analyzer.cargo.features": ["clarinet-sdk-wasm/wasm"], "rust-analyzer.check.overrideCommand": [ "cargo", "clippy", @@ -10,7 +8,7 @@ "--package=clarinet-sdk-wasm", "--features=wasm", "--target=wasm32-unknown-unknown", - "--message-format=json" - ] - } + "--message-format=json", + ], + }, } diff --git a/components/clarinet-sdk/common/package.json b/components/clarinet-sdk/common/package.json new file mode 100644 index 000000000..9a451d111 --- /dev/null +++ b/components/clarinet-sdk/common/package.json @@ -0,0 +1,7 @@ +{ + "name": "@hirosystems/clarinet-sdk-common", + "description": "Common helpers used in the Clarinet SDK for the node.js and the browser version", + "private": true, + "author": "hirosystems", + "license": "GPL-3.0" +} diff --git a/components/clarinet-sdk/common/src/sdkProxyHelpers.ts b/components/clarinet-sdk/common/src/sdkProxyHelpers.ts new file mode 100644 index 000000000..022edabb9 --- /dev/null +++ b/components/clarinet-sdk/common/src/sdkProxyHelpers.ts @@ -0,0 +1,119 @@ +import { Cl, ClarityValue } from "@stacks/transactions"; + +export type ClarityEvent = { + event: string; + data: { raw_value?: string; value?: ClarityValue; [key: string]: any }; +}; + +export type ParsedTransactionResult = { + result: ClarityValue; + events: ClarityEvent[]; +}; + +export type CallFn = ( + contract: string, + method: string, + args: ClarityValue[], + sender: string, +) => ParsedTransactionResult; + +export type DeployContractOptions = { + clarityVersion: 1 | 2 | 3; +}; +export type DeployContract = ( + name: string, + content: string, + options: DeployContractOptions | null, + sender: string, +) => ParsedTransactionResult; + +export type TransferSTX = ( + amount: number | bigint, + recipient: string, + sender: string, +) => ParsedTransactionResult; + +export type Tx = + | { + callPublicFn: { + contract: string; + method: string; + 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; + options: DeployContractOptions | null; + sender: string; + }; + transferSTX?: never; + } + | { + callPublicFn?: never; + callPrivateFn?: never; + deployContradct?: never; + transferSTX: { amount: number; recipient: string; sender: string }; + }; + +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, + options: DeployContractOptions | null, + sender: string, + ): Tx => ({ + deployContract: { name, content, options, sender }, + }), + transferSTX: (amount: number, recipient: string, sender: string): Tx => ({ + transferSTX: { amount, recipient, sender }, + }), +}; + +export function parseEvents(events: string): ClarityEvent[] { + try { + // @todo: improve type safety + return JSON.parse(events).map((e: string) => { + const { event, data } = JSON.parse(e); + if ("raw_value" in data) { + data.value = Cl.deserialize(data.raw_value); + } + return { + event: event, + data: data, + }; + }); + } catch (e) { + console.error(`Fail to parse events: ${e}`); + return []; + } +} + +export type MineBlock = (txs: Array) => ParsedTransactionResult[]; +export type Execute = (snippet: string) => ParsedTransactionResult; +export type GetDataVar = (contract: string, dataVar: string) => ClarityValue; +export type GetMapEntry = (contract: string, mapName: string, mapKey: ClarityValue) => ClarityValue; diff --git a/components/clarinet-sdk/node/README.md b/components/clarinet-sdk/node/README.md new file mode 100644 index 000000000..6d4714d6c --- /dev/null +++ b/components/clarinet-sdk/node/README.md @@ -0,0 +1,94 @@ +# Clarinet SDK for Node.js + +The Clarinet SDK allows to interact with the simnet in Node.js. + +If you want to use the Clarinet SDK in web browsers, try [@hirosystems/clarinet-sdk-browser](https://www.npmjs.com/package/@hirosystems/clarinet-sdk-browser). + +Find the API references of the SDK in [our documentation](https://docs.hiro.so/clarinet/feature-guides/clarinet-js-sdk). +Learn more about unit testing Clarity smart contracts in [this guide](https://docs.hiro.so/clarinet/feature-guides/test-contract-with-clarinet-sdk). + +You can use this SDK to: +- Interact with a clarinet project as you would with the Clarinet CLI +- Call public, read-only, and private functions from smart contracts +- Get clarity maps or data-var values +- Get contract interfaces (available functions and data) +- Write unit tests for Clarity smart contracts + +## Installation + +```sh +npm install @hirosystems/clarinet-sdk +``` + +## Usage + +```ts +import { initSimnet } from "@hirosystems/clarinet-sdk"; +import { Cl } from "@stacks/transactions"; + +async function main() { + const simnet = await initSimnet(); + + const accounts = simnet.getAccounts(); + const address1 = accounts.get("wallet_1"); + if (!address1) throw new Error("invalid wallet name."); + + + const call = simnet.callPublicFn("counter", "add", [Cl.uint(1)], address1); + console.log(Cl.prettyPrint(call.result)); // (ok u1) + + const counter = simnet.getDataVar("counter", "counter"); + console.log(Cl.prettyPrint(counter)); // 2 +} + +main(); +``` + + +By default, the SDK will look for a Clarinet.toml file in the current working directory. +It's also possible to provide the path to the manifest like so: +```ts + const simnet = await initSimnet("./path/to/Clarinet.toml"); +``` + +## Tests + +The SDK can be used to write unit tests for Clarinet projects. + +You'll need to have Node.js (>= 18) and NPM setup. If you are not sure how to set it up, [Volta](https://volta.sh/) is a nice tool to get started. + +In the terminal, run `node --version` to make sure it's available and up to date. + +Open your terminal and go to a new or existing Clarinet project: + +```sh +cd my-project +ls # you should see Clarinet.toml and package.json in the list +``` + +Install the dependencies and run the test + +```sh +npm install +npm test +``` + +Visit the [clarity starter project](https://github.com/hirosystems/clarity-starter) to see the testing framework in action. + + +### Type checking + +We recommend to use TypeScript to write the unit tests, but it's also possible to do it with JavaScript. To do so, rename your test files to `.test.js` instead of `.test.ts`. You can also delete the `tsconfig.json` and uninstall typescript with `npm uninstall typescript`. + +Note: If you want to write your test in JavaScript but still have a certain level of type safety and autocompletion, VSCode can help you with that. You can create a basic `jsconfig.json` file: + +```json +{ + "compilerOptions": { + "checkJs": true, + "strict": true + }, + "include": ["node_modules/@hirosystems/clarinet-sdk/vitest-helpers/src", "unit-tests"] +} +``` + diff --git a/components/clarinet-sdk/package.json b/components/clarinet-sdk/node/package.json similarity index 57% rename from components/clarinet-sdk/package.json rename to components/clarinet-sdk/node/package.json index 4e4111141..61c293f9b 100644 --- a/components/clarinet-sdk/package.json +++ b/components/clarinet-sdk/node/package.json @@ -1,11 +1,11 @@ { "name": "@hirosystems/clarinet-sdk", - "version": "2.7.0", - "description": "A SDK to interact with Clarity Smart Contracts", + "version": "2.8.0-beta1", + "description": "A SDK to interact with Clarity Smart Contracts in node.js", "homepage": "https://docs.hiro.so/clarinet/feature-guides/clarinet-js-sdk", "repository": { "type": "git", - "url": "https://github.com/hirosystems/clarinet" + "url": "git+https://github.com/hirosystems/clarinet.git" }, "engines": { "node": ">=18.0.0" @@ -15,37 +15,40 @@ "templates", "vitest-helpers/src" ], - "main": "dist/cjs/index.js", - "module": "dist/esm/index.js", - "types": "./dist/esm/index.d.ts", + "main": "./dist/cjs/node/src/index.js", + "module": "./dist/esm/node/src/index.js", + "types": "./dist/esm/node/src/index.d.ts", "exports": { ".": { "import": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" + "types": "./dist/esm/node/src/index.d.ts", + "default": "./dist/esm/node/src/index.js" }, "require": { - "types": "./dist/cjs/index.d.ts", - "default": "./dist/cjs/index.js" + "types": "./dist/cjs/node/src/index.d.ts", + "default": "./dist/cjs/node/src/index.js" } }, "./vitest": { "import": { - "types": "./dist/esm/vitest/index.d.ts", - "default": "./dist/esm/vitest/index.js" + "types": "./dist/esm/node/src/vitest/index.d.ts", + "default": "./dist/esm/node/src/vitest/index.js" }, "require": { - "types": "./dist/cjs/vitest/index.d.ts", - "default": "./dist/cjs/vitest/index.js" + "types": "./dist/cjs/node/src/vitest/index.d.ts", + "default": "./dist/cjs/node/src/vitest/index.js" } } }, - "bin": "./dist/cjs/bin/index.js", + "bin": { + "clarinet-sdk": "dist/cjs/node/src/bin/index.js" + }, "scripts": { "clean": "rimraf dist", "compile": "tsc -b ./tsconfig.json ./tsconfig.cjs.json", "build": "npm run clean; npm run compile; node ./scripts/prepare-esm-package.js", "prepare": "npm run build", + "pretest": "tsc -b ./tsconfig.json", "test": "vitest run" }, "keywords": [ @@ -58,10 +61,7 @@ "license": "GPL-3.0", "readme": "./README.md", "dependencies": { - "@hirosystems/clarinet-sdk-wasm": "^2.7.0", - "@stacks/encryption": "^6.13.0", - "@stacks/network": "^6.13.0", - "@stacks/stacking": "^6.13.0", + "@hirosystems/clarinet-sdk-wasm": "^2.8.0-beta1", "@stacks/transactions": "^6.13.0", "kolorist": "^1.8.0", "prompts": "^2.4.2", @@ -69,12 +69,8 @@ "yargs": "^17.7.2" }, "devDependencies": { - "@types/node": "^20.4.5", - "@types/prompts": "^2.4.5", - "@types/yargs": "^17.0.24", - "prettier": "^3.0.3", - "rimraf": "^5.0.1", - "ts-loader": "^9.4.4", - "typescript": "^5.1.6" + "@stacks/encryption": "^6.13.0", + "@stacks/network": "^6.13.0", + "@stacks/stacking": "^6.13.0" } } diff --git a/components/clarinet-sdk/scripts/prepare-esm-package.js b/components/clarinet-sdk/node/scripts/prepare-esm-package.js similarity index 100% rename from components/clarinet-sdk/scripts/prepare-esm-package.js rename to components/clarinet-sdk/node/scripts/prepare-esm-package.js diff --git a/components/clarinet-sdk/src/bin/index.ts b/components/clarinet-sdk/node/src/bin/index.ts similarity index 100% rename from components/clarinet-sdk/src/bin/index.ts rename to components/clarinet-sdk/node/src/bin/index.ts diff --git a/components/clarinet-sdk/node/src/index.ts b/components/clarinet-sdk/node/src/index.ts new file mode 100644 index 000000000..26fa0ff2b --- /dev/null +++ b/components/clarinet-sdk/node/src/index.ts @@ -0,0 +1,46 @@ +import { SDKOptions } from "@hirosystems/clarinet-sdk-wasm"; + +export { + tx, + type ClarityEvent, + type ParsedTransactionResult, + type DeployContractOptions, + type Tx, + type TransferSTX, +} from "../../common/src/sdkProxyHelpers.js"; + +import { vfs } from "./vfs.js"; +import { Simnet, getSessionProxy } from "./sdkProxy.js"; + +export { type Simnet } from "./sdkProxy.js"; + +const wasmModule = import("@hirosystems/clarinet-sdk-wasm"); + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json +// @ts-ignore +BigInt.prototype.toJSON = function () { + return this.toString(); +}; + +// load wasm only once and memoize it +function memoizedInit() { + let simnet: Simnet | null = null; + + return async ( + manifestPath = "./Clarinet.toml", + noCache = false, + options?: { trackCosts: boolean; trackCoverage: boolean }, + ) => { + if (noCache || !simnet) { + const module = await wasmModule; + let sdkOptions = new SDKOptions(!!options?.trackCosts, !!options?.trackCoverage); + simnet = new Proxy(new module.SDK(vfs, sdkOptions), getSessionProxy()) as unknown as Simnet; + } + + // start a new simnet session + await simnet.initSession(process.cwd(), manifestPath); + return simnet; + }; +} + +export const initSimnet = memoizedInit(); diff --git a/components/clarinet-sdk/node/src/sdkProxy.ts b/components/clarinet-sdk/node/src/sdkProxy.ts new file mode 100644 index 000000000..32198eff7 --- /dev/null +++ b/components/clarinet-sdk/node/src/sdkProxy.ts @@ -0,0 +1,155 @@ +import { Cl } from "@stacks/transactions"; +import { + CallFnArgs, + DeployContractArgs, + TransferSTXArgs, + ContractOptions, + type SDK, + type TransactionRes, +} from "@hirosystems/clarinet-sdk-wasm"; + +import { + parseEvents, + type CallFn, + type DeployContract, + type GetDataVar, + type GetMapEntry, + type MineBlock, + type ParsedTransactionResult, + type Execute, + type TransferSTX, +} from "../../common/src/sdkProxyHelpers.js"; + +/** @deprecated use `simnet.execute(command)` instead */ +type RunSnippet = SDK["runSnippet"]; + +// 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" | "callPrivateFn" + ? CallFn + : K extends "execute" + ? Execute + : K extends "runSnippet" + ? RunSnippet + : K extends "deployContract" + ? DeployContract + : K extends "transferSTX" + ? TransferSTX + : K extends "mineBlock" + ? MineBlock + : K extends "getDataVar" + ? GetDataVar + : K extends "getMapEntry" + ? GetMapEntry + : SDK[K]; +}; + +function parseTxResponse(response: TransactionRes): ParsedTransactionResult { + return { + result: Cl.deserialize(response.result), + events: parseEvents(response.events), + }; +} + +export function getSessionProxy() { + return { + get(session: SDK, prop: keyof SDK, receiver: any) { + // some of the WASM methods are proxied here to: + // - serialize clarity values input argument + // - deserialize output into clarity values + + if (prop === "callReadOnlyFn" || prop === "callPublicFn" || prop === "callPrivateFn") { + const callFn: CallFn = (contract, method, args, sender) => { + const response = session[prop]( + new CallFnArgs( + contract, + method, + args.map((a) => Cl.serialize(a)), + sender, + ), + ); + return parseTxResponse(response); + }; + return callFn; + } + + if (prop === "execute") { + const execute: Execute = (snippet) => { + const response = session.execute(snippet); + return parseTxResponse(response); + }; + return execute; + } + + if (prop === "deployContract") { + const callDeployContract: DeployContract = (name, content, options, sender) => { + const rustOptions = options + ? new ContractOptions(options.clarityVersion) + : new ContractOptions(); + + const response = session.deployContract( + new DeployContractArgs(name, content, rustOptions, sender), + ); + return parseTxResponse(response); + }; + return callDeployContract; + } + + if (prop === "transferSTX") { + const callTransferSTX: TransferSTX = (amount, ...args) => { + const response = session.transferSTX(new TransferSTXArgs(BigInt(amount), ...args)); + return parseTxResponse(response); + }; + return callTransferSTX; + } + + if (prop === "mineBlock") { + const callMineBlock: MineBlock = (txs) => { + const serializedTxs = txs.map((tx) => { + if (tx.callPublicFn) { + return { + callPublicFn: { + ...tx.callPublicFn, + args_maps: tx.callPublicFn.args.map(Cl.serialize), + }, + }; + } + if (tx.callPrivateFn) { + return { + callPrivateFn: { + ...tx.callPrivateFn, + args_maps: tx.callPrivateFn.args.map(Cl.serialize), + }, + }; + } + return tx; + }); + + const responses: TransactionRes[] = session.mineBlock(serializedTxs); + return responses.map(parseTxResponse); + }; + return callMineBlock; + } + + if (prop === "getDataVar") { + const getDataVar: GetDataVar = (...args) => { + const response = session.getDataVar(...args); + const result = Cl.deserialize(response); + return result; + }; + return getDataVar; + } + + if (prop === "getMapEntry") { + const getMapEntry: GetMapEntry = (contract, mapName, mapKey) => { + const response = session.getMapEntry(contract, mapName, Cl.serialize(mapKey)); + const result = Cl.deserialize(response); + return result; + }; + return getMapEntry; + } + + return Reflect.get(session, prop, receiver); + }, + }; +} diff --git a/components/clarinet-sdk/src/vfs.ts b/components/clarinet-sdk/node/src/vfs.ts similarity index 100% rename from components/clarinet-sdk/src/vfs.ts rename to components/clarinet-sdk/node/src/vfs.ts diff --git a/components/clarinet-sdk/src/vitest/index.ts b/components/clarinet-sdk/node/src/vitest/index.ts similarity index 100% rename from components/clarinet-sdk/src/vitest/index.ts rename to components/clarinet-sdk/node/src/vitest/index.ts diff --git a/components/clarinet-sdk/templates/package.json b/components/clarinet-sdk/node/templates/package.json similarity index 100% rename from components/clarinet-sdk/templates/package.json rename to components/clarinet-sdk/node/templates/package.json diff --git a/components/clarinet-sdk/templates/tests/contract.test.ts b/components/clarinet-sdk/node/templates/tests/contract.test.ts similarity index 100% rename from components/clarinet-sdk/templates/tests/contract.test.ts rename to components/clarinet-sdk/node/templates/tests/contract.test.ts diff --git a/components/clarinet-sdk/templates/tsconfig.json b/components/clarinet-sdk/node/templates/tsconfig.json similarity index 100% rename from components/clarinet-sdk/templates/tsconfig.json rename to components/clarinet-sdk/node/templates/tsconfig.json diff --git a/components/clarinet-sdk/templates/vitest.config.js b/components/clarinet-sdk/node/templates/vitest.config.js similarity index 100% rename from components/clarinet-sdk/templates/vitest.config.js rename to components/clarinet-sdk/node/templates/vitest.config.js diff --git a/components/clarinet-sdk/tests/deployment-plan.test.ts b/components/clarinet-sdk/node/tests/deployment-plan.test.ts similarity index 98% rename from components/clarinet-sdk/tests/deployment-plan.test.ts rename to components/clarinet-sdk/node/tests/deployment-plan.test.ts index 984da1c88..c94ed9fb8 100644 --- a/components/clarinet-sdk/tests/deployment-plan.test.ts +++ b/components/clarinet-sdk/node/tests/deployment-plan.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, beforeEach, afterEach } from "vitest"; // test the built package and not the source code // makes it simpler to handle wasm build -import { initSimnet } from "../dist/esm"; +import { initSimnet } from ".."; import { Cl } from "@stacks/transactions"; const nbOfBootContracts = 24; diff --git a/components/clarinet-sdk/tests/fixtures/.gitignore b/components/clarinet-sdk/node/tests/fixtures/.gitignore similarity index 100% rename from components/clarinet-sdk/tests/fixtures/.gitignore rename to components/clarinet-sdk/node/tests/fixtures/.gitignore diff --git a/components/clarinet-sdk/tests/fixtures/Clarinet.toml b/components/clarinet-sdk/node/tests/fixtures/Clarinet.toml similarity index 100% rename from components/clarinet-sdk/tests/fixtures/Clarinet.toml rename to components/clarinet-sdk/node/tests/fixtures/Clarinet.toml diff --git a/components/clarinet-sdk/tests/fixtures/InvalidManifest.toml b/components/clarinet-sdk/node/tests/fixtures/InvalidManifest.toml similarity index 100% rename from components/clarinet-sdk/tests/fixtures/InvalidManifest.toml rename to components/clarinet-sdk/node/tests/fixtures/InvalidManifest.toml diff --git a/components/clarinet-sdk/tests/fixtures/LightManifest.toml b/components/clarinet-sdk/node/tests/fixtures/LightManifest.toml similarity index 100% rename from components/clarinet-sdk/tests/fixtures/LightManifest.toml rename to components/clarinet-sdk/node/tests/fixtures/LightManifest.toml diff --git a/components/clarinet-sdk/tests/fixtures/contracts/block-height-tests.clar b/components/clarinet-sdk/node/tests/fixtures/contracts/block-height-tests.clar similarity index 100% rename from components/clarinet-sdk/tests/fixtures/contracts/block-height-tests.clar rename to components/clarinet-sdk/node/tests/fixtures/contracts/block-height-tests.clar diff --git a/components/clarinet-sdk/tests/fixtures/contracts/counter.clar b/components/clarinet-sdk/node/tests/fixtures/contracts/counter.clar similarity index 100% rename from components/clarinet-sdk/tests/fixtures/contracts/counter.clar rename to components/clarinet-sdk/node/tests/fixtures/contracts/counter.clar diff --git a/components/clarinet-sdk/tests/fixtures/contracts/invalid.clar b/components/clarinet-sdk/node/tests/fixtures/contracts/invalid.clar similarity index 100% rename from components/clarinet-sdk/tests/fixtures/contracts/invalid.clar rename to components/clarinet-sdk/node/tests/fixtures/contracts/invalid.clar diff --git a/components/clarinet-sdk/tests/fixtures/contracts/multiplier-contract.clar b/components/clarinet-sdk/node/tests/fixtures/contracts/multiplier-contract.clar similarity index 100% rename from components/clarinet-sdk/tests/fixtures/contracts/multiplier-contract.clar rename to components/clarinet-sdk/node/tests/fixtures/contracts/multiplier-contract.clar diff --git a/components/clarinet-sdk/tests/fixtures/contracts/multiplier-trait.clar b/components/clarinet-sdk/node/tests/fixtures/contracts/multiplier-trait.clar similarity index 100% rename from components/clarinet-sdk/tests/fixtures/contracts/multiplier-trait.clar rename to components/clarinet-sdk/node/tests/fixtures/contracts/multiplier-trait.clar diff --git a/components/clarinet-sdk/tests/fixtures/deployments/custom.simnet-plan.yaml b/components/clarinet-sdk/node/tests/fixtures/deployments/custom.simnet-plan.yaml similarity index 100% rename from components/clarinet-sdk/tests/fixtures/deployments/custom.simnet-plan.yaml rename to components/clarinet-sdk/node/tests/fixtures/deployments/custom.simnet-plan.yaml diff --git a/components/clarinet-sdk/tests/fixtures/settings/Devnet.toml b/components/clarinet-sdk/node/tests/fixtures/settings/Devnet.toml similarity index 100% rename from components/clarinet-sdk/tests/fixtures/settings/Devnet.toml rename to components/clarinet-sdk/node/tests/fixtures/settings/Devnet.toml diff --git a/components/clarinet-sdk/tests/pox-locking.test.ts b/components/clarinet-sdk/node/tests/pox-locking.test.ts similarity index 93% rename from components/clarinet-sdk/tests/pox-locking.test.ts rename to components/clarinet-sdk/node/tests/pox-locking.test.ts index 3169613bf..6ce0d7bbc 100644 --- a/components/clarinet-sdk/tests/pox-locking.test.ts +++ b/components/clarinet-sdk/node/tests/pox-locking.test.ts @@ -14,7 +14,7 @@ import { // test the built package and not the source code // makes it simpler to handle wasm build -import { Simnet, initSimnet } from "../dist/esm"; +import { Simnet, initSimnet } from ".."; const MAX_U128 = 340282366920938463463374607431768211455n; const maxAmount = MAX_U128; @@ -76,8 +76,8 @@ describe("test pox-3", () => { ), ); - const stxAccount = simnet.runSnippet(`(stx-account '${address1})`); - expect(stxAccount).toStrictEqual( + const stxAccount = simnet.execute(`(stx-account '${address1})`); + expect(stxAccount.result).toStrictEqual( Cl.tuple({ locked: Cl.uint(ustxAmount), unlocked: Cl.uint(initialSTXBalance - ustxAmount), @@ -102,8 +102,8 @@ describe("test pox-3", () => { simnet.callPublicFn(poxContract, "stack-stx", stackStxArgs, address1); simnet.mineEmptyBlocks(2098); - const stxAccountBefore = simnet.runSnippet(`(stx-account '${address1})`); - expect(stxAccountBefore).toStrictEqual( + const stxAccountBefore = simnet.execute(`(stx-account '${address1})`); + expect(stxAccountBefore.result).toStrictEqual( Cl.tuple({ locked: Cl.uint(ustxAmount), unlocked: Cl.uint(initialSTXBalance - ustxAmount), @@ -112,8 +112,8 @@ describe("test pox-3", () => { ); simnet.mineEmptyBlocks(1); - const stxAccountAfter = simnet.runSnippet(`(stx-account '${address1})`); - expect(stxAccountAfter).toStrictEqual( + const stxAccountAfter = simnet.execute(`(stx-account '${address1})`); + expect(stxAccountAfter.result).toStrictEqual( Cl.tuple({ locked: Cl.uint(0), unlocked: Cl.uint(initialSTXBalance), @@ -248,8 +248,8 @@ describe("test pox-4", () => { ), ); - const stxAccount = simnet.runSnippet(`(stx-account '${address1})`); - expect(stxAccount).toStrictEqual( + const stxAccount = simnet.execute(`(stx-account '${address1})`); + expect(stxAccount.result).toStrictEqual( Cl.tuple({ locked: Cl.uint(ustxAmount), unlocked: Cl.uint(initialSTXBalance - ustxAmount), diff --git a/components/clarinet-sdk/tests/reports.test.ts b/components/clarinet-sdk/node/tests/reports.test.ts similarity index 98% rename from components/clarinet-sdk/tests/reports.test.ts rename to components/clarinet-sdk/node/tests/reports.test.ts index 18a8f7f44..5e6e9eebe 100644 --- a/components/clarinet-sdk/tests/reports.test.ts +++ b/components/clarinet-sdk/node/tests/reports.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, beforeEach, afterEach } from "vitest"; // test the built package and not the source code // makes it simpler to handle wasm build -import { initSimnet } from "../dist/esm"; +import { initSimnet } from ".."; const address1 = "ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5"; diff --git a/components/clarinet-sdk/tests/simnet-usage.test.ts b/components/clarinet-sdk/node/tests/simnet-usage.test.ts similarity index 86% rename from components/clarinet-sdk/tests/simnet-usage.test.ts rename to components/clarinet-sdk/node/tests/simnet-usage.test.ts index b6a5a98dc..99d1ca4a4 100644 --- a/components/clarinet-sdk/tests/simnet-usage.test.ts +++ b/components/clarinet-sdk/node/tests/simnet-usage.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it, beforeEach, afterEach } from "vitest"; // test the built package and not the source code // makes it simpler to handle wasm build -import { Simnet, initSimnet, tx } from "../dist/esm"; +import { Simnet, initSimnet, tx } from ".."; const deployerAddr = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"; const address1 = "ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5"; @@ -67,8 +67,8 @@ describe("basic simnet interactions", () => { // the latest contract in the manifest is deployed in 2.4 expect(simnet.currentEpoch).toBe("2.4"); - simnet.setEpoch("2.0"); - expect(simnet.currentEpoch).toBe("2.0"); + simnet.setEpoch("2.5"); + expect(simnet.currentEpoch).toBe("2.5"); // @ts-ignore // "0" is an invalid epoch @@ -76,17 +76,23 @@ describe("basic simnet interactions", () => { simnet.setEpoch("0"); expect(simnet.currentEpoch).toBe("2.5"); }); + + it("can get default clarity version for current epoch", () => { + const clarityVersion = simnet.getDefaultClarityVersionForCurrentEpoch(); + expect(clarityVersion).toBe("Clarity 2"); + }); }); describe("simnet can run arbitrary snippets", () => { it("can run simple snippets", () => { - const res = simnet.runSnippet("(+ 1 2)"); - expect(res).toStrictEqual(Cl.int(3)); + const res = simnet.execute("(+ 1 2)"); + expect(res.result).toStrictEqual(Cl.int(3)); }); it("show diagnostic in case of error", () => { - const res = simnet.runSnippet("(+ 1 u2)"); - expect(res).toBe("error:\nexpecting expression of type 'int', found 'uint'"); + expect(() => { + simnet.execute("(+ 1 u2)"); + }).toThrow("error: expecting expression of type 'int', found 'uint'"); }); }); @@ -277,6 +283,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(11); @@ -324,10 +331,10 @@ describe("simnet can get contracts info and deploy contracts", () => { expect(contract2Interface.epoch).toBe("Epoch24"); expect(contract2Interface.clarity_version).toBe("Clarity2"); - simnet.setEpoch("2.0"); + simnet.setEpoch("2.5"); simnet.deployContract("contract3", source, { clarityVersion: 1 }, deployerAddr); const contract3Interface = simnet.getContractsInterfaces().get(`${simnet.deployer}.contract3`)!; - expect(contract3Interface.epoch).toBe("Epoch20"); + expect(contract3Interface.epoch).toBe("Epoch25"); expect(contract3Interface.clarity_version).toBe("Clarity1"); }); }); @@ -343,6 +350,34 @@ describe("simnet can transfer stx", () => { }); }); +describe("the simnet can execute commands", () => { + it("can mint_stx", () => { + const result = simnet.executeCommand( + "::mint_stx ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM 1000", + ); + expect(result).toBe("ā†’ ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM: 100000000001000 ĀµSTX"); + }); + + it("can get_assets_maps", () => { + simnet.executeCommand("::mint_stx ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM 1000"); + let result = simnet.executeCommand("::get_assets_maps"); + const expected = [ + "+-------------------------------------------+-----------------+", + "| Address | uSTX |", + "+-------------------------------------------+-----------------+", + "| ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM | 100000000001000 |", + "+-------------------------------------------+-----------------+", + "| ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 | 100000000000000 |", + "+-------------------------------------------+-----------------+", + "| ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG | 100000000000000 |", + "+-------------------------------------------+-----------------+", + "| STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 | 100000000000000 |", + "+-------------------------------------------+-----------------+\n", + ].join("\n"); + expect(result).toBe(expected); + }); +}); + describe("the sdk handles multiple manifests project", () => { it("handle invalid project", () => { const manifestPath = path.join(process.cwd(), "tests/fixtures/contracts/invalid.clar"); diff --git a/components/clarinet-sdk/node/tests/support-clarity-version.test.ts b/components/clarinet-sdk/node/tests/support-clarity-version.test.ts new file mode 100644 index 000000000..31a21aefe --- /dev/null +++ b/components/clarinet-sdk/node/tests/support-clarity-version.test.ts @@ -0,0 +1,70 @@ +import { Cl } from "@stacks/transactions"; +import { describe, expect, it, beforeEach } from "vitest"; + +// test the built package and not the source code +// makes it simpler to handle wasm build +import { Simnet, initSimnet } from ".."; + +let simnet: Simnet; + +beforeEach(async () => { + simnet = await initSimnet("tests/fixtures/Clarinet.toml"); +}); + +describe("the sdk handle all clarity version", () => { + it("handle clarity 1", () => { + simnet.setEpoch("2.05"); + let resOk = simnet.execute('(index-of "stacks" "s")'); + expect(resOk.result).toStrictEqual(Cl.some(Cl.uint(0))); + + // `index-of?` was introduced in clarity 2 + expect(() => simnet.execute('(index-of? "stacks" "s")')).toThrowError( + "error: use of unresolved function 'index-of?'", + ); + + // `tenure-height` was introduced in clarity 3 + expect(() => simnet.execute("(print tenure-height)")).toThrowError( + "error: use of unresolved variable 'tenure-height'", + ); + }); + + it("handle clarity 2", () => { + simnet.setEpoch("2.4"); + // `index-of` is still available in clarity 2 + let resOk1 = simnet.execute('(index-of "stacks" "s")'); + expect(resOk1.result).toStrictEqual(Cl.some(Cl.uint(0))); + + // `index-of?` is available in clarity 2 + let resOk2 = simnet.execute('(index-of? "stacks" "s")'); + expect(resOk2.result).toStrictEqual(Cl.some(Cl.uint(0))); + + // `block-height` is avaliable in clarity 1 & 2 + let resOk3 = simnet.execute("(print block-height)"); + expect(resOk3.result).toStrictEqual(Cl.uint(1)); + + // `tenure-height` was introduced in clarity 3 + expect(() => simnet.execute("(print tenure-height)")).toThrowError( + "error: use of unresolved variable 'tenure-height'", + ); + }); + + it("handle clarity 3", () => { + simnet.setEpoch("3.0"); + // `index-of` is still available in clarity 2 + let resOk1 = simnet.execute('(index-of "stacks" "s")'); + expect(resOk1.result).toStrictEqual(Cl.some(Cl.uint(0))); + + // `index-of?` is available in clarity 2 + let resOk2 = simnet.execute('(index-of? "stacks" "s")'); + expect(resOk2.result).toStrictEqual(Cl.some(Cl.uint(0))); + + // `tenure-height` was introduced in clarity 3 + let resOk3 = simnet.execute("(print tenure-height)"); + expect(resOk3.result).toStrictEqual(Cl.uint(1)); + + // `block-height` was removed in clarity 3 + expect(() => simnet.execute("(print block-height)")).toThrowError( + "error: use of unresolved variable 'block-height'", + ); + }); +}); diff --git a/components/clarinet-sdk/tsconfig.base.json b/components/clarinet-sdk/node/tsconfig.base.json similarity index 88% rename from components/clarinet-sdk/tsconfig.base.json rename to components/clarinet-sdk/node/tsconfig.base.json index a74488b30..414b57a8e 100644 --- a/components/clarinet-sdk/tsconfig.base.json +++ b/components/clarinet-sdk/node/tsconfig.base.json @@ -5,7 +5,7 @@ "lib": ["es2022"], "moduleResolution": "Node", - "rootDir": "src", + "rootDir": "../", "resolveJsonModule": false, "allowJs": false, @@ -19,6 +19,6 @@ "noImplicitAny": true, "skipLibCheck": true }, - "include": ["src"], + "include": ["./src", "../common/src"], "exclude": ["node_modules", "dist"] } diff --git a/components/clarinet-sdk/tsconfig.cjs.json b/components/clarinet-sdk/node/tsconfig.cjs.json similarity index 100% rename from components/clarinet-sdk/tsconfig.cjs.json rename to components/clarinet-sdk/node/tsconfig.cjs.json diff --git a/components/clarinet-sdk/tsconfig.json b/components/clarinet-sdk/node/tsconfig.json similarity index 100% rename from components/clarinet-sdk/tsconfig.json rename to components/clarinet-sdk/node/tsconfig.json diff --git a/components/clarinet-sdk/vitest-helpers/README.md b/components/clarinet-sdk/node/vitest-helpers/README.md similarity index 100% rename from components/clarinet-sdk/vitest-helpers/README.md rename to components/clarinet-sdk/node/vitest-helpers/README.md diff --git a/components/clarinet-sdk/vitest-helpers/src/clarityValuesMatchers.ts b/components/clarinet-sdk/node/vitest-helpers/src/clarityValuesMatchers.ts similarity index 100% rename from components/clarinet-sdk/vitest-helpers/src/clarityValuesMatchers.ts rename to components/clarinet-sdk/node/vitest-helpers/src/clarityValuesMatchers.ts diff --git a/components/clarinet-sdk/vitest-helpers/src/global.d.ts b/components/clarinet-sdk/node/vitest-helpers/src/global.d.ts similarity index 100% rename from components/clarinet-sdk/vitest-helpers/src/global.d.ts rename to components/clarinet-sdk/node/vitest-helpers/src/global.d.ts diff --git a/components/clarinet-sdk/vitest-helpers/src/vitest.d.ts b/components/clarinet-sdk/node/vitest-helpers/src/vitest.d.ts similarity index 100% rename from components/clarinet-sdk/vitest-helpers/src/vitest.d.ts rename to components/clarinet-sdk/node/vitest-helpers/src/vitest.d.ts diff --git a/components/clarinet-sdk/vitest-helpers/src/vitest.setup.ts b/components/clarinet-sdk/node/vitest-helpers/src/vitest.setup.ts similarity index 100% rename from components/clarinet-sdk/vitest-helpers/src/vitest.setup.ts rename to components/clarinet-sdk/node/vitest-helpers/src/vitest.setup.ts diff --git a/components/clarinet-sdk/vitest-helpers/tests/clarityValueMatchers.test.ts b/components/clarinet-sdk/node/vitest-helpers/tests/clarityValueMatchers.test.ts similarity index 100% rename from components/clarinet-sdk/vitest-helpers/tests/clarityValueMatchers.test.ts rename to components/clarinet-sdk/node/vitest-helpers/tests/clarityValueMatchers.test.ts diff --git a/components/clarinet-sdk/vitest-helpers/tsconfig.json b/components/clarinet-sdk/node/vitest-helpers/tsconfig.json similarity index 100% rename from components/clarinet-sdk/vitest-helpers/tsconfig.json rename to components/clarinet-sdk/node/vitest-helpers/tsconfig.json diff --git a/components/clarinet-sdk/vitest.config.mjs b/components/clarinet-sdk/node/vitest.config.mjs similarity index 100% rename from components/clarinet-sdk/vitest.config.mjs rename to components/clarinet-sdk/node/vitest.config.mjs diff --git a/components/clarinet-sdk/src/contractAst.ts b/components/clarinet-sdk/src/contractAst.ts deleted file mode 100644 index 0d2c96fb2..000000000 --- a/components/clarinet-sdk/src/contractAst.ts +++ /dev/null @@ -1,48 +0,0 @@ -type Atom = { - Atom: String; -}; - -type AtomValue = { - AtomValue: any; -}; - -type List = { - List: Expression[]; -}; - -type LiteralValue = { - LiteralValue: any; -}; - -type Field = { - Field: any; -}; - -type TraitReference = { - TraitReference: any; -}; - -type ExpressionType = Atom | AtomValue | List | LiteralValue | Field | TraitReference; - -type Span = { - start_line: number; - start_column: number; - end_line: number; - end_column: number; -}; - -type Expression = { - expr: ExpressionType; - id: number; - span: Span; -}; - -/** ContractAST basic type. To be improved */ -export type ContractAST = { - contract_identifier: any; - pre_expressions: any[]; - expressions: Expression[]; - top_level_expression_sorting: number[]; - referenced_traits: Map; - implemented_traits: any[]; -}; diff --git a/components/clarinet-sdk/src/contractInterface.ts b/components/clarinet-sdk/src/contractInterface.ts deleted file mode 100644 index 9172d5b09..000000000 --- a/components/clarinet-sdk/src/contractInterface.ts +++ /dev/null @@ -1,70 +0,0 @@ -export type ContractInterfaceFunctionAccess = "private" | "public" | "read_only"; - -export type ContractInterfaceTupleEntryType = { name: string; type: ContractInterfaceAtomType }; - -export type ContractInterfaceAtomType = - | "none" - | "int128" - | "uint128" - | "bool" - | "principal" - | { buffer: { length: number } } - | { "string-utf8": { length: number } } - | { "string-ascii": { length: number } } - | { tuple: ContractInterfaceTupleEntryType[] } - | { optional: ContractInterfaceAtomType } - | { response: { ok: ContractInterfaceAtomType; error: ContractInterfaceAtomType } } - | { list: { type: ContractInterfaceAtomType; length: number } } - | "trait_reference"; - -export type ContractInterfaceFunctionArg = { name: string; type: ContractInterfaceAtomType }; - -export type ContractInterfaceFunctionOutput = { type: ContractInterfaceAtomType }; - -export type ContractInterfaceFunction = { - name: string; - access: ContractInterfaceFunctionAccess; - args: ContractInterfaceFunctionArg[]; - outputs: ContractInterfaceFunctionOutput; -}; - -export type ContractInterfaceVariableAccess = "constant" | "variable"; - -export type ContractInterfaceVariable = { - name: string; - type: ContractInterfaceAtomType; - access: ContractInterfaceVariableAccess; -}; - -export type ContractInterfaceMap = { - name: string; - key: ContractInterfaceAtomType; - value: ContractInterfaceAtomType; -}; - -export type ContractInterfaceFungibleTokens = { name: string }; - -export type ContractInterfaceNonFungibleTokens = { name: string; type: ContractInterfaceAtomType }; - -export type StacksEpochId = - | "Epoch10" - | "Epoch20" - | "Epoch2_05" - | "Epoch21" - | "Epoch22" - | "Epoch23" - | "Epoch24" - | "Epoch25" - | "Epoch30"; - -export type ClarityVersion = "Clarity1" | "Clarity2"; - -export type ContractInterface = { - functions: ContractInterfaceFunction[]; - variables: ContractInterfaceVariable[]; - maps: ContractInterfaceMap[]; - fungible_tokens: ContractInterfaceFungibleTokens[]; - non_fungible_tokens: ContractInterfaceNonFungibleTokens[]; - epoch: StacksEpochId; - clarity_version: ClarityVersion; -}; diff --git a/components/clarinet-sdk/src/index.ts b/components/clarinet-sdk/src/index.ts deleted file mode 100644 index 631e87049..000000000 --- a/components/clarinet-sdk/src/index.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { Cl, ClarityValue } from "@stacks/transactions"; -import { - SDK, - SDKOptions, - TransactionRes, - CallFnArgs, - DeployContractArgs, - TransferSTXArgs, - ContractOptions, -} from "@hirosystems/clarinet-sdk-wasm"; - -import { vfs } from "./vfs.js"; -import type { ContractInterface } from "./contractInterface.js"; -import { ContractAST } from "./contractAst.js"; - -const wasmModule = import("@hirosystems/clarinet-sdk-wasm"); - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json -// @ts-ignore -BigInt.prototype.toJSON = function () { - return this.toString(); -}; - -export type ClarityEvent = { - event: string; - data: { raw_value?: string; value?: ClarityValue; [key: string]: any }; -}; - -export type ParsedTransactionResult = { - result: ClarityValue; - events: ClarityEvent[]; -}; - -export type CallFn = ( - contract: string, - method: string, - args: ClarityValue[], - sender: string, -) => ParsedTransactionResult; - -export type DeployContractOptions = { - clarityVersion: 1 | 2; -}; -export type DeployContract = ( - name: string, - content: string, - options: DeployContractOptions | null, - sender: string, -) => ParsedTransactionResult; - -export type TransferSTX = ( - amount: number | bigint, - recipient: string, - sender: string, -) => ParsedTransactionResult; - -export type Tx = - | { - callPublicFn: { - contract: string; - method: string; - 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; - options: DeployContractOptions | null; - sender: string; - }; - transferSTX?: never; - } - | { - callPublicFn?: never; - callPrivateFn?: never; - deployContradct?: never; - transferSTX: { amount: number; recipient: string; sender: string }; - }; - -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, - options: DeployContractOptions | null, - sender: string, - ): Tx => ({ - deployContract: { name, content, options, sender }, - }), - transferSTX: (amount: number, recipient: string, sender: string): Tx => ({ - transferSTX: { amount, recipient, sender }, - }), -}; - -export type MineBlock = (txs: Array) => ParsedTransactionResult[]; -export type GetDataVar = (contract: string, dataVar: string) => ClarityValue; -export type GetMapEntry = (contract: string, mapName: string, mapKey: ClarityValue) => ClarityValue; -export type GetContractAST = (contractId: string) => ContractAST; -export type GetContractsInterfaces = () => Map; -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" | "callPrivateFn" - ? CallFn - : K extends "runSnippet" - ? RunSnippet - : K extends "deployContract" - ? DeployContract - : K extends "transferSTX" - ? TransferSTX - : K extends "mineBlock" - ? MineBlock - : K extends "getDataVar" - ? GetDataVar - : K extends "getMapEntry" - ? GetMapEntry - : K extends "getContractAST" - ? GetContractAST - : K extends "getContractsInterfaces" - ? GetContractsInterfaces - : SDK[K]; -}; - -function parseEvents(events: string): ClarityEvent[] { - try { - // @todo: improve type safety - return JSON.parse(events).map((e: string) => { - const { event, data } = JSON.parse(e); - if ("raw_value" in data) { - data.value = Cl.deserialize(data.raw_value); - } - return { - event: event, - data: data, - }; - }); - } catch (e) { - console.error(`Fail to parse events: ${e}`); - return []; - } -} - -function parseTxResponse(response: TransactionRes): ParsedTransactionResult { - return { - result: Cl.deserialize(response.result), - events: parseEvents(response.events), - }; -} - -const getSessionProxy = () => ({ - get(session: SDK, prop: keyof SDK, receiver: any) { - // some of the WASM methods are proxied here to: - // - serialize clarity values input argument - // - deserialize output into clarity values - - if (prop === "callReadOnlyFn" || prop === "callPublicFn" || prop === "callPrivateFn") { - const callFn: CallFn = (contract, method, args, sender) => { - const response = session[prop]( - new CallFnArgs( - contract, - method, - args.map((a) => Cl.serialize(a)), - sender, - ), - ); - return parseTxResponse(response); - }; - return callFn; - } - - if (prop === "runSnippet") { - const runSnippet: RunSnippet = (snippet) => { - const response = session[prop](snippet); - if (response.startsWith("0x")) { - return Cl.deserialize(response); - } else { - return response; - } - }; - return runSnippet; - } - - if (prop === "deployContract") { - const callDeployContract: DeployContract = (name, content, options, sender) => { - const rustOptions = options - ? new ContractOptions(options.clarityVersion) - : new ContractOptions(); - - const response = session.deployContract( - new DeployContractArgs(name, content, rustOptions, sender), - ); - return parseTxResponse(response); - }; - return callDeployContract; - } - - if (prop === "transferSTX") { - const callTransferSTX: TransferSTX = (amount, ...args) => { - const response = session.transferSTX(new TransferSTXArgs(BigInt(amount), ...args)); - return parseTxResponse(response); - }; - return callTransferSTX; - } - - if (prop === "mineBlock") { - const callMineBlock: MineBlock = (txs) => { - const serializedTxs = txs.map((tx) => { - if (tx.callPublicFn) { - return { - callPublicFn: { - ...tx.callPublicFn, - args_maps: tx.callPublicFn.args.map(Cl.serialize), - }, - }; - } - if (tx.callPrivateFn) { - return { - callPrivateFn: { - ...tx.callPrivateFn, - args_maps: tx.callPrivateFn.args.map(Cl.serialize), - }, - }; - } - return tx; - }); - - const responses: TransactionRes[] = session.mineBlock(serializedTxs); - return responses.map(parseTxResponse); - }; - return callMineBlock; - } - - if (prop === "getDataVar") { - const getDataVar: GetDataVar = (...args) => { - const response = session.getDataVar(...args); - const result = Cl.deserialize(response); - return result; - }; - return getDataVar; - } - - if (prop === "getMapEntry") { - const getMapEntry: GetMapEntry = (contract, mapName, mapKey) => { - const response = session.getMapEntry(contract, mapName, Cl.serialize(mapKey)); - const result = Cl.deserialize(response); - return result; - }; - return getMapEntry; - } - - return Reflect.get(session, prop, receiver); - }, -}); - -// load wasm only once and memoize it -function memoizedInit() { - let simnet: Simnet | null = null; - - return async ( - manifestPath = "./Clarinet.toml", - noCache = false, - options?: { trackCosts: boolean; trackCoverage: boolean }, - ) => { - if (noCache || !simnet) { - const module = await wasmModule; - let sdkOptions = new SDKOptions(!!options?.trackCosts, !!options?.trackCoverage); - simnet = new Proxy(new module.SDK(vfs, sdkOptions), getSessionProxy()) as unknown as Simnet; - } - - // start a new simnet session - await simnet.initSession(process.cwd(), manifestPath); - return simnet; - }; -} - -export const initSimnet = memoizedInit(); diff --git a/components/clarinet-sdk/tests/support-clarity-version.test.ts b/components/clarinet-sdk/tests/support-clarity-version.test.ts deleted file mode 100644 index 0597347dc..000000000 --- a/components/clarinet-sdk/tests/support-clarity-version.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Cl } from "@stacks/transactions"; -import { describe, expect, it, beforeEach } from "vitest"; - -// test the built package and not the source code -// makes it simpler to handle wasm build -import { Simnet, initSimnet } from "../dist/esm"; - -let simnet: Simnet; - -beforeEach(async () => { - simnet = await initSimnet("tests/fixtures/Clarinet.toml"); -}); - -describe.only("the sdk handle all clarity version", () => { - it("handle clarity 1", () => { - simnet.setEpoch("2.05"); - let resOk = simnet.runSnippet('(index-of "stacks" "s")'); - expect(resOk).toStrictEqual(Cl.some(Cl.uint(0))); - - // `index-of?` was introduced in clarity 2 - let resFail1 = simnet.runSnippet('(index-of? "stacks" "s")'); - expect(resFail1).toBe("error:\nuse of unresolved function 'index-of?'"); - - // `tenure-height` was introduced in clarity 3 - let resFail2 = simnet.runSnippet("(print tenure-height)"); - expect(resFail2).toBe("error:\nuse of unresolved variable 'tenure-height'"); - }); - - it("handle clarity 2", () => { - simnet.setEpoch("2.4"); - // `index-of` is still available in clarity 2 - let resOk1 = simnet.runSnippet('(index-of "stacks" "s")'); - expect(resOk1).toStrictEqual(Cl.some(Cl.uint(0))); - - // `index-of?` is available in clarity 2 - let resOk2 = simnet.runSnippet('(index-of? "stacks" "s")'); - expect(resOk2).toStrictEqual(Cl.some(Cl.uint(0))); - - // `block-height` is avaliable in clarity 1 & 2 - let resOk3 = simnet.runSnippet("(print block-height)"); - expect(resOk3).toStrictEqual(Cl.uint(1)); - - // `tenure-height` was introduced in clarity 3 - let resFail = simnet.runSnippet("(print tenure-height)"); - expect(resFail).toBe("error:\nuse of unresolved variable 'tenure-height'"); - }); - - it("handle clarity 3", () => { - simnet.setEpoch("3.0"); - // `index-of` is still available in clarity 2 - let resOk1 = simnet.runSnippet('(index-of "stacks" "s")'); - expect(resOk1).toStrictEqual(Cl.some(Cl.uint(0))); - - // `index-of?` is available in clarity 2 - let resOk2 = simnet.runSnippet('(index-of? "stacks" "s")'); - expect(resOk2).toStrictEqual(Cl.some(Cl.uint(0))); - - // `tenure-height` was introduced in clarity 3 - let resOk3 = simnet.runSnippet("(print tenure-height)"); - expect(resOk3).toStrictEqual(Cl.uint(1)); - - // `block-height` was removed in clarity 3 - let resFail = simnet.runSnippet("(print block-height)"); - expect(resFail).toBe("error:\nuse of unresolved variable 'block-height'"); - }); -}); diff --git a/components/clarity-jupyter-kernel/Cargo.toml b/components/clarity-jupyter-kernel/Cargo.toml index 90d771533..47fdaa59f 100644 --- a/components/clarity-jupyter-kernel/Cargo.toml +++ b/components/clarity-jupyter-kernel/Cargo.toml @@ -27,6 +27,6 @@ zmq = { version = "0.9.1", default-features = false } uuid = { version = "1.0.0", features = ["v4"] } hmac = { version = "0.7.1" } hex = { version = "0.3.2" } -colored = { version = "1.8.0" } # should use ansi_term instead +colored = { version = "1.8.0" } dirs = { version = "4.0.0" } chrono = { version = "0.4.31" } diff --git a/components/clarity-repl/Cargo.toml b/components/clarity-repl/Cargo.toml index 6c4338876..d65135270 100644 --- a/components/clarity-repl/Cargo.toml +++ b/components/clarity-repl/Cargo.toml @@ -22,8 +22,9 @@ categories = [ ] [dependencies] -ansi_term = "0.12.1" +ansi_term = "0.12.1" # to be replaced with colored in the future chrono = "0.4.31" +colored = "2.1.0" lazy_static = "1.4.0" regex = "1.7" serde = { version = "1", features = ["derive"] } @@ -38,6 +39,7 @@ clarity = { git = "https://github.com/stacks-network/stacks-core.git", branch="f clar2wasm = { git = "https://github.com/stacks-network/clarity-wasm.git", branch = "chore/update-clarity", optional = true } # clar2wasm = { path="../../../clarity-wasm/clar2wasm", optional = true } pox-locking = { git = "https://github.com/stacks-network/stacks-core.git", branch="feat/clarity-wasm-develop", optional = true, default-features = false } +prettytable-rs = { version = "0.10.0" } # DAP Debugger tokio = { version = "1.35.1", features = ["full"], optional = true } @@ -52,27 +54,18 @@ memchr = { version = "2.4.1", optional = true } # CLI pico-args = { version = "0.5.0", optional = true } rustyline = { version = "14.0.0", optional = true } -prettytable-rs = { version = "0.10.0", optional = true } hiro_system_kit = { version = "0.1.0", package = "hiro-system-kit", path = "../hiro-system-kit", default-features = false } reqwest = { version = "0.11", default-features = false, features = [ "json", "rustls-tls", ] } -# WASM -wasm-bindgen = { version = "0.2.91", optional = true } -wasm-bindgen-futures = { version = "0.4.41", optional = true } - [dev-dependencies] test-case = "*" [lib] name = "clarity_repl" path = "src/lib.rs" -# Default type -# crate-type = ["lib"] -# Use this instead for WASM builds -crate-type = ["cdylib", "rlib"] [[bin]] name = "clarity-repl" @@ -83,7 +76,6 @@ default = ["cli", "dap"] cli = [ "pico-args", "rustyline", - "prettytable-rs", "clarity/canonical", "clarity/developer-mode", "clarity/devtools", @@ -111,15 +103,8 @@ dap = [ "log", ] wasm = [ - "wasm-bindgen", - "wasm-bindgen-futures", "clarity/wasm", "clarity/developer-mode", "clarity/devtools", "pox-locking/wasm" ] - -[package.metadata.wasm-pack.profile.release.wasm-bindgen] -debug-js-glue = false -demangle-name-section = false -dwarf-debug-info = false diff --git a/components/clarity-repl/src/bin.rs b/components/clarity-repl/src/bin.rs index 7266a3047..d126b0555 100644 --- a/components/clarity-repl/src/bin.rs +++ b/components/clarity-repl/src/bin.rs @@ -51,7 +51,7 @@ fn main() { } }; - let (_, output, _) = session.handle_command(&code_str); + let (_, output, _) = session.process_console_input(&code_str); for line in output { println!("{}", line); } diff --git a/components/clarity-repl/src/frontend/terminal.rs b/components/clarity-repl/src/frontend/terminal.rs index b7738b78c..789d0c91f 100644 --- a/components/clarity-repl/src/frontend/terminal.rs +++ b/components/clarity-repl/src/frontend/terminal.rs @@ -106,14 +106,13 @@ impl Terminal { println!("{}", black!("Enter \"::help\" for usage hints.")); println!("{}", black!("Connected to a transient in-memory database.")); - let output = match self.session.display_digest() { - Ok(output) => output, - Err(e) => { - println!("{}", e); - std::process::exit(1); - } - }; - println!("{}", output); + if let Some(contracts) = self.session.get_contracts() { + println!("{contracts}"); + } + if let Some(accounts) = self.session.get_accounts() { + println!("{accounts}"); + } + let mut editor = DefaultEditor::new().expect("Failed to initialize cli"); let mut ctrl_c_acc = 0; let mut input_buffer = vec![]; @@ -131,10 +130,12 @@ impl Terminal { let input = input_buffer.join(" "); match complete_input(&input) { Ok(Input::Complete()) => { - let (reload, output, result) = self.session.handle_command(&input); + let (reload, output, result) = + self.session.process_console_input(&input); if let Some(session_wasm) = &mut self.session_wasm { - let (_, _, result_wasm) = session_wasm.handle_command(&input); + let (_, _, result_wasm) = + session_wasm.process_console_input(&input); if let (Some(result), Some(result_wasm)) = (result, result_wasm) { match (result, result_wasm) { diff --git a/components/clarity-repl/src/repl/session.rs b/components/clarity-repl/src/repl/session.rs index 584fdf973..a7371b5d7 100644 --- a/components/clarity-repl/src/repl/session.rs +++ b/components/clarity-repl/src/repl/session.rs @@ -21,18 +21,14 @@ use clarity::vm::{ ClarityVersion, CostSynthesis, EvalHook, EvaluationResult, ExecutionResult, ParsedContract, SymbolicExpression, }; +use colored::*; +use prettytable::{Cell, Row, Table}; use std::collections::{BTreeMap, HashMap}; use std::fmt; use std::num::ParseIntError; -#[cfg(feature = "cli")] -use ansi_term::Colour; #[cfg(feature = "cli")] use clarity::vm::analysis::ContractAnalysis; -#[cfg(feature = "cli")] -use prettytable::{Cell, Row, Table}; -#[cfg(feature = "cli")] -use reqwest; use super::SessionSettings; @@ -100,6 +96,7 @@ pub struct CostsReport { #[derive(Clone, Debug)] pub struct Session { pub settings: SessionSettings, + pub current_epoch: StacksEpochId, pub contracts: BTreeMap, pub interpreter: ClarityInterpreter, api_reference: HashMap, @@ -107,12 +104,11 @@ pub struct Session { pub costs_reports: Vec, pub show_costs: bool, pub executed: Vec, - pub current_epoch: StacksEpochId, keywords_reference: HashMap, } impl Session { - pub fn new(settings: SessionSettings) -> Session { + pub fn new(settings: SessionSettings) -> Self { let tx_sender = { let address = match settings.initial_deployer { Some(ref entry) => entry.address.clone(), @@ -122,8 +118,9 @@ impl Session { .expect("Unable to parse deployer's address") }; - Session { + Self { interpreter: ClarityInterpreter::new(tx_sender, settings.repl_settings.clone()), + current_epoch: settings.epoch_id.unwrap_or(StacksEpochId::Epoch2_05), contracts: BTreeMap::new(), api_reference: build_api_reference(), coverage_reports: vec![], @@ -131,7 +128,6 @@ impl Session { show_costs: false, settings, executed: Vec::new(), - current_epoch: StacksEpochId::Epoch2_05, keywords_reference: clarity_keywords(), } } @@ -193,7 +189,7 @@ impl Session { } #[cfg(feature = "cli")] - pub fn handle_command( + pub fn process_console_input( &mut self, command: &str, ) -> ( @@ -205,58 +201,71 @@ impl Session { let mut reload = false; match command { - "::help" => self.display_help(&mut output), - "/-/" => self.easter_egg(&mut output), - cmd if cmd.starts_with("::functions") => self.display_functions(&mut output), - cmd if cmd.starts_with("::describe") => self.display_doc(&mut output, cmd), - cmd if cmd.starts_with("::mint_stx") => self.mint_stx(&mut output, cmd), - cmd if cmd.starts_with("::set_tx_sender") => { - self.parse_and_set_tx_sender(&mut output, cmd) - } - cmd if cmd.starts_with("::get_assets_maps") => self.get_accounts(&mut output), - cmd if cmd.starts_with("::get_costs") => self.get_costs(&mut output, cmd), - cmd if cmd.starts_with("::get_contracts") => self.get_contracts(&mut output), - cmd if cmd.starts_with("::get_block_height") => self.get_block_height(&mut output), - cmd if cmd.starts_with("::advance_chain_tip") => { - self.parse_and_advance_chain_tip(&mut output, cmd) - } - cmd if cmd.starts_with("::toggle_costs") => self.toggle_costs(&mut output), - cmd if cmd.starts_with("::toggle_timings") => self.toggle_timings(&mut output), - cmd if cmd.starts_with("::get_epoch") => self.get_epoch(&mut output), - cmd if cmd.starts_with("::set_epoch") => self.set_epoch(&mut output, cmd), - cmd if cmd.starts_with("::encode") => self.encode(&mut output, cmd), - cmd if cmd.starts_with("::decode") => self.decode(&mut output, cmd), - + #[cfg(feature = "cli")] + cmd if cmd.starts_with("::reload") => reload = true, + #[cfg(feature = "cli")] + cmd if cmd.starts_with("::read") => self.read(&mut output, cmd), #[cfg(feature = "cli")] cmd if cmd.starts_with("::debug") => self.debug(&mut output, cmd), #[cfg(feature = "cli")] cmd if cmd.starts_with("::trace") => self.trace(&mut output, cmd), #[cfg(feature = "cli")] - cmd if cmd.starts_with("::reload") => reload = true, - #[cfg(feature = "cli")] - cmd if cmd.starts_with("::read") => self.read(&mut output, cmd), - cmd if cmd.starts_with("::keywords") => self.keywords(&mut output), + cmd if cmd.starts_with("::get_costs") => self.get_costs(&mut output, cmd), cmd if cmd.starts_with("::") => { - output.push(yellow!(format!("Unknown command: {}", cmd))); + output.push(self.handle_command(cmd)); } snippet => { let execution_result = self.run_snippet(&mut output, self.show_costs, snippet); - return (false, output, execution_result); + return (false, output, Some(execution_result)); } } (reload, output, None) } + pub fn handle_command(&mut self, command: &str) -> String { + match command { + "::help" => self.display_help(), + + #[cfg(feature = "cli")] + cmd if cmd.starts_with("::functions") => self.display_functions(), + #[cfg(feature = "cli")] + cmd if cmd.starts_with("::keywords") => self.keywords(), + #[cfg(feature = "cli")] + cmd if cmd.starts_with("::describe") => self.display_doc(cmd), + #[cfg(feature = "cli")] + cmd if cmd.starts_with("::toggle_costs") => self.toggle_costs(), + #[cfg(feature = "cli")] + cmd if cmd.starts_with("::toggle_timings") => self.toggle_timings(), + + cmd if cmd.starts_with("::mint_stx") => self.mint_stx(cmd), + cmd if cmd.starts_with("::set_tx_sender") => self.parse_and_set_tx_sender(cmd), + cmd if cmd.starts_with("::get_assets_maps") => { + self.get_accounts().unwrap_or("No account found".into()) + } + cmd if cmd.starts_with("::get_contracts") => { + self.get_contracts().unwrap_or("No contract found".into()) + } + cmd if cmd.starts_with("::get_block_height") => self.get_block_height(), + cmd if cmd.starts_with("::advance_chain_tip") => self.parse_and_advance_chain_tip(cmd), + cmd if cmd.starts_with("::get_epoch") => self.get_epoch(), + cmd if cmd.starts_with("::set_epoch") => self.set_epoch(cmd), + cmd if cmd.starts_with("::encode") => self.encode(cmd), + cmd if cmd.starts_with("::decode") => self.decode(cmd), + + _ => "Invalid command. Try `::help`".yellow().to_string(), + } + } + #[cfg(feature = "cli")] fn run_snippet( &mut self, output: &mut Vec, cost_track: bool, cmd: &str, - ) -> Option>> { + ) -> Result> { let (mut result, cost, execution_result) = match self.formatted_interpretation( cmd.to_string(), None, @@ -268,9 +277,9 @@ impl Session { let snippet = format!("ā†’ .{} contract successfully stored. Use (contract-call? ...) for invoking the public functions:", contract_result.contract.contract_identifier.clone()); output.push(green!(snippet)); }; - (output, result.cost.clone(), Some(Ok(result))) + (output, result.cost.clone(), Ok(result)) } - Err((err_output, diagnostics)) => (err_output, None, Some(Err(diagnostics))), + Err((err_output, diagnostics)) => (err_output, None, Err(diagnostics)), }; if let Some(cost) = cost { @@ -371,11 +380,11 @@ impl Session { match &result.result { EvaluationResult::Contract(contract_result) => { if let Some(value) = &contract_result.result { - output.push(green!(format!("{}", value))); + output.push(format!("{}", value).green().to_string()); } } EvaluationResult::Snippet(snippet_result) => { - output.push(green!(format!("{}", snippet_result.result))) + output.push(format!("{}", snippet_result.result).green().to_string()) } } Ok((output, result)) @@ -395,7 +404,7 @@ impl Session { let snippet = match cmd.split_once(' ') { Some((_, snippet)) => snippet, - _ => return output.push(red!("Usage: ::debug ")), + _ => return output.push("Usage: ::debug ".red().to_string()), }; let mut debugger = CLIDebugger::new(&QualifiedContractIdentifier::transient(), snippet); @@ -409,7 +418,7 @@ impl Session { Ok((mut output, result)) => { if let EvaluationResult::Contract(contract_result) = result.result { let snippet = format!("ā†’ .{} contract successfully stored. Use (contract-call? ...) for invoking the public functions:", contract_result.contract.contract_identifier.clone()); - output.push(green!(snippet)); + output.push(snippet.green().to_string()); }; output } @@ -424,7 +433,7 @@ impl Session { let snippet = match cmd.split_once(' ') { Some((_, snippet)) => snippet, - _ => return output.push(red!("Usage: ::trace ")), + _ => return output.push("Usage: ::trace ".red().to_string()), }; let mut tracer = Tracer::new(snippet.to_string()); @@ -457,7 +466,7 @@ impl Session { let recipient = match PrincipalData::parse(&account.address) { Ok(recipient) => recipient, _ => { - output_err.push(red!("Unable to parse address to credit")); + output_err.push("Unable to parse address to credit".red().to_string()); continue; } }; @@ -467,7 +476,7 @@ impl Session { .mint_stx_balance(recipient, account.balance) { Ok(_) => {} - Err(err) => output_err.push(red!(err)), + Err(err) => output_err.push(err.red().to_string()), }; } } @@ -482,14 +491,18 @@ impl Session { pub fn read(&mut self, output: &mut Vec, cmd: &str) { let filename = match cmd.split_once(' ') { Some((_, filename)) => filename, - _ => return output.push(red!("Usage: ::read ")), + _ => return output.push("Usage: ::read ".red().to_string()), }; match std::fs::read_to_string(filename) { Ok(snippet) => { - self.run_snippet(output, self.show_costs, &snippet); + let _ = self.run_snippet(output, self.show_costs, &snippet); } - Err(err) => output.push(red!(format!("unable to read {}: {}", filename, err))), + Err(err) => output.push( + format!("unable to read {}: {}", filename, err) + .red() + .to_string(), + ), }; } @@ -592,7 +605,7 @@ impl Session { let clarity_version = ClarityVersion::default_for_epoch(self.current_epoch); - self.set_tx_sender(sender.into()); + self.set_tx_sender(sender); let execution = match self.interpreter.call_contract_fn( &contract_id, method, @@ -605,7 +618,7 @@ impl Session { ) { Ok(result) => result, Err(e) => { - self.set_tx_sender(initial_tx_sender); + self.set_tx_sender(&initial_tx_sender); return Err(vec![Diagnostic { level: Level::Error, message: format!("Error calling contract function: {e}"), @@ -614,7 +627,7 @@ impl Session { }]); } }; - self.set_tx_sender(initial_tx_sender); + self.set_tx_sender(&initial_tx_sender); if track_coverage { self.coverage_reports.push(coverage); @@ -695,156 +708,152 @@ impl Session { keys } - #[cfg(feature = "cli")] - fn display_help(&self, output: &mut Vec) { - let help_colour = Colour::Yellow; + fn display_help(&self) -> String { + let mut output: Vec = vec![]; + + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour.paint("::help\t\t\t\t\tDisplay help") + "::functions\t\t\t\tDisplay all the native functions available in clarity".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour - .paint("::functions\t\t\t\tDisplay all the native functions available in clarity") + "::keywords\t\t\t\tDisplay all the native keywords available in clarity".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour - .paint("::keywords\t\t\t\tDisplay all the native keywords available in clarity") + "::describe | \tDisplay documentation for a given native function or keyword".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour.paint( - "::describe | \tDisplay documentation for a given native function or keyword" - ) + "::toggle_costs\t\t\t\tDisplay cost analysis after every expression".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour - .paint("::mint_stx \t\tMint STX balance for a given principal") + "::toggle_timings\t\t\tDisplay the execution duration".yellow() )); + output.push(format!( "{}", - help_colour.paint("::set_tx_sender \t\tSet tx-sender variable to principal") + "::mint_stx \t\tMint STX balance for a given principal".yellow() )); output.push(format!( "{}", - help_colour.paint("::get_assets_maps\t\t\tGet assets maps for active accounts") + "::set_tx_sender \t\tSet tx-sender variable to principal".yellow() )); output.push(format!( "{}", - help_colour.paint("::get_costs \t\t\tDisplay the cost analysis") + "::get_assets_maps\t\t\tGet assets maps for active accounts".yellow() )); output.push(format!( "{}", - help_colour.paint("::get_contracts\t\t\t\tGet contracts") + "::get_contracts\t\t\t\tGet contracts".yellow() )); output.push(format!( "{}", - help_colour.paint("::get_block_height\t\t\tGet current block height") + "::get_block_height\t\t\tGet current block height".yellow() )); output.push(format!( "{}", - help_colour.paint("::advance_chain_tip \t\tSimulate mining of blocks") + "::advance_chain_tip \t\tSimulate mining of blocks".yellow() )); output.push(format!( "{}", - help_colour.paint("::set_epoch <2.0> | <2.05> | <2.1>\tUpdate the current epoch") + "::set_epoch \t\t\tUpdate the current epoch".yellow() )); output.push(format!( "{}", - help_colour.paint("::get_epoch\t\t\t\tGet current epoch") + "::get_epoch\t\t\t\tGet current epoch".yellow() )); + + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour.paint("::toggle_costs\t\t\t\tDisplay cost analysis after every expression") + "::debug \t\t\t\tStart an interactive debug session executing ".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour - .paint("::debug \t\t\t\tStart an interactive debug session executing ") + "::trace \t\t\t\tGenerate an execution trace for ".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour.paint("::trace \t\t\t\tGenerate an execution trace for ") + "::get_costs \t\t\tDisplay the cost analysis".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour.paint("::reload \t\t\t\tReload the existing contract(s) in the session") + "::reload \t\t\t\tReload the existing contract(s) in the session".yellow() )); + #[cfg(feature = "cli")] output.push(format!( "{}", - help_colour.paint("::read \t\t\tRead expressions from a file") + "::read \t\t\tRead expressions from a file".yellow() )); + output.push(format!( "{}", - help_colour.paint("::encode \t\t\t\tEncode an expression to a Clarity Value bytes representation") + "::encode \t\t\t\tEncode an expression to a Clarity Value bytes representation" + .yellow() )); output.push(format!( "{}", - help_colour.paint("::decode \t\t\tDecode a Clarity Value bytes representation") + "::decode \t\t\tDecode a Clarity Value bytes representation".yellow() )); - } - #[cfg(feature = "cli")] - fn easter_egg(&self, _output: &mut [String]) { - let result = hiro_system_kit::nestable_block_on(fetch_message()); - let message = result.unwrap_or("You found it!".to_string()); - println!("{}", message); + output.join("\n") } - #[cfg(feature = "cli")] - fn parse_and_advance_chain_tip(&mut self, output: &mut Vec, command: &str) { + fn parse_and_advance_chain_tip(&mut self, command: &str) -> String { let args: Vec<_> = command.split(' ').collect(); if args.len() != 2 { - output.push(red!("Usage: ::advance_chain_tip ")); - return; + return format!("{}", "Usage: ::advance_chain_tip ".red()); } let count = match args[1].parse::() { Ok(count) => count, _ => { - output.push(red!("Unable to parse count")); - return; + return format!("{}", "Unable to parse count".red()); } }; let new_height = self.advance_chain_tip(count); - output.push(green!(format!( - "{} blocks simulated, new height: {}", - count, new_height - ))); + format!("{} blocks simulated, new height: {}", count, new_height) + .green() + .to_string() } pub fn advance_chain_tip(&mut self, count: u32) -> u32 { self.interpreter.advance_chain_tip(count) } - #[cfg(feature = "cli")] - fn parse_and_set_tx_sender(&mut self, output: &mut Vec, command: &str) { + fn parse_and_set_tx_sender(&mut self, command: &str) -> String { let args: Vec<_> = command.split(' ').collect(); if args.len() != 2 { - output.push(red!("Usage: ::set_tx_sender
")); - return; + return format!("{}", "Usage: ::set_tx_sender
".red()); } - let tx_sender = match PrincipalData::parse_standard_principal(args[1]) { - Ok(address) => address, - _ => { - output.push(red!("Unable to parse the address")); - return; - } - }; + let tx_sender = args[1]; - self.set_tx_sender(tx_sender.to_address()); - output.push(green!(format!("tx-sender switched to {}", tx_sender))); + match PrincipalData::parse_standard_principal(args[1]) { + Ok(address) => { + self.interpreter.set_tx_sender(address); + format!("tx-sender switched to {}", tx_sender) + } + _ => format!("{}", "Unable to parse the address".red()), + } } - pub fn set_tx_sender(&mut self, address: String) { + pub fn set_tx_sender(&mut self, address: &str) { let tx_sender = - PrincipalData::parse_standard_principal(&address).expect("Unable to parse address"); + PrincipalData::parse_standard_principal(address).expect("Unable to parse address"); self.interpreter.set_tx_sender(tx_sender) } @@ -852,13 +861,11 @@ impl Session { self.interpreter.get_tx_sender().to_address() } - #[cfg(feature = "cli")] - fn get_block_height(&mut self, output: &mut Vec) { + fn get_block_height(&mut self) -> String { let height = self.interpreter.get_block_height(); - output.push(green!(format!("Current height: {}", height))); + format!("Current height: {}", height) } - #[cfg(feature = "cli")] fn get_account_name(&self, address: &String) -> Option<&String> { for account in self.settings.initial_accounts.iter() { if &account.address == address { @@ -872,24 +879,26 @@ impl Session { self.interpreter.get_assets_maps() } - pub fn toggle_costs(&mut self, output: &mut Vec) { + pub fn toggle_costs(&mut self) -> String { self.show_costs = !self.show_costs; - output.push(green!(format!("Always show costs: {}", self.show_costs))) + format!("Always show costs: {}", self.show_costs) } - pub fn toggle_timings(&mut self, output: &mut Vec) { + pub fn toggle_timings(&mut self) -> String { self.interpreter.repl_settings.show_timings = !self.interpreter.repl_settings.show_timings; - output.push(green!(format!( + format!( "Always show timings: {}", self.interpreter.repl_settings.show_timings - ))) + ) + .green() + .to_string() } - pub fn get_epoch(&mut self, output: &mut Vec) { - output.push(format!("Current epoch: {}", self.current_epoch)) + pub fn get_epoch(&mut self) -> String { + format!("Current epoch: {}", self.current_epoch) } - pub fn set_epoch(&mut self, output: &mut Vec, cmd: &str) { + pub fn set_epoch(&mut self, cmd: &str) -> String { let epoch = match cmd.split_once(' ').map(|(_, epoch)| epoch) { Some("2.0") => StacksEpochId::Epoch20, Some("2.05") => StacksEpochId::Epoch2_05, @@ -900,13 +909,13 @@ impl Session { Some("2.5") => StacksEpochId::Epoch25, Some("3.0") => StacksEpochId::Epoch30, _ => { - return output.push(red!( - "Usage: ::set_epoch 2.0 | 2.05 | 2.1 | 2.2 | 2.3 | 2.4 | 2.5 | 3.0" - )) + return "Usage: ::set_epoch 2.0 | 2.05 | 2.1 | 2.2 | 2.3 | 2.4 | 2.5 | 3.0" + .red() + .to_string() } }; self.update_epoch(epoch); - output.push(green!(format!("Epoch updated to: {epoch}"))); + format!("Epoch updated to: {epoch}").green().to_string() } pub fn update_epoch(&mut self, epoch: StacksEpochId) { @@ -917,14 +926,14 @@ impl Session { } } - pub fn encode(&mut self, output: &mut Vec, cmd: &str) { + pub fn encode(&mut self, cmd: &str) -> String { let snippet = match cmd.split_once(' ') { Some((_, snippet)) => snippet, - _ => return output.push(red!("Usage: ::encode ")), + _ => return "Usage: ::encode ".red().to_string(), }; let result = self.eval(snippet.to_string(), None, false); - let value = match result { + match result { Ok(result) => { let mut tx_bytes = vec![]; let value = match result.result { @@ -932,215 +941,182 @@ impl Session { if let Some(value) = contract_result.result { value } else { - return output.push("No value".to_string()); + return "No value".to_string(); } } EvaluationResult::Snippet(snippet_result) => snippet_result.result, }; if let Err(e) = value.consensus_serialize(&mut tx_bytes) { - return output.push(red!(format!("{}", e))); + return format!("{}", e).red().to_string(); }; let mut s = String::with_capacity(2 * tx_bytes.len()); for byte in tx_bytes { s = format!("{}{:02x}", s, byte); } - green!(s) + s.green().to_string() } Err(diagnostics) => { let lines: Vec = snippet.split('\n').map(|s| s.to_string()).collect(); - for d in diagnostics { - output.append(&mut output_diagnostic(&d, "encode", &lines)); - } - red!("encoding failed") + let mut output: Vec = diagnostics + .iter() + .flat_map(|d| output_diagnostic(d, "encode", &lines)) + .collect(); + output.push("encoding failed".into()); + output.join("\n") } - }; - output.push(value); + } } - pub fn decode(&mut self, output: &mut Vec, cmd: &str) { + pub fn decode(&mut self, cmd: &str) -> String { let byte_string = match cmd.split_once(' ') { Some((_, bytes)) => bytes, - _ => return output.push(red!("Usage: ::decode ")), + _ => return "Usage: ::decode ".red().to_string(), }; let tx_bytes = match decode_hex(byte_string) { Ok(tx_bytes) => tx_bytes, - Err(e) => return output.push(red!(format!("Parsing error: {}", e))), + Err(e) => return format!("Parsing error: {}", e).red().to_string(), }; let value = match Value::consensus_deserialize(&mut &tx_bytes[..]) { Ok(value) => value, - Err(e) => return output.push(red!(format!("{}", e))), + Err(e) => return format!("{}", e).red().to_string(), }; - output.push(green!(format!("{}", value_to_string(&value)))); + + format!("{}", value_to_string(&value).green()) } + #[cfg(feature = "cli")] pub fn get_costs(&mut self, output: &mut Vec, cmd: &str) { let expr = match cmd.split_once(' ') { Some((_, expr)) => expr, - _ => return output.push(red!("Usage: ::get_costs ")), + _ => return output.push("Usage: ::get_costs ".red().to_string()), }; - self.run_snippet(output, true, expr); + let _ = self.run_snippet(output, true, expr); } - #[cfg(feature = "cli")] - fn get_accounts(&self, output: &mut Vec) { + pub fn get_accounts(&self) -> Option { let accounts = self.interpreter.get_accounts(); - if !accounts.is_empty() { - let tokens = self.interpreter.get_tokens(); - let mut headers = vec!["Address".to_string()]; - for token in tokens.iter() { - if token == "STX" { - headers.push(String::from("uSTX")); - } else { - headers.push(String::from(token)); - } - } + if accounts.is_empty() { + return None; + } - let mut headers_cells = vec![]; - for header in headers.iter() { - headers_cells.push(Cell::new(header)); + let tokens = self.interpreter.get_tokens(); + let mut headers = vec!["Address".to_string()]; + for token in tokens.iter() { + if token == "STX" { + headers.push(String::from("uSTX")); + } else { + headers.push(String::from(token)); } - let mut table = Table::new(); - table.add_row(Row::new(headers_cells)); - for account in accounts.iter() { - let mut cells = vec![]; - - if let Some(name) = self.get_account_name(account) { - cells.push(Cell::new(&format!("{} ({})", account, name))); - } else { - cells.push(Cell::new(account)); - } + } - for token in tokens.iter() { - let balance = self.interpreter.get_balance_for_account(account, token); - cells.push(Cell::new(&format!("{}", balance))); - } - table.add_row(Row::new(cells)); - } - output.push(format!("{}", table)); + let mut headers_cells = vec![]; + for header in headers.iter() { + headers_cells.push(Cell::new(header)); } - } + let mut table = Table::new(); + table.add_row(Row::new(headers_cells)); + for account in accounts.iter() { + let mut cells = vec![]; + + if let Some(name) = self.get_account_name(account) { + cells.push(Cell::new(&format!("{} ({})", account, name))); + } else { + cells.push(Cell::new(account)); + } - #[cfg(feature = "cli")] - fn get_contracts(&self, output: &mut Vec) { - if !self.contracts.is_empty() { - let mut table = Table::new(); - table.add_row(row!["Contract identifier", "Public functions"]); - let contracts = self.contracts.clone(); - for (contract_id, contract) in contracts.iter() { - let contract_id_str = contract_id.to_string(); - if !contract_id_str.starts_with(BOOT_TESTNET_ADDRESS) - && !contract_id_str.starts_with(BOOT_MAINNET_ADDRESS) - { - let mut formatted_methods = vec![]; - for (method_name, method_args) in contract.function_args.iter() { - let formatted_args = if method_args.is_empty() { - String::new() - } else if method_args.len() == 1 { - format!(" {}", method_args.join(" ")) - } else { - format!("\n {}", method_args.join("\n ")) - }; - formatted_methods.push(format!("({}{})", method_name, formatted_args)); - } - let formatted_spec = formatted_methods.join("\n").to_string(); - table.add_row(Row::new(vec![ - Cell::new(&contract_id_str), - Cell::new(&formatted_spec), - ])); - } + for token in tokens.iter() { + let balance = self.interpreter.get_balance_for_account(account, token); + cells.push(Cell::new(&format!("{}", balance))); } - output.push(format!("{}", table)); + table.add_row(Row::new(cells)); } + Some(format!("{}", table)) } - #[cfg(not(feature = "cli"))] - fn run_snippet(&mut self, output: &mut Vec, cost_track: bool, cmd: &str) { - let (mut result, cost) = - match self.formatted_interpretation(cmd.to_string(), None, cost_track, None) { - Ok((output, result)) => (output, result.cost.clone()), - Err((output, _)) => (output, None), - }; - - if let Some(cost) = cost { - output.push(format!( - "Execution: {:?}\nLimit: {:?}", - cost.total, cost.limit - )); + #[cfg(feature = "cli")] + pub fn get_contracts(&self) -> Option { + if self.contracts.is_empty() { + return None; } - output.append(&mut result); - } - #[cfg(not(feature = "cli"))] - fn get_accounts(&self, output: &mut Vec) { - if !self.settings.initial_accounts.is_empty() { - let mut initial_accounts = self.settings.initial_accounts.clone(); - for account in initial_accounts.drain(..) { - output.push(format!( - "{}: {} ({})", - account.address, account.balance, account.name - )); + let mut table = Table::new(); + table.add_row(row!["Contract identifier", "Public functions"]); + let contracts = self.contracts.clone(); + for (contract_id, contract) in contracts.iter() { + let contract_id_str = contract_id.to_string(); + if !contract_id_str.starts_with(BOOT_TESTNET_ADDRESS) + && !contract_id_str.starts_with(BOOT_MAINNET_ADDRESS) + { + let mut formatted_methods = vec![]; + for (method_name, method_args) in contract.function_args.iter() { + let formatted_args = if method_args.is_empty() { + String::new() + } else if method_args.len() == 1 { + format!(" {}", method_args.join(" ")) + } else { + format!("\n {}", method_args.join("\n ")) + }; + formatted_methods.push(format!("({}{})", method_name, formatted_args)); + } + let formatted_spec = formatted_methods.join("\n").to_string(); + table.add_row(Row::new(vec![ + Cell::new(&contract_id_str), + Cell::new(&formatted_spec), + ])); } } + Some(format!("{}", table)) } #[cfg(not(feature = "cli"))] - fn get_contracts(&self, output: &mut Vec) { - for (contract_id, _methods) in self.contracts.iter() { - if !contract_id.to_string().ends_with(".pox") - && !contract_id.to_string().ends_with(".bns") - && !contract_id.to_string().ends_with(".costs") - { - output.push(contract_id.to_string()); - } + fn get_contracts(&self) -> Option { + if self.contracts.is_empty() { + return None; } + Some( + self.contracts + .keys() + .map(|contract_id| contract_id.to_string()) + .collect::>() + .join("\n"), + ) } - #[cfg(feature = "cli")] - fn mint_stx(&mut self, output: &mut Vec, command: &str) { + fn mint_stx(&mut self, command: &str) -> String { let args: Vec<_> = command.split(' ').collect(); if args.len() != 3 { - output.push(red!("Usage: ::mint_stx ")); - return; + return "Usage: ::mint_stx " + .red() + .to_string(); } let recipient = match PrincipalData::parse(args[1]) { Ok(address) => address, - _ => { - output.push(red!("Unable to parse the address")); - return; - } + _ => return "Unable to parse the address".red().to_string(), }; let amount: u64 = match args[2].parse() { Ok(recipient) => recipient, - _ => { - output.push(red!("Unable to parse the balance")); - return; - } + _ => return "Unable to parse the balance".red().to_string(), }; match self.interpreter.mint_stx_balance(recipient, amount) { - Ok(msg) => output.push(green!(msg)), - Err(err) => output.push(red!(err)), - }; + Ok(msg) => msg.green().to_string(), + Err(err) => err.red().to_string(), + } } #[cfg(feature = "cli")] - fn display_functions(&self, output: &mut Vec) { - let help_colour = Colour::Yellow; + fn display_functions(&self) -> String { let api_reference_index = self.get_api_reference_index(); - output.push(format!( - "{}", - help_colour.paint(api_reference_index.join("\n")) - )); + format!("{}", api_reference_index.join("\n").yellow()) } #[cfg(feature = "cli")] - fn display_doc(&self, output: &mut Vec, command: &str) { - let help_colour = Colour::Yellow; + fn display_doc(&self, command: &str) -> String { let keyword = { let mut s = command.to_string(); s = s.replace("::describe", ""); @@ -1148,28 +1124,19 @@ impl Session { s }; - let result = match self.lookup_functions_or_keywords_docs(&keyword) { - Some(doc) => format!("{}", help_colour.paint(doc)), + match self.lookup_functions_or_keywords_docs(&keyword) { + Some(doc) => format!("{}", doc.yellow()), None => format!( "{}", - Colour::Red.paint("It looks like there aren't matches for your search") + "It looks like there aren't matches for your search".red() ), - }; - output.push(result); - } - - pub fn display_digest(&self) -> Result { - let mut output = vec![]; - self.get_contracts(&mut output); - self.get_accounts(&mut output); - Ok(output.join("\n")) + } } #[cfg(feature = "cli")] - fn keywords(&self, output: &mut Vec) { - let help_colour = Colour::Yellow; + fn keywords(&self) -> String { let keywords = self.get_clarity_keywords(); - output.push(format!("{}", help_colour.paint(keywords.join("\n")))); + format!("{}", keywords.join("\n").yellow()) } } @@ -1330,66 +1297,44 @@ mod tests { ); } - #[test] - fn encode_simple() { - let mut session = Session::new(SessionSettings::default()); - let mut output: Vec = Vec::new(); - session.encode(&mut output, "::encode 42"); - assert_eq!(output.len(), 1); - assert_eq!(output[0], green!("000000000000000000000000000000002a")); - } - - #[test] - fn encode_map() { - let mut session = Session::new(SessionSettings::default()); - let mut output: Vec = Vec::new(); - session.encode(&mut output, "::encode { foo: \"hello\", bar: false }"); - assert_eq!(output.len(), 1); - assert_eq!( - output[0], - green!("0c00000002036261720403666f6f0d0000000568656c6c6f") - ); - } - #[test] fn set_epoch_command() { let mut session = Session::new(SessionSettings::default()); let initial_epoch = session.handle_command("::get_epoch"); // initial epoch is 2.05 - assert_eq!(initial_epoch.1[0], "Current epoch: 2.05"); + assert_eq!(initial_epoch, "Current epoch: 2.05"); // it can be lowered to 2.0 // it's possible that in the feature we want to start from 2.0 and forbid lowering the epoch // this test would have to be updated session.handle_command("::set_epoch 2.0"); let current_epoch = session.handle_command("::get_epoch"); - assert_eq!(current_epoch.1[0], "Current epoch: 2.0"); + assert_eq!(current_epoch, "Current epoch: 2.0"); session.handle_command("::set_epoch 2.4"); let current_epoch = session.handle_command("::get_epoch"); - assert_eq!(current_epoch.1[0], "Current epoch: 2.4"); + assert_eq!(current_epoch, "Current epoch: 2.4"); session.handle_command("::set_epoch 3.0"); let current_epoch = session.handle_command("::get_epoch"); - assert_eq!(current_epoch.1[0], "Current epoch: 3.0"); + assert_eq!(current_epoch, "Current epoch: 3.0"); } #[test] fn encode_error() { let mut session = Session::new(SessionSettings::default()); - let mut output: Vec = Vec::new(); - session.encode(&mut output, "::encode { foo false }"); + let result = session.encode("::encode { foo false }"); assert_eq!( - output[0], - format_err!("Tuple literal construction expects a colon at index 1") + result, + format_err!("Tuple literal construction expects a colon at index 1\nencoding failed") ); - session.encode(&mut output, "::encode (foo 1)"); + let result = session.encode("::encode (foo 1)"); assert_eq!( - output[2], + result.split('\n').next().unwrap(), format!( "encode:1:1: {} use of unresolved function 'foo'", - red!("error:") + "error:".red() ) ); } @@ -1397,40 +1342,35 @@ mod tests { #[test] fn decode_simple() { let mut session = Session::new(SessionSettings::default()); - let mut output: Vec = Vec::new(); - session.decode(&mut output, "::decode 0000000000000000 0000000000000000 2a"); - assert_eq!(output.len(), 1); - assert_eq!(output[0], green!("42")); + + let result = session.decode("::decode 0000000000000000 0000000000000000 2a"); + assert_eq!(result, "42".green().to_string()); } #[test] fn decode_map() { let mut session = Session::new(SessionSettings::default()); - let mut output: Vec = Vec::new(); - session.decode( - &mut output, - "::decode 0x0c00000002036261720403666f6f0d0000000568656c6c6f", - ); - assert_eq!(output.len(), 1); - assert_eq!(output[0], green!("{ bar: false, foo: \"hello\" }")); + let result = session.decode("::decode 0x0c00000002036261720403666f6f0d0000000568656c6c6f"); + assert_eq!(result, "{ bar: false, foo: \"hello\" }".green().to_string()); } #[test] fn decode_error() { let mut session = Session::new(SessionSettings::default()); - let mut output: Vec = Vec::new(); - session.decode(&mut output, "::decode 42"); - assert_eq!(output.len(), 1); + let result = session.decode("::decode 42"); assert_eq!( - output[0], - red!("Failed to decode clarity value: DeserializationError(\"Bad type prefix\")") + result, + "Failed to decode clarity value: DeserializationError(\"Bad type prefix\")" + .red() + .to_string() ); - session.decode(&mut output, "::decode 4g"); - assert_eq!(output.len(), 2); + let result = session.decode("::decode 4g"); assert_eq!( - output[1], - red!("Parsing error: invalid digit found in string") + result, + "Parsing error: invalid digit found in string" + .red() + .to_string() ); } @@ -1511,23 +1451,29 @@ mod tests { // assert data-var is set to 0 assert_eq!( - session.handle_command("(contract-call? .contract get-x)").1[0], - green!("u0") + session + .process_console_input("(contract-call? .contract get-x)") + .1[0], + "u0".green().to_string() ); // advance chain tip and test at-block session.advance_chain_tip(10000); assert_eq!( - session.handle_command("(contract-call? .contract get-x)").1[0], - green!("u0") + session + .process_console_input("(contract-call? .contract get-x)") + .1[0], + "u0".green().to_string() ); - session.handle_command("(contract-call? .contract incr)"); + session.process_console_input("(contract-call? .contract incr)"); assert_eq!( - session.handle_command("(contract-call? .contract get-x)").1[0], - green!("u1") + session + .process_console_input("(contract-call? .contract get-x)") + .1[0], + "u1".green().to_string() ); - assert_eq!(session.handle_command("(at-block (unwrap-panic (get-block-info? id-header-hash u0)) (contract-call? .contract get-x))").1[0], green!("u0")); - assert_eq!(session.handle_command("(at-block (unwrap-panic (get-block-info? id-header-hash u5000)) (contract-call? .contract get-x))").1[0], green!("u0")); + assert_eq!(session.process_console_input("(at-block (unwrap-panic (get-block-info? id-header-hash u0)) (contract-call? .contract get-x))").1[0], "u0".green().to_string()); + assert_eq!(session.process_console_input("(at-block (unwrap-panic (get-block-info? id-header-hash u5000)) (contract-call? .contract get-x))").1[0], "u0".green().to_string()); // advance chain tip again and test at-block // do this twice to make sure that the lookup table is being updated properly @@ -1535,15 +1481,19 @@ mod tests { session.advance_chain_tip(10); assert_eq!( - session.handle_command("(contract-call? .contract get-x)").1[0], - green!("u1") + session + .process_console_input("(contract-call? .contract get-x)") + .1[0], + "u1".green().to_string() ); - session.handle_command("(contract-call? .contract incr)"); + session.process_console_input("(contract-call? .contract incr)"); assert_eq!( - session.handle_command("(contract-call? .contract get-x)").1[0], - green!("u2") + session + .process_console_input("(contract-call? .contract get-x)") + .1[0], + "u2".green().to_string() ); - assert_eq!(session.handle_command("(at-block (unwrap-panic (get-block-info? id-header-hash u10000)) (contract-call? .contract get-x))").1[0], green!("u1")); + assert_eq!(session.process_console_input("(at-block (unwrap-panic (get-block-info? id-header-hash u10000)) (contract-call? .contract get-x))").1[0], "u1".green().to_string()); } #[test] @@ -1569,9 +1519,6 @@ mod tests { session.update_epoch(StacksEpochId::Epoch25); session.load_boot_contracts(); - let mut output: Vec = vec![]; - session.get_contracts(&mut output); - // call pox4 get-info let result = session.call_contract_fn( format!("{}.pox-4", BOOT_MAINNET_ADDRESS).as_str(), @@ -1638,11 +1585,3 @@ mod tests { assert_execution_result_value(&result, Value::UInt(1)); } } - -#[cfg(not(feature = "wasm"))] -async fn fetch_message() -> Result { - let gist: &str = "https://storage.googleapis.com/hiro-public/assets/clarinet-egg.txt"; - let response = reqwest::get(gist).await?; - let message = response.text().await?; - Ok(message) -} diff --git a/components/clarity-repl/src/repl/settings.rs b/components/clarity-repl/src/repl/settings.rs index 90279638c..3ef9feaf9 100644 --- a/components/clarity-repl/src/repl/settings.rs +++ b/components/clarity-repl/src/repl/settings.rs @@ -2,6 +2,7 @@ use std::convert::TryInto; use crate::analysis; use clarity::types::chainstate::StacksAddress; +use clarity::types::StacksEpochId; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData}; #[derive(Clone, Debug)] @@ -49,6 +50,7 @@ pub struct SessionSettings { pub lazy_initial_contracts_interpretation: bool, pub disk_cache_enabled: bool, pub repl_settings: Settings, + pub epoch_id: Option, } #[derive(Debug, Default, Clone, Deserialize, Serialize)] diff --git a/components/clarinet-sdk/package-lock.json b/package-lock.json similarity index 95% rename from components/clarinet-sdk/package-lock.json rename to package-lock.json index c6ad96f9e..98ab23c2f 100644 --- a/components/clarinet-sdk/package-lock.json +++ b/package-lock.json @@ -1,18 +1,57 @@ { - "name": "@hirosystems/clarinet-sdk", - "version": "2.7.0", + "name": "clarinet-sdk-workspace", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "clarinet-sdk-workspace", + "license": "GPL-3.0", + "workspaces": [ + "components/clarinet-sdk-wasm/pkg-node", + "components/clarinet-sdk-wasm/pkg-browser", + "components/clarinet-sdk/common", + "components/clarinet-sdk/node", + "components/clarinet-sdk/browser" + ], + "devDependencies": { + "@types/node": "^20.4.5", + "@types/prompts": "^2.4.5", + "@types/yargs": "^17.0.24", + "prettier": "^3.0.3", + "rimraf": "^5.0.1", + "ts-loader": "^9.4.4", + "typescript": "^5.1.6" + } + }, + "components/clarinet-sdk-wasm/pkg-browser": { + "name": "@hirosystems/clarinet-sdk-wasm-browser", + "version": "2.8.0-beta1", + "license": "GPL-3.0" + }, + "components/clarinet-sdk-wasm/pkg-node": { + "name": "@hirosystems/clarinet-sdk-wasm", + "version": "2.8.0-beta1", + "license": "GPL-3.0" + }, + "components/clarinet-sdk/browser": { + "name": "@hirosystems/clarinet-sdk-browser", + "version": "2.8.0-beta1", + "license": "GPL-3.0", + "dependencies": { + "@hirosystems/clarinet-sdk-wasm-browser": "^2.8.0-beta1", + "@stacks/transactions": "^6.13.0" + } + }, + "components/clarinet-sdk/common": { + "name": "@hirosystems/clarinet-sdk-common", + "license": "GPL-3.0" + }, + "components/clarinet-sdk/node": { "name": "@hirosystems/clarinet-sdk", - "version": "2.7.0", + "version": "2.8.0-beta1", "license": "GPL-3.0", "dependencies": { - "@hirosystems/clarinet-sdk-wasm": "^2.7.0", - "@stacks/encryption": "^6.13.0", - "@stacks/network": "^6.13.0", - "@stacks/stacking": "^6.13.0", + "@hirosystems/clarinet-sdk-wasm": "^2.8.0-beta1", "@stacks/transactions": "^6.13.0", "kolorist": "^1.8.0", "prompts": "^2.4.2", @@ -20,16 +59,12 @@ "yargs": "^17.7.2" }, "bin": { - "clarinet-sdk": "dist/cjs/bin/index.js" + "clarinet-sdk": "dist/cjs/node/src/bin/index.js" }, "devDependencies": { - "@types/node": "^20.4.5", - "@types/prompts": "^2.4.5", - "@types/yargs": "^17.0.24", - "prettier": "^3.0.3", - "rimraf": "^5.0.1", - "ts-loader": "^9.4.4", - "typescript": "^5.1.6" + "@stacks/encryption": "^6.13.0", + "@stacks/network": "^6.13.0", + "@stacks/stacking": "^6.13.0" }, "engines": { "node": ">=18.0.0" @@ -403,11 +438,25 @@ "node": ">=12" } }, + "node_modules/@hirosystems/clarinet-sdk": { + "resolved": "components/clarinet-sdk/node", + "link": true + }, + "node_modules/@hirosystems/clarinet-sdk-browser": { + "resolved": "components/clarinet-sdk/browser", + "link": true + }, + "node_modules/@hirosystems/clarinet-sdk-common": { + "resolved": "components/clarinet-sdk/common", + "link": true + }, "node_modules/@hirosystems/clarinet-sdk-wasm": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@hirosystems/clarinet-sdk-wasm/-/clarinet-sdk-wasm-2.7.0.tgz", - "integrity": "sha512-Uz4/fx7rGC6hkR+Th44p+damQNvWX+WgwJliStL/WShdI/q7SLtYLkz3EGMLX0f/xv7hZ8H6X5fyE3Ca+RFt2Q==", - "license": "GPL-3.0" + "resolved": "components/clarinet-sdk-wasm/pkg-node", + "link": true + }, + "node_modules/@hirosystems/clarinet-sdk-wasm-browser": { + "resolved": "components/clarinet-sdk-wasm/pkg-browser", + "link": true }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -754,6 +803,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", + "dev": true, "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -763,6 +813,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", + "dev": true, "funding": [ { "type": "individual", @@ -782,9 +833,9 @@ "license": "MIT" }, "node_modules/@stacks/common": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.13.0.tgz", - "integrity": "sha512-wwzyihjaSdmL6NxKvDeayy3dqM0L0Q2sawmdNtzJDi0FnXuJGm5PeapJj7bEfcI9XwI7Bw5jZoC6mCn9nc5YIw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", + "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", "license": "MIT", "dependencies": { "@types/bn.js": "^5.1.0", @@ -801,15 +852,16 @@ } }, "node_modules/@stacks/encryption": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.15.0.tgz", - "integrity": "sha512-506BdBvWhbXY1jxCdUcdbBzcSJctO2nzgzfenQwUuoBABSc1N/MFwQdlR9ZusY+E31zBxQPLfbr36V05/p2cfQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.16.1.tgz", + "integrity": "sha512-DtVNNW/iipyxxRDz73S9DbLfRmBMqQCCog89F1Q1i6JUnl2kBB1PR9SPQfYv9zcAJ37oHoNB4i4b2tJWYr01vg==", + "dev": true, "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -821,34 +873,36 @@ "version": "18.19.39", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@stacks/network": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.13.0.tgz", - "integrity": "sha512-Ss/Da4BNyPBBj1OieM981fJ7SkevKqLPkzoI1+Yo7cYR2df+0FipIN++Z4RfpJpc8ne60vgcx7nJZXQsiGhKBQ==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.16.0.tgz", + "integrity": "sha512-uqz9Nb6uf+SeyCKENJN+idt51HAfEeggQKrOMfGjpAeFgZV2CR66soB/ci9+OVQR/SURvasncAz2ScI1blfS8A==", "license": "MIT", "dependencies": { - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "cross-fetch": "^3.1.5" } }, "node_modules/@stacks/stacking": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.15.0.tgz", - "integrity": "sha512-ZAjcF3mrB82XTaqJKuUpo0Lmo2IvJLyTIrnRUC5wgDm01N5UBn8IWW/45F+RlSi63EXA1Vz4QvRirfZ8aldR2Q==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", + "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", + "dev": true, "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.15.0", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.15.0", + "@stacks/transactions": "^6.16.1", "bs58": "^5.0.0" } }, @@ -856,6 +910,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "dev": true, "funding": [ { "type": "individual", @@ -868,18 +923,19 @@ "version": "0.61.0", "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-0.61.0.tgz", "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==", + "dev": true, "license": "ISC" }, "node_modules/@stacks/transactions": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.15.0.tgz", - "integrity": "sha512-P6XKDcqqycPy+KBJBw8+5N+u57D8moJN7msYdde1gYXERmvOo9ht/MNREWWQ7SAM7Nlhau5mpezCdYCzXOCilQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.16.1.tgz", + "integrity": "sha512-yCtUM+8IN0QJbnnlFhY1wBW7Q30Cxje3Zmy8DgqdBoM/EPPWadez/8wNWFANVAMyUZeQ9V/FY+8MAw4E+pCReA==", "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" } @@ -932,9 +988,9 @@ "peer": true }, "node_modules/@types/node": { - "version": "20.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", - "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -1230,9 +1286,9 @@ "peer": true }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1347,6 +1403,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -1424,6 +1481,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dev": true, "license": "MIT", "dependencies": { "base-x": "^4.0.0" @@ -1460,9 +1518,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001638", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", - "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", "dev": true, "funding": [ { @@ -1712,9 +1770,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.814", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.814.tgz", - "integrity": "sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==", + "version": "1.4.818", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz", + "integrity": "sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==", "dev": true, "license": "ISC", "peer": true @@ -1980,9 +2038,9 @@ } }, "node_modules/glob": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", - "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", "dev": true, "license": "ISC", "dependencies": { @@ -1997,7 +2055,7 @@ "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2075,16 +2133,16 @@ "license": "ISC" }, "node_modules/jackspeak": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", - "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.1.tgz", + "integrity": "sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2206,13 +2264,13 @@ } }, "node_modules/lru-cache": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", - "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz", + "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==", "dev": true, "license": "ISC", "engines": { - "node": "14 || >=16.14" + "node": ">=18" } }, "node_modules/magic-string": { @@ -2504,20 +2562,20 @@ } }, "node_modules/pkg-types": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", - "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", "license": "MIT", "dependencies": { "confbox": "^0.1.7", - "mlly": "^1.7.0", + "mlly": "^1.7.1", "pathe": "^1.1.2" } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "funding": [ { "type": "opencollective", @@ -2535,7 +2593,7 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -2635,9 +2693,9 @@ } }, "node_modules/rimraf": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", - "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.8.tgz", + "integrity": "sha512-XSh0V2/yNhDEi8HwdIefD8MLgs4LQXPag/nEJWs3YUc3Upn+UHa1GyIkEg9xSSNt7HnkO5FjTvmcRzgf+8UZuw==", "dev": true, "license": "ISC", "dependencies": { @@ -2647,7 +2705,7 @@ "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14.18" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2657,6 +2715,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/ripemd160-min/-/ripemd160-min-0.0.6.tgz", "integrity": "sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==", + "dev": true, "engines": { "node": ">=8" } @@ -2700,6 +2759,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -3140,9 +3200,9 @@ } }, "node_modules/typescript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3166,9 +3226,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -3212,19 +3272,20 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.1" } }, "node_modules/vite": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", - "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", + "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "rollup": "^4.13.0" }, "bin": { @@ -3654,9 +3715,9 @@ } }, "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "license": "MIT", "engines": { "node": ">=12.20" diff --git a/package.json b/package.json new file mode 100644 index 000000000..29cf00bc1 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "clarinet-sdk-workspace", + "private": true, + "description": "Workspace hosting the Clarinet SDK for Node.js and Browser", + "author": "hirosystems", + "license": "GPL-3.0", + "type": "module", + "workspaces": [ + "components/clarinet-sdk-wasm/pkg-node", + "components/clarinet-sdk-wasm/pkg-browser", + "components/clarinet-sdk/common", + "components/clarinet-sdk/node", + "components/clarinet-sdk/browser" + ], + "scripts": { + "build:wasm": "node components/clarinet-sdk-wasm/build.mjs", + "test": "npm test --workspaces --if-present", + "publish:sdk-wasm": "npm publish -w components/clarinet-sdk-wasm/pkg-node -w components/clarinet-sdk-wasm/pkg-browser --tag beta", + "publish:sdk": "npm publish -w components/clarinet-sdk/node -w components/clarinet-sdk/browser --tag beta" + }, + "devDependencies": { + "@types/node": "^20.4.5", + "@types/prompts": "^2.4.5", + "@types/yargs": "^17.0.24", + "prettier": "^3.0.3", + "rimraf": "^5.0.1", + "ts-loader": "^9.4.4", + "typescript": "^5.1.6" + } +}