diff --git a/codegenerator/Cargo.lock b/codegenerator/Cargo.lock index 65117179e..3a5427489 100644 --- a/codegenerator/Cargo.lock +++ b/codegenerator/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -1595,6 +1595,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -2101,16 +2110,17 @@ dependencies = [ [[package]] name = "inquire" -version = "0.6.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "crossterm", "dyn-clone", - "lazy_static", + "fuzzy-matcher", + "fxhash", "newline-converter", - "thiserror", + "once_cell", "unicode-segmentation", "unicode-width", ] @@ -2432,9 +2442,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "newline-converter" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" dependencies = [ "unicode-segmentation", ] diff --git a/codegenerator/cli/Cargo.toml b/codegenerator/cli/Cargo.toml index 9a006a5cc..e76abb412 100644 --- a/codegenerator/cli/Cargo.toml +++ b/codegenerator/cli/Cargo.toml @@ -12,7 +12,7 @@ ethers = "2.0.14" graphql-parser = "0.4.0" handlebars = "6.0.0" include_dir = "0.7.3" -inquire = "0.6.1" +inquire = "0.7.5" pathdiff = "0.2.1" serde = { version = "1.0.159", features = ["derive"] } serde_json = "1.0.95" diff --git a/codegenerator/cli/src/cli_args/clap_definitions.rs b/codegenerator/cli/src/cli_args/clap_definitions.rs index 6f8b54547..bb716f29f 100644 --- a/codegenerator/cli/src/cli_args/clap_definitions.rs +++ b/codegenerator/cli/src/cli_args/clap_definitions.rs @@ -168,10 +168,7 @@ pub enum InitFlow { } pub mod evm { - use crate::{ - config_parsing::chain_helpers::{Network, NetworkWithExplorer}, - evm, init_config, - }; + use crate::{config_parsing::chain_helpers::Network, evm, init_config}; use anyhow::Context; use clap::{Args, Subcommand}; @@ -238,7 +235,7 @@ pub mod evm { pub struct ExplorerImportArgs { ///Network to import the contract from #[arg(short, long)] - pub blockchain: Option, + pub blockchain: Option, ///API token for the block explorer #[arg(long)] diff --git a/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs b/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs index ac455cb90..65db053c7 100644 --- a/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs +++ b/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs @@ -6,7 +6,7 @@ use super::{ prompt_abi_file_path, prompt_contract_address, prompt_contract_name, prompt_events_selection, prompt_to_continue_adding, Contract, SelectItem, }, - validation::UniqueValueValidator, + // validation::UniqueValueValidator, }; use crate::{ clap_definitions::evm::NetworkOrChainId, @@ -221,46 +221,92 @@ fn prompt_for_network_id( already_selected_ids: Vec, ) -> Result { //Select one of our supported networks - let networks = HypersyncNetwork::iter() + let mut networks: Vec = NetworkWithExplorer::iter() //Don't allow selection of networks that have been previously //selected. .filter(|n| { let network_id = *n as u64; !already_selected_ids.contains(&network_id) }) - .map(NetworkSelection::Network) - .collect::>(); - - //User's options to either enter an id or select a supported network - let options = [vec![NetworkSelection::EnterNetworkId], networks].concat(); - - //Action prompt - let choose_from_networks = Select::new("Choose network:", options) - .prompt() - .context("Failed during prompt for network")?; - - let selected = match choose_from_networks { - //If the user's choice evaluates to the enter network id option, prompt them for - //a network id - NetworkSelection::EnterNetworkId => { - let network_id = CustomType::::new("Enter the network id:") - //Validate that this ID is not already selected - .with_validator(UniqueValueValidator::new(already_selected_ids)) - .with_error_message("Invalid network id input, please enter a number") - .prompt()?; - - //Convert the id into a supported or unsupported network. - //If unsupported, it will use the optional rpc url or prompt - //for an rpc url - get_converter_network_u64(network_id, opt_rpc_url, opt_start_block)? + .collect(); + + // Sort networks alphabetically by name for default ordering + networks.sort_by(|a, b| { + let a_name = Network::from(*a).to_string(); + let b_name = Network::from(*b).to_string(); + a_name.cmp(&b_name) + }); + + // Custom scorer that provides dynamic ordering based on input type + let network_scorer = |input: &str, + _option: &NetworkWithExplorer, + _option_string: &str, + option_index: usize| + -> Option { + if input.trim().is_empty() { + // No input: maintain alphabetical order (use negative index for descending order) + return Some(100000 - option_index as i64); } - //If a supported network choice was selected. We should be able to - //parse it back to a supported network since it was serialized as a - //string - NetworkSelection::Network(network) => converters::NetworkKind::Supported(network), + + let chain_id = *_option as u64; + let display_name = _option.get_pretty_name().to_lowercase(); + let input_lower = input.to_lowercase(); + + // Check if input is a number + if let Ok(input_chain_id) = input.parse::() { + // Numeric input: prioritize by chain ID proximity and exact matches + if chain_id == input_chain_id { + // Exact chain ID match gets highest priority + return Some(1000000); + } + + // Chain ID contains the input digits gets high priority + if chain_id.to_string().contains(&input) { + // Score based on how close the chain ID is to the input number + let diff = if chain_id > input_chain_id { + chain_id - input_chain_id + } else { + input_chain_id - chain_id + }; + return Some(500000 - diff as i64); + } + + // Name matches for numeric input get lower priority + if display_name.contains(&input_lower) { + return Some(100000); + } + } else { + // Text input: prioritize by name matching, maintain alphabetical sub-ordering + if display_name.contains(&input_lower) { + // Calculate match quality (earlier matches = higher score) + let match_pos = display_name.find(&input_lower).unwrap_or(0); + return Some(200000 - match_pos as i64); + } + + // Chain ID string contains input (for mixed searches) + if chain_id.to_string().contains(&input) { + return Some(50000); + } + } + + // No match + None }; - Ok(selected) + let selected_explorer_network = Select::new( + "Which blockchain would you like to import a contract from?", + networks, + ) + .with_scorer(&network_scorer) + .prompt()?; + + // Convert the NetworkWithExplorer to NetworkKind + let network_id = selected_explorer_network as u64; + Ok(get_converter_network_u64( + network_id, + opt_rpc_url, + opt_start_block, + )?) } //Takes a u64 network ID and turns it into either "Supported" network or @@ -325,14 +371,116 @@ impl ExplorerImportArgs { ///for a user to select one. fn get_network_with_explorer(&self) -> Result { let chosen_network = match &self.blockchain { - Some(chain) => *chain, + Some(network_or_chain_id) => { + match network_or_chain_id { + NetworkOrChainId::NetworkName(network) => { + // Try to convert Network to NetworkWithExplorer + match NetworkWithExplorer::try_from(*network) { + Ok(network_with_explorer) => network_with_explorer, + Err(_) => { + return Err(anyhow::anyhow!( + "The selected network does not support explorer-based import" + )); + } + } + } + NetworkOrChainId::ChainId(chain_id) => { + // Try to convert chain_id to Network first + match Network::from_network_id(*chain_id) { + Ok(network) => { + // Then try to convert to NetworkWithExplorer + match NetworkWithExplorer::try_from(network) { + Ok(network_with_explorer) => network_with_explorer, + Err(_) => { + return Err(anyhow::anyhow!( + "The network with chain ID {} does not support explorer-based import", + chain_id + )); + } + } + } + Err(_) => { + return Err(anyhow::anyhow!( + "Unsupported chain ID: {}. Network not found", + chain_id + )); + } + } + } + } + } None => { - let options = NetworkWithExplorer::iter().collect(); + // Filter out already selected networks and show both network name and chain ID + let mut networks: Vec = NetworkWithExplorer::iter().collect(); + + // Sort networks alphabetically by name for default ordering + networks.sort_by(|a, b| { + let a_name = Network::from(*a).to_string(); + let b_name = Network::from(*b).to_string(); + a_name.cmp(&b_name) + }); + + // Use the same custom scorer as in prompt_for_network_id for consistency + let network_scorer = |input: &str, + _option: &NetworkWithExplorer, + _option_string: &str, + option_index: usize| + -> Option { + if input.trim().is_empty() { + // No input: maintain alphabetical order (use negative index for descending order) + return Some(100000 - option_index as i64); + } + + let chain_id = *_option as u64; + let display_name = _option.get_pretty_name().to_lowercase(); + let input_lower = input.to_lowercase(); + + // Check if input is a number + if let Ok(input_chain_id) = input.parse::() { + // Numeric input: prioritize by chain ID proximity and exact matches + if chain_id == input_chain_id { + // Exact chain ID match gets highest priority + return Some(1000000); + } + + // Chain ID contains the input digits gets high priority + if chain_id.to_string().contains(&input) { + // Score based on how close the chain ID is to the input number + let diff = if chain_id > input_chain_id { + chain_id - input_chain_id + } else { + input_chain_id - chain_id + }; + return Some(500000 - diff as i64); + } + + // Name matches for numeric input get lower priority + if display_name.contains(&input_lower) { + return Some(100000); + } + } else { + // Text input: prioritize by name matching, maintain alphabetical sub-ordering + if display_name.contains(&input_lower) { + // Calculate match quality (earlier matches = higher score) + let match_pos = display_name.find(&input_lower).unwrap_or(0); + return Some(200000 - match_pos as i64); + } + + // Chain ID string contains input (for mixed searches) + if chain_id.to_string().contains(&input) { + return Some(50000); + } + } + + // No match + None + }; Select::new( "Which blockchain would you like to import a contract from?", - options, + networks, ) + .with_scorer(&network_scorer) .prompt()? } }; diff --git a/codegenerator/cli/src/config_parsing/chain_helpers.rs b/codegenerator/cli/src/config_parsing/chain_helpers.rs index 28163528c..ca729059a 100644 --- a/codegenerator/cli/src/config_parsing/chain_helpers.rs +++ b/codegenerator/cli/src/config_parsing/chain_helpers.rs @@ -610,10 +610,17 @@ impl fmt::Display for HypersyncNetwork { impl NetworkWithExplorer { pub fn get_pretty_name(&self) -> String { + let chain_id = *self as u64; let network = Network::from(*self); + let network_str = network.to_string(); + + // Try to get the tier (icon) if it's a HypersyncNetwork match HypersyncNetwork::try_from(network) { - Ok(hypersync_network) => hypersync_network.get_pretty_name(), - Err(_) => network.to_string(), + Ok(hypersync_network) => { + let tier = hypersync_network.get_tier(); + format!("{} ({}) {}", network_str, chain_id, tier.get_icon()) + } + Err(_) => format!("{} ({})", network_str, chain_id), } } }