zint/
contract.rs

1use crate::utils::{find_up, FoundryConfig};
2use crate::{lookup, Bytes32, Info, EVM};
3use anyhow::{anyhow, Context, Result};
4use serde::Deserialize;
5use std::fs;
6use std::path::PathBuf;
7use zinkc::{Artifact, Compiler, Config, Constructor, InitStorage};
8
9/// Represents the bytecode object in Foundry output
10#[derive(Deserialize)]
11struct BytecodeObject {
12    object: String,
13}
14
15/// Represents a Foundry output JSON file (e.g., out/Storage.sol/Storage.json)
16#[derive(Deserialize)]
17pub struct FoundryOutput {
18    bytecode: BytecodeObject,
19}
20
21impl FoundryOutput {
22    /// Get the bytecode as a string
23    pub fn bytecode(&self) -> &str {
24        &self.bytecode.object
25    }
26}
27
28/// Contract instance for testing.
29#[derive(Default)]
30pub struct Contract {
31    /// If enable dispatcher.
32    pub dispatcher: bool,
33    /// The artifact of the contract.
34    pub artifact: Artifact,
35    /// The source WASM of the contract.
36    pub wasm: Vec<u8>,
37    /// Bytecode constructor
38    pub constructor: Constructor,
39    /// Address in evm
40    pub address: [u8; 20],
41}
42
43impl<T> From<T> for Contract
44where
45    T: AsRef<[u8]>,
46{
47    fn from(wasm: T) -> Self {
48        crate::setup_logger();
49
50        Self {
51            wasm: wasm.as_ref().into(),
52            dispatcher: true,
53            ..Default::default()
54        }
55    }
56}
57
58impl Contract {
59    /// Locate Foundry outputs and return a list of (contract_name, abi_path, bytecode)
60    pub fn find_foundry_outputs() -> Result<Vec<(String, PathBuf, Vec<u8>)>> {
61        // Find foundry.toml
62        let foundry_toml_path = find_up("foundry.toml")?;
63        let foundry_toml_content =
64            fs::read_to_string(&foundry_toml_path).context("Failed to read foundry.toml")?;
65        let foundry_config: FoundryConfig =
66            toml::from_str(&foundry_toml_content).context("Failed to parse foundry.toml")?;
67
68        // Determine the output directory
69        let out_dir = foundry_config
70            .profile
71            .default
72            .out
73            .unwrap_or_else(|| "out".to_string());
74        let out_path = foundry_toml_path.parent().unwrap().join(&out_dir);
75
76        let mut outputs = Vec::new();
77        for entry in fs::read_dir(&out_path).context("Failed to read Foundry out directory")? {
78            let entry = entry?;
79            let path = entry.path();
80            if path.is_dir() {
81                // Skip the build-info directory
82                if path.file_name().and_then(|s| s.to_str()) == Some("build-info") {
83                    continue;
84                }
85                // Look for .json files in subdirectories (e.g., out/Storage.sol/)
86                for sub_entry in fs::read_dir(&path)? {
87                    let sub_entry = sub_entry.context("Failed to read directory entry")?;
88                    let sub_path = sub_entry.path();
89                    if sub_path.extension().and_then(|s| s.to_str()) == Some("json") {
90                        let file_name = sub_path
91                            .file_name()
92                            .context("Failed to get file name")?
93                            .to_str()
94                            .context("File name is not valid UTF-8")?;
95                        let Some(contract_name) = file_name.strip_suffix(".json") else {
96                            continue;
97                        };
98                        // Only process files where the contract name (before .json) looks like a contract name
99                        if contract_name
100                            .chars()
101                            .all(|c| c.is_alphanumeric() || c == '_')
102                        {
103                            let content = fs::read_to_string(&sub_path)?;
104                            let output: FoundryOutput = serde_json::from_str(&content)
105                                .context(format!("Failed to parse JSON for {file_name}"))?;
106                            let bytecode = hex::decode(output.bytecode().trim_start_matches("0x"))
107                                .context("Failed to decode bytecode")?;
108                            outputs.push((contract_name.to_string(), sub_path, bytecode));
109                        }
110                    }
111                }
112            }
113        }
114
115        Ok(outputs)
116    }
117
118    /// Get the bytecode of the contract.
119    pub fn bytecode(&self) -> Result<Vec<u8>> {
120        let bytecode = self
121            .constructor
122            .finish(self.artifact.runtime_bytecode.clone().into())
123            .map(|v| v.to_vec())?;
124
125        tracing::debug!("runtime bytecode: {}", hex::encode(&bytecode));
126        Ok(bytecode)
127    }
128
129    /// Preset the storage of the contract, similar with the concept `constructor`
130    /// in solidity, but just in time.
131    pub fn construct(&mut self, storage: InitStorage) -> Result<&mut Self> {
132        self.constructor.storage(storage)?;
133        Ok(self)
134    }
135
136    /// Compile WASM to EVM bytecode.
137    pub fn compile(mut self) -> Result<Self> {
138        let config = Config::default().dispatcher(self.dispatcher);
139        let compiler = Compiler::new(config);
140        self.artifact = compiler.compile(&self.wasm)?;
141
142        // tracing::debug!("abi: {:#}", self.json_abi()?);
143        tracing::debug!("bytecode: {}", hex::encode(&self.artifact.runtime_bytecode));
144        Ok(self)
145    }
146
147    /// Deploy self to evm
148    pub fn deploy<'e>(&mut self) -> Result<EVM<'e>> {
149        let mut evm = EVM::default();
150        let info = evm.deploy(&self.bytecode()?)?;
151
152        self.address.copy_from_slice(&info.address);
153        Ok(evm)
154    }
155
156    /// Load zink contract defined in the current
157    /// package.
158    ///
159    /// NOTE: This only works if the current contract
160    /// is not an example.
161    pub fn current() -> Result<Self> {
162        Self::search(&lookup::pkg_name()?)
163    }
164
165    /// Encode call data
166    pub fn encode<Param>(&self, inputs: impl AsRef<[Param]>) -> Result<Vec<u8>>
167    where
168        Param: Bytes32,
169    {
170        let mut calldata = Vec::new();
171        let mut inputs = inputs.as_ref();
172        if self.dispatcher {
173            if inputs.is_empty() {
174                return Err(anyhow!("no selector provided"));
175            }
176
177            calldata.extend_from_slice(&zabi::selector::parse(&inputs[0].to_vec()));
178            inputs = &inputs[1..];
179        }
180
181        for input in inputs {
182            calldata.extend_from_slice(&input.to_bytes32());
183        }
184
185        tracing::debug!("calldata: {}", hex::encode(&calldata));
186        Ok(calldata)
187    }
188
189    /// Execute the contract.
190    pub fn execute<Param>(&mut self, inputs: impl AsRef<[Param]>) -> Result<Info>
191    where
192        Param: Bytes32,
193    {
194        EVM::interp(&self.artifact.runtime_bytecode, &self.encode(inputs)?)
195    }
196
197    /// Get the JSON ABI of the contract.
198    pub fn json_abi(&self) -> Result<String> {
199        serde_json::to_string_pretty(&self.artifact.abi).map_err(Into::into)
200    }
201
202    /// Disable dispatcher.
203    pub fn pure(mut self) -> Self {
204        self.dispatcher = false;
205        self
206    }
207
208    /// Search for zink contract in the target directory.
209    pub fn search(name: &str) -> Result<Self> {
210        // TODO(g4tianx): `Contract::search` to fail properly
211        // when the contract file isn’t found
212        crate::setup_logger();
213        let wasm = lookup::wasm(name)?;
214        zinkc::utils::wasm_opt(&wasm, &wasm)?;
215
216        tracing::debug!("loading contract from {}", wasm.display());
217        Ok(Self::from(fs::read(wasm)?))
218    }
219}