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