1use crate::command::keys::{SupportedKey, generate_key, import_key};
2use crate::utils::{print_info, print_section_header, print_success};
3use alloy_primitives::Address;
4use blueprint_chain_setup::anvil::start_default_anvil_testnet;
5use blueprint_core::debug;
6use blueprint_crypto::k256::K256Ecdsa;
7use blueprint_keystore::backends::Backend;
8use blueprint_keystore::{Keystore, KeystoreConfig};
9use blueprint_runner::config::{Protocol, SupportedChains};
10use blueprint_std::fs;
11use blueprint_std::path::Path;
12use blueprint_std::process::Command;
13use blueprint_std::str::FromStr;
14use color_eyre::Result;
15use color_eyre::owo_colors::OwoColorize;
16use dialoguer::console::style;
17use dialoguer::{Confirm, Input, Select};
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::collections::HashMap;
21use tokio::signal;
22use url::Url;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct EigenlayerDeployOpts {
26 pub(crate) rpc_url: Url,
28 pub(crate) contracts_path: String,
30 pub(crate) constructor_args: Option<HashMap<String, Vec<String>>>,
32 pub(crate) ordered_deployment: bool,
34 pub(crate) chain: SupportedChains,
36 pub(crate) keystore_path: String,
38}
39
40impl EigenlayerDeployOpts {
41 #[must_use]
46 pub fn new<T: TryInto<Url>>(
47 rpc_url: T,
48 contracts_path: Option<String>,
49 ordered_deployment: bool,
50 chain: SupportedChains,
51 keystore_path: Option<impl AsRef<Path>>,
52 ) -> Self
53 where
54 <T as TryInto<Url>>::Error: std::fmt::Debug,
55 {
56 let rpc_url = rpc_url.try_into().unwrap();
57
58 let keystore_path = if keystore_path.is_none()
59 && chain == SupportedChains::LocalTestnet
60 && (rpc_url.as_str().contains("127.0.0.1") || rpc_url.as_str().contains("localhost"))
61 {
62 let temp_dir = tempfile::tempdir()
64 .expect("Failed to create temporary directory")
65 .keep();
66 temp_dir.to_string_lossy().to_string()
67 } else {
68 keystore_path.map_or_else(
69 || "./keystore".to_string(),
70 |p| p.as_ref().to_string_lossy().to_string(),
71 )
72 };
73
74 Self {
75 rpc_url,
76 contracts_path: contracts_path.unwrap_or_else(|| "./contracts".to_string()),
77 constructor_args: None,
78 ordered_deployment,
79 chain,
80 keystore_path,
81 }
82 }
83
84 fn get_private_key(&self) -> Result<String> {
85 let mut config = KeystoreConfig::new();
86 if !Path::new(&self.keystore_path).exists() {
88 std::fs::create_dir_all(&self.keystore_path)?;
89 }
90 config = config.fs_root(&self.keystore_path);
91 let keystore = Keystore::new(config)?;
92
93 if (self.rpc_url.as_str().contains("127.0.0.1")
94 || self.rpc_url.as_str().contains("localhost"))
95 && self.chain == SupportedChains::LocalTestnet
96 {
97 Ok("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string())
98 } else {
99 let keys = keystore.list_local::<K256Ecdsa>()?;
101 if keys.is_empty() {
102 println!(
103 "No ECDSA key found at {}. Let's set one up.",
104 self.keystore_path
105 );
106 let keys = crate::command::keys::prompt_for_keys(vec![SupportedKey::Ecdsa])?;
107 let (key_type, secret) = keys
108 .first()
109 .ok_or(color_eyre::eyre::eyre!("No ECDSA key found in keystore."))?;
110 let private_key = secret.clone();
111 let _public = crate::command::keys::import_key(
112 Protocol::Eigenlayer,
113 *key_type,
114 secret,
115 Path::new(&self.keystore_path),
116 )?;
117 return Ok(private_key);
118 }
119
120 Err(color_eyre::eyre::eyre!(
121 "No ECDSA key found in keystore. Please add one using 'cargo tangle key import' or set EIGENLAYER_PRIVATE_KEY environment variable"
122 ))
123 }
124 }
125}
126
127pub fn initialize_test_keystore() -> Result<()> {
136 let keystore_path = Path::new("./test-keystore");
138 let mut config = KeystoreConfig::new();
139 if !keystore_path.exists() {
140 fs::create_dir_all(keystore_path)?;
141 }
142 config = config.fs_root(keystore_path);
143 let _keystore = Keystore::new(config)?;
144 import_key(
146 Protocol::Eigenlayer,
147 SupportedKey::Ecdsa,
148 "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
149 keystore_path,
150 )?;
151 generate_key(SupportedKey::Bn254, Some(&keystore_path), None, false)?;
152 Ok(())
153}
154
155fn parse_contract_path(contract_path: &str) -> Result<String> {
156 let path = Path::new(contract_path);
157
158 if path.extension().and_then(|ext| ext.to_str()) != Some("sol") {
160 return Err(color_eyre::eyre::eyre!(
161 "Contract file must have a .sol extension"
162 ));
163 }
164
165 let file_name = path
166 .file_name()
167 .and_then(|name| name.to_str())
168 .ok_or_else(|| color_eyre::eyre::eyre!("Invalid contract file name"))?;
169
170 let contract_name = file_name.trim_end_matches(".sol");
171
172 let mut new_path = path.to_path_buf();
174 new_path.set_file_name(file_name); let formatted_path = new_path
177 .to_str()
178 .ok_or_else(|| color_eyre::eyre::eyre!("Failed to convert path to string"))?;
179
180 Ok(format!("{}:{}", formatted_path, contract_name))
181}
182
183fn find_contract_files(contracts_path: &str) -> Result<Vec<String>> {
184 let path = Path::new(contracts_path);
185 if !path.exists() {
186 return Err(color_eyre::eyre::eyre!(
187 "Contracts path does not exist: {}",
188 contracts_path
189 ));
190 }
191
192 let mut contract_files = Vec::new();
193 let src_path = path.join("src");
194
195 if src_path.exists() {
196 for entry in fs::read_dir(src_path)? {
197 let entry = entry?;
198 let path = entry.path();
199 if path.is_file() && path.extension().is_some_and(|ext| ext == "sol") {
200 let content = fs::read_to_string(&path)?;
202 if content.contains("interface I") {
203 debug!("Skipping interface file: {}", path.display());
204 continue;
205 }
206
207 if let Some(path_str) = path.to_str() {
208 contract_files.push(path_str.to_string());
209 }
210 }
211 }
212 }
213
214 if contract_files.is_empty() {
215 return Err(color_eyre::eyre::eyre!(
216 "No deployable Solidity contract files found in {}/src",
217 contracts_path
218 ));
219 }
220
221 Ok(contract_files)
222}
223
224fn select_next_contract(available_contracts: &[String]) -> Result<String> {
225 if available_contracts.is_empty() {
226 return Err(color_eyre::eyre::eyre!("No contracts available to deploy"));
227 }
228
229 if available_contracts.len() == 1 {
230 println!(
231 "\n{}",
232 style(format!(
233 "Only one contract available to deploy: {}",
234 style(&available_contracts[0]).yellow()
235 ))
236 .cyan()
237 );
238 return Ok(available_contracts[0].clone());
239 }
240
241 print_section_header("Contract Selection");
242 println!("{}", style("Available contracts to deploy:").cyan());
243 let selection = Select::new()
244 .with_prompt(
245 style("Select the contract to deploy (use arrow keys ↑↓)")
246 .dim()
247 .to_string(),
248 )
249 .items(available_contracts)
250 .default(0)
251 .interact()?;
252
253 println!(
254 "\n{}",
255 style(format!(
256 "Now deploying contract: {}",
257 style(&available_contracts[selection]).yellow()
258 ))
259 .cyan()
260 );
261
262 Ok(available_contracts[selection].clone())
263}
264
265fn get_constructor_args(
266 contract_json: &Value,
267 contract_name: &str,
268 provided_args: Option<&HashMap<String, Vec<String>>>,
269) -> Option<Vec<String>> {
270 let abi = contract_json.get("abi")?.as_array()?;
272 let constructor = abi
273 .iter()
274 .find(|item| item.get("type").and_then(|t| t.as_str()) == Some("constructor"))?;
275
276 let inputs = constructor.get("inputs")?.as_array()?;
278 if inputs.is_empty() {
279 return None;
280 }
281
282 let contract_map_name = contract_name.rsplit(':').next().unwrap_or_default();
283
284 if let Some(args_map) = provided_args {
286 if let Some(args) = args_map.get(contract_map_name) {
287 if args.len() == inputs.len() {
288 return Some(args.clone());
289 }
290 }
291 }
292
293 print_section_header(&format!(
294 "Constructor Arguments for {}",
295 style(contract_map_name).yellow()
296 ));
297
298 let mut args = Vec::new();
300 for input in inputs {
301 let name = input.get("name")?.as_str()?;
302 let type_str = input.get("type")?.as_str()?;
303
304 let value: String = Input::new()
305 .with_prompt(format!(
306 "{} ({})",
307 style(name).yellow(),
308 style(type_str).cyan()
309 ))
310 .interact()
311 .ok()?;
312
313 args.push(value);
314 }
315
316 Some(args)
317}
318
319fn get_function_args_from_abi(
320 contract_json: &Value,
321 function_name: &str,
322) -> Option<Vec<(String, String)>> {
323 contract_json
324 .get("abi")
325 .and_then(|abi| abi.as_array())
326 .and_then(|abi_array| {
327 abi_array.iter().find(|func| {
328 func.get("type").and_then(|t| t.as_str()) == Some("function")
329 && func.get("name").and_then(|n| n.as_str()) == Some(function_name)
330 })
331 })
332 .and_then(|function| {
333 function.get("inputs").and_then(|inputs| {
334 inputs.as_array().map(|input_array| {
335 input_array
336 .iter()
337 .filter_map(|input| {
338 let name = input.get("name").and_then(|n| n.as_str())?;
339 let type_str = input.get("type").and_then(|t| t.as_str())?;
340 Some((name.to_string(), type_str.to_string()))
341 })
342 .collect()
343 })
344 })
345 })
346}
347
348fn build_function_signature(function_name: &str, args: &[(String, String)]) -> String {
349 let args_str = args
350 .iter()
351 .map(|(_, type_str)| type_str.as_str())
352 .collect::<Vec<_>>()
353 .join(",");
354 format!("{}({})", function_name, args_str)
355}
356
357fn initialize_contract_if_needed(
358 opts: &EigenlayerDeployOpts,
359 contract_json: &Value,
360 contract_name: &str,
361 contract_address: Address,
362) -> Result<()> {
363 if let Some(init_args) = get_function_args_from_abi(contract_json, "initialize") {
365 print_section_header(&format!("Initialize {}", style(contract_name).yellow()));
366
367 let should_initialize = Confirm::new()
368 .with_prompt(format!(
369 "Do you want to initialize {}?",
370 style(contract_name).yellow()
371 ))
372 .default(false)
373 .interact()?;
374
375 if should_initialize {
376 println!(
377 "\n{}",
378 style("Collecting initialization arguments...").cyan()
379 );
380 let mut init_values = Vec::new();
381
382 for (arg_name, arg_type) in &init_args {
383 let value: String = Input::new()
384 .with_prompt(format!(
385 "{} ({})",
386 style(arg_name).yellow(),
387 style(arg_type).cyan()
388 ))
389 .interact()?;
390
391 let formatted_value = if arg_type == "string" || arg_type.contains("bytes") {
393 format!("\"{}\"", value)
394 } else {
395 value
396 };
397
398 init_values.push(formatted_value);
399 }
400
401 let function_sig = build_function_signature("initialize", &init_args);
402
403 print_info("Generating initialization calldata...");
404
405 let calldata_cmd = format!(
407 "cast calldata \"{}\" {}",
408 function_sig,
409 init_values.join(" ")
410 );
411
412 debug!("Calldata command: {}", calldata_cmd);
413
414 let calldata_output = Command::new("sh").arg("-c").arg(&calldata_cmd).output()?;
415
416 if !calldata_output.status.success() {
417 return Err(color_eyre::eyre::eyre!(
418 "Failed to generate calldata: {}",
419 String::from_utf8_lossy(&calldata_output.stderr)
420 ));
421 }
422
423 let calldata = String::from_utf8_lossy(&calldata_output.stdout)
424 .trim()
425 .to_string();
426 debug!("Generated calldata: {}", calldata);
427
428 let from_cmd = format!(
430 "cast wallet address --private-key {}",
431 opts.get_private_key()?
432 );
433
434 let from_output = Command::new("sh").arg("-c").arg(&from_cmd).output()?;
435
436 if !from_output.status.success() {
437 return Err(color_eyre::eyre::eyre!(
438 "Failed to get from address: {}",
439 String::from_utf8_lossy(&from_output.stderr)
440 ));
441 }
442
443 let from_address = String::from_utf8_lossy(&from_output.stdout)
444 .trim()
445 .to_string();
446
447 let tx_params = format!(
449 "{{\"from\":\"{}\",\"to\":\"{}\",\"data\":\"{}\"}}",
450 from_address, contract_address, calldata
451 );
452
453 print_info("Sending initialization transaction...");
454
455 let command_str = format!(
457 "cast rpc --rpc-url {} eth_sendTransaction '{}'",
458 opts.rpc_url, tx_params
459 );
460
461 debug!("Running command: {}", command_str);
462
463 let mut cmd = Command::new("sh");
464 cmd.arg("-c").arg(&command_str);
465
466 let output = cmd.output()?;
467 if !output.status.success() {
468 return Err(color_eyre::eyre::eyre!(
469 "Failed to initialize contract: {}",
470 String::from_utf8_lossy(&output.stderr)
471 ));
472 }
473
474 print_success(&format!("Initialized {}", contract_name), None);
475 } else {
476 print_info(&format!("Skipping initialization of {}", contract_name));
477 }
478 }
479
480 Ok(())
481}
482
483fn deploy_single_contract(
484 opts: &EigenlayerDeployOpts,
485 contract_path: &str,
486) -> Result<(String, Address)> {
487 let contract_name = parse_contract_path(contract_path)?;
488 let contract_output = contract_name.rsplit('/').next().ok_or_else(|| {
489 color_eyre::eyre::eyre!("Failed to get contract output from path: {}", contract_name)
490 })?;
491 let contract_output = contract_output.replace(':', "/");
492
493 let out_dir = Path::new(&opts.contracts_path).join("out");
495 let json_path = out_dir.join(format!("{}.json", contract_output));
496
497 let json_content = fs::read_to_string(&json_path)?;
499 let contract_json: Value = serde_json::from_str(&json_content)?;
500
501 let mut cmd_str = format!(
503 "forge create {} --rpc-url {} --private-key {} --broadcast --evm-version shanghai --out {}",
504 contract_name,
505 opts.rpc_url,
506 opts.get_private_key()?,
507 Path::new(&opts.contracts_path).join("out").display()
508 );
509
510 if let Some(args) = get_constructor_args(
511 &contract_json,
512 &contract_name,
513 opts.constructor_args.as_ref(),
514 ) {
515 if !args.is_empty() {
516 cmd_str.push_str(" --constructor-args");
517 for value in args {
518 let formatted_value = if value.starts_with('"') {
520 value
521 } else {
522 format!("\"{}\"", value)
523 };
524 cmd_str = format!("{cmd_str} {}", formatted_value.replace("0x", ""));
525 }
526 }
527 }
528
529 let mut cmd = Command::new("sh");
531 cmd.arg("-c").arg(&cmd_str);
532
533 let output = cmd.output()?;
534
535 if !output.status.success() {
536 return Err(color_eyre::eyre::eyre!(
537 "Failed to deploy contract: {}",
538 String::from_utf8_lossy(&output.stderr)
539 ));
540 }
541
542 let address = extract_address_from_output(&output.stdout)
544 .or_else(|_| extract_address_from_output(&output.stderr))
545 .map_err(|_| {
546 color_eyre::eyre::eyre!("Failed to find contract address in deployment output")
547 })?;
548
549 println!(
551 "\n{}",
552 style("Contract Deployed Successfully").green().bold()
553 );
554 println!("{}", style("━".repeat(50)).purple());
555 println!(
556 "{}: {}",
557 style(&contract_name).yellow().bold(),
558 style(format!("0x{:x}", address)).cyan().bold()
559 );
560 println!("{}", style("━".repeat(50)).purple());
561
562 initialize_contract_if_needed(opts, &contract_json, &contract_name, address)?;
564
565 Ok((contract_name, address))
566}
567
568pub fn deploy_avs_contracts(opts: &EigenlayerDeployOpts) -> Result<HashMap<String, Address>> {
577 let mut deployed_addresses = HashMap::new();
578 let contract_files = find_contract_files(&opts.contracts_path)?;
579
580 if opts.ordered_deployment {
581 print_section_header("Ordered Contract Deployment");
582 println!("Contract files: {:?}", contract_files);
583 let mut remaining_contracts = contract_files.clone();
584 while !remaining_contracts.is_empty() {
585 let selected_contract = select_next_contract(&remaining_contracts)?;
586 let (contract_name, address) = deploy_single_contract(opts, &selected_contract)?;
587 deployed_addresses.insert(contract_name, address);
588
589 remaining_contracts.retain(|c| c != &selected_contract);
591 }
592 } else {
593 print_section_header("Contract Deployment");
594
595 for contract_path in contract_files {
596 let (contract_name, address) = deploy_single_contract(opts, &contract_path)?;
597 deployed_addresses.insert(contract_name, address);
598 }
599 }
600
601 Ok(deployed_addresses)
602}
603
604pub fn deploy_to_eigenlayer(opts: &EigenlayerDeployOpts) -> Result<()> {
610 let addresses = deploy_avs_contracts(opts)?;
611 print_section_header("Deployment Summary");
612 println!("{}", style("━".repeat(50)).cyan());
613 for (contract, address) in addresses {
614 println!(
615 "{}: {}",
616 style(&contract).yellow().bold(),
617 style(format!("0x{:x}", address)).cyan().bold()
618 );
619 }
620 println!("{}", style("━".repeat(50)).cyan());
621 Ok(())
622}
623
624fn extract_address_from_output(output: &[u8]) -> Result<Address> {
625 let output = String::from_utf8_lossy(output);
626 debug!("Attempting to extract address from output:\n{}", output);
627
628 let patterns = [
630 "Deployed to:",
631 "Contract Address:",
632 "Deployed at:",
633 "at address:",
634 ];
635
636 for pattern in patterns {
637 if let Some(line) = output.lines().find(|line| line.contains(pattern)) {
638 debug!("Found matching line with pattern '{}': {}", pattern, line);
639
640 let addr_str = line
642 .split(pattern)
643 .last()
644 .and_then(|s| s.split_whitespace().next())
645 .or_else(|| line.split_whitespace().last());
646
647 if let Some(addr) = addr_str {
648 debug!("Found potential address: {}", addr);
649 if let Ok(address) = Address::from_str(addr) {
650 debug!("Successfully parsed address: {}", address);
651 return Ok(address);
652 }
653 }
654 }
655 }
656
657 Err(color_eyre::eyre::eyre!(
659 "Failed to find or parse contract address in output"
660 ))
661}
662
663fn display_devnet_info(http_endpoint: &str) {
669 println!("\n{}", style("Local Testnet Active").green().bold());
670 println!("\n{}", style("To run your AVS:").cyan().bold());
671 println!("{}", style("1. Open a new terminal window").dim());
672 println!(
673 "{}",
674 style("2. Set your AVS-specific environment variables:").dim()
675 );
676 println!(
677 " {}",
678 style(
679 "# Your AVS may require specific environment variables from the deployment output above"
680 )
681 .dim()
682 );
683 println!(
684 " {}",
685 style("# For example: TASK_MANAGER_ADDRESS=<address> or other contract addresses").dim()
686 );
687 println!("\n{}", style("3. Run your AVS with:").dim());
688 println!(
689 " {}\n {}\n {}\n {}",
690 style("cargo tangle blueprint run \\").yellow(),
691 style(" -p eigenlayer \\").yellow(),
692 style(format!(" -u {} \\", http_endpoint)).yellow(),
693 style(" --keystore-path ./test-keystore").yellow()
694 );
695 println!(
696 "\n{}",
697 style("The deployment variables above show all contract addresses you may need.").dim()
698 );
699 println!("{}", style("Press Ctrl+C to stop the testnet...").dim());
700}
701
702pub async fn deploy_eigenlayer(
721 rpc_url: Option<String>,
722 contracts_path: Option<String>,
723 ordered_deployment: bool,
724 network: String,
725 devnet: bool,
726 keystore_path: Option<std::path::PathBuf>,
727) -> Result<()> {
728 let build_status = blueprint_std::process::Command::new("cargo")
729 .args(["build", "--release"])
730 .status()?;
731
732 if !build_status.success() {
733 return Err(color_eyre::Report::msg("Cargo build failed"));
734 }
735
736 if devnet && network.to_lowercase() != "local" {
738 return Err(color_eyre::Report::msg(
739 "The --devnet flag can only be used with --network local",
740 ));
741 }
742
743 let chain = match network.to_lowercase().as_str() {
744 "local" => SupportedChains::LocalTestnet,
745 "testnet" => SupportedChains::Testnet,
746 "mainnet" => {
747 if rpc_url
748 .as_ref()
749 .is_some_and(|url| url.contains("127.0.0.1") || url.contains("localhost"))
750 {
751 SupportedChains::LocalMainnet
752 } else {
753 SupportedChains::Mainnet
754 }
755 }
756 _ => {
757 return Err(color_eyre::Report::msg(format!(
758 "Invalid network: {}",
759 network
760 )));
761 }
762 };
763
764 if chain == SupportedChains::LocalTestnet && devnet {
765 let testnet = start_default_anvil_testnet(true).await;
767
768 initialize_test_keystore()?;
769
770 let opts = EigenlayerDeployOpts::new(
772 testnet.http_endpoint.clone(),
773 contracts_path,
774 ordered_deployment,
775 chain,
776 keystore_path,
777 );
778 deploy_to_eigenlayer(&opts)?;
779
780 display_devnet_info(testnet.http_endpoint.as_str());
782
783 signal::ctrl_c().await?;
785 println!("{}", style("\nShutting down devnet...").yellow());
786 } else {
787 let opts = EigenlayerDeployOpts::new(
788 rpc_url.as_deref().ok_or_else(|| {
789 color_eyre::Report::msg(
790 "The --rpc-url flag is required when deploying to a non-local network",
791 )
792 })?,
793 contracts_path,
794 ordered_deployment,
795 chain,
796 keystore_path,
797 );
798 deploy_to_eigenlayer(&opts)?;
799 }
800
801 Ok(())
802}