diff --git a/bin/katana/src/cli/config.rs b/bin/katana/src/cli/config.rs index 2bd493bdc0..7ffebd4435 100644 --- a/bin/katana/src/cli/config.rs +++ b/bin/katana/src/cli/config.rs @@ -2,20 +2,39 @@ use anyhow::Result; use clap::Args; use katana_chain_spec::rollup::file::ChainConfigDir; use katana_primitives::chain::ChainId; +use starknet::core::utils::parse_cairo_short_string; #[derive(Debug, Args)] pub struct ConfigArgs { /// The chain id. #[arg(value_parser = ChainId::parse)] - chain: ChainId, + chain: Option, } impl ConfigArgs { pub fn execute(self) -> Result<()> { - let cs = ChainConfigDir::open(&self.chain)?; - let path = cs.config_path(); - let config = std::fs::read_to_string(&path)?; - println!("File: {}\n\n{config}", path.display()); + match self.chain { + Some(chain) => { + let cs = ChainConfigDir::open(&chain)?; + let path = cs.config_path(); + let config = std::fs::read_to_string(&path)?; + println!("File: {}\n\n{config}", path.display()); + } + + None => { + let chains = katana_chain_spec::rollup::file::list()?; + for chain in chains { + // TODO: + // We can't just assume that the id is a valid (and readable) ascii string + // as we don' yet handle that elegently in the `ChainId` type itself. The ids + // returned by `list` will be of the `ChainId::Id` variant and thus + // will display in hex form. But for now, it's fine to assume that because we + // only limit valid ASCII string in the `katana init` flow. + let name = parse_cairo_short_string(&chain.id())?; + println!("{name}"); + } + } + } Ok(()) } } diff --git a/crates/katana/chain-spec/src/rollup/file.rs b/crates/katana/chain-spec/src/rollup/file.rs index 43af73e2bf..12529bfdd7 100644 --- a/crates/katana/chain-spec/src/rollup/file.rs +++ b/crates/katana/chain-spec/src/rollup/file.rs @@ -1,6 +1,6 @@ use std::fs::File; use std::io::{self, BufReader, BufWriter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use katana_primitives::chain::ChainId; use katana_primitives::genesis::json::GenesisJson; @@ -33,7 +33,23 @@ pub enum Error { } pub fn read(id: &ChainId) -> Result { - let dir = ChainConfigDir::open(id)?; + read_at(local_dir()?, id) +} + +pub fn write(chain_spec: &ChainSpec) -> Result<(), Error> { + write_at(local_dir()?, chain_spec) +} + +/// List all of the available chain configurations. +/// +/// This will list only the configurations that are stored in the default local directory. See +/// [`local_dir`]. +pub fn list() -> Result, Error> { + list_at(local_dir()?) +} + +fn read_at>(dir: P, id: &ChainId) -> Result { + let dir = ChainConfigDir::open_at(dir, id)?; let chain_spec: ChainSpecFile = { let content = std::fs::read_to_string(dir.config_path())?; @@ -54,8 +70,8 @@ pub fn read(id: &ChainId) -> Result { }) } -pub fn write(chain_spec: &ChainSpec) -> Result<(), Error> { - let dir = ChainConfigDir::create(&chain_spec.id)?; +fn write_at>(dir: P, chain_spec: &ChainSpec) -> Result<(), Error> { + let dir = ChainConfigDir::create_at(dir, &chain_spec.id)?; { let cfg = ChainSpecFile { @@ -77,6 +93,35 @@ pub fn write(chain_spec: &ChainSpec) -> Result<(), Error> { Ok(()) } +fn list_at>(dir: P) -> Result, Error> { + let mut chains = Vec::new(); + let dir = dir.as_ref(); + + if dir.exists() { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + + // Ignore entry that is:- + // + // - not a directory + // - name can't be parse as chain id + // - config file is not found inside the directory + if entry.file_type()?.is_dir() { + if let Some(name) = entry.file_name().to_str() { + if let Ok(chain_id) = ChainId::parse(name) { + let cs = ChainConfigDir::open_at(dir, &chain_id).expect("must exist"); + if cs.config_path().exists() { + chains.push(chain_id); + } + } + } + } + } + } + + Ok(chains) +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] struct ChainSpecFile { @@ -93,12 +138,31 @@ const KATANA_LOCAL_DIR: &str = "katana"; pub struct ChainConfigDir(PathBuf); impl ChainConfigDir { - /// Create a new config directory for the given chain ID. + /// Creates a new config directory for the given chain ID. + /// + /// The directory will be created at `$LOCAL_DIR/`, where `$LOCAL_DIR` is the path returned + /// by [`local_dir`]. /// /// This will create the directory if it does not yet exist. pub fn create(id: &ChainId) -> Result { + Self::create_at(local_dir()?, id) + } + + /// Opens an existing config directory for the given chain ID. + /// + /// The path of the directory is expected to be `$LOCAL_DIR/`, where `$LOCAL_DIR` is the + /// path returned by [`local_dir`]. + /// + /// # Errors + /// + /// This function will return an error if no directory exists with the given chain ID. + pub fn open(id: &ChainId) -> Result { + Self::open_at(local_dir()?, id) + } + + pub fn create_at>(dir: P, id: &ChainId) -> Result { let id = id.to_string(); - let path = local_dir()?.join(id); + let path = dir.as_ref().join(id); if !path.exists() { std::fs::create_dir_all(&path)?; @@ -107,12 +171,9 @@ impl ChainConfigDir { Ok(Self(path)) } - /// Open an existing config directory for the given chain ID. - /// - /// This will return an error if the no config directory exists for the given chain ID. - pub fn open(id: &ChainId) -> Result { + pub fn open_at>(dir: P, id: &ChainId) -> Result { let id = id.to_string(); - let path = local_dir()?.join(&id); + let path = dir.as_ref().join(&id); if !path.exists() { return Err(Error::DirectoryNotFound { id: id.clone() }); @@ -151,25 +212,43 @@ pub fn local_dir() -> Result { #[cfg(test)] mod tests { + use std::path::Path; + use std::sync::OnceLock; + + use katana_primitives::chain::ChainId; + use katana_primitives::genesis::Genesis; use katana_primitives::ContractAddress; + use tempfile::TempDir; use url::Url; - use super::*; + use super::Error; + use crate::rollup::file::{local_dir, ChainConfigDir, KATANA_LOCAL_DIR}; + use crate::rollup::{ChainSpec, FeeContract}; + use crate::SettlementLayer; + + static TEMPDIR: OnceLock = OnceLock::new(); - // To make sure the path returned by `local_dir` is always the same across - // testes and is created inside of a temp dir - fn init() { - let temp_dir = tempfile::TempDir::new().unwrap(); - let path = temp_dir.path(); + fn with_temp_dir(f: impl FnOnce(&Path) -> T) -> T { + f(TEMPDIR.get_or_init(|| tempfile::TempDir::new().unwrap()).path()) + } - #[cfg(target_os = "linux")] - if std::env::var("XDG_CONFIG_HOME").is_err() { - std::env::set_var("XDG_CONFIG_HOME", path); + /// Test version of [`super::read`]. + fn read(id: &ChainId) -> Result { + with_temp_dir(|dir| super::read_at(dir, id)) + } + + /// Test version of [`super::write`]. + fn write(chain_spec: &ChainSpec) -> Result<(), Error> { + with_temp_dir(|dir| super::write_at(dir, chain_spec)) + } + + impl ChainConfigDir { + fn open_tmp(id: &ChainId) -> Result { + with_temp_dir(|dir| Self::open_at(dir, id)) } - #[cfg(target_os = "macos")] - if std::env::var("HOME").is_err() { - std::env::set_var("HOME", path); + fn create_tmp(id: &ChainId) -> Result { + with_temp_dir(|dir| Self::create_at(dir, id)) } } @@ -189,8 +268,6 @@ mod tests { #[test] fn test_read_write_chainspec() { - init(); - let chain_spec = chainspec(); let id = chain_spec.id; @@ -204,39 +281,58 @@ mod tests { #[test] fn test_chain_config_dir() { - init(); - let chain_id = ChainId::parse("test").unwrap(); // Test creation - let config_dir = ChainConfigDir::create(&chain_id).unwrap(); + let config_dir = ChainConfigDir::create_tmp(&chain_id).unwrap(); assert!(config_dir.0.exists()); // Test opening existing dir - let opened_dir = ChainConfigDir::open(&chain_id).unwrap(); + let opened_dir = ChainConfigDir::open_tmp(&chain_id).unwrap(); assert_eq!(config_dir.0, opened_dir.0); // Test opening non-existent dir let bad_id = ChainId::parse("nonexistent").unwrap(); - assert!(matches!(ChainConfigDir::open(&bad_id), Err(Error::DirectoryNotFound { .. }))); + assert!(matches!(ChainConfigDir::open_tmp(&bad_id), Err(Error::DirectoryNotFound { .. }))); } #[test] fn test_local_dir() { - init(); - let dir = local_dir().unwrap(); assert!(dir.ends_with(KATANA_LOCAL_DIR)); } #[test] fn test_config_paths() { - init(); - let chain_id = ChainId::parse("test").unwrap(); - let config_dir = ChainConfigDir::create(&chain_id).unwrap(); + let config_dir = ChainConfigDir::create_tmp(&chain_id).unwrap(); assert!(config_dir.config_path().ends_with("config.toml")); assert!(config_dir.genesis_path().ends_with("genesis.json")); } + + #[test] + fn test_list_chain_specs() { + let dir = tempfile::TempDir::new().unwrap().into_path(); + + let listed_chains = super::list_at(&dir).unwrap(); + assert_eq!(listed_chains.len(), 0, "Must be empty initially"); + + // Create some dummy chain specs + let mut chain_specs = Vec::new(); + for i in 1..=3 { + let mut spec = chainspec(); + // update the chain id to make they're unqiue + spec.id = ChainId::parse(&format!("chain_{i}")).unwrap(); + chain_specs.push(spec); + } + + // Write them to disk + for spec in &chain_specs { + super::write_at(&dir, spec).unwrap(); + } + + let listed_chains = super::list_at(&dir).unwrap(); + assert_eq!(listed_chains.len(), chain_specs.len()); + } }