fhevm_forge/commands/
deploy.rs1use 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 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 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}