use std::{env, fs, path::Path, process::Command};
const CONTRACTS_TO_COPY: [&str; 3] = ["IBoundlessMarket.sol", "IHitPoints.sol", "types"];
const EXCLUDE_CONTRACTS: [&str; 2] = [
"Account.sol",
"IHitPoints.sol",
];
const ARTIFACT_TARGET_CONTRACTS: [&str; 9] = [
"BoundlessMarket",
"HitPoints",
"RiscZeroMockVerifier",
"RiscZeroSetVerifier",
"ERC1967Proxy",
"RiscZeroVerifierRouter",
"RiscZeroGroth16Verifier",
"Blake3Groth16Verifier",
"MockCallback",
];
const BOUNDLESS_MARKET_RS: &str = "boundless_market_generated.rs";
fn find_matching_brace(contents: &str) -> Option<usize> {
let mut stack = Vec::new();
for (i, c) in contents.char_indices() {
match c {
'{' => stack.push(c),
'}' => {
stack.pop();
if stack.is_empty() {
return Some(i);
}
}
_ => {}
}
}
None
}
fn rewrite_solidity_interface_files() {
println!("cargo::rerun-if-env-changed=CARGO_MANIFEST_DIR");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let sol_iface_dir = Path::new(&manifest_dir).join("src").join("contracts").join("artifacts");
println!("cargo::rerun-if-changed={}", sol_iface_dir.to_string_lossy());
println!("cargo::rerun-if-env-changed=CARGO_CFG_TARGET_OS");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let mut combined_sol_contents = String::new();
for entry in fs::read_dir(sol_iface_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("sol") {
if EXCLUDE_CONTRACTS.contains(&path.file_name().unwrap().to_str().unwrap()) {
continue;
}
let mut sol_contents = fs::read_to_string(&path).unwrap();
while let Some(start) = sol_contents.find("library ") {
if let Some(end) = find_matching_brace(&sol_contents[start..]) {
sol_contents.replace_range(start..start + end + 1, "");
} else {
panic!("Unmatched brace in library {entry:?}");
}
}
if target_os != "zkvm" {
if let Some(iface_pos) = sol_contents.find("interface ") {
sol_contents.insert_str(iface_pos, "#[sol(rpc)]\n");
}
}
combined_sol_contents.push_str(&sol_contents);
}
}
let mut alloy_import = "alloy_sol_types";
if target_os != "zkvm" {
alloy_import = "alloy";
}
println!("cargo::rerun-if-env-changed=OUT_DIR");
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join(BOUNDLESS_MARKET_RS);
fs::write(
dest_path,
format!(
"#[allow(missing_docs, clippy::too_many_arguments)]
pub mod boundless_market_contract {{
{alloy_import}::sol! {{
#![sol(all_derives)]
#![sol(extra_derives(serde::Serialize, serde::Deserialize))]
{combined_sol_contents}
}}
}}
"
),
)
.unwrap();
}
fn copy_interfaces_and_types() {
println!("cargo::rerun-if-env-changed=CARGO_CFG_TARGET_OS");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let dest_path = Path::new(&manifest_dir).join("src/contracts/artifacts");
fs::create_dir_all(&dest_path).unwrap();
let src_path =
Path::new(&manifest_dir).parent().unwrap().parent().unwrap().join("contracts").join("src");
if !src_path.is_dir() {
return;
}
let contracts_to_copy: Vec<String> = CONTRACTS_TO_COPY
.iter()
.flat_map(|contract| {
if contract.ends_with(".sol") {
vec![contract.to_string()]
} else {
let dir_path = src_path.join(contract);
fs::read_dir(dir_path)
.unwrap()
.filter_map(|entry| {
let path = entry.unwrap().path();
if path.extension().and_then(|s| s.to_str()) == Some("sol") {
Some(format!(
"{}/{}",
contract,
path.file_name().unwrap().to_str().unwrap()
))
} else {
None
}
})
.collect()
}
})
.collect();
println!("contracts_to_copy: {contracts_to_copy:?}");
for contract in contracts_to_copy {
let source_path = src_path.join(&contract);
println!("cargo:rerun-if-changed={}", source_path.display());
if source_path.exists() {
let dest_file_name = contract.split('/').next_back().unwrap();
let dest_file_path = dest_path.join(dest_file_name);
println!("Copying {source_path:?} to {dest_file_path:?}");
std::fs::copy(&source_path, dest_file_path).unwrap();
}
}
}
fn generate_contracts_rust_file() {
println!("cargo::rerun-if-env-changed=CARGO_CFG_TARGET_OS");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let src_path = Path::new(&manifest_dir).parent().unwrap().parent().unwrap().join("out");
if !src_path.exists() {
println!("cargo:warning=Skipping contract bytecode generation during cargo publish");
return;
}
let mut rust_content = String::from("// Auto-generated file, do not edit manually\n\n");
for contract in ARTIFACT_TARGET_CONTRACTS {
let source_path = src_path.join(format!("{contract}.sol/{contract}.json"));
println!("cargo:rerun-if-changed={}", source_path.display());
if source_path.exists() {
let json_content = fs::read_to_string(&source_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&json_content).unwrap();
let bytecode = json["bytecode"]["object"]
.as_str()
.ok_or(format!(
"failed to extract bytecode from {}",
source_path.as_os_str().to_string_lossy()
))
.unwrap()
.trim_start_matches("0x");
rust_content.push_str(&format!(
r#"alloy::sol! {{
#[sol(rpc, bytecode = "{}")]
contract {} {{
{}
}}
}}"#,
bytecode,
contract,
get_interfaces(contract)
));
if contract != *ARTIFACT_TARGET_CONTRACTS.last().unwrap() {
rust_content.push_str("\n\n");
}
}
}
rust_content.push('\n');
let dest_path = Path::new(&manifest_dir).join("src/contracts/bytecode.rs");
fs::write(dest_path, rust_content).unwrap();
}
fn get_interfaces(contract: &str) -> &str {
match contract {
"RiscZeroMockVerifier" => "constructor(bytes4 selector) {}",
"RiscZeroSetVerifier" => {
"constructor(address verifier, bytes32 imageId, string memory imageUrl) {}"
}
"BoundlessMarket" => {
r#"constructor(address verifier, address applicationVerifier, bytes32 assessorId, bytes32 deprecatedAssessorId, uint32 deprecatedAssessorDuration, address stakeTokenContract) {}
function initialize(address initialOwner, string calldata imageUrl) {}"#
}
"ERC1967Proxy" => "constructor(address implementation, bytes memory data) payable {}",
"HitPoints" => "constructor(address initialOwner) payable {}",
"RiscZeroVerifierRouter" => {
r#"constructor(address owner) {}
function addVerifier(bytes4 selector, address verifier) {}"#
}
"RiscZeroGroth16Verifier" => {
r#"constructor(bytes32 control_root, bytes32 bn254_control_id) {}"#
}
"Blake3Groth16Verifier" => {
r#"constructor(bytes32 control_root, bytes32 bn254_control_id) {}"#
}
"MockCallback" => {
r#"constructor(address verifier, address boundlessMarket, bytes32 imageId, uint256 _targetGas) {}
function getCallCount() external view returns (uint256) {}"#
}
_ => "",
}
}
fn main() {
println!("cargo::rerun-if-changed=build.rs");
copy_interfaces_and_types();
rewrite_solidity_interface_files();
println!("cargo::rerun-if-env-changed=CARGO_CFG_TARGET_OS");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
if target_os != "zkvm" {
generate_contracts_rust_file();
if Path::new("../../.git").exists() {
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=.git/refs/heads");
let output = Command::new("git").args(["rev-parse", "--short", "HEAD"]).output();
match output {
Ok(output) if output.status.success() => {
let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("cargo:rustc-env=BOUNDLESS_GIT_HASH={}", git_hash);
}
_ => {
println!("cargo:warning=Failed to get Git hash; using 'unknown'");
println!("cargo:rustc-env=BOUNDLESS_GIT_HASH=unknown");
}
}
} else {
println!("cargo:warning=No .git dir; setting BOUNDLESS_GIT_HASH=unknown");
println!("cargo:rustc-env=BOUNDLESS_GIT_HASH=unknown");
}
}
}