cargo_tangle/command/deploy/
eigenlayer.rs

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    /// The RPC URL to connect to
27    pub(crate) rpc_url: String,
28    /// Path to the contracts, defaults to `"./contracts"`
29    pub(crate) contracts_path: String,
30    /// Optional constructor arguments for contracts, keyed by contract name
31    pub(crate) constructor_args: Option<HashMap<String, Vec<String>>>,
32    /// Whether to deploy contracts in an interactive ordered manner
33    pub(crate) ordered_deployment: bool,
34    /// The type of the target chain
35    pub(crate) chain: SupportedChains,
36    /// The path to the keystore
37    pub(crate) keystore_path: String,
38}
39
40impl EigenlayerDeployOpts {
41    /// # Panics
42    ///
43    /// When used in a local testnet environment with no specified keystore, this will panic if it
44    /// cannot create a temporary directory to use for the keystore.
45    #[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            // For local testnet with no specified keystore, use a temporary directory
58            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        // Check if keystore exists and create it if it doesn't
82        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            // Try to get the ECDSA key from the keystore
94            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
121// TODO(donovan): use a tempdir
122/// Initializes the test keystore with Anvil's Account 0. Generating the `./test-keystore` directory if it doesn't exist
123///
124/// Returns the path to the Temporary Directory, which must be kept alive as long as the keystore needs to be accessed.
125///
126/// # Errors
127///
128/// See [`Keystore::new()`]
129pub fn initialize_test_keystore() -> Result<()> {
130    // For local testnet with no specified keystore, use a temporary directory
131    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    // TODO: Add support for Tangle here, taking the protocol as an input and controlling the key type and key input(s)
139    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    // Check if the file has a .sol extension
153    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    // Reconstruct the path with the contract name appended
167    let mut new_path = path.to_path_buf();
168    new_path.set_file_name(file_name); // Ensure we keep the .sol extension
169
170    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                // Read the file content to check if it's an interface
195                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    // Find the constructor in the ABI
265    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    // Get constructor inputs
271    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 we have pre-provided arguments for this contract, use those
279    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    // For each input parameter, prompt the user for a value
293    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    // Check if contract has an initialize function
358    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                // Format the value based on its type
386                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            // First generate the calldata using cast calldata
400            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            // Get the from address from the private key
423            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            // Construct the transaction parameters
442            let tx_params = format!(
443                "{{\"from\":\"{}\",\"to\":\"{}\",\"data\":\"{}\"}}",
444                from_address, contract_address, calldata
445            );
446
447            print_info("Sending initialization transaction...");
448
449            // Send the transaction using eth_sendTransaction
450            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    // Read the contract's JSON artifact to check for constructor args
488    let out_dir = Path::new(&opts.contracts_path).join("out");
489    let json_path = out_dir.join(format!("{}.json", contract_output));
490
491    // Read and parse the contract JSON
492    let json_content = fs::read_to_string(&json_path)?;
493    let contract_json: Value = serde_json::from_str(&json_content)?;
494
495    // Build the forge create command as a single string
496    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                // Quote the value if it's not already quoted
513                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    // Execute the command through sh -c
524    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    // Try to find address in stdout first, then stderr if not found
537    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    // Print deployment success with prominent address
544    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    // Check for initialization
557    initialize_contract_if_needed(opts, &contract_json, &contract_name, address)?;
558
559    Ok((contract_name, address))
560}
561
562/// Deploy all contracts under the contracts directory
563///
564/// # Errors
565///
566/// * The specified `contracts_path` does not exist
567/// * [forge create] fails
568///
569/// [forge create]: https://book.getfoundry.sh/reference/forge/forge-create
570pub 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            // Remove the deployed contract from remaining contracts
584            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
598/// Deploy all contracts and print a summary
599///
600/// # Errors
601///
602/// See [`deploy_avs_contracts()`]
603pub 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    // Possible patterns to search for deployed address
623    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            // Try to extract address
635            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    // If we get here, we couldn't find a valid address
652    Err(color_eyre::eyre::eyre!(
653        "Failed to find or parse contract address in output"
654    ))
655}
656
657/// Display helpful information about the devnet and deployed contracts
658///
659/// # Arguments
660///
661/// * `http_endpoint` - The HTTP endpoint of the devnet
662fn 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
696/// Deploy an AVS to Eigenlayer
697///
698/// # Arguments
699///
700/// * `rpc_url` - The RPC URL to connect to
701/// * `contracts_path` - Path to the contracts directory
702/// * `ordered_deployment` - Whether to deploy contracts in an interactive ordered manner
703/// * `network` - The target chain type (local, testnet, mainnet). Local mainnet is inferred by URL.
704/// * `devnet` - Whether to start a local devnet
705/// * `keystore_path` - Path to the keystore
706///
707/// # Returns
708///
709/// A `Result` indicating success or an error
710///
711/// # Errors
712///
713/// Returns a `color_eyre::Report` if an error occurs during deployment.
714pub 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    // Validate that devnet is only used with local network
731    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        // Start local Anvil testnet
760        let testnet = start_default_anvil_testnet(true).await;
761
762        initialize_test_keystore()?;
763
764        // Deploy to local devnet
765        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        // Keep the process running and show helpful instructions
775        display_devnet_info(&testnet.http_endpoint);
776
777        // Wait for Ctrl+C to shut down
778        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}