From cbdd2f0c3b641d1ca39a5f5cb6a5382a03c1eaf1 Mon Sep 17 00:00:00 2001 From: glihm Date: Tue, 12 Nov 2024 23:03:24 -0600 Subject: [PATCH] refactor: move cli args to separate crates (#2681) * refactor: move cli args to separate crates * move node args methods to katana-cli crate as well * fix: add missing derive and conversion to config file * remove silent arg * create directory for db dir torii * move to new module * rename node -> args * dont re-export node config file * fmt --------- Co-authored-by: Ammar Arif Co-authored-by: Nasr --- Cargo.lock | 64 +++- Cargo.toml | 4 + bin/katana/Cargo.toml | 21 +- bin/katana/src/cli/mod.rs | 5 +- bin/katana/src/main.rs | 1 - bin/katana/src/utils.rs | 79 ----- bin/torii/Cargo.toml | 1 + bin/torii/src/main.rs | 271 +-------------- crates/katana/cli/Cargo.toml | 38 +++ .../node.rs => crates/katana/cli/src/args.rs | 245 +++----------- crates/katana/cli/src/file.rs | 74 +++++ crates/katana/cli/src/lib.rs | 9 + .../cli => crates/katana/cli/src}/options.rs | 6 + crates/katana/cli/src/utils.rs | 203 ++++++++++++ .../katana/cli}/test-data/genesis.json | 0 .../katana/core/src/service/messaging/mod.rs | 2 +- crates/torii/cli/Cargo.toml | 24 ++ crates/torii/cli/src/args.rs | 312 ++++++++++++++++++ crates/torii/cli/src/lib.rs | 7 + .../torii => crates/torii/cli}/src/options.rs | 24 +- 20 files changed, 805 insertions(+), 585 deletions(-) delete mode 100644 bin/katana/src/utils.rs create mode 100644 crates/katana/cli/Cargo.toml rename bin/katana/src/cli/node.rs => crates/katana/cli/src/args.rs (74%) create mode 100644 crates/katana/cli/src/file.rs create mode 100644 crates/katana/cli/src/lib.rs rename {bin/katana/src/cli => crates/katana/cli/src}/options.rs (98%) create mode 100644 crates/katana/cli/src/utils.rs rename {bin/katana/tests => crates/katana/cli}/test-data/genesis.json (100%) create mode 100644 crates/torii/cli/Cargo.toml create mode 100644 crates/torii/cli/src/args.rs create mode 100644 crates/torii/cli/src/lib.rs rename {bin/torii => crates/torii/cli}/src/options.rs (94%) diff --git a/Cargo.lock b/Cargo.lock index 9c06203059..e0012c75a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8063,31 +8063,17 @@ dependencies = [ name = "katana" version = "1.0.0" dependencies = [ - "alloy-primitives", "anyhow", "assert_matches", "byte-unit", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", "clap", "clap_complete", "comfy-table", - "console", - "dojo-utils", - "katana-core", + "katana-cli", "katana-db", "katana-node", - "katana-primitives", - "katana-slot-controller", - "serde", - "serde_json", "shellexpand", "starknet 0.12.0", - "tokio", - "toml 0.8.19", - "tracing", - "tracing-log 0.1.4", - "tracing-subscriber", - "url", ] [[package]] @@ -8105,6 +8091,33 @@ dependencies = [ "starknet_api", ] +[[package]] +name = "katana-cli" +version = "1.0.0" +dependencies = [ + "alloy-primitives", + "anyhow", + "assert_matches", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", + "clap", + "console", + "dojo-utils", + "katana-core", + "katana-node", + "katana-primitives", + "katana-slot-controller", + "serde", + "serde_json", + "shellexpand", + "starknet 0.12.0", + "tokio", + "toml 0.8.19", + "tracing", + "tracing-log 0.1.4", + "tracing-subscriber", + "url", +] + [[package]] name = "katana-codecs" version = "1.0.0" @@ -12817,9 +12830,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -14924,6 +14937,7 @@ dependencies = [ "tokio-stream", "tokio-util", "toml 0.8.19", + "torii-cli", "torii-core", "torii-graphql", "torii-grpc", @@ -14937,6 +14951,22 @@ dependencies = [ "webbrowser", ] +[[package]] +name = "torii-cli" +version = "1.0.0" +dependencies = [ + "anyhow", + "assert_matches", + "camino", + "clap", + "dojo-utils", + "serde", + "starknet 0.12.0", + "toml 0.8.19", + "torii-core", + "url", +] + [[package]] name = "torii-client" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index b22e0bf8c2..a5b69bf2e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "crates/katana/storage/provider", "crates/katana/tasks", "crates/katana/trie", + "crates/katana/cli", "crates/metrics", "crates/saya/core", "crates/saya/provider", @@ -46,6 +47,7 @@ members = [ "crates/torii/client", "crates/torii/server", "crates/torii/types-test", + "crates/torii/cli", "examples/spawn-and-move", "scripts/verify_db_balances", "xtask/generate-test-db", @@ -104,6 +106,7 @@ katana-runner = { path = "crates/katana/runner" } katana-slot-controller = { path = "crates/katana/controller" } katana-tasks = { path = "crates/katana/tasks" } katana-trie = { path = "crates/katana/trie" } +katana-cli = { path = "crates/katana/cli" } # torii torii-client = { path = "crates/torii/client" } @@ -112,6 +115,7 @@ torii-graphql = { path = "crates/torii/graphql" } torii-grpc = { path = "crates/torii/grpc" } torii-relay = { path = "crates/torii/libp2p" } torii-server = { path = "crates/torii/server" } +torii-cli = { path = "crates/torii/cli" } # saya saya-core = { path = "crates/saya/core" } diff --git a/bin/katana/Cargo.toml b/bin/katana/Cargo.toml index dcb52e154d..1845e968f5 100644 --- a/bin/katana/Cargo.toml +++ b/bin/katana/Cargo.toml @@ -7,38 +7,23 @@ repository.workspace = true version.workspace = true [dependencies] -katana-core.workspace = true +katana-cli.workspace = true katana-db.workspace = true katana-node.workspace = true -katana-primitives.workspace = true -katana-slot-controller = { workspace = true, optional = true } -alloy-primitives.workspace = true anyhow.workspace = true byte-unit = "5.1.4" -cainome-cairo-serde.workspace = true clap.workspace = true clap_complete.workspace = true comfy-table = "7.1.1" -console.workspace = true -dojo-utils.workspace = true -serde.workspace = true -serde_json.workspace = true shellexpand = "3.1.0" -tokio.workspace = true -toml.workspace = true -tracing.workspace = true -tracing-log.workspace = true -tracing-subscriber.workspace = true -url.workspace = true [dev-dependencies] assert_matches.workspace = true starknet.workspace = true [features] -default = [ "jemalloc", "slot" ] +default = [ "jemalloc", "katana-cli/slot" ] jemalloc = [ ] -slot = [ "dep:katana-slot-controller", "katana-primitives/slot" ] -starknet-messaging = [ "katana-node/starknet-messaging" ] +starknet-messaging = [ "katana-cli/starknet-messaging" ] diff --git a/bin/katana/src/cli/mod.rs b/bin/katana/src/cli/mod.rs index 04aa84992e..da4cd4e34b 100644 --- a/bin/katana/src/cli/mod.rs +++ b/bin/katana/src/cli/mod.rs @@ -1,10 +1,9 @@ mod db; -mod node; -mod options; use anyhow::Result; use clap::{Args, CommandFactory, Parser, Subcommand}; use clap_complete::Shell; +use katana_cli::NodeArgs; use katana_node::version::VERSION; #[derive(Parser)] @@ -14,7 +13,7 @@ pub struct Cli { commands: Option, #[command(flatten)] - node: node::NodeArgs, + node: NodeArgs, } impl Cli { diff --git a/bin/katana/src/main.rs b/bin/katana/src/main.rs index b9eb84e798..ad7b3709ed 100644 --- a/bin/katana/src/main.rs +++ b/bin/katana/src/main.rs @@ -1,7 +1,6 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] mod cli; -mod utils; use anyhow::Result; use clap::Parser; diff --git a/bin/katana/src/utils.rs b/bin/katana/src/utils.rs deleted file mode 100644 index 394ae8e596..0000000000 --- a/bin/katana/src/utils.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::fmt::Display; -use std::path::PathBuf; - -use anyhow::{Context, Result}; -use clap::builder::PossibleValue; -use clap::ValueEnum; -use katana_primitives::block::{BlockHash, BlockHashOrNumber, BlockNumber}; -use katana_primitives::genesis::json::GenesisJson; -use katana_primitives::genesis::Genesis; -use serde::{Deserialize, Serialize}; - -pub fn parse_seed(seed: &str) -> [u8; 32] { - let seed = seed.as_bytes(); - - if seed.len() >= 32 { - unsafe { *(seed[..32].as_ptr() as *const [u8; 32]) } - } else { - let mut actual_seed = [0u8; 32]; - seed.iter().enumerate().for_each(|(i, b)| actual_seed[i] = *b); - actual_seed - } -} - -/// Used as clap value parser for [Genesis]. -pub fn parse_genesis(value: &str) -> Result { - let path = PathBuf::from(shellexpand::full(value)?.into_owned()); - let genesis = Genesis::try_from(GenesisJson::load(path)?)?; - Ok(genesis) -} - -/// If the value starts with `0x`, it is parsed as a [`BlockHash`], otherwise as a [`BlockNumber`]. -pub fn parse_block_hash_or_number(value: &str) -> Result { - if value.starts_with("0x") { - Ok(BlockHashOrNumber::Hash(BlockHash::from_hex(value)?)) - } else { - let num = value.parse::().context("could not parse block number")?; - Ok(BlockHashOrNumber::Num(num)) - } -} - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)] -pub enum LogFormat { - Json, - #[default] - Full, -} - -impl ValueEnum for LogFormat { - fn value_variants<'a>() -> &'a [Self] { - &[Self::Json, Self::Full] - } - - fn to_possible_value(&self) -> Option { - match self { - Self::Json => Some(PossibleValue::new("json")), - Self::Full => Some(PossibleValue::new("full")), - } - } -} - -impl Display for LogFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Json => write!(f, "json"), - Self::Full => write!(f, "full"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_genesis_file() { - let path = "./tests/test-data/genesis.json"; - parse_genesis(path).unwrap(); - } -} diff --git a/bin/torii/Cargo.toml b/bin/torii/Cargo.toml index a23a6cfcd8..b9bf3cf1cf 100644 --- a/bin/torii/Cargo.toml +++ b/bin/torii/Cargo.toml @@ -33,6 +33,7 @@ starknet.workspace = true tokio-stream = "0.1.11" tokio-util = "0.7.7" tokio.workspace = true +torii-cli.workspace = true torii-core.workspace = true torii-graphql.workspace = true torii-grpc = { workspace = true, features = [ "server" ] } diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index fd130ce2f0..dae312ad73 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -12,28 +12,24 @@ use std::cmp; use std::net::SocketAddr; -use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use anyhow::Result; use clap::Parser; use dojo_metrics::exporters::prometheus::PrometheusRecorder; -use dojo_utils::parse::parse_url; use dojo_world::contracts::world::WorldContractReader; -use serde::{Deserialize, Serialize}; use sqlx::sqlite::{ SqliteAutoVacuum, SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous, }; use sqlx::SqlitePool; -use starknet::core::types::Felt; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use tempfile::NamedTempFile; use tokio::sync::broadcast; use tokio::sync::broadcast::Sender; use tokio_stream::StreamExt; +use torii_cli::ToriiArgs; use torii_core::engine::{Engine, EngineConfig, IndexingFlags, Processors}; use torii_core::executor::Executor; use torii_core::processors::store_transaction::StoreTransactionProcessor; @@ -46,140 +42,8 @@ use tracing::{error, info}; use tracing_subscriber::{fmt, EnvFilter}; use url::{form_urlencoded, Url}; -mod options; - -use options::*; - pub(crate) const LOG_TARGET: &str = "torii::cli"; -const DEFAULT_RPC_URL: &str = "http://0.0.0.0:5050"; - -/// Dojo World Indexer -#[derive(Parser, Debug)] -#[command(name = "torii", author, version, about, long_about = None)] -struct ToriiArgs { - /// The world to index - #[arg(short, long = "world", env = "DOJO_WORLD_ADDRESS")] - world_address: Option, - - /// The sequencer rpc endpoint to index. - #[arg(long, value_name = "URL", default_value = DEFAULT_RPC_URL, value_parser = parse_url)] - rpc: Url, - - /// Database filepath (ex: indexer.db). If specified file doesn't exist, it will be - /// created. Defaults to in-memory database. - #[arg(long)] - #[arg( - value_name = "PATH", - help = "Database filepath. If specified directory doesn't exist, it will be created. \ - Defaults to in-memory database." - )] - db_dir: Option, - - /// The external url of the server, used for configuring the GraphQL Playground in a hosted - /// environment - #[arg(long, value_parser = parse_url, help = "The external url of the server, used for configuring the GraphQL Playground in a hosted environment.")] - external_url: Option, - - /// Open World Explorer on the browser. - #[arg(long, help = "Open World Explorer on the browser.")] - explorer: bool, - - #[command(flatten)] - metrics: MetricsOptions, - - #[command(flatten)] - indexing: IndexingOptions, - - #[command(flatten)] - events: EventsOptions, - - #[command(flatten)] - server: ServerOptions, - - #[command(flatten)] - relay: RelayOptions, - - /// Configuration file - #[arg(long, help = "Configuration file to setup Torii.")] - config: Option, -} - -impl ToriiArgs { - pub fn with_config_file(mut self) -> Result { - let config: ToriiArgsConfig = if let Some(path) = &self.config { - toml::from_str(&std::fs::read_to_string(path)?)? - } else { - return Ok(self); - }; - - // the CLI (self) takes precedence over the config file. - // Currently, the merge is made at the top level of the commands. - // We may add recursive merging in the future. - - if self.world_address.is_none() { - self.world_address = config.world_address; - } - - if self.rpc == Url::parse(DEFAULT_RPC_URL).unwrap() { - if let Some(rpc) = config.rpc { - self.rpc = rpc; - } - } - - if self.db_dir.is_none() { - self.db_dir = config.db_dir; - } - - if self.external_url.is_none() { - self.external_url = config.external_url; - } - - // Currently the comparison it's only at the top level. - // Need to make it more granular. - - if !self.explorer { - self.explorer = config.explorer.unwrap_or_default(); - } - - if self.metrics == MetricsOptions::default() { - self.metrics = config.metrics.unwrap_or_default(); - } - - if self.indexing == IndexingOptions::default() { - self.indexing = config.indexing.unwrap_or_default(); - } - - if self.events == EventsOptions::default() { - self.events = config.events.unwrap_or_default(); - } - - if self.server == ServerOptions::default() { - self.server = config.server.unwrap_or_default(); - } - - if self.relay == RelayOptions::default() { - self.relay = config.relay.unwrap_or_default(); - } - - Ok(self) - } -} - -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct ToriiArgsConfig { - pub world_address: Option, - pub rpc: Option, - pub db_dir: Option, - pub external_url: Option, - pub explorer: Option, - pub metrics: Option, - pub indexing: Option, - pub events: Option, - pub server: Option, - pub relay: Option, -} - #[tokio::main] async fn main() -> anyhow::Result<()> { let mut args = ToriiArgs::parse().with_config_file()?; @@ -212,8 +76,14 @@ async fn main() -> anyhow::Result<()> { .expect("Error setting Ctrl-C handler"); let tempfile = NamedTempFile::new()?; - let database_path = - if let Some(db_dir) = args.db_dir { db_dir } else { tempfile.path().to_path_buf() }; + let database_path = if let Some(db_dir) = args.db_dir { + // Create the directory if it doesn't exist + std::fs::create_dir_all(&db_dir)?; + // Set the database file path inside the directory + db_dir.join("torii.db") + } else { + tempfile.path().to_path_buf() + }; let mut options = SqliteConnectOptions::from_str(&database_path.to_string_lossy())? .create_if_missing(true) @@ -378,126 +248,3 @@ async fn spawn_rebuilding_graphql_server( } } } - -#[cfg(test)] -mod test { - use std::net::{IpAddr, Ipv4Addr}; - - use super::*; - - #[test] - fn test_cli_precedence() { - // CLI args must take precedence over the config file. - let content = r#" - world_address = "0x1234" - rpc = "http://0.0.0.0:5050" - db_dir = "/tmp/torii-test" - - [events] - raw = true - historical = [ - "ns-E", - "ns-EH" - ] - "#; - let path = std::env::temp_dir().join("torii-config2.json"); - std::fs::write(&path, content).unwrap(); - - let path_str = path.to_string_lossy().to_string(); - - let args = vec![ - "torii", - "--world", - "0x9999", - "--rpc", - "http://0.0.0.0:6060", - "--db-dir", - "/tmp/torii-test2", - "--events.raw", - "false", - "--events.historical", - "a-A", - "--config", - path_str.as_str(), - ]; - - let torii_args = ToriiArgs::parse_from(args).with_config_file().unwrap(); - - assert_eq!(torii_args.world_address, Some(Felt::from_str("0x9999").unwrap())); - assert_eq!(torii_args.rpc, Url::parse("http://0.0.0.0:6060").unwrap()); - assert_eq!(torii_args.db_dir, Some(PathBuf::from("/tmp/torii-test2"))); - assert!(!torii_args.events.raw); - assert_eq!(torii_args.events.historical, Some(vec!["a-A".to_string()])); - assert_eq!(torii_args.server, ServerOptions::default()); - } - - #[test] - fn test_config_fallback() { - let content = r#" - world_address = "0x1234" - rpc = "http://0.0.0.0:2222" - db_dir = "/tmp/torii-test" - - [events] - raw = false - historical = [ - "ns-E", - "ns-EH" - ] - - [server] - http_addr = "127.0.0.1" - http_port = 7777 - http_cors_origins = ["*"] - - [indexing] - events_chunk_size = 9999 - index_pending = true - max_concurrent_tasks = 1000 - index_transactions = false - contracts = [ - "erc20:0x1234", - "erc721:0x5678" - ] - "#; - let path = std::env::temp_dir().join("torii-config.json"); - std::fs::write(&path, content).unwrap(); - - let path_str = path.to_string_lossy().to_string(); - - let args = vec!["torii", "--config", path_str.as_str()]; - - let torii_args = ToriiArgs::parse_from(args).with_config_file().unwrap(); - - assert_eq!(torii_args.world_address, Some(Felt::from_str("0x1234").unwrap())); - assert_eq!(torii_args.rpc, Url::parse("http://0.0.0.0:2222").unwrap()); - assert_eq!(torii_args.db_dir, Some(PathBuf::from("/tmp/torii-test"))); - assert!(!torii_args.events.raw); - assert_eq!( - torii_args.events.historical, - Some(vec!["ns-E".to_string(), "ns-EH".to_string()]) - ); - assert_eq!(torii_args.indexing.events_chunk_size, 9999); - assert_eq!(torii_args.indexing.blocks_chunk_size, 10240); - assert!(torii_args.indexing.index_pending); - assert_eq!(torii_args.indexing.polling_interval, 500); - assert_eq!(torii_args.indexing.max_concurrent_tasks, 1000); - assert!(!torii_args.indexing.index_transactions); - assert_eq!( - torii_args.indexing.contracts, - vec![ - Contract { - address: Felt::from_str("0x1234").unwrap(), - r#type: ContractType::ERC20 - }, - Contract { - address: Felt::from_str("0x5678").unwrap(), - r#type: ContractType::ERC721 - } - ] - ); - assert_eq!(torii_args.server.http_addr, IpAddr::V4(Ipv4Addr::LOCALHOST)); - assert_eq!(torii_args.server.http_port, 7777); - assert_eq!(torii_args.server.http_cors_origins, Some(vec!["*".to_string()])); - } -} diff --git a/crates/katana/cli/Cargo.toml b/crates/katana/cli/Cargo.toml new file mode 100644 index 0000000000..9d58c2c038 --- /dev/null +++ b/crates/katana/cli/Cargo.toml @@ -0,0 +1,38 @@ +[package] +edition.workspace = true +license.workspace = true +name = "katana-cli" +repository.workspace = true +version.workspace = true + +[dependencies] +dojo-utils.workspace = true +katana-core.workspace = true +katana-node.workspace = true +katana-primitives.workspace = true +katana-slot-controller = { workspace = true, optional = true } + +alloy-primitives.workspace = true +anyhow.workspace = true +cainome-cairo-serde.workspace = true +clap.workspace = true +console.workspace = true +serde.workspace = true +serde_json = "1.0.132" +shellexpand = "3.1.0" +tokio.workspace = true +toml.workspace = true +tracing.workspace = true +tracing-log.workspace = true +tracing-subscriber.workspace = true +url.workspace = true + +[dev-dependencies] +assert_matches.workspace = true +starknet.workspace = true + +[features] +default = [ "slot", "server" ] +slot = [ "dep:katana-slot-controller", "katana-primitives/slot" ] +server = [ ] +starknet-messaging = [ "katana-node/starknet-messaging" ] diff --git a/bin/katana/src/cli/node.rs b/crates/katana/cli/src/args.rs similarity index 74% rename from bin/katana/src/cli/node.rs rename to crates/katana/cli/src/args.rs index 461fc6e036..c2e5524f9b 100644 --- a/bin/katana/src/cli/node.rs +++ b/crates/katana/cli/src/args.rs @@ -1,14 +1,4 @@ -//! Katana binary executable. -//! -//! ## Feature Flags -//! -//! - `jemalloc`: Uses [jemallocator](https://github.com/tikv/jemallocator) as the global allocator. -//! This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc) -//! for more info. -//! - `jemalloc-prof`: Enables [jemallocator's](https://github.com/tikv/jemallocator) heap profiling -//! and leak detection functionality. See [jemalloc's opt.prof](https://jemalloc.net/jemalloc.3.html#opt.prof) -//! documentation for usage details. This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc) -//! for more info. +//! Katana node CLI options and configuration. use std::collections::HashSet; use std::path::PathBuf; @@ -16,7 +6,6 @@ use std::path::PathBuf; use alloy_primitives::U256; use anyhow::{Context, Result}; use clap::Parser; -use console::Style; use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; use katana_core::service::messaging::MessagingConfig; use katana_node::config::db::DbConfig; @@ -27,22 +16,22 @@ use katana_node::config::metrics::MetricsConfig; use katana_node::config::rpc::{ApiKind, RpcConfig}; use katana_node::config::{Config, SequencingConfig}; use katana_primitives::chain_spec::{self, ChainSpec}; -use katana_primitives::class::ClassHash; -use katana_primitives::contract::ContractAddress; -use katana_primitives::genesis::allocation::{DevAllocationsGenerator, GenesisAccountAlloc}; -use katana_primitives::genesis::constant::{ - DEFAULT_LEGACY_ERC20_CLASS_HASH, DEFAULT_LEGACY_UDC_CLASS_HASH, - DEFAULT_PREFUNDED_ACCOUNT_BALANCE, DEFAULT_UDC_ADDRESS, -}; +use katana_primitives::genesis::allocation::DevAllocationsGenerator; +use katana_primitives::genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; use serde::{Deserialize, Serialize}; use tracing::{info, Subscriber}; use tracing_log::LogTracer; use tracing_subscriber::{fmt, EnvFilter}; -use super::options::*; +use crate::file::NodeArgsConfig; +use crate::options::*; +use crate::utils; use crate::utils::{parse_seed, LogFormat}; -#[derive(Parser, Debug, Serialize, Deserialize, Default)] +pub(crate) const LOG_TARGET: &str = "katana::cli"; + +#[derive(Parser, Debug, Serialize, Deserialize, Default, Clone)] +#[command(next_help_heading = "Node options")] pub struct NodeArgs { /// Don't print anything on startup. #[arg(long)] @@ -66,6 +55,10 @@ pub struct NodeArgs { #[arg(value_name = "PATH")] pub db_dir: Option, + /// Configuration file + #[arg(long)] + config: Option, + /// Configure the messaging with an other chain. /// /// Configure the messaging to allow Katana listening/sending messages on a @@ -78,9 +71,11 @@ pub struct NodeArgs { #[command(flatten)] pub logging: LoggingOptions, + #[cfg(feature = "server")] #[command(flatten)] pub metrics: MetricsOptions, + #[cfg(feature = "server")] #[command(flatten)] pub server: ServerOptions, @@ -99,36 +94,10 @@ pub struct NodeArgs { #[cfg(feature = "slot")] #[command(flatten)] pub slot: SlotOptions, - - /// Configuration file - #[arg(long)] - config: Option, } -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct NodeArgsConfig { - pub silent: Option, - pub no_mining: Option, - pub block_time: Option, - pub db_dir: Option, - pub messaging: Option, - pub logging: Option, - pub metrics: Option, - pub server: Option, - pub starknet: Option, - pub gpo: Option, - pub forking: Option, - #[serde(rename = "dev")] - pub development: Option, - - #[cfg(feature = "slot")] - pub slot: Option, -} - -pub(crate) const LOG_TARGET: &str = "katana::cli"; - impl NodeArgs { - pub fn execute(self) -> Result<()> { + pub fn execute(&self) -> Result<()> { self.init_logging()?; tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -137,13 +106,13 @@ impl NodeArgs { .block_on(self.start_node()) } - async fn start_node(self) -> Result<()> { + async fn start_node(&self) -> Result<()> { // Build the node let config = self.config()?; let node = katana_node::build(config).await.context("failed to build node")?; if !self.silent { - print_intro(&self, &node.backend.chain_spec); + utils::print_intro(self, &node.backend.chain_spec); } // Launch the node @@ -191,7 +160,7 @@ impl NodeArgs { Ok(tracing::subscriber::set_global_default(subscriber)?) } - fn config(&self) -> Result { + pub fn config(&self) -> Result { let db = self.db_config(); let rpc = self.rpc_config(); let dev = self.dev_config(); @@ -216,12 +185,20 @@ impl NodeArgs { apis.insert(ApiKind::Dev); } - RpcConfig { - apis, - port: self.server.http_port, - addr: self.server.http_addr, - max_connections: self.server.max_connections, - cors_origins: self.server.http_cors_origins.clone(), + #[cfg(feature = "server")] + { + RpcConfig { + apis, + port: self.server.http_port, + addr: self.server.http_addr, + max_connections: self.server.max_connections, + cors_origins: self.server.http_cors_origins.clone(), + } + } + + #[cfg(not(feature = "server"))] + { + RpcConfig { apis, ..Default::default() } } } @@ -306,18 +283,22 @@ impl NodeArgs { } fn metrics_config(&self) -> Option { + #[cfg(feature = "server")] if self.metrics.metrics { Some(MetricsConfig { addr: self.metrics.metrics_addr, port: self.metrics.metrics_port }) } else { None } + + #[cfg(not(feature = "server"))] + None } /// Parse the node config from the command line arguments and the config file, /// and merge them together prioritizing the command line arguments. pub fn with_config_file(mut self) -> Result { - let config: NodeArgsConfig = if let Some(path) = &self.config { - toml::from_str(&std::fs::read_to_string(path)?)? + let config = if let Some(path) = &self.config { + NodeArgsConfig::read(path)? } else { return Ok(self); }; @@ -326,10 +307,6 @@ impl NodeArgs { // Currently, the merge is made at the top level of the commands. // We may add recursive merging in the future. - if !self.silent { - self.silent = config.silent.unwrap_or_default(); - } - if !self.no_mining { self.no_mining = config.no_mining.unwrap_or_default(); } @@ -348,15 +325,18 @@ impl NodeArgs { } } - if self.metrics == MetricsOptions::default() { - if let Some(metrics) = config.metrics { - self.metrics = metrics; + #[cfg(feature = "server")] + { + if self.server == ServerOptions::default() { + if let Some(server) = config.server { + self.server = server; + } } - } - if self.server == ServerOptions::default() { - if let Some(server) = config.server { - self.server = server; + if self.metrics == MetricsOptions::default() { + if let Some(metrics) = config.metrics { + self.metrics = metrics; + } } } @@ -375,129 +355,10 @@ impl NodeArgs { } } - #[cfg(feature = "slot")] - if self.slot == SlotOptions::default() { - if let Some(slot) = config.slot { - self.slot = slot; - } - } - Ok(self) } } -fn print_intro(args: &NodeArgs, chain: &ChainSpec) { - let mut accounts = chain.genesis.accounts().peekable(); - let account_class_hash = accounts.peek().map(|e| e.1.class_hash()); - let seed = &args.development.seed; - - if args.logging.log_format == LogFormat::Json { - info!( - target: LOG_TARGET, - "{}", - serde_json::json!({ - "accounts": accounts.map(|a| serde_json::json!(a)).collect::>(), - "seed": format!("{}", seed), - }) - ) - } else { - println!( - "{}", - Style::new().red().apply_to( - r" - - -██╗ ██╗ █████╗ ████████╗ █████╗ ███╗ ██╗ █████╗ -██║ ██╔╝██╔══██╗╚══██╔══╝██╔══██╗████╗ ██║██╔══██╗ -█████╔╝ ███████║ ██║ ███████║██╔██╗ ██║███████║ -██╔═██╗ ██╔══██║ ██║ ██╔══██║██║╚██╗██║██╔══██║ -██║ ██╗██║ ██║ ██║ ██║ ██║██║ ╚████║██║ ██║ -╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ -" - ) - ); - - print_genesis_contracts(chain, account_class_hash); - print_genesis_accounts(accounts); - - println!( - r" - -ACCOUNTS SEED -============= -{seed} - " - ); - } -} - -fn print_genesis_contracts(chain: &ChainSpec, account_class_hash: Option) { - println!( - r" -PREDEPLOYED CONTRACTS -================== - -| Contract | ETH Fee Token -| Address | {} -| Class Hash | {:#064x} - -| Contract | STRK Fee Token -| Address | {} -| Class Hash | {:#064x}", - chain.fee_contracts.eth, - DEFAULT_LEGACY_ERC20_CLASS_HASH, - chain.fee_contracts.strk, - DEFAULT_LEGACY_ERC20_CLASS_HASH - ); - - println!( - r" -| Contract | Universal Deployer -| Address | {} -| Class Hash | {:#064x}", - DEFAULT_UDC_ADDRESS, DEFAULT_LEGACY_UDC_CLASS_HASH - ); - - if let Some(hash) = account_class_hash { - println!( - r" -| Contract | Account Contract -| Class Hash | {hash:#064x}" - ) - } -} - -fn print_genesis_accounts<'a, Accounts>(accounts: Accounts) -where - Accounts: Iterator, -{ - println!( - r" - -PREFUNDED ACCOUNTS -==================" - ); - - for (addr, account) in accounts { - if let Some(pk) = account.private_key() { - println!( - r" -| Account address | {addr} -| Private key | {pk:#x} -| Public key | {:#x}", - account.public_key() - ) - } else { - println!( - r" -| Account address | {addr} -| Public key | {:#x}", - account.public_key() - ) - } - } -} - #[cfg(test)] mod test { use std::str::FromStr; @@ -511,7 +372,7 @@ mod test { DEFAULT_INVOCATION_MAX_STEPS, DEFAULT_VALIDATION_MAX_STEPS, }; use katana_primitives::chain::ChainId; - use katana_primitives::{address, felt, Felt}; + use katana_primitives::{address, felt, ContractAddress, Felt}; use super::*; @@ -644,7 +505,7 @@ mod test { let config = NodeArgs::parse_from([ "katana", "--genesis", - "./tests/test-data/genesis.json", + "./test-data/genesis.json", "--gpo.l1-eth-gas-price", "100", "--gpo.l1-strk-gas-price", @@ -700,7 +561,7 @@ chain_id.Named = "Mainnet" "--config", path_str.as_str(), "--genesis", - "./tests/test-data/genesis.json", + "./test-data/genesis.json", "--validate-max-steps", "1234", "--dev", diff --git a/crates/katana/cli/src/file.rs b/crates/katana/cli/src/file.rs new file mode 100644 index 0000000000..b9cd8a0c4c --- /dev/null +++ b/crates/katana/cli/src/file.rs @@ -0,0 +1,74 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use katana_core::service::messaging::MessagingConfig; +use serde::{Deserialize, Serialize}; + +use crate::options::*; +use crate::NodeArgs; + +/// Node arguments configuration file. +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct NodeArgsConfig { + pub no_mining: Option, + pub block_time: Option, + pub db_dir: Option, + pub messaging: Option, + pub logging: Option, + pub starknet: Option, + pub gpo: Option, + pub forking: Option, + #[serde(rename = "dev")] + pub development: Option, + #[cfg(feature = "server")] + pub server: Option, + #[cfg(feature = "server")] + pub metrics: Option, +} + +impl NodeArgsConfig { + pub fn read(path: impl AsRef) -> Result { + let file = std::fs::read_to_string(path)?; + Ok(toml::from_str(&file)?) + } +} + +impl TryFrom for NodeArgsConfig { + type Error = anyhow::Error; + + fn try_from(args: NodeArgs) -> Result { + // Ensure the config file is merged with the CLI arguments. + let args = args.with_config_file()?; + + let mut node_config = NodeArgsConfig { + no_mining: if args.no_mining { Some(true) } else { None }, + block_time: args.block_time, + db_dir: args.db_dir, + messaging: args.messaging, + ..Default::default() + }; + + // Only include the following options if they are not the default. + // This makes the config file more readable. + node_config.logging = + if args.logging == LoggingOptions::default() { None } else { Some(args.logging) }; + node_config.starknet = + if args.starknet == StarknetOptions::default() { None } else { Some(args.starknet) }; + node_config.gpo = + if args.gpo == GasPriceOracleOptions::default() { None } else { Some(args.gpo) }; + node_config.forking = + if args.forking == ForkingOptions::default() { None } else { Some(args.forking) }; + node_config.development = + if args.development == DevOptions::default() { None } else { Some(args.development) }; + + #[cfg(feature = "server")] + { + node_config.server = + if args.server == ServerOptions::default() { None } else { Some(args.server) }; + node_config.metrics = + if args.metrics == MetricsOptions::default() { None } else { Some(args.metrics) }; + } + + Ok(node_config) + } +} diff --git a/crates/katana/cli/src/lib.rs b/crates/katana/cli/src/lib.rs new file mode 100644 index 0000000000..b8a2a0a70b --- /dev/null +++ b/crates/katana/cli/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +pub mod args; +pub mod file; +pub mod options; +pub mod utils; + +pub use args::NodeArgs; +pub use options::*; diff --git a/bin/katana/src/cli/options.rs b/crates/katana/cli/src/options.rs similarity index 98% rename from bin/katana/src/cli/options.rs rename to crates/katana/cli/src/options.rs index 83b90eb0b5..60658c3354 100644 --- a/bin/katana/src/cli/options.rs +++ b/crates/katana/cli/src/options.rs @@ -12,6 +12,7 @@ use std::net::IpAddr; use clap::Args; use katana_node::config::execution::{DEFAULT_INVOCATION_MAX_STEPS, DEFAULT_VALIDATION_MAX_STEPS}; use katana_node::config::metrics::{DEFAULT_METRICS_ADDR, DEFAULT_METRICS_PORT}; +#[cfg(feature = "server")] use katana_node::config::rpc::{DEFAULT_RPC_ADDR, DEFAULT_RPC_MAX_CONNECTIONS, DEFAULT_RPC_PORT}; use katana_primitives::block::BlockHashOrNumber; use katana_primitives::chain::ChainId; @@ -57,6 +58,7 @@ impl Default for MetricsOptions { } } +#[cfg(feature = "server")] #[derive(Debug, Args, Clone, Serialize, Deserialize, PartialEq)] #[command(next_help_heading = "Server options")] pub struct ServerOptions { @@ -84,6 +86,7 @@ pub struct ServerOptions { pub max_connections: u32, } +#[cfg(feature = "server")] impl Default for ServerOptions { fn default() -> Self { ServerOptions { @@ -332,14 +335,17 @@ fn default_invoke_max_steps() -> u32 { DEFAULT_INVOCATION_MAX_STEPS } +#[cfg(feature = "server")] fn default_http_addr() -> IpAddr { DEFAULT_RPC_ADDR } +#[cfg(feature = "server")] fn default_http_port() -> u16 { DEFAULT_RPC_PORT } +#[cfg(feature = "server")] fn default_max_connections() -> u32 { DEFAULT_RPC_MAX_CONNECTIONS } diff --git a/crates/katana/cli/src/utils.rs b/crates/katana/cli/src/utils.rs new file mode 100644 index 0000000000..28cc0ffddc --- /dev/null +++ b/crates/katana/cli/src/utils.rs @@ -0,0 +1,203 @@ +use std::fmt::Display; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use clap::builder::PossibleValue; +use clap::ValueEnum; +use console::Style; +use katana_primitives::block::{BlockHash, BlockHashOrNumber, BlockNumber}; +use katana_primitives::chain_spec::ChainSpec; +use katana_primitives::class::ClassHash; +use katana_primitives::contract::ContractAddress; +use katana_primitives::genesis::allocation::GenesisAccountAlloc; +use katana_primitives::genesis::constant::{ + DEFAULT_LEGACY_ERC20_CLASS_HASH, DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_UDC_ADDRESS, +}; +use katana_primitives::genesis::json::GenesisJson; +use katana_primitives::genesis::Genesis; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::args::LOG_TARGET; +use crate::NodeArgs; + +pub fn parse_seed(seed: &str) -> [u8; 32] { + let seed = seed.as_bytes(); + + if seed.len() >= 32 { + unsafe { *(seed[..32].as_ptr() as *const [u8; 32]) } + } else { + let mut actual_seed = [0u8; 32]; + seed.iter().enumerate().for_each(|(i, b)| actual_seed[i] = *b); + actual_seed + } +} + +/// Used as clap value parser for [Genesis]. +pub fn parse_genesis(value: &str) -> Result { + let path = PathBuf::from(shellexpand::full(value)?.into_owned()); + let genesis = Genesis::try_from(GenesisJson::load(path)?)?; + Ok(genesis) +} + +/// If the value starts with `0x`, it is parsed as a [`BlockHash`], otherwise as a [`BlockNumber`]. +pub fn parse_block_hash_or_number(value: &str) -> Result { + if value.starts_with("0x") { + Ok(BlockHashOrNumber::Hash(BlockHash::from_hex(value)?)) + } else { + let num = value.parse::().context("could not parse block number")?; + Ok(BlockHashOrNumber::Num(num)) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)] +pub enum LogFormat { + Json, + #[default] + Full, +} + +impl ValueEnum for LogFormat { + fn value_variants<'a>() -> &'a [Self] { + &[Self::Json, Self::Full] + } + + fn to_possible_value(&self) -> Option { + match self { + Self::Json => Some(PossibleValue::new("json")), + Self::Full => Some(PossibleValue::new("full")), + } + } +} + +impl Display for LogFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Json => write!(f, "json"), + Self::Full => write!(f, "full"), + } + } +} + +pub fn print_intro(args: &NodeArgs, chain: &ChainSpec) { + let mut accounts = chain.genesis.accounts().peekable(); + let account_class_hash = accounts.peek().map(|e| e.1.class_hash()); + let seed = &args.development.seed; + + if args.logging.log_format == LogFormat::Json { + info!( + target: LOG_TARGET, + "{}", + serde_json::json!({ + "accounts": accounts.map(|a| serde_json::json!(a)).collect::>(), + "seed": format!("{}", seed), + }) + ) + } else { + println!( + "{}", + Style::new().red().apply_to( + r" + + +██╗ ██╗ █████╗ ████████╗ █████╗ ███╗ ██╗ █████╗ +██║ ██╔╝██╔══██╗╚══██╔══╝██╔══██╗████╗ ██║██╔══██╗ +█████╔╝ ███████║ ██║ ███████║██╔██╗ ██║███████║ +██╔═██╗ ██╔══██║ ██║ ██╔══██║██║╚██╗██║██╔══██║ +██║ ██╗██║ ██║ ██║ ██║ ██║██║ ╚████║██║ ██║ +╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ +" + ) + ); + + print_genesis_contracts(chain, account_class_hash); + print_genesis_accounts(accounts); + + println!( + r" + +ACCOUNTS SEED +============= +{seed} + " + ); + } +} + +fn print_genesis_contracts(chain: &ChainSpec, account_class_hash: Option) { + println!( + r" +PREDEPLOYED CONTRACTS +================== + +| Contract | ETH Fee Token +| Address | {} +| Class Hash | {:#064x} + +| Contract | STRK Fee Token +| Address | {} +| Class Hash | {:#064x}", + chain.fee_contracts.eth, + DEFAULT_LEGACY_ERC20_CLASS_HASH, + chain.fee_contracts.strk, + DEFAULT_LEGACY_ERC20_CLASS_HASH + ); + + println!( + r" +| Contract | Universal Deployer +| Address | {} +| Class Hash | {:#064x}", + DEFAULT_UDC_ADDRESS, DEFAULT_LEGACY_UDC_CLASS_HASH + ); + + if let Some(hash) = account_class_hash { + println!( + r" +| Contract | Account Contract +| Class Hash | {hash:#064x}" + ) + } +} + +fn print_genesis_accounts<'a, Accounts>(accounts: Accounts) +where + Accounts: Iterator, +{ + println!( + r" + +PREFUNDED ACCOUNTS +==================" + ); + + for (addr, account) in accounts { + if let Some(pk) = account.private_key() { + println!( + r" +| Account address | {addr} +| Private key | {pk:#x} +| Public key | {:#x}", + account.public_key() + ) + } else { + println!( + r" +| Account address | {addr} +| Public key | {:#x}", + account.public_key() + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_genesis_file() { + let path = "./test-data/genesis.json"; + parse_genesis(path).unwrap(); + } +} diff --git a/bin/katana/tests/test-data/genesis.json b/crates/katana/cli/test-data/genesis.json similarity index 100% rename from bin/katana/tests/test-data/genesis.json rename to crates/katana/cli/test-data/genesis.json diff --git a/crates/katana/core/src/service/messaging/mod.rs b/crates/katana/core/src/service/messaging/mod.rs index 88423a219b..2626947ad5 100644 --- a/crates/katana/core/src/service/messaging/mod.rs +++ b/crates/katana/core/src/service/messaging/mod.rs @@ -94,7 +94,7 @@ impl From for Error { } /// The config used to initialize the messaging service. -#[derive(Debug, Default, Deserialize, Clone, Serialize)] +#[derive(Debug, Default, Deserialize, Clone, Serialize, PartialEq)] pub struct MessagingConfig { /// The settlement chain. pub chain: String, diff --git a/crates/torii/cli/Cargo.toml b/crates/torii/cli/Cargo.toml new file mode 100644 index 0000000000..0f8b708b33 --- /dev/null +++ b/crates/torii/cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition.workspace = true +license.workspace = true +name = "torii-cli" +repository.workspace = true +version.workspace = true + +[dependencies] +anyhow.workspace = true +clap.workspace = true +dojo-utils.workspace = true +serde.workspace = true +starknet.workspace = true +torii-core.workspace = true +toml.workspace = true +url.workspace = true + +[dev-dependencies] +assert_matches.workspace = true +camino.workspace = true + +[features] +default = [ "server" ] +server = [ ] diff --git a/crates/torii/cli/src/args.rs b/crates/torii/cli/src/args.rs new file mode 100644 index 0000000000..d8d7f8b3dd --- /dev/null +++ b/crates/torii/cli/src/args.rs @@ -0,0 +1,312 @@ +use std::path::PathBuf; + +use anyhow::Result; +use clap::Parser; +use dojo_utils::parse::parse_url; +use serde::{Deserialize, Serialize}; +use starknet::core::types::Felt; +use url::Url; + +use super::options::*; + +pub const DEFAULT_RPC_URL: &str = "http://0.0.0.0:5050"; + +/// Dojo World Indexer +#[derive(Parser, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[command(name = "torii", author, about, long_about = None)] +#[command(next_help_heading = "Torii general options")] +pub struct ToriiArgs { + /// The world to index + #[arg(short, long = "world", env = "DOJO_WORLD_ADDRESS")] + pub world_address: Option, + + /// The sequencer rpc endpoint to index. + #[arg(long, value_name = "URL", default_value = DEFAULT_RPC_URL, value_parser = parse_url)] + pub rpc: Url, + + /// Database filepath (ex: indexer.db). If specified file doesn't exist, it will be + /// created. Defaults to in-memory database. + #[arg(long)] + #[arg( + value_name = "PATH", + help = "Database filepath. If specified directory doesn't exist, it will be created. \ + Defaults to in-memory database." + )] + pub db_dir: Option, + + /// The external url of the server, used for configuring the GraphQL Playground in a hosted + /// environment + #[arg(long, value_parser = parse_url, help = "The external url of the server, used for configuring the GraphQL Playground in a hosted environment.")] + pub external_url: Option, + + /// Open World Explorer on the browser. + #[arg(long, help = "Open World Explorer on the browser.")] + pub explorer: bool, + + /// Configuration file + #[arg(long, help = "Configuration file to setup Torii.")] + pub config: Option, + + #[command(flatten)] + pub indexing: IndexingOptions, + + #[command(flatten)] + pub events: EventsOptions, + + #[cfg(feature = "server")] + #[command(flatten)] + pub metrics: MetricsOptions, + + #[cfg(feature = "server")] + #[command(flatten)] + pub server: ServerOptions, + + #[cfg(feature = "server")] + #[command(flatten)] + pub relay: RelayOptions, +} + +impl ToriiArgs { + pub fn with_config_file(mut self) -> Result { + let config: ToriiArgsConfig = if let Some(path) = &self.config { + toml::from_str(&std::fs::read_to_string(path)?)? + } else { + return Ok(self); + }; + + // the CLI (self) takes precedence over the config file. + // Currently, the merge is made at the top level of the commands. + // We may add recursive merging in the future. + + if self.world_address.is_none() { + self.world_address = config.world_address; + } + + if self.rpc == Url::parse(DEFAULT_RPC_URL).unwrap() { + if let Some(rpc) = config.rpc { + self.rpc = rpc; + } + } + + if self.db_dir.is_none() { + self.db_dir = config.db_dir; + } + + if self.external_url.is_none() { + self.external_url = config.external_url; + } + + // Currently the comparison it's only at the top level. + // Need to make it more granular. + + if !self.explorer { + self.explorer = config.explorer.unwrap_or_default(); + } + + if self.indexing == IndexingOptions::default() { + self.indexing = config.indexing.unwrap_or_default(); + } + + if self.events == EventsOptions::default() { + self.events = config.events.unwrap_or_default(); + } + + #[cfg(feature = "server")] + { + if self.server == ServerOptions::default() { + self.server = config.server.unwrap_or_default(); + } + + if self.relay == RelayOptions::default() { + self.relay = config.relay.unwrap_or_default(); + } + + if self.metrics == MetricsOptions::default() { + self.metrics = config.metrics.unwrap_or_default(); + } + } + + Ok(self) + } +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ToriiArgsConfig { + pub world_address: Option, + pub rpc: Option, + pub db_dir: Option, + pub external_url: Option, + pub explorer: Option, + pub indexing: Option, + pub events: Option, + #[cfg(feature = "server")] + pub metrics: Option, + #[cfg(feature = "server")] + pub server: Option, + #[cfg(feature = "server")] + pub relay: Option, +} + +impl TryFrom for ToriiArgsConfig { + type Error = anyhow::Error; + + fn try_from(args: ToriiArgs) -> Result { + // Ensure the config file is merged with the CLI arguments. + let args = args.with_config_file()?; + + let mut config = + ToriiArgsConfig { world_address: args.world_address, ..Default::default() }; + + config.world_address = args.world_address; + config.rpc = + if args.rpc == Url::parse(DEFAULT_RPC_URL).unwrap() { None } else { Some(args.rpc) }; + config.db_dir = args.db_dir; + config.external_url = args.external_url; + config.explorer = Some(args.explorer); + + // Only include the following options if they are not the default. + // This makes the config file more readable. + config.indexing = + if args.indexing == IndexingOptions::default() { None } else { Some(args.indexing) }; + config.events = + if args.events == EventsOptions::default() { None } else { Some(args.events) }; + + #[cfg(feature = "server")] + { + config.server = + if args.server == ServerOptions::default() { None } else { Some(args.server) }; + config.relay = + if args.relay == RelayOptions::default() { None } else { Some(args.relay) }; + config.metrics = + if args.metrics == MetricsOptions::default() { None } else { Some(args.metrics) }; + } + + Ok(config) + } +} + +#[cfg(test)] +mod test { + use std::net::{IpAddr, Ipv4Addr}; + use std::str::FromStr; + + use torii_core::types::{Contract, ContractType}; + + use super::*; + + #[test] + fn test_cli_precedence() { + // CLI args must take precedence over the config file. + let content = r#" + world_address = "0x1234" + rpc = "http://0.0.0.0:5050" + db_dir = "/tmp/torii-test" + + [events] + raw = true + historical = [ + "ns-E", + "ns-EH" + ] + "#; + let path = std::env::temp_dir().join("torii-config2.json"); + std::fs::write(&path, content).unwrap(); + + let path_str = path.to_string_lossy().to_string(); + + let args = vec![ + "torii", + "--world", + "0x9999", + "--rpc", + "http://0.0.0.0:6060", + "--db-dir", + "/tmp/torii-test2", + "--events.raw", + "false", + "--events.historical", + "a-A", + "--config", + path_str.as_str(), + ]; + + let torii_args = ToriiArgs::parse_from(args).with_config_file().unwrap(); + + assert_eq!(torii_args.world_address, Some(Felt::from_str("0x9999").unwrap())); + assert_eq!(torii_args.rpc, Url::parse("http://0.0.0.0:6060").unwrap()); + assert_eq!(torii_args.db_dir, Some(PathBuf::from("/tmp/torii-test2"))); + assert!(!torii_args.events.raw); + assert_eq!(torii_args.events.historical, Some(vec!["a-A".to_string()])); + assert_eq!(torii_args.server, ServerOptions::default()); + } + + #[test] + fn test_config_fallback() { + let content = r#" + world_address = "0x1234" + rpc = "http://0.0.0.0:2222" + db_dir = "/tmp/torii-test" + + [events] + raw = false + historical = [ + "ns-E", + "ns-EH" + ] + + [server] + http_addr = "127.0.0.1" + http_port = 7777 + http_cors_origins = ["*"] + + [indexing] + events_chunk_size = 9999 + index_pending = true + max_concurrent_tasks = 1000 + index_transactions = false + contracts = [ + "erc20:0x1234", + "erc721:0x5678" + ] + "#; + let path = std::env::temp_dir().join("torii-config.json"); + std::fs::write(&path, content).unwrap(); + + let path_str = path.to_string_lossy().to_string(); + + let args = vec!["torii", "--config", path_str.as_str()]; + + let torii_args = ToriiArgs::parse_from(args).with_config_file().unwrap(); + + assert_eq!(torii_args.world_address, Some(Felt::from_str("0x1234").unwrap())); + assert_eq!(torii_args.rpc, Url::parse("http://0.0.0.0:2222").unwrap()); + assert_eq!(torii_args.db_dir, Some(PathBuf::from("/tmp/torii-test"))); + assert!(!torii_args.events.raw); + assert_eq!( + torii_args.events.historical, + Some(vec!["ns-E".to_string(), "ns-EH".to_string()]) + ); + assert_eq!(torii_args.indexing.events_chunk_size, 9999); + assert_eq!(torii_args.indexing.blocks_chunk_size, 10240); + assert!(torii_args.indexing.index_pending); + assert_eq!(torii_args.indexing.polling_interval, 500); + assert_eq!(torii_args.indexing.max_concurrent_tasks, 1000); + assert!(!torii_args.indexing.index_transactions); + assert_eq!( + torii_args.indexing.contracts, + vec![ + Contract { + address: Felt::from_str("0x1234").unwrap(), + r#type: ContractType::ERC20 + }, + Contract { + address: Felt::from_str("0x5678").unwrap(), + r#type: ContractType::ERC721 + } + ] + ); + assert_eq!(torii_args.server.http_addr, IpAddr::V4(Ipv4Addr::LOCALHOST)); + assert_eq!(torii_args.server.http_port, 7777); + assert_eq!(torii_args.server.http_cors_origins, Some(vec!["*".to_string()])); + } +} diff --git a/crates/torii/cli/src/lib.rs b/crates/torii/cli/src/lib.rs new file mode 100644 index 0000000000..04dd46b33c --- /dev/null +++ b/crates/torii/cli/src/lib.rs @@ -0,0 +1,7 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +pub mod args; +pub mod options; + +pub use args::ToriiArgs; +pub use options::*; diff --git a/bin/torii/src/options.rs b/crates/torii/cli/src/options.rs similarity index 94% rename from bin/torii/src/options.rs rename to crates/torii/cli/src/options.rs index be1636c16a..e71d8ab991 100644 --- a/bin/torii/src/options.rs +++ b/crates/torii/cli/src/options.rs @@ -7,18 +7,18 @@ use serde::{Deserialize, Serialize}; use starknet::core::types::Felt; use torii_core::types::{Contract, ContractType}; -const DEFAULT_HTTP_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); -const DEFAULT_HTTP_PORT: u16 = 8080; -const DEFAULT_METRICS_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); -const DEFAULT_METRICS_PORT: u16 = 9200; -const DEFAULT_EVENTS_CHUNK_SIZE: u64 = 1024; -const DEFAULT_BLOCKS_CHUNK_SIZE: u64 = 10240; -const DEFAULT_POLLING_INTERVAL: u64 = 500; -const DEFAULT_MAX_CONCURRENT_TASKS: usize = 100; - -const DEFAULT_RELAY_PORT: u16 = 9090; -const DEFAULT_RELAY_WEBRTC_PORT: u16 = 9091; -const DEFAULT_RELAY_WEBSOCKET_PORT: u16 = 9092; +pub const DEFAULT_HTTP_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); +pub const DEFAULT_HTTP_PORT: u16 = 8080; +pub const DEFAULT_METRICS_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); +pub const DEFAULT_METRICS_PORT: u16 = 9200; +pub const DEFAULT_EVENTS_CHUNK_SIZE: u64 = 1024; +pub const DEFAULT_BLOCKS_CHUNK_SIZE: u64 = 10240; +pub const DEFAULT_POLLING_INTERVAL: u64 = 500; +pub const DEFAULT_MAX_CONCURRENT_TASKS: usize = 100; + +pub const DEFAULT_RELAY_PORT: u16 = 9090; +pub const DEFAULT_RELAY_WEBRTC_PORT: u16 = 9091; +pub const DEFAULT_RELAY_WEBSOCKET_PORT: u16 = 9092; #[derive(Debug, clap::Args, Clone, Serialize, Deserialize, PartialEq)] #[command(next_help_heading = "Relay options")]