use crate::utils::{find_up, FoundryConfig};
use crate::{lookup, Bytes32, Info, EVM};
use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use zinkc::{Artifact, Compiler, Config, Constructor, InitStorage};
/// Represents the bytecode object in Foundry output
#[derive(Deserialize)]
struct BytecodeObject {
object: String,
}
/// Represents a Foundry output JSON file (e.g., out/Storage.sol/Storage.json)
#[derive(Deserialize)]
pub struct FoundryOutput {
bytecode: BytecodeObject,
}
impl FoundryOutput {
/// Get the bytecode as a string
pub fn bytecode(&self) -> &str {
&self.bytecode.object
}
}
/// Contract instance for testing.
#[derive(Default)]
pub struct Contract {
/// If enable dispatcher.
pub dispatcher: bool,
/// The artifact of the contract.
pub artifact: Artifact,
/// The source WASM of the contract.
pub wasm: Vec<u8>,
/// Bytecode constructor
pub constructor: Constructor,
/// Address in evm
pub address: [u8; 20],
}
impl<T> From<T> for Contract
where
T: AsRef<[u8]>,
{
fn from(wasm: T) -> Self {
crate::setup_logger();
Self {
wasm: wasm.as_ref().into(),
dispatcher: true,
..Default::default()
}
}
}
impl Contract {
/// Locate Foundry outputs and return a list of (contract_name, abi_path, bytecode)
pub fn find_foundry_outputs() -> Result<Vec<(String, PathBuf, Vec<u8>)>> {
// Find foundry.toml
let foundry_toml_path = find_up("foundry.toml")?;
let foundry_toml_content =
fs::read_to_string(&foundry_toml_path).context("Failed to read foundry.toml")?;
let foundry_config: FoundryConfig =
toml::from_str(&foundry_toml_content).context("Failed to parse foundry.toml")?;
// Determine the output directory
let out_dir = foundry_config
.profile
.default
.out
.unwrap_or_else(|| "out".to_string());
let out_path = foundry_toml_path.parent().unwrap().join(&out_dir);
let mut outputs = Vec::new();
for entry in fs::read_dir(&out_path).context("Failed to read Foundry out directory")? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
// Skip the build-info directory
if path.file_name().and_then(|s| s.to_str()) == Some("build-info") {
continue;
}
// Look for .json files in subdirectories (e.g., out/Storage.sol/)
for sub_entry in fs::read_dir(&path)? {
let sub_entry = sub_entry.context("Failed to read directory entry")?;
let sub_path = sub_entry.path();
if sub_path.extension().and_then(|s| s.to_str()) == Some("json") {
let file_name = sub_path
.file_name()
.context("Failed to get file name")?
.to_str()
.context("File name is not valid UTF-8")?;
let Some(contract_name) = file_name.strip_suffix(".json") else {
continue;
};
// Only process files where the contract name (before .json) looks like a contract name
if contract_name
.chars()
.all(|c| c.is_alphanumeric() || c == '_')
{
let content = fs::read_to_string(&sub_path)?;
let output: FoundryOutput = serde_json::from_str(&content)
.context(format!("Failed to parse JSON for {file_name}"))?;
let bytecode = hex::decode(output.bytecode().trim_start_matches("0x"))
.context("Failed to decode bytecode")?;
outputs.push((contract_name.to_string(), sub_path, bytecode));
}
}
}
}
}
Ok(outputs)
}
/// Get the bytecode of the contract.
pub fn bytecode(&self) -> Result<Vec<u8>> {
let bytecode = self
.constructor
.finish(self.artifact.runtime_bytecode.clone().into())
.map(|v| v.to_vec())?;
tracing::debug!("runtime bytecode: {}", hex::encode(&bytecode));
Ok(bytecode)
}
/// Preset the storage of the contract, similar with the concept `constructor`
/// in solidity, but just in time.
pub fn construct(&mut self, storage: InitStorage) -> Result<&mut Self> {
self.constructor.storage(storage)?;
Ok(self)
}
/// Compile WASM to EVM bytecode.
pub fn compile(mut self) -> Result<Self> {
let config = Config::default().dispatcher(self.dispatcher);
let compiler = Compiler::new(config);
self.artifact = compiler.compile(&self.wasm)?;
// tracing::debug!("abi: {:#}", self.json_abi()?);
tracing::debug!("bytecode: {}", hex::encode(&self.artifact.runtime_bytecode));
Ok(self)
}
/// Deploy self to evm
pub fn deploy<'e>(&mut self) -> Result<EVM<'e>> {
let mut evm = EVM::default();
let info = evm.deploy(&self.bytecode()?)?;
self.address.copy_from_slice(&info.address);
Ok(evm)
}
/// Load zink contract defined in the current
/// package.
///
/// NOTE: This only works if the current contract
/// is not an example.
pub fn current() -> Result<Self> {
Self::search(&lookup::pkg_name()?)
}
/// Encode call data
pub fn encode<Param>(&self, inputs: impl AsRef<[Param]>) -> Result<Vec<u8>>
where
Param: Bytes32,
{
let mut calldata = Vec::new();
let mut inputs = inputs.as_ref();
if self.dispatcher {
if inputs.is_empty() {
return Err(anyhow!("no selector provided"));
}
calldata.extend_from_slice(&zabi::selector::parse(&inputs[0].to_vec()));
inputs = &inputs[1..];
}
for input in inputs {
calldata.extend_from_slice(&input.to_bytes32());
}
tracing::debug!("calldata: {}", hex::encode(&calldata));
Ok(calldata)
}
/// Execute the contract.
pub fn execute<Param>(&mut self, inputs: impl AsRef<[Param]>) -> Result<Info>
where
Param: Bytes32,
{
EVM::interp(&self.artifact.runtime_bytecode, &self.encode(inputs)?)
}
/// Get the JSON ABI of the contract.
pub fn json_abi(&self) -> Result<String> {
serde_json::to_string_pretty(&self.artifact.abi).map_err(Into::into)
}
/// Disable dispatcher.
pub fn pure(mut self) -> Self {
self.dispatcher = false;
self
}
/// Search for zink contract in the target directory.
pub fn search(name: &str) -> Result<Self> {
// TODO(g4tianx): `Contract::search` to fail properly
// when the contract file isn’t found
crate::setup_logger();
let wasm = lookup::wasm(name)?;
zinkc::utils::wasm_opt(&wasm, &wasm)?;
tracing::debug!("loading contract from {}", wasm.display());
Ok(Self::from(fs::read(wasm)?))
}
}