Skip to main content

cargo_tangle/command/deploy/
eigenlayer.rs

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    /// The RPC URL to connect to
27    pub(crate) rpc_url: Url,
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<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            // For local testnet with no specified keystore, use a temporary directory
63            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        // Check if keystore exists and create it if it doesn't
87        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            // Try to get the ECDSA key from the keystore
100            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
127// TODO(donovan): use a tempdir
128/// Initializes the test keystore with Anvil's Account 0. Generating the `./test-keystore` directory if it doesn't exist
129///
130/// Returns the path to the Temporary Directory, which must be kept alive as long as the keystore needs to be accessed.
131///
132/// # Errors
133///
134/// See [`Keystore::new()`]
135pub fn initialize_test_keystore() -> Result<()> {
136    // For local testnet with no specified keystore, use a temporary directory
137    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    // TODO: Add support for Tangle here, taking the protocol as an input and controlling the key type and key input(s)
145    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    // Check if the file has a .sol extension
159    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    // Reconstruct the path with the contract name appended
173    let mut new_path = path.to_path_buf();
174    new_path.set_file_name(file_name); // Ensure we keep the .sol extension
175
176    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                // Read the file content to check if it's an interface
201                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    // Find the constructor in the ABI
271    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    // Get constructor inputs
277    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 we have pre-provided arguments for this contract, use those
285    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    // For each input parameter, prompt the user for a value
299    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    // Check if contract has an initialize function
364    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                // Format the value based on its type
392                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            // First generate the calldata using cast calldata
406            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            // Get the from address from the private key
429            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            // Construct the transaction parameters
448            let tx_params = format!(
449                "{{\"from\":\"{}\",\"to\":\"{}\",\"data\":\"{}\"}}",
450                from_address, contract_address, calldata
451            );
452
453            print_info("Sending initialization transaction...");
454
455            // Send the transaction using eth_sendTransaction
456            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    // Read the contract's JSON artifact to check for constructor args
494    let out_dir = Path::new(&opts.contracts_path).join("out");
495    let json_path = out_dir.join(format!("{}.json", contract_output));
496
497    // Read and parse the contract JSON
498    let json_content = fs::read_to_string(&json_path)?;
499    let contract_json: Value = serde_json::from_str(&json_content)?;
500
501    // Build the forge create command as a single string
502    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                // Quote the value if it's not already quoted
519                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    // Execute the command through sh -c
530    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    // Try to find address in stdout first, then stderr if not found
543    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    // Print deployment success with prominent address
550    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    // Check for initialization
563    initialize_contract_if_needed(opts, &contract_json, &contract_name, address)?;
564
565    Ok((contract_name, address))
566}
567
568/// Deploy all contracts under the contracts directory
569///
570/// # Errors
571///
572/// * The specified `contracts_path` does not exist
573/// * [forge create] fails
574///
575/// [forge create]: https://book.getfoundry.sh/reference/forge/forge-create
576pub 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            // Remove the deployed contract from remaining contracts
590            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
604/// Deploy all contracts and print a summary
605///
606/// # Errors
607///
608/// See [`deploy_avs_contracts()`]
609pub 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    // Possible patterns to search for deployed address
629    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            // Try to extract address
641            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    // If we get here, we couldn't find a valid address
658    Err(color_eyre::eyre::eyre!(
659        "Failed to find or parse contract address in output"
660    ))
661}
662
663/// Display helpful information about the devnet and deployed contracts
664///
665/// # Arguments
666///
667/// * `http_endpoint` - The HTTP endpoint of the devnet
668fn 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
702/// Deploy an AVS to Eigenlayer
703///
704/// # Arguments
705///
706/// * `rpc_url` - The RPC URL to connect to
707/// * `contracts_path` - Path to the contracts directory
708/// * `ordered_deployment` - Whether to deploy contracts in an interactive ordered manner
709/// * `network` - The target chain type (local, testnet, mainnet). Local mainnet is inferred by URL.
710/// * `devnet` - Whether to start a local devnet
711/// * `keystore_path` - Path to the keystore
712///
713/// # Returns
714///
715/// A `Result` indicating success or an error
716///
717/// # Errors
718///
719/// Returns a `color_eyre::Report` if an error occurs during deployment.
720pub 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    // Validate that devnet is only used with local network
737    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        // Start local Anvil testnet
766        let testnet = start_default_anvil_testnet(true).await;
767
768        initialize_test_keystore()?;
769
770        // Deploy to local devnet
771        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        // Keep the process running and show helpful instructions
781        display_devnet_info(testnet.http_endpoint.as_str());
782
783        // Wait for Ctrl+C to shut down
784        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}