Skip to main content

fhevm_forge/commands/
deploy.rs

1use anyhow::{bail, Result};
2use colored::Colorize;
3use serde_json::{json, Value};
4use std::{collections::HashMap, env, fs};
5use crate::deployer::{chains::supported_chains, runner::ForgeRunner};
6use crate::config::FhevmForgeConfig;
7
8#[derive(Debug)]
9pub struct DeployResult {
10    pub chain:            String,
11    pub contract_address: String,
12    pub tx_hash:          String,
13}
14
15pub async fn run(chains: &[&str], contract: &str, dry_run: bool) -> Result<()> {
16    let cfg = FhevmForgeConfig::load()?;
17
18    // Resolve chains: CLI arg takes precedence; fall back to config defaults.
19    let default_chains: Vec<String> = if chains == ["sepolia"] && !cfg.deploy.chains.is_empty() {
20        cfg.deploy.chains.clone()
21    } else {
22        chains.iter().map(|s| s.to_string()).collect()
23    };
24    let chains_to_deploy: Vec<&str> = default_chains.iter().map(String::as_str).collect();
25
26    // Resolve contract: CLI arg takes precedence; fall back to config default.
27    let resolved_contract = if contract.is_empty() {
28        cfg.deploy.default_contract.as_deref().unwrap_or(contract)
29    } else {
30        contract
31    };
32
33    if resolved_contract.is_empty() {
34        bail!("No contract specified. Pass --contract <Name> or set [deploy] default_contract in fhevm-forge.toml");
35    }
36
37    let all_chains = supported_chains();
38    let (chains, contract) = (chains_to_deploy.as_slice(), resolved_contract);
39    let mut results: Vec<DeployResult> = Vec::new();
40    let mut manifest: HashMap<String, Value> = HashMap::new();
41
42    for &chain_key in chains {
43        let chain = all_chains.get(chain_key)
44            .ok_or_else(|| anyhow::anyhow!(
45                "Unknown chain '{}'. Supported: {}",
46                chain_key,
47                all_chains.keys().cloned().collect::<Vec<_>>().join(", ")
48            ))?;
49
50        if !chain.is_fully_configured() {
51            eprintln!(
52                "{} Chain '{}' does not have FHEVM contract addresses configured yet. \
53                 Check back when Zama deploys to this network.",
54                "⚠️ ".yellow(),
55                chain_key
56            );
57            continue;
58        }
59
60        let rpc_url = env::var(&chain.rpc_env_var).unwrap_or_default();
61        if rpc_url.is_empty() {
62            bail!(
63                "Environment variable '{}' is not set. \
64                 Add it to your .env file before deploying to {}.",
65                chain.rpc_env_var,
66                chain.name
67            );
68        }
69
70        println!(
71            "\n{} {} to {}...",
72            "Deploying".cyan().bold(),
73            contract.yellow(),
74            chain.name.cyan()
75        );
76
77        let result = deploy_to_chain(chain_key, chain, contract, &rpc_url, dry_run).await;
78
79        match result {
80            Ok(r) => {
81                println!(
82                    "  {} {}  {}",
83                    "✅".green(),
84                    chain.name.green(),
85                    r.contract_address.cyan()
86                );
87                println!(
88                    "     {} {}/address/{}",
89                    "Explorer:".dimmed(),
90                    chain.explorer_url,
91                    r.contract_address
92                );
93
94                manifest.insert(chain_key.to_string(), json!({
95                    "address": r.contract_address,
96                    "tx": r.tx_hash,
97                    "chain_id": chain.chain_id,
98                    "explorer": format!("{}/address/{}", chain.explorer_url, r.contract_address),
99                }));
100
101                results.push(r);
102            }
103            Err(e) => {
104                eprintln!("  {} {} failed: {}", "❌".red(), chain.name, e);
105            }
106        }
107    }
108
109    if !results.is_empty() {
110        fs::create_dir_all("deployments")?;
111        let manifest_path = format!("deployments/{}.json", contract);
112        let manifest_json = serde_json::to_string_pretty(&json!({
113            "contract": contract,
114            "deployed_at": chrono::Utc::now().to_rfc3339(),
115            "deployments": manifest,
116        }))?;
117        fs::write(&manifest_path, manifest_json)?;
118
119        let deployed_chains: Vec<&str> = results.iter().map(|r| r.chain.as_str()).collect();
120        println!(
121            "\n{} {} deployed to: {}",
122            "📋".dimmed(),
123            contract.yellow(),
124            deployed_chains.join(", ").cyan()
125        );
126        println!("{} {}", "Deployment manifest:".dimmed(), manifest_path.cyan());
127    }
128
129    Ok(())
130}
131
132async fn deploy_to_chain(
133    chain_key: &str,
134    chain: &crate::deployer::chains::FhevmChain,
135    contract: &str,
136    rpc_url: &str,
137    dry_run: bool,
138) -> Result<DeployResult> {
139    let script_path = format!("script/Deploy{}.s.sol:Deploy{}Script", contract, contract);
140
141    let mut runner = ForgeRunner::new(rpc_url).verbose(true);
142
143    if !dry_run {
144        runner = runner.arg("--verify");
145        let api_key = env::var(&chain.explorer_api_key_env).unwrap_or_default();
146        if !api_key.is_empty() {
147            runner = runner.arg("--etherscan-api-key").arg(api_key);
148        }
149    }
150
151    runner = runner
152        .env("FHEVM_ACL_ADDRESS",                    &chain.acl_address)
153        .env("FHEVM_KMS_VERIFIER",                   &chain.kms_verifier)
154        .env("FHEVM_INPUT_VERIFIER",                 &chain.input_verifier)
155        .env("FHEVM_VERIFYING_CONTRACT_DECRYPTION",  &chain.verifying_contract_decryption)
156        .env("FHEVM_VERIFYING_CONTRACT_INPUT_VERIF", &chain.verifying_contract_input_verification)
157        .env("FHEVM_GATEWAY_CHAIN_ID",               chain.gateway_chain_id.to_string())
158        .env("DEPLOYER_PRIVATE_KEY",                 env::var("DEPLOYER_PRIVATE_KEY").unwrap_or_default());
159
160    let stdout = runner.run_script(&script_path, !dry_run).await?;
161
162    let address = parse_forge_address(&stdout)
163        .unwrap_or_else(|| "unknown (check deployments/broadcast/)".to_string());
164    let tx_hash = parse_forge_tx_hash(&stdout)
165        .unwrap_or_else(|| "unknown".to_string());
166
167    Ok(DeployResult {
168        chain: chain_key.to_string(),
169        contract_address: address,
170        tx_hash,
171    })
172}
173
174fn parse_forge_address(output: &str) -> Option<String> {
175    output.lines()
176        .find(|l| l.contains("Contract Address:"))
177        .and_then(|l| l.split_whitespace().last())
178        .map(|s| s.to_string())
179}
180
181fn parse_forge_tx_hash(output: &str) -> Option<String> {
182    output.lines()
183        .find(|l| l.contains("Transaction hash:"))
184        .and_then(|l| l.split_whitespace().last())
185        .map(|s| s.to_string())
186}