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;
23use url::Url;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct EigenlayerDeployOpts {
27    /// The RPC URL to connect to
28    pub(crate) rpc_url: Url,
29    /// Path to the contracts, defaults to `"./contracts"`
30    pub(crate) contracts_path: String,
31    /// Optional constructor arguments for contracts, keyed by contract name
32    pub(crate) constructor_args: Option<HashMap<String, Vec<String>>>,
33    /// Whether to deploy contracts in an interactive ordered manner
34    pub(crate) ordered_deployment: bool,
35    /// The type of the target chain
36    pub(crate) chain: SupportedChains,
37    /// The path to the keystore
38    pub(crate) keystore_path: String,
39}
40
41impl EigenlayerDeployOpts {
42    /// # Panics
43    ///
44    /// When used in a local testnet environment with no specified keystore, this will panic if it
45    /// cannot create a temporary directory to use for the keystore.
46    #[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            // For local testnet with no specified keystore, use a temporary directory
64            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        // Check if keystore exists and create it if it doesn't
88        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            // Try to get the ECDSA key from the keystore
101            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
128// TODO(donovan): use a tempdir
129/// Initializes the test keystore with Anvil's Account 0. Generating the `./test-keystore` directory if it doesn't exist
130///
131/// Returns the path to the Temporary Directory, which must be kept alive as long as the keystore needs to be accessed.
132///
133/// # Errors
134///
135/// See [`Keystore::new()`]
136pub fn initialize_test_keystore() -> Result<()> {
137    // For local testnet with no specified keystore, use a temporary directory
138    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    // TODO: Add support for Tangle here, taking the protocol as an input and controlling the key type and key input(s)
146    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    // Check if the file has a .sol extension
160    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    // Reconstruct the path with the contract name appended
174    let mut new_path = path.to_path_buf();
175    new_path.set_file_name(file_name); // Ensure we keep the .sol extension
176
177    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                // Read the file content to check if it's an interface
202                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    // Find the constructor in the ABI
272    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    // Get constructor inputs
278    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 we have pre-provided arguments for this contract, use those
286    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    // For each input parameter, prompt the user for a value
300    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    // Check if contract has an initialize function
365    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                // Format the value based on its type
393                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            // First generate the calldata using cast calldata
407            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            // Get the from address from the private key
430            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            // Construct the transaction parameters
449            let tx_params = format!(
450                "{{\"from\":\"{}\",\"to\":\"{}\",\"data\":\"{}\"}}",
451                from_address, contract_address, calldata
452            );
453
454            print_info("Sending initialization transaction...");
455
456            // Send the transaction using eth_sendTransaction
457            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    // Read the contract's JSON artifact to check for constructor args
495    let out_dir = Path::new(&opts.contracts_path).join("out");
496    let json_path = out_dir.join(format!("{}.json", contract_output));
497
498    // Read and parse the contract JSON
499    let json_content = fs::read_to_string(&json_path)?;
500    let contract_json: Value = serde_json::from_str(&json_content)?;
501
502    // Build the forge create command as a single string
503    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                // Quote the value if it's not already quoted
520                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    // Execute the command through sh -c
531    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    // Try to find address in stdout first, then stderr if not found
544    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    // Print deployment success with prominent address
551    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    // Check for initialization
564    initialize_contract_if_needed(opts, &contract_json, &contract_name, address)?;
565
566    Ok((contract_name, address))
567}
568
569/// Deploy all contracts under the contracts directory
570///
571/// # Errors
572///
573/// * The specified `contracts_path` does not exist
574/// * [forge create] fails
575///
576/// [forge create]: https://book.getfoundry.sh/reference/forge/forge-create
577pub 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            // Remove the deployed contract from remaining contracts
591            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
605/// Deploy all contracts and print a summary
606///
607/// # Errors
608///
609/// See [`deploy_avs_contracts()`]
610pub 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    // Possible patterns to search for deployed address
630    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            // Try to extract address
642            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    // If we get here, we couldn't find a valid address
659    Err(color_eyre::eyre::eyre!(
660        "Failed to find or parse contract address in output"
661    ))
662}
663
664/// Display helpful information about the devnet and deployed contracts
665///
666/// # Arguments
667///
668/// * `http_endpoint` - The HTTP endpoint of the devnet
669fn 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
703/// Deploy an AVS to Eigenlayer
704///
705/// # Arguments
706///
707/// * `rpc_url` - The RPC URL to connect to
708/// * `contracts_path` - Path to the contracts directory
709/// * `ordered_deployment` - Whether to deploy contracts in an interactive ordered manner
710/// * `network` - The target chain type (local, testnet, mainnet). Local mainnet is inferred by URL.
711/// * `devnet` - Whether to start a local devnet
712/// * `keystore_path` - Path to the keystore
713///
714/// # Returns
715///
716/// A `Result` indicating success or an error
717///
718/// # Errors
719///
720/// Returns a `color_eyre::Report` if an error occurs during deployment.
721pub 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    // Validate that devnet is only used with local network
738    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        // Start local Anvil testnet
767        let testnet = start_default_anvil_testnet(true).await;
768
769        initialize_test_keystore()?;
770
771        // Deploy to local devnet
772        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        // Keep the process running and show helpful instructions
782        display_devnet_info(testnet.http_endpoint.as_str());
783
784        // Wait for Ctrl+C to shut down
785        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}