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