From dea2c51fb0563f3cbc603cb11a207f83fbd80de9 Mon Sep 17 00:00:00 2001 From: Mark Toda Date: Wed, 1 Jan 2025 16:39:00 -0500 Subject: [PATCH] feat: update flow --- Cargo.lock | 57 +++++-- Cargo.toml | 3 +- src/chain.rs | 417 +++++++++++++++++++++++++++++++++++++++-------- src/chainlist.rs | 2 +- src/config.rs | 134 ++++++++------- src/init.rs | 67 +++----- src/key.rs | 54 +++--- src/main.rs | 16 +- src/opt.rs | 113 ++++++++++--- src/var.rs | 37 +++++ 10 files changed, 649 insertions(+), 251 deletions(-) create mode 100644 src/var.rs diff --git a/Cargo.lock b/Cargo.lock index cd5ef22..aa30e4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -951,6 +951,7 @@ dependencies = [ "colored", "dialoguer", "dirs", + "futures", "reqwest", "rpassword", "serde", @@ -1144,6 +1145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" dependencies = [ "console", + "fuzzy-matcher", "shell-words", "tempfile", "thiserror", @@ -1351,9 +1353,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1366,9 +1368,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1376,15 +1378,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1393,15 +1395,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1410,21 +1412,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1444,6 +1446,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" +[[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 = "generic-array" version = "0.14.7" @@ -3021,6 +3032,16 @@ dependencies = [ "syn 2.0.70", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "threadpool" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 39849db..ee34774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" alloy = { version = "0.1.4", features = ["full"] } alloy-primitives = "0.7.7" anyhow = "1.0.86" -dialoguer = "0.11.0" +dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } dirs = "5.0.1" reqwest = "0.12.9" rpassword = "7.3.1" @@ -16,3 +16,4 @@ serde_json = "1.0.120" structopt = "0.3.26" tokio = { version = "1.38.0", features = ["full"] } colored = "2.0" +futures = "0.3.31" diff --git a/src/chain.rs b/src/chain.rs index bb8ae70..5f3820a 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -1,10 +1,16 @@ -use crate::{chainlist::fetch_chain_data, key::Key, opt::AddArgs}; +use crate::{ + chainlist::{fetch_all_chains, fetch_chain_data, ChainlistEntry}, + config::Chainz, + key::Key, + opt::{AddArgs, UpdateArgs}, +}; use alloy::{ providers::{Provider, ProviderBuilder}, transports::BoxTransport, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; use colored::*; +use dialoguer::{FuzzySelect, Input}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{Debug, Display}; @@ -16,7 +22,7 @@ pub struct ChainDefinition { pub name: String, pub chain_id: u64, pub rpc_urls: Vec, - pub selected_rpc: Option, + pub selected_rpc: String, pub verification_api_key: Option, pub key_name: String, } @@ -28,66 +34,20 @@ pub struct ChainInstance { pub key: Key, } -impl ChainDefinition { - pub async fn new(args: &AddArgs) -> Result { - // if no chain_id or name, then throw - if args.chain_id.is_none() && args.name.is_none() { - return Err(anyhow!("Either chain_id or name must be provided")); - } - - let chain_data = fetch_chain_data(args.chain_id, args.name.clone()).await?; - - // Get name and chain_id from either args, chainlist, or generate from chain_id - let name = args.name.clone().unwrap_or(chain_data.name); - let chain_id = args.chain_id.unwrap_or(chain_data.chain_id); - Ok(Self { - name, - chain_id, - selected_rpc: None, - // given rpc url is first in list to try if given - rpc_urls: match &args.rpc_url { - Some(rpc_url) => { - let mut urls = vec![rpc_url.clone()]; - urls.extend(chain_data.rpc); - urls - } - None => chain_data.rpc, - }, - verification_api_key: args.verification_api_key.clone(), - key_name: args - .key_name - .clone() - .unwrap_or(DEFAULT_KEY_NAME.to_string()), - }) - } +pub struct Rpc { + pub rpc_url: String, + pub provider: Box>, +} - pub fn resolve_variables(&self, variables: &HashMap) -> Self { - let new_rpc_urls = self - .rpc_urls - .iter() - .map(|url| interpolate_variables(url, variables)) - .collect(); - let mut new_config = self.clone(); - new_config.rpc_urls = new_rpc_urls; - new_config +impl ChainDefinition { + pub async fn get_rpc(&self, variables: &HashMap) -> Result { + resolve_rpc(&self.selected_rpc, variables).await } +} - pub async fn get_rpc(&self) -> Result<(String, Box>)> { - // First try the last working RPC if available - if let Some(selected) = &self.selected_rpc { - if let Some(rpc_url) = test_rpc(selected, self.chain_id).await { - return Ok((rpc_url.clone(), create_provider(&rpc_url).await?)); - } - } - - // If last working RPC failed or doesn't exist, try others - for rpc_url in &self.rpc_urls { - if let Some(rpc_url) = test_rpc(rpc_url, self.chain_id).await { - return Ok((rpc_url.clone(), create_provider(&rpc_url).await?)); - } - } - - Err(anyhow!("No valid RPC urls found")) +impl Display for Rpc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.rpc_url) } } @@ -111,10 +71,7 @@ impl Display for ChainDefinition { "{}─ {}: {}", "├".bright_black(), "Active RPC".bright_blue(), - self.selected_rpc - .as_deref() - .unwrap_or("None") - .bright_green() + self.selected_rpc.bright_green() )?; writeln!( f, @@ -171,6 +128,288 @@ impl Display for ChainInstance { } } +/// Helper function to manually enter chain details +pub async fn manual_chain_entry( + name: Option, + chain_id: Option, +) -> Result { + println!("\n{}", "Manual Chain Entry".bright_yellow().bold()); + let name = if let Some(n) = name { + n + } else { + Input::new().with_prompt("Chain name").interact_text()? + }; + let chain_id = if let Some(id) = chain_id { + id + } else { + Input::new().with_prompt("Chain ID").interact_text()? + }; + + Ok(ChainlistEntry { + name, + chain_id, + rpc: vec![], + }) +} + +/// Helper function to select or enter RPC URL +pub async fn select_rpc( + chain_name: &str, + chain_id: u64, + available_rpcs: Vec, +) -> Result { + println!("\nTesting RPCs..."); + + // Initialize displays with "testing" status + let mut rpc_displays: Vec = available_rpcs + .iter() + .map(|rpc| format!("{} {}", "⋯".bright_yellow(), rpc)) + .collect(); + + // Create a vector of futures for testing RPCs + let mut test_futures = Vec::new(); + for (idx, rpc) in available_rpcs.iter().enumerate() { + // Clone the necessary data for the spawned task + let rpc_to_test = Rpc { + rpc_url: rpc.rpc_url.clone(), + provider: create_provider(&rpc.rpc_url).await?, + }; + + let test_future = async move { + let result = test_rpc(&rpc_to_test, chain_id).await; + (idx, result) + }; + test_futures.push(tokio::spawn(test_future)); + } + + // Process results as they complete + for (idx, result) in (futures::future::join_all(test_futures).await) + .into_iter() + .flatten() + { + if result.is_ok() { + rpc_displays[idx] = format!("{} {}", "✓".bright_green(), available_rpcs[idx]); + } else { + rpc_displays[idx] = format!("{} {}", "✗".bright_red(), available_rpcs[idx]); + } + } + + // Add manual entry option + rpc_displays.push("Enter RPC URL manually...".to_string()); + + let rpc_selection = fuzzy_select( + &format!("Select an RPC URL for {}", chain_name.yellow()), + &rpc_displays, + 0, + )?; + + if rpc_selection == rpc_displays.len() - 1 { + Ok(select_manual_rpc(chain_id).await?.rpc_url) + } else if let Some(rpc) = available_rpcs.get(rpc_selection) { + Ok(rpc.rpc_url.clone()) + } else { + anyhow::bail!("Selected RPC is not working") + } +} + +async fn select_manual_rpc(chain_id: u64) -> Result { + loop { + let rpc_url: String = Input::new().with_prompt("Enter RPC URL").interact_text()?; + println!("Testing RPC..."); + let rpc = resolve_rpc(&rpc_url, &HashMap::new()).await?; + + if test_rpc(&rpc, chain_id).await.is_ok() { + println!("{} RPC working", "✓".bright_green()); + return Ok(rpc); + } + + println!( + "{} RPC failed. Try again? (Ctrl+C to exit)", + "✗".bright_red() + ); + } +} + +/// Helper function to select or create a key +pub async fn select_key(chainz: &mut Chainz) -> Result { + let keys = chainz.list_keys()?; + + // Create display strings with addresses + let mut key_displays: Vec<(String, String)> = keys + .iter() + .map(|(name, key)| { + let addr = key + .address() + .map(|a| a.to_string()) + .unwrap_or("Invalid key".to_string()); + (name.clone(), format!("{} ({})", name, addr)) + }) + .collect(); + + // Add the "Add new key" option + key_displays.push(("Add new key".to_string(), "Add new key".to_string())); + + let key_selection = fuzzy_select( + "Select a key", + &key_displays + .iter() + .map(|(_, display)| display) + .collect::>(), + 0, + )?; + + if key_selection == key_displays.len() - 1 { + let kname: String = Input::new().with_prompt("Enter key name").interact_text()?; + let private_key: String = Input::new() + .with_prompt("Enter private key") + .interact_text()?; + chainz.add_key(&kname, Key::PrivateKey(private_key)).await?; + Ok(kname) + } else { + Ok(key_displays[key_selection].0.clone()) + } +} + +impl UpdateArgs { + pub async fn handle(&self, chainz: &mut Chainz) -> Result { + println!("\n{}", "Chain Update".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + + // Select chain to update + let chains: Vec = chainz + .list_chains() + .iter() + .map(|c| format!("{} ({})", c.name, c.chain_id)) + .collect(); + + if chains.is_empty() { + anyhow::bail!("No chains configured. Use 'chainz add' to add a chain first."); + } + + let chain_selection = fuzzy_select("Select chain to update", &chains, 0)?; + + let mut chain = chainz.list_chains()[chain_selection].clone(); + + // Select what to update + let options = vec!["RPC URL", "Key", "Verification API Key"]; + + println!("\n{}", "Update Options".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + println!("Current configuration:"); + println!("{}", chain); + + let selection = fuzzy_select("What would you like to update?", &options, 0)?; + + match selection { + 0 => { + // Update RPC URL + println!("\n{}", "RPC Configuration".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + + // Try to get fresh RPC list from chainlist + let chainlist_entry = fetch_chain_data(Some(chain.chain_id), None).await; + let available_rpcs = chainlist_entry + .map(|c| c.rpc) + .unwrap_or_else(|_| chain.rpc_urls.clone()); + + let new_rpc = select_rpc( + &chain.name, + chain.chain_id, + resolve_rpcs(available_rpcs, &chainz.config.variables).await?, + ) + .await?; + chain.selected_rpc = new_rpc; + } + 1 => { + // Update key + println!("\n{}", "Key Configuration".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + + let new_key = select_key(chainz).await?; + chain.key_name = new_key; + } + 2 => { + // Update verification API key + println!( + "\n{}", + "Verification Key Configuration".bright_blue().bold() + ); + println!("{}", "═".bright_black().repeat(50)); + + let new_key: String = Input::new() + .with_prompt("Enter verification API key (empty to remove)") + .allow_empty(true) + .default(chain.verification_api_key.clone().unwrap_or_default()) + .interact_text()?; + + chain.verification_api_key = if new_key.is_empty() { + None + } else { + Some(new_key) + }; + } + _ => unreachable!(), + } + + // Save changes + chainz.add_chain(chain.clone()).await?; + chainz.save().await?; + println!("\n{}", "Chain updated successfully".bright_green()); + + Ok(chain) + } +} + +impl AddArgs { + pub async fn handle(&self, chainz: &mut Chainz) -> Result { + println!("\n{}", "Chain Selection".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + + // Interactive flow with chainlist + let chains = fetch_all_chains().await?; + let items: Vec = chains + .iter() + .map(|c| format!("{} ({})", c.name, c.chain_id)) + .collect(); + + let selected_chain = match fuzzy_select("Type to search and select a chain", &items, 0) { + Ok(selection) => chains[selection].clone(), + Err(_) => manual_chain_entry(None, None).await?, + }; + + println!("\n{}", "RPC Configuration".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + + let selected_rpc = select_rpc( + &selected_chain.name, + selected_chain.chain_id, + resolve_rpcs(selected_chain.rpc.clone(), &chainz.config.variables).await?, + ) + .await?; + + println!("\n{}", "Key Configuration".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + + let key_name = select_key(chainz).await?; + + // TODO: add handler + let verification_api_key = None; + + // Create and add the chain + let chain_def = ChainDefinition { + name: selected_chain.name.clone(), + chain_id: selected_chain.chain_id, + rpc_urls: selected_chain.rpc, + selected_rpc, + verification_api_key, + key_name, + }; + chainz.add_chain(chain_def.clone()).await?; + chainz.save().await?; + Ok(chain_def) + } +} + fn interpolate_variables(input: &str, variables: &HashMap) -> String { let mut result = input.to_string(); @@ -220,17 +459,38 @@ fn find_next_var(input: &str) -> Option<(usize, usize)> { let end = input[start..].find("}")?.checked_add(start + 1)?; Some((start, end)) } -async fn test_rpc(rpc_url: &str, expected_chain_id: u64) -> Option { - // First try the last working RPC if available - if let Ok(provider) = create_provider(rpc_url).await { - if let Ok(chain_id) = provider.get_chain_id().await { - if chain_id == expected_chain_id { - return Some(rpc_url.to_string()); - } + +async fn test_rpc(rpc: &Rpc, expected_chain_id: u64) -> Result<()> { + // Try the resolved RPC URL + if let Ok(chain_id) = rpc.provider.get_chain_id().await { + if chain_id == expected_chain_id { + return Ok(()); // Return original URL with variables + } + } + anyhow::bail!("Invalid chain ID"); +} + +pub async fn resolve_rpcs( + rpc_urls: Vec, + variables: &HashMap, +) -> Result> { + let mut result = Vec::new(); + for rpc in rpc_urls { + if let Ok(r) = resolve_rpc(&rpc, variables).await { + result.push(r); } } - None + Ok(result) } + +pub async fn resolve_rpc(rpc_url: &str, variables: &HashMap) -> Result { + let rpc_url = interpolate_variables(rpc_url, variables); + Ok(Rpc { + rpc_url: rpc_url.clone(), + provider: create_provider(&rpc_url).await?, + }) +} + async fn create_provider(rpc_url: &str) -> Result>> { Ok(Box::new( ProviderBuilder::new() @@ -240,6 +500,19 @@ async fn create_provider(rpc_url: &str) -> Result )) } +// Helper function to handle fuzzy select with ESC cancellation +fn fuzzy_select(prompt: &str, items: &[T], default: usize) -> Result { + match FuzzySelect::new() + .with_prompt(format!("{} (ESC to exit)", prompt)) + .items(items) + .default(default) + .interact_opt()? + { + Some(selection) => Ok(selection), + None => anyhow::bail!("Operation cancelled by user"), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/chainlist.rs b/src/chainlist.rs index f369ac3..993d184 100644 --- a/src/chainlist.rs +++ b/src/chainlist.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use serde::Deserialize; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct ChainlistEntry { pub name: String, #[serde(rename = "chainId")] diff --git a/src/config.rs b/src/config.rs index 8961848..7b90eb8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,6 @@ use crate::{ chain::{ChainDefinition, ChainInstance}, key::Key, - opt::AddArgs, }; use anyhow::{anyhow, Result}; use dirs::home_dir; @@ -26,9 +25,9 @@ pub struct Chainz { } impl Chainz { - pub fn new(config: Config) -> Self { + pub fn new() -> Self { Self { - config, + config: Config::default(), active_chains: HashMap::new(), } } @@ -54,37 +53,94 @@ impl Chainz { } async fn instantiate_chain(&self, def: &ChainDefinition) -> Result { - let (rpc_url, provider) = def.get_rpc().await?; - let key = self.config.get_key(&def.key_name.clone())?; + let rpc = def.get_rpc(&self.config.variables).await?; + let key = self.get_key(&def.key_name.clone())?; Ok(ChainInstance { definition: def.clone(), - provider, - rpc_url, + provider: rpc.provider, + rpc_url: rpc.rpc_url, key, }) } // Key management methods pub async fn add_key(&mut self, name: &str, key: Key) -> Result<()> { - self.config.add_key(name, key).await?; + if self.config.keys.contains_key(name) { + anyhow::bail!("Key '{}' already exists", name); + } + self.config.keys.insert(name.to_string(), key); Ok(()) } pub fn list_keys(&self) -> Result> { - self.config.list_keys() + Ok(self + .config + .keys + .iter() + .map(|(n, k)| (n.clone(), k.clone())) + .collect()) } pub fn remove_key(&mut self, name: &str) -> Result<()> { - self.config.remove_key(name) + if !self.config.keys.contains_key(name) { + anyhow::bail!("Key '{}' not found", name); + } + self.config.keys.remove(name); + Ok(()) } - pub async fn add_chain(&mut self, args: &AddArgs) -> Result { - self.config.add_chain(args).await + pub fn get_key(&self, key_name: &str) -> Result { + self.config + .keys + .get(key_name) + .cloned() + .ok_or(anyhow!("Key '{}' not found", key_name)) + } + + pub async fn add_chain(&mut self, chain: ChainDefinition) -> Result<()> { + // Replace if exists, otherwise add + if let Some(pos) = self.config.chains.iter().position(|c| c.name == chain.name) { + self.config.chains[pos] = chain.clone(); + } else { + self.config.chains.push(chain.clone()); + } + + Ok(()) } pub fn list_chains(&self) -> &[ChainDefinition] { - self.config.list_chains() + &self.config.chains + } + + /// Add or update a custom variable + pub fn set_variable(&mut self, name: &str, value: &str) { + self.config + .variables + .insert(name.to_string(), value.to_string()); + } + + /// Get a custom variable's value + pub fn get_variable(&self, name: &str) -> Option<&String> { + self.config.variables.get(name) + } + + /// Remove a custom variable + pub fn remove_variable(&mut self, name: &str) -> Result<()> { + if !self.config.variables.contains_key(name) { + anyhow::bail!("Variable '{}' not found", name); + } + self.config.variables.remove(name); + Ok(()) + } + + /// List all custom variables + pub fn list_variables(&self) -> Vec<(String, String)> { + self.config + .variables + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() } pub async fn save(&self) -> Result<()> { @@ -106,41 +162,6 @@ impl Config { Ok(config) } - pub async fn add_key(&mut self, name: &str, key: Key) -> Result<()> { - if self.keys.contains_key(name) { - anyhow::bail!("Key '{}' already exists", name); - } - self.keys.insert(name.to_string(), key); - Ok(()) - } - - pub fn list_keys(&self) -> Result> { - Ok(self - .keys - .iter() - .map(|(n, k)| (n.clone(), k.clone())) - .collect()) - } - - pub fn remove_key(&mut self, name: &str) -> Result<()> { - if !self.keys.contains_key(name) { - anyhow::bail!("Key '{}' not found", name); - } - self.keys.remove(name); - Ok(()) - } - - pub fn get_key(&self, key_name: &str) -> Result { - self.keys - .get(key_name) - .cloned() - .ok_or(anyhow!("Key '{}' not found", key_name)) - } - - pub fn list_chains(&self) -> &[ChainDefinition] { - &self.chains - } - pub fn get_chain(&self, name_or_id: &str) -> Result { // Try to parse as chain ID first if let Ok(chain_id) = name_or_id.parse::() { @@ -159,7 +180,7 @@ impl Config { pub fn get_chain_config_by_name(&self, name: &str) -> Result { Ok(self - .list_chains() + .chains .iter() .find(|chain| chain.name == name) .ok_or(anyhow!("Chain not found"))? @@ -168,26 +189,13 @@ impl Config { pub fn get_chain_config_by_id(&self, chain_id: u64) -> Result { Ok(self - .list_chains() + .chains .iter() .find(|chain| chain.chain_id == chain_id) .ok_or(anyhow!("Chain not found"))? .clone()) } - pub async fn add_chain(&mut self, args: &AddArgs) -> Result { - let chain = ChainDefinition::new(args).await?; - - // Replace if exists, otherwise add - if let Some(pos) = self.chains.iter().position(|c| c.name == chain.name) { - self.chains[pos] = chain.clone(); - } else { - self.chains.push(chain.clone()); - } - - Ok(chain) - } - pub async fn write(&self) -> Result<()> { let json = serde_json::to_string_pretty(self)?; tokio::fs::write( diff --git a/src/init.rs b/src/init.rs index e7e5a7f..694d640 100644 --- a/src/init.rs +++ b/src/init.rs @@ -3,19 +3,16 @@ use crate::{ chain::DEFAULT_KEY_NAME, - chainlist::fetch_all_chains, - config::{config_exists, Chainz, Config}, + config::{config_exists, Chainz}, key::Key, opt, }; use alloy::signers::local::PrivateKeySigner; use anyhow::Result; -use dialoguer::{Confirm, Input, MultiSelect}; +use colored::Colorize; +use dialoguer::{Confirm, Input}; const INFURA_API_KEY_ENV_VAR: &str = "INFURA_API_KEY"; -static DEFAULT_INIT_CHAINS: &[u64] = &[ - 1, 56, 8453, 42161, 43114, 137, 130, 1301, 10, 81457, 59144, 100, 167000, 534352, 11155111, -]; pub async fn handle_init() -> Result<()> { if config_exists().await? { @@ -37,10 +34,10 @@ pub async fn handle_init() -> Result<()> { } async fn initialize_with_wizard() -> Result { + println!("\n{}", "Chainz Initialization".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); println!("Chainz Init"); - let mut config = Config::default(); - - // Configure environment prefix + let mut chainz = Chainz::new(); let private_key = { let input = rpassword::prompt_password("Enter default private key (Optional): ")?; @@ -52,7 +49,7 @@ async fn initialize_with_wizard() -> Result { input } }; - config + chainz .add_key(DEFAULT_KEY_NAME, Key::PrivateKey(private_key)) .await?; @@ -62,45 +59,27 @@ async fn initialize_with_wizard() -> Result { .allow_empty(true) .interact_text()?; if !infura_api_key.is_empty() { - config - .variables - .insert(INFURA_API_KEY_ENV_VAR.to_string(), infura_api_key); + chainz.set_variable(INFURA_API_KEY_ENV_VAR, &infura_api_key); } - let mut chainz = Chainz::new(config); + // Add chains in a loop until user chooses to exit + loop { + println!("\n{}", "Chain Management".bright_blue().bold()); + println!("{}", "═".bright_black().repeat(50)); + let should_add = Confirm::new() + .with_prompt("Would you like to add another chain?") + .default(true) + .interact()?; - // Select chains to add - // TODO: fzf? - let available_chains = fetch_all_chains() - .await? - .into_iter() - .map(|c| (c.name, c.chain_id)) - .filter(|(_, id)| DEFAULT_INIT_CHAINS.contains(id)) - .collect::>(); + if !should_add { + break; + } - let selections = MultiSelect::new() - .with_prompt("Select chains to configure") - .items( - &available_chains - .iter() - .map(|(name, _)| name) - .collect::>(), - ) - .interact()?; + let args = opt::AddArgs {}; - for &idx in selections.iter() { - let (name, chain_id) = &available_chains[idx]; - let args = opt::AddArgs { - name: Some(name.to_lowercase().replace(" ", "_")), - chain_id: Some(*chain_id), - rpc_url: None, - verification_api_key: None, - // TODO: allow key override - key_name: None, - }; - match chainz.add_chain(&args).await { - Ok(_) => println!("Added {}", name), - Err(e) => println!("Failed to add {}: {}", name, e), + match args.handle(&mut chainz).await { + Ok(chain) => println!("Added chain: {}", chain.name), + Err(e) => println!("Failed to add chain: {}", e), } } diff --git a/src/key.rs b/src/key.rs index ee36d47..189760f 100644 --- a/src/key.rs +++ b/src/key.rs @@ -41,34 +41,36 @@ impl Key { } // TODO: encrypt keys -pub async fn handle_key_command(config: &mut Chainz, cmd: KeyCommand) -> Result<()> { - match cmd { - KeyCommand::Add { name, key } => { - let key = if let Some(k) = key { - k - } else { - rpassword::prompt_password("Enter private key: ")? - }; - config.add_key(&name, Key::PrivateKey(key)).await?; - println!("Added key '{}'", name); - config.save().await?; - } - KeyCommand::List => { - let keys = config.list_keys()?; - if keys.is_empty() { - println!("No stored keys"); - } else { - println!("Stored keys:"); - for (name, key) in keys { - println!("- {}: {}", name, key.address().unwrap_or_default()); +impl KeyCommand { + pub async fn handle(self, config: &mut Chainz) -> Result<()> { + match self { + KeyCommand::Add { name, key } => { + let key = if let Some(k) = key { + k + } else { + rpassword::prompt_password("Enter private key: ")? + }; + config.add_key(&name, Key::PrivateKey(key)).await?; + println!("Added key '{}'", name); + config.save().await?; + } + KeyCommand::List => { + let keys = config.list_keys()?; + if keys.is_empty() { + println!("No stored keys"); + } else { + println!("Stored keys:"); + for (name, key) in keys { + println!("- {}: {}", name, key.address().unwrap_or_default()); + } } } + KeyCommand::Remove { name } => { + config.remove_key(&name)?; + println!("Removed key '{}'", name); + config.save().await?; + } } - KeyCommand::Remove { name } => { - config.remove_key(&name)?; - println!("Removed key '{}'", name); - config.save().await?; - } + Ok(()) } - Ok(()) } diff --git a/src/main.rs b/src/main.rs index f6ad2e4..4561630 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ pub mod config; pub mod init; pub mod key; pub mod opt; +pub mod var; pub mod variables; use config::Chainz; @@ -24,12 +25,19 @@ async fn main() -> Result<()> { init::handle_init().await?; } opt::Command::Key { cmd } => { - key::handle_key_command(&mut chainz, cmd).await?; + cmd.handle(&mut chainz).await?; + } + opt::Command::Var { cmd } => { + cmd.handle(&mut chainz).await?; } opt::Command::Add { args } => { - chainz.add_chain(&args).await?; - println!("Added chain {}", args.name.unwrap_or_default()); - chainz.save().await?; + let chain = args.handle(&mut chainz).await?; + println!("Added chain {}", chain.name); + } + opt::Command::Update { args } => { + let chain = args.handle(&mut chainz).await?; + println!("\nFinal configuration:"); + println!("{}", chain); } opt::Command::List => { let chains = chainz.list_chains(); diff --git a/src/opt.rs b/src/opt.rs index 22383f8..5f9d702 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -13,30 +13,59 @@ pub struct Opt { #[derive(Debug, StructOpt)] #[structopt(about = "Subcommands for chainz")] pub enum Command { - /// Initialize a new configuration with wizard + /// Initialize a new configuration through an interactive wizard + /// + /// Guides you through setting up your first chain and private key. + /// Creates a new configuration file if none exists. Init {}, - #[structopt(about = "Add a new chain")] + + /// Add a new chain configuration + /// + /// Supports both interactive and command-line configuration. + /// If options are omitted, will prompt for values. + /// + /// Example: chainz add --name ethereum --chain-id 1 --rpc-url https://eth.llamarpc.com Add { #[structopt(flatten)] args: AddArgs, }, - #[structopt( - about = "Use a chain by name or chainid. Writes to a local .env which can be sourced" - )] + + /// Update an existing chain's configuration + Update { + #[structopt(flatten)] + args: UpdateArgs, + }, + + /// Use a specific chain configuration + /// + /// Sets up environment for working with a specific chain. + /// Can identify chain by name or chain ID. + /// + /// Flags: + /// -p, --print : Print variables to stdout instead of writing to .env + /// -e, --export : Include 'export' prefix in output + /// + /// Example: chainz use ethereum --print Use { + /// Chain name or ID to select name_or_id: String, + /// Print to stdout instead of writing to .env #[structopt(short, long)] print: bool, + /// Include 'export' prefix in output #[structopt(short, long)] export: bool, }, - #[structopt(about = "List all chains")] + + /// List all configured chains + /// + /// Displays all chains with their details: + /// - Name + /// - Chain ID + /// - RPC URL + /// - Associated private key name List, - #[structopt(about = "Manage Private Keys")] - Key { - #[structopt(subcommand)] - cmd: KeyCommand, - }, + #[structopt( about = "Execute a command", long_about = "Execute a command with chain-specific variables expanded.\n\n\ @@ -56,6 +85,31 @@ pub enum Command { #[structopt(last = true)] command: Vec, }, + + /// Manage private keys + /// + /// Subcommands for adding, listing, and removing private keys. + /// Keys are stored encrypted in the configuration. + /// + /// Example: chainz key add mykey + Key { + #[structopt(subcommand)] + cmd: KeyCommand, + }, + + /// Manage custom variables + /// + /// Variables can be used in command expansion with @varname syntax + /// + /// Subcommands: + /// set : Set or update a variable + /// get : Get a variable's value + /// list : List all variables + /// rm : Remove a variable + Var { + #[structopt(subcommand)] + cmd: VarCommand, + }, } #[derive(Debug, StructOpt)] @@ -78,15 +132,30 @@ pub enum KeyCommand { } #[derive(Debug, StructOpt)] -pub struct AddArgs { - #[structopt(short, long)] - pub name: Option, - #[structopt(short, long)] - pub chain_id: Option, - #[structopt(short, long)] - pub rpc_url: Option, - #[structopt(short, long)] - pub verification_api_key: Option, - #[structopt(short, long)] - pub key_name: Option, +pub enum VarCommand { + /// Set or update a variable + Set { + /// Variable name + name: String, + /// Variable value + value: String, + }, + /// Get a variable's value + Get { + /// Variable name + name: String, + }, + /// List all variables + List, + /// Remove a variable + Rm { + /// Variable name + name: String, + }, } + +#[derive(Debug, StructOpt)] +pub struct UpdateArgs {} + +#[derive(Debug, StructOpt)] +pub struct AddArgs {} diff --git a/src/var.rs b/src/var.rs new file mode 100644 index 0000000..9c1baf3 --- /dev/null +++ b/src/var.rs @@ -0,0 +1,37 @@ +// module for storing configurations of encrypted private keys + +use crate::{config::Chainz, opt::VarCommand}; +use anyhow::Result; + +impl VarCommand { + pub async fn handle(self, chainz: &mut Chainz) -> Result<()> { + match self { + VarCommand::Set { name, value } => { + chainz.set_variable(&name, &value); + chainz.save().await?; + println!("Set variable {} = {}", name, value); + } + VarCommand::Get { name } => match chainz.get_variable(&name) { + Some(value) => println!("{} = {}", name, value), + None => println!("Variable '{}' not found", name), + }, + VarCommand::List => { + let vars = chainz.list_variables(); + if vars.is_empty() { + println!("No variables set"); + } else { + println!("Variables:"); + for (name, value) in vars { + println!(" {} = {}", name, value); + } + } + } + VarCommand::Rm { name } => { + chainz.remove_variable(&name)?; + chainz.save().await?; + println!("Removed variable '{}'", name); + } + } + Ok(()) + } +}