diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb80d773..03e25d22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,18 +10,14 @@ on: jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - rust: ['1.91', '1.92'] steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Rust uses: dtolnay/rust-toolchain@master with: - toolchain: ${{ matrix.rust }} + toolchain: 1.92 components: rustfmt, clippy, rust-src - targets: riscv64imac-unknown-none-elf - name: Install solc run: | SOLC_VERSION=0.8.26 @@ -29,6 +25,9 @@ jobs: -o solc chmod +x solc sudo mv solc /usr/local/bin/solc + - name: Install Rust nightly for scaffold tests + run: | + rustup toolchain install nightly --component rust-src --profile minimal - name: Run formatting check run: cargo fmt --check - name: Run clippy diff --git a/Cargo.lock b/Cargo.lock index 37e6e39d..ad5369a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,16 +184,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cargo-pvm-contract" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "askama", "assert_cmd", "clap", + "convert_case", "env_logger", "include_dir", "inquire", "log", + "polkavm-linker", "predicates", "serde", "serde_json", @@ -203,7 +205,7 @@ dependencies = [ [[package]] name = "cargo-pvm-contract-builder" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "log", @@ -263,6 +265,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "crossterm" version = "0.25.0" diff --git a/Cargo.toml b/Cargo.toml index b17098fc..80b6e99b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.2.2" +version = "0.2.3" edition = "2024" license = "MIT" repository = "https://github.com/paritytech/cargo-pvm-contract" @@ -22,5 +22,6 @@ polkavm-linker = "0.30.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tiny-keccak = { version = "2.0", features = ["keccak"] } +convert_case = "0.6" toml_edit = "0.22" inquire = "0.7" diff --git a/crates/cargo-pvm-contract-builder/src/lib.rs b/crates/cargo-pvm-contract-builder/src/lib.rs index b7ab4d6e..c4401a3d 100644 --- a/crates/cargo-pvm-contract-builder/src/lib.rs +++ b/crates/cargo-pvm-contract-builder/src/lib.rs @@ -193,13 +193,7 @@ fn build_elf( profile: &Profile, bins: &[String], ) -> Result<()> { - let immediate_abort = check_immediate_abort_support()?; - - let rustflags = if immediate_abort { - "-Zunstable-options -Cpanic=immediate-abort" - } else { - "" - }; + let rustflags = "-Zunstable-options -Cpanic=immediate-abort"; let mut args = polkavm_linker::TargetJsonArgs::default(); args.is_64_bit = true; @@ -229,10 +223,6 @@ fn build_elf( .arg(&target_json) .arg("-Zbuild-std=core,alloc"); - if !immediate_abort { - cmd.arg("-Zbuild-std-features=panic_immediate_abort"); - } - for bin in bins { cmd.arg("--bin").arg(bin); } @@ -249,35 +239,6 @@ fn build_elf( Ok(()) } -/// Check if rustc supports immediate abort (>= 1.92). -fn check_immediate_abort_support() -> Result { - let output = Command::new("rustc") - .arg("--version") - .output() - .context("Failed to run rustc --version")?; - - let version_str = String::from_utf8(output.stdout).context("Invalid rustc version output")?; - - let version = version_str - .split_whitespace() - .nth(1) - .context("Unexpected rustc version format")?; - - let mut parts = version.split('.'); - let major: u32 = parts - .next() - .context("Missing major version")? - .parse() - .context("Invalid major version")?; - let minor: u32 = parts - .next() - .context("Missing minor version")? - .parse() - .context("Invalid minor version")?; - - Ok(major > 1 || (major == 1 && minor >= 92)) -} - /// Link an ELF binary to PolkaVM bytecode. fn link_to_polkavm(elf_path: &Path, output_path: &Path) -> Result<()> { let elf_bytes = fs::read(elf_path) diff --git a/crates/cargo-pvm-contract/Cargo.toml b/crates/cargo-pvm-contract/Cargo.toml index 4d073bf1..62769e47 100644 --- a/crates/cargo-pvm-contract/Cargo.toml +++ b/crates/cargo-pvm-contract/Cargo.toml @@ -27,6 +27,8 @@ serde_json = { workspace = true } inquire = { workspace = true } tiny-keccak = { workspace = true } askama = { workspace = true } +polkavm-linker = { workspace = true } +convert_case = { workspace = true } [dev-dependencies] assert_cmd = "2.0" diff --git a/crates/cargo-pvm-contract/src/main.rs b/crates/cargo-pvm-contract/src/main.rs index 5157eea6..b9910982 100644 --- a/crates/cargo-pvm-contract/src/main.rs +++ b/crates/cargo-pvm-contract/src/main.rs @@ -3,10 +3,7 @@ use clap::{Parser, Subcommand, ValueEnum}; use include_dir::{Dir, include_dir}; use inquire::{Select, Text}; use log::debug; -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::path::PathBuf; mod scaffold; @@ -77,24 +74,58 @@ impl std::fmt::Display for MemoryModel { #[derive(Debug, Clone, PartialEq, Eq)] struct ExampleContract { name: String, - filename: String, + folder: String, + sol_filename: String, + rust_no_alloc: String, + rust_with_alloc: String, } impl ExampleContract { - fn from_path(path: &Path) -> Option { - if path.extension().and_then(|ext| ext.to_str()) != Some("sol") { - return None; - } - - let filename = path.file_name()?.to_str()?.to_string(); - let name = path.file_stem()?.to_str()?.to_string(); - Some(Self { name, filename }) + fn from_dir(dir: &Dir) -> Option { + let sol_file = dir + .files() + .find(|file| file.path().extension().and_then(|ext| ext.to_str()) == Some("sol"))?; + let sol_filename = sol_file.path().file_name()?.to_str()?.to_string(); + let name = sol_file.path().file_stem()?.to_str()?.to_string(); + + let rust_no_alloc = dir + .files() + .find(|file| { + file.path() + .file_name() + .and_then(|filename| filename.to_str()) + .is_some_and(|filename| filename.ends_with("_no_alloc.rs")) + })? + .path() + .file_name()? + .to_str()? + .to_string(); + let rust_with_alloc = dir + .files() + .find(|file| { + file.path() + .file_name() + .and_then(|filename| filename.to_str()) + .is_some_and(|filename| filename.ends_with("_with_alloc.rs")) + })? + .path() + .file_name()? + .to_str()? + .to_string(); + + Some(Self { + name, + folder: dir.path().to_str()?.to_string(), + sol_filename, + rust_no_alloc, + rust_with_alloc, + }) } fn matches(&self, query: &str) -> bool { let query = query.trim().to_ascii_lowercase(); let name = self.name.to_ascii_lowercase(); - let filename = self.filename.to_ascii_lowercase(); + let filename = self.sol_filename.to_ascii_lowercase(); query == name || query == filename } } @@ -110,8 +141,8 @@ fn load_examples() -> Result> { .get_dir("examples") .ok_or_else(|| anyhow::anyhow!("Examples directory not found in templates"))?; let mut examples: Vec = examples_dir - .files() - .filter_map(|file| ExampleContract::from_path(file.path())) + .dirs() + .filter_map(ExampleContract::from_dir) .collect(); examples.sort_by(|left, right| left.name.cmp(&right.name)); @@ -176,7 +207,7 @@ fn init_command(args: PvmContractArgs) -> Result<()> { check_dir_exists(&contract_name)?; debug!( "Initializing from example: {} with memory model: {:?}", - example.filename, memory_model + example.sol_filename, memory_model ); init_from_example(&example, &contract_name, memory_model) @@ -264,39 +295,30 @@ fn init_from_example( contract_name: &str, memory_model: MemoryModel, ) -> Result<()> { - // Get the embedded example .sol file - let example_path = format!("examples/{}", example.filename); - let example_file = TEMPLATES_DIR - .get_file(&example_path) - .ok_or_else(|| anyhow::anyhow!("Example file not found: {}", example_path))?; - - // Write to a temporary file - let temp_dir = std::env::temp_dir(); - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .context("Failed to read system time")? - .as_nanos(); - let example_temp_dir = temp_dir.join(format!("cargo-pvm-contract-{timestamp}")); - fs::create_dir_all(&example_temp_dir).with_context(|| { - format!("Failed to create temporary directory for example: {example_temp_dir:?}") - })?; - let temp_sol_path = example_temp_dir.join(example.filename.as_str()); - fs::write(&temp_sol_path, example_file.contents()) - .with_context(|| format!("Failed to write temporary .sol file: {:?}", temp_sol_path))?; + let sol_path = format!("{}/{}", example.folder, example.sol_filename); + let sol_file = TEMPLATES_DIR + .get_file(&sol_path) + .ok_or_else(|| anyhow::anyhow!("Example file not found: {sol_path}"))?; let use_alloc = memory_model == MemoryModel::AllocWithAlloy; + let rust_example_name = if use_alloc { + example.rust_with_alloc.as_str() + } else { + example.rust_no_alloc.as_str() + }; - // Use scaffold to initialize from the temp file - let result = scaffold::init_from_solidity_file( - temp_sol_path.to_str().unwrap(), + let rust_path = format!("{}/{}", example.folder, rust_example_name); + let rust_file = TEMPLATES_DIR + .get_file(&rust_path) + .ok_or_else(|| anyhow::anyhow!("Example file not found: {rust_path}"))?; + + scaffold::init_from_example_files( + sol_file.contents(), + &example.sol_filename, + rust_file.contents(), contract_name, use_alloc, - ); - - // Clean up temp file - let _ = fs::remove_dir_all(&example_temp_dir); - - result + ) } fn check_dir_exists(contract_name: &str) -> Result<()> { diff --git a/crates/cargo-pvm-contract/src/scaffold.rs b/crates/cargo-pvm-contract/src/scaffold.rs index 40d45cce..266fd66c 100644 --- a/crates/cargo-pvm-contract/src/scaffold.rs +++ b/crates/cargo-pvm-contract/src/scaffold.rs @@ -1,14 +1,11 @@ use anyhow::{Context, Result}; use askama::Template; +use convert_case::{Case, Casing}; use serde::Deserialize; use std::io::Write; use std::{fs, path::PathBuf, process::Command}; use tiny_keccak::{Hasher, Keccak}; -// ============================================================================ -// Askama Templates -// ============================================================================ - #[derive(Template)] #[template(path = "scaffold/contract_alloc.rs.txt")] struct ContractAllocTemplate<'a> { @@ -32,6 +29,7 @@ const BUILDER_VERSION: &str = env!("CARGO_PKG_VERSION"); #[template(path = "scaffold/cargo_toml.txt")] struct CargoTomlTemplate<'a> { contract_name: &'a str, + bin_source: &'a str, use_alloc: bool, builder_version: &'a str, builder_path: Option, @@ -45,10 +43,6 @@ struct ContractBlankTemplate; #[template(path = "scaffold/build.rs.txt")] struct BuildRsTemplate; -// ============================================================================ -// Template Data Structures -// ============================================================================ - struct AllocFunctionInfo { name: String, name_snake: String, @@ -84,10 +78,6 @@ struct ParamDecode { decode_line: String, } -// ============================================================================ -// Solc Output Parsing Structures -// ============================================================================ - #[derive(Debug, Deserialize)] struct SolcOutput { contracts: std::collections::HashMap>, @@ -149,10 +139,6 @@ struct AbiOutput { type_name: String, } -// ============================================================================ -// Helper Functions -// ============================================================================ - /// Compute the keccak256 hash of a string fn keccak256(input: &str) -> [u8; 32] { let mut hasher = Keccak::v256(); @@ -198,40 +184,10 @@ fn format_bytes32_multiline(bytes: &[u8; 32]) -> String { .join(",\n ") } -fn to_pascal_case(s: &str) -> String { - s.split(|c: char| !c.is_alphanumeric()) - .filter(|s| !s.is_empty()) - .map(|word| { - let mut chars = word.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().chain(chars).collect(), - } - }) - .collect() -} - -fn to_snake_case(s: &str) -> String { - let mut result = String::new(); - for (i, c) in s.chars().enumerate() { - if c.is_uppercase() && i > 0 { - result.push('_'); - } - result.push(c.to_lowercase().next().unwrap()); - } - result -} - -fn to_screaming_snake_case(s: &str) -> String { - to_snake_case(s).to_uppercase() -} - -// ============================================================================ -// Public API -// ============================================================================ - +/// Create a new blank contract project. pub fn init_blank_contract(contract_name: &str) -> Result<()> { - let target_dir = std::env::current_dir()?.join(contract_name); + let contract_name = contract_name.to_case(Case::Kebab); + let target_dir = std::env::current_dir()?.join(&contract_name); if target_dir.exists() { anyhow::bail!("Directory already exists: {target_dir:?}"); } @@ -239,25 +195,47 @@ pub fn init_blank_contract(contract_name: &str) -> Result<()> { fs::create_dir(&target_dir) .with_context(|| format!("Failed to create directory: {target_dir:?}"))?; + let (target_json_path, target_json_name) = resolve_target_json()?; + let target_json_dest = target_dir.join(target_json_name); + fs::copy(&target_json_path, &target_json_dest).with_context(|| { + format!( + "Failed to copy target JSON from {} to {}", + target_json_path.display(), + target_json_dest.display() + ) + })?; + + let target_json_name = target_json_dest + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| anyhow::anyhow!("Target JSON path is missing a file name"))?; + let cargo_config_dir = target_dir.join(".cargo"); fs::create_dir(&cargo_config_dir)?; fs::write( cargo_config_dir.join("config.toml"), - r#"[build] -target = "riscv64imac-unknown-none-elf" -"#, + format!( + "[build]\n target = \"{}\"\n\n[unstable]\n build-std = [\"core\", \"alloc\"]\n\n[env]\n RUSTC_BOOTSTRAP = \"1\"\n", + target_json_name + ), )?; fs::write(target_dir.join(".gitignore"), "/target\n*.polkavm\n")?; - + fs::write( + target_dir.join("rust-toolchain.toml"), + "[toolchain]\nchannel = \"nightly\"\n", + )?; fs::create_dir(target_dir.join("src"))?; let lib_rs_content = generate_blank_contract()?; - fs::write(target_dir.join("src/lib.rs"), lib_rs_content)?; + fs::write( + target_dir.join(format!("src/{}.rs", contract_name)), + lib_rs_content, + )?; let build_rs_content = generate_build_rs()?; fs::write(target_dir.join("build.rs"), build_rs_content)?; - let cargo_toml_content = generate_cargo_toml(contract_name, false)?; + let cargo_toml_content = generate_cargo_toml(&contract_name, &contract_name, false)?; fs::write(target_dir.join("Cargo.toml"), cargo_toml_content)?; println!("Successfully initialized blank contract project: {target_dir:?}"); @@ -267,13 +245,13 @@ target = "riscv64imac-unknown-none-elf" Ok(()) } +/// Create a new contract project from a Solidity file. pub fn init_from_solidity_file(sol_file: &str, contract_name: &str, use_alloc: bool) -> Result<()> { let sol_path = PathBuf::from(sol_file); if !sol_path.exists() { anyhow::bail!("Solidity file not found: {sol_file}"); } - // Get absolute path to .sol file let sol_abs_path = sol_path .canonicalize() .with_context(|| format!("Failed to get absolute path for {sol_file}"))?; @@ -281,72 +259,128 @@ pub fn init_from_solidity_file(sol_file: &str, contract_name: &str, use_alloc: b let sol_file_name = sol_path .file_name() .and_then(|s| s.to_str()) - .ok_or_else(|| anyhow::anyhow!("Invalid file name"))?; + .ok_or_else(|| anyhow::anyhow!("Invalid file name"))? + .to_string(); + + let sol_content = fs::read(&sol_abs_path) + .with_context(|| format!("Failed to read Solidity file: {sol_abs_path:?}"))?; - log::debug!("Extracting metadata from {sol_file}"); - let (metadata, actual_contract_name) = extract_solc_metadata(&sol_abs_path, sol_file_name)?; + init_from_example_files_inner(&sol_content, &sol_file_name, None, contract_name, use_alloc) +} + +pub fn init_from_example_files( + sol_contents: &[u8], + sol_file_name: &str, + rust_contents: &[u8], + contract_name: &str, + use_alloc: bool, +) -> Result<()> { + init_from_example_files_inner( + sol_contents, + sol_file_name, + Some(rust_contents), + contract_name, + use_alloc, + ) +} + +fn init_from_example_files_inner( + sol_contents: &[u8], + sol_file_name: &str, + rust_contents: Option<&[u8]>, + contract_name: &str, + use_alloc: bool, +) -> Result<()> { + let contract_name = contract_name.to_case(Case::Kebab); + let sol_file_name = sol_file_name.to_string(); + + log::debug!("Extracting metadata from {sol_file_name}"); + let (metadata, actual_contract_name) = + extract_solc_metadata_from_bytes(sol_contents, &sol_file_name)?; + let actual_contract_kebab = actual_contract_name.to_case(Case::Kebab); // Create project directory - let target_dir = std::env::current_dir()?.join(contract_name); + let target_dir = std::env::current_dir()?.join(&contract_name); if target_dir.exists() { anyhow::bail!("Directory already exists: {target_dir:?}"); } fs::create_dir(&target_dir) .with_context(|| format!("Failed to create directory: {target_dir:?}"))?; + let (target_json_path, target_json_name) = resolve_target_json()?; + let target_json_dest = target_dir.join(target_json_name); + fs::copy(&target_json_path, &target_json_dest).with_context(|| { + format!( + "Failed to copy target JSON from {} to {}", + target_json_path.display(), + target_json_dest.display() + ) + })?; + + let target_json_name = target_json_dest + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| anyhow::anyhow!("Target JSON path is missing a file name"))?; + // Copy .sol file to project - let target_sol_path = target_dir.join(sol_file_name); - fs::copy(&sol_abs_path, &target_sol_path) - .with_context(|| format!("Failed to copy {sol_file} to {target_sol_path:?}"))?; + let target_sol_path = target_dir.join(&sol_file_name); + fs::write(&target_sol_path, sol_contents) + .with_context(|| format!("Failed to write {sol_file_name} to {target_sol_path:?}"))?; // Create .cargo directory and config let cargo_config_dir = target_dir.join(".cargo"); fs::create_dir(&cargo_config_dir)?; fs::write( cargo_config_dir.join("config.toml"), - r#"[build] -target = "riscv64imac-unknown-none-elf" -"#, + format!( + "[build]\n target = \"{}\"\n\n[unstable]\n build-std = [\"core\", \"alloc\"]\n\n[env]\n RUSTC_BOOTSTRAP = \"1\"\n", + target_json_name + ), )?; // Create .gitignore fs::write(target_dir.join(".gitignore"), "/target\n*.polkavm\n")?; - - // Generate src/lib.rs + fs::write( + target_dir.join("rust-toolchain.toml"), + "[toolchain]\nchannel = \"nightly\"\n", + )?; + // Generate src/{contract}.rs fs::create_dir(target_dir.join("src"))?; - let lib_rs_content = if use_alloc { - generate_rust_code_alloc(sol_file_name, &metadata, &actual_contract_name)? + let lib_rs_content = if let Some(contents) = rust_contents { + String::from_utf8(contents.to_vec()).context("Example Rust file is not valid UTF-8")? + } else if use_alloc { + generate_rust_code_alloc(&sol_file_name, &metadata, &actual_contract_name)? } else { generate_rust_code_no_alloc(&metadata, &actual_contract_name)? }; - fs::write(target_dir.join("src/lib.rs"), lib_rs_content)?; + fs::write( + target_dir.join(format!("src/{}.rs", actual_contract_kebab)), + lib_rs_content, + )?; let build_rs_content = generate_build_rs()?; fs::write(target_dir.join("build.rs"), build_rs_content)?; // Create Cargo.toml - let cargo_toml_content = generate_cargo_toml(contract_name, use_alloc)?; + let cargo_toml_content = + generate_cargo_toml(&contract_name, &actual_contract_kebab, use_alloc)?; fs::write(target_dir.join("Cargo.toml"), cargo_toml_content)?; - println!("Successfully initialized contract project from {sol_file}: {target_dir:?}"); + println!("Successfully initialized contract project from {sol_file_name}: {target_dir:?}"); println!("\nNext steps:"); println!(" cd {contract_name}"); println!(" cargo build"); Ok(()) } -// ============================================================================ -// Internal Functions -// ============================================================================ - -fn extract_solc_metadata( - sol_abs_path: &PathBuf, +/// Internal helpers for template generation. +fn extract_solc_metadata_from_bytes( + sol_contents: &[u8], sol_file_name: &str, ) -> Result<(ContractMetadata, String)> { - // Read the Solidity source code - let sol_content = fs::read_to_string(sol_abs_path) - .with_context(|| format!("Failed to read Solidity file: {sol_abs_path:?}"))?; + let sol_content = + String::from_utf8(sol_contents.to_vec()).context("Solidity file is not valid UTF-8")?; let solc_input = serde_json::json!({ "language": "Solidity", @@ -436,7 +470,7 @@ fn generate_rust_code_alloc( metadata: &ContractMetadata, contract_name: &str, ) -> Result { - let contract_name_pascal = to_pascal_case(contract_name); + let contract_name_pascal = contract_name.to_case(Case::Pascal); let functions: Vec = metadata .output @@ -445,7 +479,7 @@ fn generate_rust_code_alloc( .filter_map(|item| match item { AbiItem::Function { name, .. } => Some(AllocFunctionInfo { name: name.clone(), - name_snake: to_snake_case(name), + name_snake: name.to_case(Case::Snake), call_type: format!("{contract_name_pascal}::{name}Call"), }), _ => None, @@ -471,7 +505,7 @@ fn generate_rust_code_no_alloc(metadata: &ContractMetadata, contract_name: &str) if let AbiItem::Function { name, inputs, .. } = item { let signature = build_function_signature(name, inputs); let selector = compute_selector(&signature); - let const_name = format!("{}_SELECTOR", to_screaming_snake_case(name)); + let const_name = format!("{}_SELECTOR", name.to_case(Case::UpperSnake)); selectors.push(SelectorConst { const_name: const_name.clone(), @@ -481,44 +515,18 @@ fn generate_rust_code_no_alloc(metadata: &ContractMetadata, contract_name: &str) // Generate decode params let mut params = Vec::new(); - let mut offset = 4usize; for (idx, input) in inputs.iter().enumerate() { let param_name = if input.name.is_empty() { format!("param_{}", idx) } else { - to_snake_case(&input.name) + input.name.to_case(Case::Snake) }; - let decode_line = match input.type_name.as_str() { - "address" => { - format!( - "let {param_name} = decode_address(&call_data[{offset}..{}]);", - offset + 32 - ) - } - "uint256" | "uint128" | "uint64" | "uint32" | "uint16" | "uint8" => { - format!( - "let {param_name} = decode_u128(&call_data[{offset}..{}]);", - offset + 32 - ) - } - "bool" => { - format!("let {param_name} = call_data[{}] != 0;", offset + 31) - } - "bytes32" => { - format!( - "let {param_name}: [u8; 32] = call_data[{offset}..{}].try_into().unwrap();", - offset + 32 - ) - } - _ => { - format!("// TODO: decode {param_name} of type {}", input.type_name) - } - }; + let decode_line = + format!("// TODO: decode {param_name} of type {}", input.type_name); params.push(ParamDecode { decode_line }); - offset += 32; } functions.push(NoAllocFunctionInfo { @@ -540,7 +548,7 @@ fn generate_rust_code_no_alloc(metadata: &ContractMetadata, contract_name: &str) let signature = build_function_signature(name, inputs); let hash = keccak256(&signature); Some(EventConst { - const_name: format!("{}_EVENT_SIGNATURE", to_screaming_snake_case(name)), + const_name: format!("{}_EVENT_SIGNATURE", name.to_case(Case::UpperSnake)), bytes_hex: format_bytes32_multiline(&hash), signature, }) @@ -560,7 +568,7 @@ fn generate_rust_code_no_alloc(metadata: &ContractMetadata, contract_name: &str) let signature = build_function_signature(name, inputs); let selector = compute_selector(&signature); Some(ErrorConst { - const_name: format!("{}_ERROR", to_screaming_snake_case(name)), + const_name: format!("{}_ERROR", name.to_case(Case::UpperSnake)), bytes_hex: format_bytes_as_hex(&selector), signature, }) @@ -583,7 +591,22 @@ fn generate_rust_code_no_alloc(metadata: &ContractMetadata, contract_name: &str) .context("Failed to render no-alloc template") } -fn generate_cargo_toml(contract_name: &str, use_alloc: bool) -> Result { +fn resolve_target_json() -> Result<(PathBuf, String)> { + let mut args = polkavm_linker::TargetJsonArgs::default(); + args.is_64_bit = true; + let target_json = polkavm_linker::target_json_path(args) + .map_err(|e| anyhow::anyhow!("Failed to get target JSON: {e}"))?; + + let target_name = target_json + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| anyhow::anyhow!("Target JSON path is missing a file name"))? + .to_string(); + + Ok((target_json, target_name)) +} + +fn generate_cargo_toml(contract_name: &str, bin_source: &str, use_alloc: bool) -> Result { let builder_path = std::env::var("CARGO_PVM_CONTRACT_BUILDER_PATH") .ok() .filter(|value| !value.trim().is_empty()); @@ -597,6 +620,7 @@ fn generate_cargo_toml(contract_name: &str, use_alloc: bool) -> Result { let template = CargoTomlTemplate { contract_name, + bin_source, use_alloc, builder_version: BUILDER_VERSION, builder_path, diff --git a/crates/cargo-pvm-contract/templates/examples/Fibonacci.sol b/crates/cargo-pvm-contract/templates/examples/fibonacci/Fibonacci.sol similarity index 100% rename from crates/cargo-pvm-contract/templates/examples/Fibonacci.sol rename to crates/cargo-pvm-contract/templates/examples/fibonacci/Fibonacci.sol diff --git a/crates/cargo-pvm-contract/templates/examples/fibonacci/fibonacci_no_alloc.rs b/crates/cargo-pvm-contract/templates/examples/fibonacci/fibonacci_no_alloc.rs new file mode 100644 index 00000000..d506d7df --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/fibonacci/fibonacci_no_alloc.rs @@ -0,0 +1,78 @@ +#![no_main] +#![no_std] + +use pallet_revive_uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +// ============================================================================ +// FIBONACCI CONTRACT - Generated from Solidity ABI +// ============================================================================ + +// Function selectors + +const FIBONACCI_SELECTOR: [u8; 4] = [0xe4, 0x44, 0xa7, 0x09]; // fibonacci(uint32) + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Safety: The unimp instruction is guaranteed to trap + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked(); + } +} + +/// Contract entry points. + +/// This is the constructor which is called once per contract. +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +/// This is the regular entry point when the contract is called. +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let call_data_len = api::call_data_size() as usize; + + // Fixed buffer for call data + let mut call_data = [0u8; 256]; + if call_data_len > call_data.len() { + panic!("Call data too large"); + } + + api::call_data_copy(&mut call_data[..call_data_len], 0); + + if call_data_len < 4 { + panic!("Call data too short"); + } + + let selector: [u8; 4] = call_data[0..4].try_into().unwrap(); + + match selector { + FIBONACCI_SELECTOR => { + if call_data_len < 36 { + panic!("Invalid fibonacci call data"); + } + + let mut input = [0u8; 4]; + api::call_data_copy(&mut input, 32); + + let n = u32::from_be_bytes(input); + let result = _fibonacci(n); + let output = result.to_be_bytes(); + + let mut response = [0u8; 32]; + response[28..].copy_from_slice(&output); + api::return_value(ReturnFlags::empty(), &response); + } + + _ => panic!("Unknown function selector"), + } +} + +fn _fibonacci(n: u32) -> u32 { + if n == 0 { + 0 + } else if n == 1 { + 1 + } else { + _fibonacci(n - 1) + _fibonacci(n - 2) + } +} diff --git a/crates/cargo-pvm-contract/templates/examples/fibonacci/fibonacci_with_alloc.rs b/crates/cargo-pvm-contract/templates/examples/fibonacci/fibonacci_with_alloc.rs new file mode 100644 index 00000000..f2b42d51 --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/fibonacci/fibonacci_with_alloc.rs @@ -0,0 +1,65 @@ +#![no_main] +#![no_std] + +use alloy_core::{sol, sol_types::SolCall}; +use pallet_revive_uapi::{HostFn, HostFnImpl as api, ReturnFlags}; + +extern crate alloc; +use alloc::vec; + +sol!("Fibonacci.sol"); + +#[global_allocator] +static mut ALLOC: picoalloc::Mutex>> = { + static mut ARRAY: picoalloc::Array<1024> = picoalloc::Array([0u8; 1024]); + + picoalloc::Mutex::new(picoalloc::Allocator::new(unsafe { + picoalloc::ArrayPointer::new(&raw mut ARRAY) + })) +}; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Safety: The unimp instruction is guaranteed to trap + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked(); + } +} + +/// This is the constructor which is called once per contract. +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +/// This is the regular entry point when the contract is called. +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let call_data_len = api::call_data_size(); + let mut call_data = vec![0u8; call_data_len as usize]; + api::call_data_copy(&mut call_data, 0); + + let selector: [u8; 4] = call_data[0..4].try_into().unwrap(); + + match selector { + Fibonacci::fibonacciCall::SELECTOR => { + let fibonacci_call = Fibonacci::fibonacciCall::abi_decode(&call_data, true) + .expect("Failed to decode fibonacci call"); + + let result = _fibonacci(fibonacci_call._0); + let returns = Fibonacci::fibonacciCall::abi_encode_returns(&(result,)); + api::return_value(ReturnFlags::empty(), &returns); + } + + _ => panic!("Unknown function selector"), + } +} + +fn _fibonacci(n: u32) -> u32 { + if n == 0 { + 0 + } else if n == 1 { + 1 + } else { + _fibonacci(n - 1) + _fibonacci(n - 2) + } +} diff --git a/crates/cargo-pvm-contract/templates/examples/MyToken.sol b/crates/cargo-pvm-contract/templates/examples/mytoken/MyToken.sol similarity index 100% rename from crates/cargo-pvm-contract/templates/examples/MyToken.sol rename to crates/cargo-pvm-contract/templates/examples/mytoken/MyToken.sol diff --git a/crates/cargo-pvm-contract/templates/examples/mytoken/mytoken_no_alloc.rs b/crates/cargo-pvm-contract/templates/examples/mytoken/mytoken_no_alloc.rs new file mode 100644 index 00000000..56689407 --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/mytoken/mytoken_no_alloc.rs @@ -0,0 +1,240 @@ +#![no_main] +#![no_std] + +use pallet_revive_uapi::{HostFn, HostFnImpl as api, ReturnFlags, StorageFlags}; + +// ============================================================================ +// MYTOKEN CONTRACT - Generated from Solidity ABI +// ============================================================================ + +// Function selectors + +const BALANCE_OF_SELECTOR: [u8; 4] = [0x70, 0xa0, 0x82, 0x31]; // balanceOf(address) + +const MINT_SELECTOR: [u8; 4] = [0x40, 0xc1, 0x0f, 0x19]; // mint(address,uint256) + +const TOTAL_SUPPLY_SELECTOR: [u8; 4] = [0x18, 0x16, 0x0d, 0xdd]; // totalSupply() + +const TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb]; // transfer(address,uint256) + +// Event signatures + +const TRANSFER_EVENT_SIGNATURE: [u8; 32] = [ + 0xdd, 0xf2, 0x52, 0xad, 0x1b, 0xe2, 0xc8, 0x9b, 0x69, 0xc2, 0xb0, 0x68, 0xfc, 0x37, 0x8d, 0xaa, + 0x95, 0x2b, 0xa7, 0xf1, 0x63, 0xc4, 0xa1, 0x16, 0x28, 0xf5, 0x5a, 0x4d, 0xf5, 0x23, 0xb3, 0xef, +]; // Transfer(address,address,uint256) + +// Error selectors + +const INSUFFICIENT_BALANCE_ERROR: [u8; 4] = [0xf4, 0xd6, 0x78, 0xb8]; // InsufficientBalance() + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Safety: The unimp instruction is guaranteed to trap + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked(); + } +} + +/// Contract entry points. + +/// This is the constructor which is called once per contract. +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +/// This is the regular entry point when the contract is called. +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let call_data_len = api::call_data_size() as usize; + + // Fixed buffer for call data + let mut call_data = [0u8; 256]; + if call_data_len > call_data.len() { + panic!("Call data too large"); + } + + api::call_data_copy(&mut call_data[..call_data_len], 0); + + if call_data_len < 4 { + panic!("Call data too short"); + } + + let selector: [u8; 4] = call_data[0..4].try_into().unwrap(); + + match selector { + BALANCE_OF_SELECTOR => { + if call_data_len < 36 { + panic!("Invalid balanceOf call data"); + } + + let account = decode_address(&call_data[4..36]); + let balance = get_balance(&account); + let output = to_word(balance); + api::return_value(ReturnFlags::empty(), &output); + } + + MINT_SELECTOR => { + if call_data_len < 68 { + panic!("Invalid mint call data"); + } + + let to = decode_address(&call_data[4..36]); + let amount = decode_u128(&call_data[36..68]); + + let new_recipient_balance = get_balance(&to).saturating_add(amount); + set_balance(&to, new_recipient_balance); + + let new_supply = get_total_supply().saturating_add(amount); + set_total_supply(new_supply); + + let zero_address = [0u8; 20]; + emit_transfer(&zero_address, &to, amount); + } + + TOTAL_SUPPLY_SELECTOR => { + if call_data_len < 4 { + panic!("Invalid totalSupply call data"); + } + + let output = to_word(get_total_supply()); + api::return_value(ReturnFlags::empty(), &output); + } + + TRANSFER_SELECTOR => { + if call_data_len < 68 { + panic!("Invalid transfer call data"); + } + + let to = decode_address(&call_data[4..36]); + let amount = decode_u128(&call_data[36..68]); + + let caller = get_caller(); + let sender_balance = get_balance(&caller); + + if sender_balance < amount { + revert_insufficient_balance(); + } + + let new_sender_balance = sender_balance - amount; + let recipient_balance = get_balance(&to); + let new_recipient_balance = recipient_balance + amount; + + set_balance(&caller, new_sender_balance); + set_balance(&to, new_recipient_balance); + emit_transfer(&caller, &to, amount); + } + + _ => panic!("Unknown function selector"), + } +} + +/// Storage key for totalSupply (slot 0) +#[inline(always)] +fn total_supply_key() -> [u8; 32] { + [0u8; 32] // Slot 0 +} + +/// Helper function to compute storage key for balances[address] +/// Storage slot for balances mapping is 1 (totalSupply is at slot 0) +/// Follows Solidity convention: keccak256(leftPad32(key) ++ leftPad32(slot)) +fn balance_key(addr: &[u8; 20]) -> [u8; 32] { + let mut input = [0u8; 64]; // 32 bytes (padded address) + 32 bytes (slot) + + // First 32 bytes: address left-padded to 32 bytes (12 zeros + 20 address bytes) + input[12..32].copy_from_slice(addr); + + // Last 32 bytes: slot 1 for balances mapping (slot 0 is totalSupply) + input[63] = 1; + + let mut key = [0u8; 32]; + api::hash_keccak_256(&input, &mut key); + key +} + +/// Get totalSupply from storage +fn get_total_supply() -> u128 { + let key = total_supply_key(); + let mut supply_bytes = [0u8; 16]; + let mut supply_slice = &mut supply_bytes[..]; + + match api::get_storage(StorageFlags::empty(), &key, &mut supply_slice) { + Ok(_) => u128::from_be_bytes(supply_bytes), + Err(_) => 0u128, + } +} + +#[inline(always)] +fn to_word(value: u128) -> [u8; 32] { + let mut out = [0u8; 32]; + out[16..].copy_from_slice(&value.to_be_bytes()); + out +} + +/// Set totalSupply in storage +fn set_total_supply(amount: u128) { + let key = total_supply_key(); + let bytes = amount.to_be_bytes(); + api::set_storage(StorageFlags::empty(), &key, &bytes); +} + +/// Get the balance for a given address from storage +fn get_balance(addr: &[u8; 20]) -> u128 { + let key = balance_key(addr); + let mut balance_bytes = [0u8; 16]; + let mut balance_slice = &mut balance_bytes[..]; + + match api::get_storage(StorageFlags::empty(), &key, &mut balance_slice) { + Ok(_) => u128::from_be_bytes(balance_bytes), + Err(_) => 0u128, + } +} + +/// Set the balance for a given address in storage +#[inline(always)] +fn set_balance(addr: &[u8; 20], amount: u128) { + let key = balance_key(addr); + let bytes = amount.to_be_bytes(); + api::set_storage(StorageFlags::empty(), &key, &bytes); +} + +/// Emit a Transfer event +fn emit_transfer(from: &[u8; 20], to: &[u8; 20], value: u128) { + let mut from_topic = [0u8; 32]; + from_topic[12..32].copy_from_slice(from); + + let mut to_topic = [0u8; 32]; + to_topic[12..32].copy_from_slice(to); + + let topics = [TRANSFER_EVENT_SIGNATURE, from_topic, to_topic]; + let data = to_word(value); + api::deposit_event(&topics, &data); +} + +/// Revert with an InsufficientBalance error +#[inline(always)] +fn revert_insufficient_balance() -> ! { + api::return_value(ReturnFlags::REVERT, &INSUFFICIENT_BALANCE_ERROR); +} + +/// Get the caller's address +#[inline(always)] +fn get_caller() -> [u8; 20] { + let mut caller = [0u8; 20]; + api::caller(&mut caller); + caller +} + +/// Decode address from ABI-encoded data (32 bytes, address is in the last 20 bytes) +#[inline] +fn decode_address(data: &[u8]) -> [u8; 20] { + let mut addr = [0u8; 20]; + addr.copy_from_slice(&data[12..32]); + addr +} + +/// Decode u128 from ABI-encoded data (32 bytes) +#[inline] +fn decode_u128(data: &[u8]) -> u128 { + u128::from_be_bytes(data[16..32].try_into().unwrap()) +} diff --git a/crates/cargo-pvm-contract/templates/examples/mytoken/mytoken_with_alloc.rs b/crates/cargo-pvm-contract/templates/examples/mytoken/mytoken_with_alloc.rs new file mode 100644 index 00000000..802da4f3 --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/mytoken/mytoken_with_alloc.rs @@ -0,0 +1,193 @@ +#![no_main] +#![no_std] + +use alloy_core::{ + primitives::{Address, U256}, + sol, + sol_types::{SolCall, SolError, SolEvent}, +}; +use pallet_revive_uapi::{HostFn, HostFnImpl as api, ReturnFlags, StorageFlags}; + +extern crate alloc; +use alloc::vec; + +sol!("MyToken.sol"); + +#[global_allocator] +static mut ALLOC: picoalloc::Mutex>> = { + static mut ARRAY: picoalloc::Array<1024> = picoalloc::Array([0u8; 1024]); + + picoalloc::Mutex::new(picoalloc::Allocator::new(unsafe { + picoalloc::ArrayPointer::new(&raw mut ARRAY) + })) +}; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Safety: The unimp instruction is guaranteed to trap + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked(); + } +} + +/// This is the constructor which is called once per contract. +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +/// This is the regular entry point when the contract is called. +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let call_data_len = api::call_data_size(); + let mut call_data = vec![0u8; call_data_len as usize]; + api::call_data_copy(&mut call_data, 0); + + let selector: [u8; 4] = call_data[0..4].try_into().unwrap(); + + match selector { + MyToken::balanceOfCall::SELECTOR => { + let balance_of_call = MyToken::balanceOfCall::abi_decode(&call_data, true) + .expect("Failed to decode balanceOf call"); + + let balance = get_balance(&balance_of_call.account.into_array()); + api::return_value(ReturnFlags::empty(), &balance.to_be_bytes::<32>()); + } + + MyToken::mintCall::SELECTOR => { + let mint_call = MyToken::mintCall::abi_decode(&call_data, true) + .expect("Failed to decode mint call"); + + let new_recipient_balance = + get_balance(&mint_call.to.into_array()).saturating_add(mint_call.amount); + set_balance(&mint_call.to.into_array(), new_recipient_balance); + + let new_supply = get_total_supply().saturating_add(mint_call.amount); + set_total_supply(new_supply); + + emit_transfer(Address::ZERO, mint_call.to, mint_call.amount); + } + + MyToken::totalSupplyCall::SELECTOR => { + let total_supply = get_total_supply(); + api::return_value(ReturnFlags::empty(), &total_supply.to_be_bytes::<32>()); + } + + MyToken::transferCall::SELECTOR => { + let transfer_call = MyToken::transferCall::abi_decode(&call_data, true) + .expect("Failed to decode transfer call"); + + let caller = get_caller(); + let sender_balance = get_balance(&caller); + + if sender_balance < transfer_call.amount { + revert_insufficient_balance(); + } + + let new_sender_balance = sender_balance - transfer_call.amount; + + let recipient_balance = get_balance(&transfer_call.to.into_array()); + let new_recipient_balance = recipient_balance + transfer_call.amount; + + set_balance(&caller, new_sender_balance); + set_balance(&transfer_call.to.into_array(), new_recipient_balance); + emit_transfer( + Address::from(caller), + transfer_call.to, + transfer_call.amount, + ); + } + + _ => panic!("Unknown function selector"), + } +} + +/// Storage key for totalSupply (slot 0) +#[inline] +fn total_supply_key() -> [u8; 32] { + [0u8; 32] // Slot 0 +} + +/// Helper function to compute storage key for balances[address] +/// Storage slot for balances mapping is 1 (totalSupply is at slot 0) +/// Follows Solidity convention: keccak256(leftPad32(key) ++ leftPad32(slot)) +fn balance_key(addr: &[u8; 20]) -> [u8; 32] { + let mut input = [0u8; 64]; // 32 bytes (padded address) + 32 bytes (slot) + + // First 32 bytes: address left-padded to 32 bytes (12 zeros + 20 address bytes) + input[12..32].copy_from_slice(addr); + + // Last 32 bytes: slot 1 for balances mapping (slot 0 is totalSupply) + input[63] = 1; + + let mut key = [0u8; 32]; + api::hash_keccak_256(&input, &mut key); + key +} + +/// Get totalSupply from storage +fn get_total_supply() -> U256 { + let key = total_supply_key(); + let mut supply_bytes = vec![0u8; 32]; + let mut supply_output = supply_bytes.as_mut_slice(); + + match api::get_storage(StorageFlags::empty(), &key, &mut supply_output) { + Ok(_) => U256::from_be_bytes::<32>(supply_output[0..32].try_into().unwrap()), + Err(_) => U256::ZERO, + } +} + +/// Set totalSupply in storage +#[inline] +fn set_total_supply(amount: U256) { + let key = total_supply_key(); + api::set_storage(StorageFlags::empty(), &key, &amount.to_be_bytes::<32>()); +} + +/// Get the balance for a given address from storage +#[inline] +fn get_balance(addr: &[u8; 20]) -> U256 { + let key = balance_key(addr); + let mut balance_bytes = vec![0u8; 32]; + let mut balance_output = balance_bytes.as_mut_slice(); + + match api::get_storage(StorageFlags::empty(), &key, &mut balance_output) { + Ok(_) => U256::from_be_bytes::<32>(balance_output[0..32].try_into().unwrap()), + Err(_) => U256::ZERO, + } +} + +/// Set the balance for a given address in storage +#[inline] +fn set_balance(addr: &[u8; 20], amount: U256) { + let key = balance_key(addr); + api::set_storage(StorageFlags::empty(), &key, &amount.to_be_bytes::<32>()); +} + +/// Emit a Transfer event +#[inline] +fn emit_transfer(from: Address, to: Address, value: U256) { + let event = MyToken::Transfer { from, to, value }; + let topics = [ + MyToken::Transfer::SIGNATURE_HASH.0, + event.from.into_word().0, + event.to.into_word().0, + ]; + let data = event.value.to_be_bytes::<32>(); + api::deposit_event(&topics, &data); +} + +/// Revert with an InsufficientBalance error +#[inline] +fn revert_insufficient_balance() -> ! { + let error = MyToken::InsufficientBalance {}; + let encoded_error = ::abi_encode(&error); + api::return_value(ReturnFlags::REVERT, &encoded_error); +} + +/// Get the caller's address +#[inline] +fn get_caller() -> [u8; 20] { + let mut caller = [0u8; 20]; + api::caller(&mut caller); + caller +} diff --git a/crates/cargo-pvm-contract/templates/scaffold/cargo_toml.txt b/crates/cargo-pvm-contract/templates/scaffold/cargo_toml.txt index 6b54d3b4..bff958c1 100644 --- a/crates/cargo-pvm-contract/templates/scaffold/cargo_toml.txt +++ b/crates/cargo-pvm-contract/templates/scaffold/cargo_toml.txt @@ -1,12 +1,13 @@ [package] name = "{{ contract_name }}" version = "0.1.0" -edition = "2024" +edition = "2021" +rust-version = "1.92" build = "build.rs" [[bin]] name = "{{ contract_name }}" -path = "src/lib.rs" +path = "src/{{ bin_source }}.rs" [dependencies] {% if use_alloc -%} diff --git a/crates/cargo-pvm-contract/templates/scaffold/contract_no_alloc.rs.txt b/crates/cargo-pvm-contract/templates/scaffold/contract_no_alloc.rs.txt index 50f8c166..aafcb561 100644 --- a/crates/cargo-pvm-contract/templates/scaffold/contract_no_alloc.rs.txt +++ b/crates/cargo-pvm-contract/templates/scaffold/contract_no_alloc.rs.txt @@ -33,47 +33,7 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { } } -// ============================================================================ -// ABI Decoding Helpers -// ============================================================================ - -/// Decode address from ABI-encoded data (32 bytes, address is in the last 20 bytes) -#[inline] -fn decode_address(data: &[u8]) -> [u8; 20] { - let mut addr = [0u8; 20]; - addr.copy_from_slice(&data[12..32]); - addr -} - -/// Decode u128 from ABI-encoded data (32 bytes, takes lower 16 bytes) -#[inline] -fn decode_u128(data: &[u8]) -> u128 { - u128::from_be_bytes(data[16..32].try_into().unwrap()) -} - -/// Convert u128 to 32-byte word (big-endian, left-padded) -#[inline] -fn to_word(v: u128) -> [u8; 32] { - let mut out = [0u8; 32]; - out[16..].copy_from_slice(&v.to_be_bytes()); - out -} - -// ============================================================================ -// Storage Helpers -// ============================================================================ - -/// Get the caller's address -#[inline] -fn get_caller() -> [u8; 20] { - let mut caller = [0u8; 20]; - api::caller(&mut caller); - caller -} - -// ============================================================================ -// Contract Entry Points -// ============================================================================ +/// Contract entry points. /// This is the constructor which is called once per contract. #[polkavm_derive::polkavm_export] diff --git a/crates/cargo-pvm-contract/tests/test_cmd.rs b/crates/cargo-pvm-contract/tests/test_cmd.rs index 585fa0a3..f320ab24 100644 --- a/crates/cargo-pvm-contract/tests/test_cmd.rs +++ b/crates/cargo-pvm-contract/tests/test_cmd.rs @@ -2,12 +2,6 @@ use assert_cmd::Command; use std::path::{Path, PathBuf}; use tempfile::TempDir; -fn cargo_path() -> PathBuf { - std::env::var("CARGO") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("cargo")) -} - fn scaffold_example(temp_dir: &TempDir, name: &str, memory_model: &str) -> PathBuf { let builder_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../cargo-pvm-contract-builder"); let project_dir = temp_dir.path().join(name); @@ -30,8 +24,11 @@ fn scaffold_example(temp_dir: &TempDir, name: &str, memory_model: &str) -> PathB } fn build_scaffolded_project(project_dir: &Path) { - let status = std::process::Command::new(cargo_path()) + let status = std::process::Command::new("cargo") .current_dir(project_dir) + // Remove env vars that override rust-toolchain.toml + .env_remove("CARGO") + .env_remove("RUSTUP_TOOLCHAIN") .arg("build") .status() .expect("run cargo build");