From 63e00aef70f8664e3af5135231682e93fbb56567 Mon Sep 17 00:00:00 2001 From: glihm Date: Mon, 11 Nov 2024 20:37:14 -0600 Subject: [PATCH 1/9] refactor: move cli args to separate crates --- Cargo.lock | 45 +- Cargo.toml | 4 + bin/katana/Cargo.toml | 10 +- bin/katana/src/cli/mod.rs | 6 +- bin/katana/src/cli/node.rs | 632 ++---------------- bin/katana/src/main.rs | 1 - bin/torii/Cargo.toml | 1 + bin/torii/src/main.rs | 261 +------- crates/katana/cli/Cargo.toml | 29 + crates/katana/cli/src/lib.rs | 8 + crates/katana/cli/src/node.rs | 537 +++++++++++++++ .../cli => crates/katana/cli/src}/options.rs | 0 .../katana => crates/katana/cli}/src/utils.rs | 2 +- .../katana/cli}/test-data/genesis.json | 0 crates/torii/cli/Cargo.toml | 20 + crates/torii/cli/src/args.rs | 264 ++++++++ crates/torii/cli/src/lib.rs | 7 + .../torii => crates/torii/cli}/src/options.rs | 24 +- 18 files changed, 978 insertions(+), 873 deletions(-) create mode 100644 crates/katana/cli/Cargo.toml create mode 100644 crates/katana/cli/src/lib.rs create mode 100644 crates/katana/cli/src/node.rs rename {bin/katana/src/cli => crates/katana/cli/src}/options.rs (100%) rename {bin/katana => crates/katana/cli}/src/utils.rs (97%) 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 dbb6be2642..4862dfcf3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8063,31 +8063,25 @@ 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 +8099,26 @@ 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", + "katana-core", + "katana-node", + "katana-primitives", + "katana-slot-controller", + "serde", + "shellexpand", + "starknet 0.12.0", + "toml 0.8.19", + "url", +] + [[package]] name = "katana-codecs" version = "1.0.0" @@ -14915,6 +14929,7 @@ dependencies = [ "tokio-stream", "tokio-util", "toml 0.8.19", + "torii-cli", "torii-core", "torii-graphql", "torii-grpc", @@ -14928,6 +14943,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 c6c6bc8659..608a1da2da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "crates/katana/storage/provider", "crates/katana/tasks", "crates/katana/trie", + "crates/katana/cli", "crates/metrics", "crates/saya/core", "crates/saya/provider", @@ -45,6 +46,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", @@ -103,6 +105,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" } @@ -111,6 +114,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..08b0efa12c 100644 --- a/bin/katana/Cargo.toml +++ b/bin/katana/Cargo.toml @@ -7,30 +7,24 @@ repository.workspace = true version.workspace = true [dependencies] -katana-core.workspace = true katana-db.workspace = true katana-node.workspace = true katana-primitives.workspace = true -katana-slot-controller = { workspace = true, optional = true } +katana-cli.workspace = 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 @@ -40,5 +34,5 @@ starknet.workspace = true default = [ "jemalloc", "slot" ] jemalloc = [ ] -slot = [ "dep:katana-slot-controller", "katana-primitives/slot" ] +slot = [ "katana-primitives/slot", "katana-cli/slot" ] starknet-messaging = [ "katana-node/starknet-messaging" ] diff --git a/bin/katana/src/cli/mod.rs b/bin/katana/src/cli/mod.rs index 04aa84992e..b3514e6f9b 100644 --- a/bin/katana/src/cli/mod.rs +++ b/bin/katana/src/cli/mod.rs @@ -1,10 +1,10 @@ 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 +14,7 @@ pub struct Cli { commands: Option, #[command(flatten)] - node: node::NodeArgs, + node: NodeArgs, } impl Cli { @@ -26,7 +26,7 @@ impl Cli { }; } - self.node.with_config_file()?.execute() + node::execute(&self.node.with_config_file()?) } } diff --git a/bin/katana/src/cli/node.rs b/bin/katana/src/cli/node.rs index 461fc6e036..492c5c35c9 100644 --- a/bin/katana/src/cli/node.rs +++ b/bin/katana/src/cli/node.rs @@ -10,380 +10,84 @@ //! 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. -use std::collections::HashSet; -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; -use katana_node::config::dev::{DevConfig, FixedL1GasPriceConfig}; -use katana_node::config::execution::ExecutionConfig; -use katana_node::config::fork::ForkingConfig; -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_cli::node::NodeArgs; +use katana_cli::utils::LogFormat; +use katana_primitives::chain_spec::ChainSpec; use katana_primitives::class::ClassHash; use katana_primitives::contract::ContractAddress; -use katana_primitives::genesis::allocation::{DevAllocationsGenerator, GenesisAccountAlloc}; +use katana_primitives::genesis::allocation::GenesisAccountAlloc; use katana_primitives::genesis::constant::{ - DEFAULT_LEGACY_ERC20_CLASS_HASH, DEFAULT_LEGACY_UDC_CLASS_HASH, - DEFAULT_PREFUNDED_ACCOUNT_BALANCE, DEFAULT_UDC_ADDRESS, + DEFAULT_LEGACY_ERC20_CLASS_HASH, DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_UDC_ADDRESS, }; -use serde::{Deserialize, Serialize}; use tracing::{info, Subscriber}; use tracing_log::LogTracer; use tracing_subscriber::{fmt, EnvFilter}; -use super::options::*; -use crate::utils::{parse_seed, LogFormat}; - -#[derive(Parser, Debug, Serialize, Deserialize, Default)] -pub struct NodeArgs { - /// Don't print anything on startup. - #[arg(long)] - pub silent: bool, - - /// Disable auto and interval mining, and mine on demand instead via an endpoint. - #[arg(long)] - #[arg(conflicts_with = "block_time")] - pub no_mining: bool, - - /// Block time in milliseconds for interval mining. - #[arg(short, long)] - #[arg(value_name = "MILLISECONDS")] - pub block_time: Option, - - /// Directory path of the database to initialize from. - /// - /// The path must either be an empty directory or a directory which already contains a - /// previously initialized Katana database. - #[arg(long)] - #[arg(value_name = "PATH")] - pub db_dir: Option, - - /// Configure the messaging with an other chain. - /// - /// Configure the messaging to allow Katana listening/sending messages on a - /// settlement chain that can be Ethereum or an other Starknet sequencer. - #[arg(long)] - #[arg(value_name = "PATH")] - #[arg(value_parser = katana_core::service::messaging::MessagingConfig::parse)] - pub messaging: Option, - - #[command(flatten)] - pub logging: LoggingOptions, - - #[command(flatten)] - pub metrics: MetricsOptions, - - #[command(flatten)] - pub server: ServerOptions, - - #[command(flatten)] - pub starknet: StarknetOptions, - - #[command(flatten)] - pub gpo: GasPriceOracleOptions, - - #[command(flatten)] - pub forking: ForkingOptions, - - #[command(flatten)] - pub development: DevOptions, - - #[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<()> { - self.init_logging()?; - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .context("failed to build tokio runtime")? - .block_on(self.start_node()) - } - - 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); - } - - // Launch the node - let handle = node.launch().await.context("failed to launch node")?; - - // Wait until an OS signal (ie SIGINT, SIGTERM) is received or the node is shutdown. - tokio::select! { - _ = dojo_utils::signal::wait_signals() => { - // Gracefully shutdown the node before exiting - handle.stop().await?; - }, - - _ = handle.stopped() => { } - } - - info!("Shutting down."); - - Ok(()) - } - - fn init_logging(&self) -> Result<()> { - const DEFAULT_LOG_FILTER: &str = "info,tasks=debug,executor=trace,forking::backend=trace,\ - blockifier=off,jsonrpsee_server=off,hyper=off,\ - messaging=debug,node=error"; - - let filter = if self.development.dev { - &format!("{DEFAULT_LOG_FILTER},server=debug") - } else { - DEFAULT_LOG_FILTER - }; - - LogTracer::init()?; - - // If the user has set the `RUST_LOG` environment variable, then we prioritize it. - // Otherwise, we use the default log filter. - // TODO: change env var to `KATANA_LOG`. - let filter = EnvFilter::try_from_default_env().or(EnvFilter::try_new(filter))?; - let builder = fmt::Subscriber::builder().with_env_filter(filter); - - let subscriber: Box = match self.logging.log_format { - LogFormat::Full => Box::new(builder.finish()), - LogFormat::Json => Box::new(builder.json().finish()), - }; - - Ok(tracing::subscriber::set_global_default(subscriber)?) - } - - fn config(&self) -> Result { - let db = self.db_config(); - let rpc = self.rpc_config(); - let dev = self.dev_config(); - let chain = self.chain_spec()?; - let metrics = self.metrics_config(); - let forking = self.forking_config()?; - let execution = self.execution_config(); - let sequencing = self.sequencer_config(); - let messaging = self.messaging.clone(); - - Ok(Config { metrics, db, dev, rpc, chain, execution, sequencing, messaging, forking }) - } - - fn sequencer_config(&self) -> SequencingConfig { - SequencingConfig { block_time: self.block_time, no_mining: self.no_mining } - } - - fn rpc_config(&self) -> RpcConfig { - let mut apis = HashSet::from([ApiKind::Starknet, ApiKind::Torii, ApiKind::Saya]); - // only enable `katana` API in dev mode - if self.development.dev { - 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(), - } - } - - fn chain_spec(&self) -> Result { - let mut chain_spec = chain_spec::DEV_UNALLOCATED.clone(); - - if let Some(id) = self.starknet.environment.chain_id { - chain_spec.id = id; - } - - if let Some(genesis) = self.starknet.genesis.clone() { - chain_spec.genesis = genesis; - } else { - chain_spec.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS; - } - - // generate dev accounts - let accounts = DevAllocationsGenerator::new(self.development.total_accounts) - .with_seed(parse_seed(&self.development.seed)) - .with_balance(U256::from(DEFAULT_PREFUNDED_ACCOUNT_BALANCE)) - .generate(); - - chain_spec.genesis.extend_allocations(accounts.into_iter().map(|(k, v)| (k, v.into()))); - - #[cfg(feature = "slot")] - if self.slot.controller { - katana_slot_controller::add_controller_account(&mut chain_spec.genesis)?; - } - - Ok(chain_spec) - } - - fn dev_config(&self) -> DevConfig { - let mut fixed_gas_prices = None; - - if self.gpo.l1_eth_gas_price > 0 { - let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); - prices.gas_price.eth = self.gpo.l1_eth_gas_price; - } - - if self.gpo.l1_strk_gas_price > 0 { - let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); - prices.gas_price.strk = self.gpo.l1_strk_gas_price; - } - - if self.gpo.l1_eth_data_gas_price > 0 { - let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); - prices.data_gas_price.eth = self.gpo.l1_eth_data_gas_price; - } - - if self.gpo.l1_strk_data_gas_price > 0 { - let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); - prices.data_gas_price.strk = self.gpo.l1_strk_data_gas_price; - } +pub fn execute(args: &NodeArgs) -> Result<()> { + init_logging(args)?; + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("failed to build tokio runtime")? + .block_on(start_node(args)) +} - DevConfig { - fixed_gas_prices, - fee: !self.development.no_fee, - account_validation: !self.development.no_account_validation, - } - } +async fn start_node(args: &NodeArgs) -> Result<()> { + // Build the node + let config = args.config()?; + let node = katana_node::build(config).await.context("failed to build node")?; - fn execution_config(&self) -> ExecutionConfig { - ExecutionConfig { - invocation_max_steps: self.starknet.environment.invoke_max_steps, - validation_max_steps: self.starknet.environment.validate_max_steps, - ..Default::default() - } + if !args.silent { + print_intro(args, &node.backend.chain_spec); } - fn forking_config(&self) -> Result> { - if let Some(ref url) = self.forking.fork_provider { - let cfg = ForkingConfig { url: url.clone(), block: self.forking.fork_block }; - return Ok(Some(cfg)); - } + // Launch the node + let handle = node.launch().await.context("failed to launch node")?; - Ok(None) - } + // Wait until an OS signal (ie SIGINT, SIGTERM) is received or the node is shutdown. + tokio::select! { + _ = dojo_utils::signal::wait_signals() => { + // Gracefully shutdown the node before exiting + handle.stop().await?; + }, - fn db_config(&self) -> DbConfig { - DbConfig { dir: self.db_dir.clone() } + _ = handle.stopped() => { } } - fn metrics_config(&self) -> Option { - if self.metrics.metrics { - Some(MetricsConfig { addr: self.metrics.metrics_addr, port: self.metrics.metrics_port }) - } else { - 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)?)? - } 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.silent { - self.silent = config.silent.unwrap_or_default(); - } - - if !self.no_mining { - self.no_mining = config.no_mining.unwrap_or_default(); - } - - if self.block_time.is_none() { - self.block_time = config.block_time; - } - - if self.db_dir.is_none() { - self.db_dir = config.db_dir; - } + info!("Shutting down."); - if self.logging == LoggingOptions::default() { - if let Some(logging) = config.logging { - self.logging = logging; - } - } + Ok(()) +} - if self.metrics == MetricsOptions::default() { - if let Some(metrics) = config.metrics { - self.metrics = metrics; - } - } +fn init_logging(args: &NodeArgs) -> Result<()> { + const DEFAULT_LOG_FILTER: &str = "info,tasks=debug,executor=trace,forking::backend=trace,\ + blockifier=off,jsonrpsee_server=off,hyper=off,\ + messaging=debug,node=error"; - if self.server == ServerOptions::default() { - if let Some(server) = config.server { - self.server = server; - } - } - - self.starknet.merge(config.starknet.as_ref()); - self.development.merge(config.development.as_ref()); + let filter = if args.development.dev { + &format!("{DEFAULT_LOG_FILTER},server=debug") + } else { + DEFAULT_LOG_FILTER + }; - if self.gpo == GasPriceOracleOptions::default() { - if let Some(gpo) = config.gpo { - self.gpo = gpo; - } - } + LogTracer::init()?; - if self.forking == ForkingOptions::default() { - if let Some(forking) = config.forking { - self.forking = forking; - } - } + // If the user has set the `RUST_LOG` environment variable, then we prioritize it. + // Otherwise, we use the default log filter. + // TODO: change env var to `KATANA_LOG`. + let filter = EnvFilter::try_from_default_env().or(EnvFilter::try_new(filter))?; + let builder = fmt::Subscriber::builder().with_env_filter(filter); - #[cfg(feature = "slot")] - if self.slot == SlotOptions::default() { - if let Some(slot) = config.slot { - self.slot = slot; - } - } + let subscriber: Box = match args.logging.log_format { + LogFormat::Full => Box::new(builder.finish()), + LogFormat::Json => Box::new(builder.json().finish()), + }; - Ok(self) - } + Ok(tracing::subscriber::set_global_default(subscriber)?) } fn print_intro(args: &NodeArgs, chain: &ChainSpec) { @@ -497,237 +201,3 @@ PREFUNDED ACCOUNTS } } } - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use assert_matches::assert_matches; - use katana_core::constants::{ - DEFAULT_ETH_L1_DATA_GAS_PRICE, DEFAULT_ETH_L1_GAS_PRICE, DEFAULT_STRK_L1_DATA_GAS_PRICE, - DEFAULT_STRK_L1_GAS_PRICE, - }; - use katana_node::config::execution::{ - DEFAULT_INVOCATION_MAX_STEPS, DEFAULT_VALIDATION_MAX_STEPS, - }; - use katana_primitives::chain::ChainId; - use katana_primitives::{address, felt, Felt}; - - use super::*; - - #[test] - fn test_starknet_config_default() { - let args = NodeArgs::parse_from(["katana"]); - let config = args.config().unwrap(); - - assert!(config.dev.fee); - assert!(config.dev.account_validation); - assert!(config.forking.is_none()); - assert_eq!(config.execution.invocation_max_steps, DEFAULT_INVOCATION_MAX_STEPS); - assert_eq!(config.execution.validation_max_steps, DEFAULT_VALIDATION_MAX_STEPS); - assert_eq!(config.db.dir, None); - assert_eq!(config.chain.id, ChainId::parse("KATANA").unwrap()); - assert_eq!(config.chain.genesis.sequencer_address, *DEFAULT_SEQUENCER_ADDRESS); - } - - #[test] - fn test_starknet_config_custom() { - let args = NodeArgs::parse_from([ - "katana", - "--dev", - "--dev.no-fee", - "--dev.no-account-validation", - "--chain-id", - "SN_GOERLI", - "--invoke-max-steps", - "200", - "--validate-max-steps", - "100", - "--db-dir", - "/path/to/db", - ]); - let config = args.config().unwrap(); - - assert!(!config.dev.fee); - assert!(!config.dev.account_validation); - assert_eq!(config.execution.invocation_max_steps, 200); - assert_eq!(config.execution.validation_max_steps, 100); - assert_eq!(config.db.dir, Some(PathBuf::from("/path/to/db"))); - assert_eq!(config.chain.id, ChainId::GOERLI); - assert_eq!(config.chain.genesis.sequencer_address, *DEFAULT_SEQUENCER_ADDRESS); - } - - #[test] - fn custom_fixed_gas_prices() { - let config = NodeArgs::parse_from(["katana"]).config().unwrap(); - assert!(config.dev.fixed_gas_prices.is_none()); - - let config = - NodeArgs::parse_from(["katana", "--gpo.l1-eth-gas-price", "10"]).config().unwrap(); - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, 10); - assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); - assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); - assert_eq!(prices.data_gas_price.strk, DEFAULT_STRK_L1_DATA_GAS_PRICE); - }); - - let config = - NodeArgs::parse_from(["katana", "--gpo.l1-strk-gas-price", "20"]).config().unwrap(); - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, DEFAULT_ETH_L1_GAS_PRICE); - assert_eq!(prices.gas_price.strk, 20); - assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); - assert_eq!(prices.data_gas_price.strk, DEFAULT_STRK_L1_DATA_GAS_PRICE); - }); - - let config = - NodeArgs::parse_from(["katana", "--gpo.l1-eth-data-gas-price", "1"]).config().unwrap(); - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, DEFAULT_ETH_L1_GAS_PRICE); - assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); - assert_eq!(prices.data_gas_price.eth, 1); - assert_eq!(prices.data_gas_price.strk, DEFAULT_STRK_L1_DATA_GAS_PRICE); - }); - - let config = - NodeArgs::parse_from(["katana", "--gpo.l1-strk-data-gas-price", "2"]).config().unwrap(); - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, DEFAULT_ETH_L1_GAS_PRICE); - assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); - assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); - assert_eq!(prices.data_gas_price.strk, 2); - }); - - let config = NodeArgs::parse_from([ - "katana", - "--gpo.l1-eth-gas-price", - "10", - "--gpo.l1-strk-data-gas-price", - "2", - ]) - .config() - .unwrap(); - - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, 10); - assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); - assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); - assert_eq!(prices.data_gas_price.strk, 2); - }); - - // Set all the gas prices options - - let config = NodeArgs::parse_from([ - "katana", - "--gpo.l1-eth-gas-price", - "10", - "--gpo.l1-strk-gas-price", - "20", - "--gpo.l1-eth-data-gas-price", - "1", - "--gpo.l1-strk-data-gas-price", - "2", - ]) - .config() - .unwrap(); - - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, 10); - assert_eq!(prices.gas_price.strk, 20); - assert_eq!(prices.data_gas_price.eth, 1); - assert_eq!(prices.data_gas_price.strk, 2); - }) - } - - #[test] - fn genesis_with_fixed_gas_prices() { - let config = NodeArgs::parse_from([ - "katana", - "--genesis", - "./tests/test-data/genesis.json", - "--gpo.l1-eth-gas-price", - "100", - "--gpo.l1-strk-gas-price", - "200", - "--gpo.l1-eth-data-gas-price", - "111", - "--gpo.l1-strk-data-gas-price", - "222", - ]) - .config() - .unwrap(); - - assert_eq!(config.chain.genesis.number, 0); - assert_eq!(config.chain.genesis.parent_hash, felt!("0x999")); - assert_eq!(config.chain.genesis.timestamp, 5123512314); - assert_eq!(config.chain.genesis.state_root, felt!("0x99")); - assert_eq!(config.chain.genesis.sequencer_address, address!("0x100")); - assert_eq!(config.chain.genesis.gas_prices.eth, 9999); - assert_eq!(config.chain.genesis.gas_prices.strk, 8888); - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, 100); - assert_eq!(prices.gas_price.strk, 200); - assert_eq!(prices.data_gas_price.eth, 111); - assert_eq!(prices.data_gas_price.strk, 222); - }) - } - - #[test] - fn config_from_file_and_cli() { - // CLI args must take precedence over the config file. - let content = r#" -[gpo] -l1_eth_gas_price = "0xfe" -l1_strk_gas_price = "200" -l1_eth_data_gas_price = "111" -l1_strk_data_gas_price = "222" - -[dev] -total_accounts = 20 - -[starknet.env] -validate_max_steps = 500 -invoke_max_steps = 9988 -chain_id.Named = "Mainnet" - "#; - let path = std::env::temp_dir().join("katana-config.json"); - std::fs::write(&path, content).unwrap(); - - let path_str = path.to_string_lossy().to_string(); - - let args = vec![ - "katana", - "--config", - path_str.as_str(), - "--genesis", - "./tests/test-data/genesis.json", - "--validate-max-steps", - "1234", - "--dev", - "--dev.no-fee", - "--chain-id", - "0x123", - ]; - - let config = - NodeArgs::parse_from(args.clone()).with_config_file().unwrap().config().unwrap(); - - assert_eq!(config.execution.validation_max_steps, 1234); - assert_eq!(config.execution.invocation_max_steps, 9988); - assert!(!config.dev.fee); - assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { - assert_eq!(prices.gas_price.eth, 254); - assert_eq!(prices.gas_price.strk, 200); - assert_eq!(prices.data_gas_price.eth, 111); - assert_eq!(prices.data_gas_price.strk, 222); - }); - assert_eq!(config.chain.genesis.number, 0); - assert_eq!(config.chain.genesis.parent_hash, felt!("0x999")); - assert_eq!(config.chain.genesis.timestamp, 5123512314); - assert_eq!(config.chain.genesis.state_root, felt!("0x99")); - assert_eq!(config.chain.genesis.sequencer_address, address!("0x100")); - assert_eq!(config.chain.genesis.gas_prices.eth, 9999); - assert_eq!(config.chain.genesis.gas_prices.strk, 8888); - assert_eq!(config.chain.id, ChainId::Id(Felt::from_str("0x123").unwrap())); - } -} 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/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..e4f7acec8e 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()?; @@ -378,126 +242,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..c5f247587c --- /dev/null +++ b/crates/katana/cli/Cargo.toml @@ -0,0 +1,29 @@ +[package] +edition.workspace = true +license.workspace = true +name = "katana-cli" +repository.workspace = true +version.workspace = true + +[dependencies] +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 +serde.workspace = true +shellexpand = "3.1.0" +toml.workspace = true +url.workspace = true + +[dev-dependencies] +assert_matches.workspace = true +starknet.workspace = true + +[features] +default = [ "slot" ] +slot = [ "dep:katana-slot-controller", "katana-primitives/slot" ] diff --git a/crates/katana/cli/src/lib.rs b/crates/katana/cli/src/lib.rs new file mode 100644 index 0000000000..116b49b24c --- /dev/null +++ b/crates/katana/cli/src/lib.rs @@ -0,0 +1,8 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +pub mod node; +pub mod options; +pub mod utils; + +pub use node::{NodeArgs, NodeArgsConfig}; +pub use options::*; diff --git a/crates/katana/cli/src/node.rs b/crates/katana/cli/src/node.rs new file mode 100644 index 0000000000..ea49e6d468 --- /dev/null +++ b/crates/katana/cli/src/node.rs @@ -0,0 +1,537 @@ +//! Katana node CLI options and configuration. + +use std::collections::HashSet; +use std::path::PathBuf; + +use alloy_primitives::U256; +use anyhow::Result; +use clap::Parser; +use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; +use katana_core::service::messaging::MessagingConfig; +use katana_node::config::db::DbConfig; +use katana_node::config::dev::{DevConfig, FixedL1GasPriceConfig}; +use katana_node::config::execution::ExecutionConfig; +use katana_node::config::fork::ForkingConfig; +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::genesis::allocation::DevAllocationsGenerator; +use katana_primitives::genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; +use serde::{Deserialize, Serialize}; + +use super::options::*; +use crate::utils::parse_seed; + +#[derive(Parser, Debug, Serialize, Deserialize, Default)] +pub struct NodeArgs { + /// Don't print anything on startup. + #[arg(long)] + pub silent: bool, + + /// Disable auto and interval mining, and mine on demand instead via an endpoint. + #[arg(long)] + #[arg(conflicts_with = "block_time")] + pub no_mining: bool, + + /// Block time in milliseconds for interval mining. + #[arg(short, long)] + #[arg(value_name = "MILLISECONDS")] + pub block_time: Option, + + /// Directory path of the database to initialize from. + /// + /// The path must either be an empty directory or a directory which already contains a + /// previously initialized Katana database. + #[arg(long)] + #[arg(value_name = "PATH")] + pub db_dir: Option, + + /// Configure the messaging with an other chain. + /// + /// Configure the messaging to allow Katana listening/sending messages on a + /// settlement chain that can be Ethereum or an other Starknet sequencer. + #[arg(long)] + #[arg(value_name = "PATH")] + #[arg(value_parser = katana_core::service::messaging::MessagingConfig::parse)] + pub messaging: Option, + + #[command(flatten)] + pub logging: LoggingOptions, + + #[command(flatten)] + pub metrics: MetricsOptions, + + #[command(flatten)] + pub server: ServerOptions, + + #[command(flatten)] + pub starknet: StarknetOptions, + + #[command(flatten)] + pub gpo: GasPriceOracleOptions, + + #[command(flatten)] + pub forking: ForkingOptions, + + #[command(flatten)] + pub development: DevOptions, + + #[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, +} + +impl NodeArgs { + pub fn config(&self) -> Result { + let db = self.db_config(); + let rpc = self.rpc_config(); + let dev = self.dev_config(); + let chain = self.chain_spec()?; + let metrics = self.metrics_config(); + let forking = self.forking_config()?; + let execution = self.execution_config(); + let sequencing = self.sequencer_config(); + let messaging = self.messaging.clone(); + + Ok(Config { metrics, db, dev, rpc, chain, execution, sequencing, messaging, forking }) + } + + fn sequencer_config(&self) -> SequencingConfig { + SequencingConfig { block_time: self.block_time, no_mining: self.no_mining } + } + + fn rpc_config(&self) -> RpcConfig { + let mut apis = HashSet::from([ApiKind::Starknet, ApiKind::Torii, ApiKind::Saya]); + // only enable `katana` API in dev mode + if self.development.dev { + 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(), + } + } + + fn chain_spec(&self) -> Result { + let mut chain_spec = chain_spec::DEV_UNALLOCATED.clone(); + + if let Some(id) = self.starknet.environment.chain_id { + chain_spec.id = id; + } + + if let Some(genesis) = self.starknet.genesis.clone() { + chain_spec.genesis = genesis; + } else { + chain_spec.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS; + } + + // generate dev accounts + let accounts = DevAllocationsGenerator::new(self.development.total_accounts) + .with_seed(parse_seed(&self.development.seed)) + .with_balance(U256::from(DEFAULT_PREFUNDED_ACCOUNT_BALANCE)) + .generate(); + + chain_spec.genesis.extend_allocations(accounts.into_iter().map(|(k, v)| (k, v.into()))); + + #[cfg(feature = "slot")] + if self.slot.controller { + katana_slot_controller::add_controller_account(&mut chain_spec.genesis)?; + } + + Ok(chain_spec) + } + + fn dev_config(&self) -> DevConfig { + let mut fixed_gas_prices = None; + + if self.gpo.l1_eth_gas_price > 0 { + let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); + prices.gas_price.eth = self.gpo.l1_eth_gas_price; + } + + if self.gpo.l1_strk_gas_price > 0 { + let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); + prices.gas_price.strk = self.gpo.l1_strk_gas_price; + } + + if self.gpo.l1_eth_data_gas_price > 0 { + let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); + prices.data_gas_price.eth = self.gpo.l1_eth_data_gas_price; + } + + if self.gpo.l1_strk_data_gas_price > 0 { + let prices = fixed_gas_prices.get_or_insert(FixedL1GasPriceConfig::default()); + prices.data_gas_price.strk = self.gpo.l1_strk_data_gas_price; + } + + DevConfig { + fixed_gas_prices, + fee: !self.development.no_fee, + account_validation: !self.development.no_account_validation, + } + } + + fn execution_config(&self) -> ExecutionConfig { + ExecutionConfig { + invocation_max_steps: self.starknet.environment.invoke_max_steps, + validation_max_steps: self.starknet.environment.validate_max_steps, + ..Default::default() + } + } + + fn forking_config(&self) -> Result> { + if let Some(ref url) = self.forking.fork_provider { + let cfg = ForkingConfig { url: url.clone(), block: self.forking.fork_block }; + return Ok(Some(cfg)); + } + + Ok(None) + } + + fn db_config(&self) -> DbConfig { + DbConfig { dir: self.db_dir.clone() } + } + + fn metrics_config(&self) -> Option { + if self.metrics.metrics { + Some(MetricsConfig { addr: self.metrics.metrics_addr, port: self.metrics.metrics_port }) + } else { + 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)?)? + } 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.silent { + self.silent = config.silent.unwrap_or_default(); + } + + if !self.no_mining { + self.no_mining = config.no_mining.unwrap_or_default(); + } + + if self.block_time.is_none() { + self.block_time = config.block_time; + } + + if self.db_dir.is_none() { + self.db_dir = config.db_dir; + } + + if self.logging == LoggingOptions::default() { + if let Some(logging) = config.logging { + self.logging = logging; + } + } + + if self.metrics == MetricsOptions::default() { + if let Some(metrics) = config.metrics { + self.metrics = metrics; + } + } + + if self.server == ServerOptions::default() { + if let Some(server) = config.server { + self.server = server; + } + } + + self.starknet.merge(config.starknet.as_ref()); + self.development.merge(config.development.as_ref()); + + if self.gpo == GasPriceOracleOptions::default() { + if let Some(gpo) = config.gpo { + self.gpo = gpo; + } + } + + if self.forking == ForkingOptions::default() { + if let Some(forking) = config.forking { + self.forking = forking; + } + } + + #[cfg(feature = "slot")] + if self.slot == SlotOptions::default() { + if let Some(slot) = config.slot { + self.slot = slot; + } + } + + Ok(self) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use assert_matches::assert_matches; + use katana_core::constants::{ + DEFAULT_ETH_L1_DATA_GAS_PRICE, DEFAULT_ETH_L1_GAS_PRICE, DEFAULT_STRK_L1_DATA_GAS_PRICE, + DEFAULT_STRK_L1_GAS_PRICE, + }; + use katana_node::config::execution::{ + DEFAULT_INVOCATION_MAX_STEPS, DEFAULT_VALIDATION_MAX_STEPS, + }; + use katana_primitives::chain::ChainId; + use katana_primitives::{address, felt, ContractAddress, Felt}; + + use super::*; + + #[test] + fn test_starknet_config_default() { + let args = NodeArgs::parse_from(["katana"]); + let config = args.config().unwrap(); + + assert!(config.dev.fee); + assert!(config.dev.account_validation); + assert!(config.forking.is_none()); + assert_eq!(config.execution.invocation_max_steps, DEFAULT_INVOCATION_MAX_STEPS); + assert_eq!(config.execution.validation_max_steps, DEFAULT_VALIDATION_MAX_STEPS); + assert_eq!(config.db.dir, None); + assert_eq!(config.chain.id, ChainId::parse("KATANA").unwrap()); + assert_eq!(config.chain.genesis.sequencer_address, *DEFAULT_SEQUENCER_ADDRESS); + } + + #[test] + fn test_starknet_config_custom() { + let args = NodeArgs::parse_from([ + "katana", + "--dev", + "--dev.no-fee", + "--dev.no-account-validation", + "--chain-id", + "SN_GOERLI", + "--invoke-max-steps", + "200", + "--validate-max-steps", + "100", + "--db-dir", + "/path/to/db", + ]); + let config = args.config().unwrap(); + + assert!(!config.dev.fee); + assert!(!config.dev.account_validation); + assert_eq!(config.execution.invocation_max_steps, 200); + assert_eq!(config.execution.validation_max_steps, 100); + assert_eq!(config.db.dir, Some(PathBuf::from("/path/to/db"))); + assert_eq!(config.chain.id, ChainId::GOERLI); + assert_eq!(config.chain.genesis.sequencer_address, *DEFAULT_SEQUENCER_ADDRESS); + } + + #[test] + fn custom_fixed_gas_prices() { + let config = NodeArgs::parse_from(["katana"]).config().unwrap(); + assert!(config.dev.fixed_gas_prices.is_none()); + + let config = + NodeArgs::parse_from(["katana", "--gpo.l1-eth-gas-price", "10"]).config().unwrap(); + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, 10); + assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); + assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); + assert_eq!(prices.data_gas_price.strk, DEFAULT_STRK_L1_DATA_GAS_PRICE); + }); + + let config = + NodeArgs::parse_from(["katana", "--gpo.l1-strk-gas-price", "20"]).config().unwrap(); + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, DEFAULT_ETH_L1_GAS_PRICE); + assert_eq!(prices.gas_price.strk, 20); + assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); + assert_eq!(prices.data_gas_price.strk, DEFAULT_STRK_L1_DATA_GAS_PRICE); + }); + + let config = + NodeArgs::parse_from(["katana", "--gpo.l1-eth-data-gas-price", "1"]).config().unwrap(); + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, DEFAULT_ETH_L1_GAS_PRICE); + assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); + assert_eq!(prices.data_gas_price.eth, 1); + assert_eq!(prices.data_gas_price.strk, DEFAULT_STRK_L1_DATA_GAS_PRICE); + }); + + let config = + NodeArgs::parse_from(["katana", "--gpo.l1-strk-data-gas-price", "2"]).config().unwrap(); + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, DEFAULT_ETH_L1_GAS_PRICE); + assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); + assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); + assert_eq!(prices.data_gas_price.strk, 2); + }); + + let config = NodeArgs::parse_from([ + "katana", + "--gpo.l1-eth-gas-price", + "10", + "--gpo.l1-strk-data-gas-price", + "2", + ]) + .config() + .unwrap(); + + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, 10); + assert_eq!(prices.gas_price.strk, DEFAULT_STRK_L1_GAS_PRICE); + assert_eq!(prices.data_gas_price.eth, DEFAULT_ETH_L1_DATA_GAS_PRICE); + assert_eq!(prices.data_gas_price.strk, 2); + }); + + // Set all the gas prices options + + let config = NodeArgs::parse_from([ + "katana", + "--gpo.l1-eth-gas-price", + "10", + "--gpo.l1-strk-gas-price", + "20", + "--gpo.l1-eth-data-gas-price", + "1", + "--gpo.l1-strk-data-gas-price", + "2", + ]) + .config() + .unwrap(); + + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, 10); + assert_eq!(prices.gas_price.strk, 20); + assert_eq!(prices.data_gas_price.eth, 1); + assert_eq!(prices.data_gas_price.strk, 2); + }) + } + + #[test] + fn genesis_with_fixed_gas_prices() { + let config = NodeArgs::parse_from([ + "katana", + "--genesis", + "./test-data/genesis.json", + "--gpo.l1-eth-gas-price", + "100", + "--gpo.l1-strk-gas-price", + "200", + "--gpo.l1-eth-data-gas-price", + "111", + "--gpo.l1-strk-data-gas-price", + "222", + ]) + .config() + .unwrap(); + + assert_eq!(config.chain.genesis.number, 0); + assert_eq!(config.chain.genesis.parent_hash, felt!("0x999")); + assert_eq!(config.chain.genesis.timestamp, 5123512314); + assert_eq!(config.chain.genesis.state_root, felt!("0x99")); + assert_eq!(config.chain.genesis.sequencer_address, address!("0x100")); + assert_eq!(config.chain.genesis.gas_prices.eth, 9999); + assert_eq!(config.chain.genesis.gas_prices.strk, 8888); + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, 100); + assert_eq!(prices.gas_price.strk, 200); + assert_eq!(prices.data_gas_price.eth, 111); + assert_eq!(prices.data_gas_price.strk, 222); + }) + } + + #[test] + fn config_from_file_and_cli() { + // CLI args must take precedence over the config file. + let content = r#" +[gpo] +l1_eth_gas_price = "0xfe" +l1_strk_gas_price = "200" +l1_eth_data_gas_price = "111" +l1_strk_data_gas_price = "222" + +[dev] +total_accounts = 20 + +[starknet.env] +validate_max_steps = 500 +invoke_max_steps = 9988 +chain_id.Named = "Mainnet" + "#; + let path = std::env::temp_dir().join("katana-config.json"); + std::fs::write(&path, content).unwrap(); + + let path_str = path.to_string_lossy().to_string(); + + let args = vec![ + "katana", + "--config", + path_str.as_str(), + "--genesis", + "./test-data/genesis.json", + "--validate-max-steps", + "1234", + "--dev", + "--dev.no-fee", + "--chain-id", + "0x123", + ]; + + let config = + NodeArgs::parse_from(args.clone()).with_config_file().unwrap().config().unwrap(); + + assert_eq!(config.execution.validation_max_steps, 1234); + assert_eq!(config.execution.invocation_max_steps, 9988); + assert!(!config.dev.fee); + assert_matches!(config.dev.fixed_gas_prices, Some(prices) => { + assert_eq!(prices.gas_price.eth, 254); + assert_eq!(prices.gas_price.strk, 200); + assert_eq!(prices.data_gas_price.eth, 111); + assert_eq!(prices.data_gas_price.strk, 222); + }); + assert_eq!(config.chain.genesis.number, 0); + assert_eq!(config.chain.genesis.parent_hash, felt!("0x999")); + assert_eq!(config.chain.genesis.timestamp, 5123512314); + assert_eq!(config.chain.genesis.state_root, felt!("0x99")); + assert_eq!(config.chain.genesis.sequencer_address, address!("0x100")); + assert_eq!(config.chain.genesis.gas_prices.eth, 9999); + assert_eq!(config.chain.genesis.gas_prices.strk, 8888); + assert_eq!(config.chain.id, ChainId::Id(Felt::from_str("0x123").unwrap())); + } +} diff --git a/bin/katana/src/cli/options.rs b/crates/katana/cli/src/options.rs similarity index 100% rename from bin/katana/src/cli/options.rs rename to crates/katana/cli/src/options.rs diff --git a/bin/katana/src/utils.rs b/crates/katana/cli/src/utils.rs similarity index 97% rename from bin/katana/src/utils.rs rename to crates/katana/cli/src/utils.rs index 394ae8e596..736781f147 100644 --- a/bin/katana/src/utils.rs +++ b/crates/katana/cli/src/utils.rs @@ -73,7 +73,7 @@ mod tests { #[test] fn parse_genesis_file() { - let path = "./tests/test-data/genesis.json"; + 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/torii/cli/Cargo.toml b/crates/torii/cli/Cargo.toml new file mode 100644 index 0000000000..fc67fbd3fd --- /dev/null +++ b/crates/torii/cli/Cargo.toml @@ -0,0 +1,20 @@ +[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 diff --git a/crates/torii/cli/src/args.rs b/crates/torii/cli/src/args.rs new file mode 100644 index 0000000000..dd5602b78d --- /dev/null +++ b/crates/torii/cli/src/args.rs @@ -0,0 +1,264 @@ +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)] +#[command(name = "torii", author, version, about, long_about = None)] +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, + + #[command(flatten)] + pub metrics: MetricsOptions, + + #[command(flatten)] + pub indexing: IndexingOptions, + + #[command(flatten)] + pub events: EventsOptions, + + #[command(flatten)] + pub server: ServerOptions, + + #[command(flatten)] + pub relay: RelayOptions, + + /// Configuration file + #[arg(long, help = "Configuration file to setup Torii.")] + pub 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, +} + +#[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")] From da719b5fd225495697126ef1c184a0efaa5529a8 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 12 Nov 2024 11:24:37 +0700 Subject: [PATCH 2/9] move node args methods to katana-cli crate as well --- Cargo.lock | 19 ++- bin/katana/Cargo.toml | 15 +-- bin/katana/src/cli/mod.rs | 3 +- bin/katana/src/cli/node.rs | 203 --------------------------------- crates/katana/cli/Cargo.toml | 8 ++ crates/katana/cli/src/node.rs | 75 +++++++++++- crates/katana/cli/src/utils.rs | 124 ++++++++++++++++++++ 7 files changed, 217 insertions(+), 230 deletions(-) delete mode 100644 bin/katana/src/cli/node.rs diff --git a/Cargo.lock b/Cargo.lock index 4862dfcf3a..5464bf50fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8069,19 +8069,11 @@ dependencies = [ "clap", "clap_complete", "comfy-table", - "console", - "dojo-utils", "katana-cli", "katana-db", "katana-node", - "katana-primitives", - "serde_json", "shellexpand", "starknet 0.12.0", - "tokio", - "tracing", - "tracing-log 0.1.4", - "tracing-subscriber", ] [[package]] @@ -8108,14 +8100,21 @@ dependencies = [ "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", ] @@ -12822,9 +12821,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", diff --git a/bin/katana/Cargo.toml b/bin/katana/Cargo.toml index 08b0efa12c..1845e968f5 100644 --- a/bin/katana/Cargo.toml +++ b/bin/katana/Cargo.toml @@ -7,32 +7,23 @@ repository.workspace = true version.workspace = true [dependencies] +katana-cli.workspace = true katana-db.workspace = true katana-node.workspace = true -katana-primitives.workspace = true -katana-cli.workspace = true anyhow.workspace = true byte-unit = "5.1.4" clap.workspace = true clap_complete.workspace = true comfy-table = "7.1.1" -console.workspace = true -dojo-utils.workspace = true -serde_json.workspace = true shellexpand = "3.1.0" -tokio.workspace = true -tracing.workspace = true -tracing-log.workspace = true -tracing-subscriber.workspace = true [dev-dependencies] assert_matches.workspace = true starknet.workspace = true [features] -default = [ "jemalloc", "slot" ] +default = [ "jemalloc", "katana-cli/slot" ] jemalloc = [ ] -slot = [ "katana-primitives/slot", "katana-cli/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 b3514e6f9b..da4cd4e34b 100644 --- a/bin/katana/src/cli/mod.rs +++ b/bin/katana/src/cli/mod.rs @@ -1,5 +1,4 @@ mod db; -mod node; use anyhow::Result; use clap::{Args, CommandFactory, Parser, Subcommand}; @@ -26,7 +25,7 @@ impl Cli { }; } - node::execute(&self.node.with_config_file()?) + self.node.with_config_file()?.execute() } } diff --git a/bin/katana/src/cli/node.rs b/bin/katana/src/cli/node.rs deleted file mode 100644 index 492c5c35c9..0000000000 --- a/bin/katana/src/cli/node.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! 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. - -use anyhow::{Context, Result}; -use console::Style; -use katana_cli::node::NodeArgs; -use katana_cli::utils::LogFormat; -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 tracing::{info, Subscriber}; -use tracing_log::LogTracer; -use tracing_subscriber::{fmt, EnvFilter}; - -pub(crate) const LOG_TARGET: &str = "katana::cli"; - -pub fn execute(args: &NodeArgs) -> Result<()> { - init_logging(args)?; - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .context("failed to build tokio runtime")? - .block_on(start_node(args)) -} - -async fn start_node(args: &NodeArgs) -> Result<()> { - // Build the node - let config = args.config()?; - let node = katana_node::build(config).await.context("failed to build node")?; - - if !args.silent { - print_intro(args, &node.backend.chain_spec); - } - - // Launch the node - let handle = node.launch().await.context("failed to launch node")?; - - // Wait until an OS signal (ie SIGINT, SIGTERM) is received or the node is shutdown. - tokio::select! { - _ = dojo_utils::signal::wait_signals() => { - // Gracefully shutdown the node before exiting - handle.stop().await?; - }, - - _ = handle.stopped() => { } - } - - info!("Shutting down."); - - Ok(()) -} - -fn init_logging(args: &NodeArgs) -> Result<()> { - const DEFAULT_LOG_FILTER: &str = "info,tasks=debug,executor=trace,forking::backend=trace,\ - blockifier=off,jsonrpsee_server=off,hyper=off,\ - messaging=debug,node=error"; - - let filter = if args.development.dev { - &format!("{DEFAULT_LOG_FILTER},server=debug") - } else { - DEFAULT_LOG_FILTER - }; - - LogTracer::init()?; - - // If the user has set the `RUST_LOG` environment variable, then we prioritize it. - // Otherwise, we use the default log filter. - // TODO: change env var to `KATANA_LOG`. - let filter = EnvFilter::try_from_default_env().or(EnvFilter::try_new(filter))?; - let builder = fmt::Subscriber::builder().with_env_filter(filter); - - let subscriber: Box = match args.logging.log_format { - LogFormat::Full => Box::new(builder.finish()), - LogFormat::Json => Box::new(builder.json().finish()), - }; - - Ok(tracing::subscriber::set_global_default(subscriber)?) -} - -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() - ) - } - } -} diff --git a/crates/katana/cli/Cargo.toml b/crates/katana/cli/Cargo.toml index c5f247587c..d0b434076e 100644 --- a/crates/katana/cli/Cargo.toml +++ b/crates/katana/cli/Cargo.toml @@ -6,6 +6,7 @@ repository.workspace = true version.workspace = true [dependencies] +dojo-utils.workspace = true katana-core.workspace = true katana-node.workspace = true katana-primitives.workspace = true @@ -15,9 +16,15 @@ 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] @@ -27,3 +34,4 @@ starknet.workspace = true [features] default = [ "slot" ] slot = [ "dep:katana-slot-controller", "katana-primitives/slot" ] +starknet-messaging = [ "katana-node/starknet-messaging" ] diff --git a/crates/katana/cli/src/node.rs b/crates/katana/cli/src/node.rs index ea49e6d468..d950544aac 100644 --- a/crates/katana/cli/src/node.rs +++ b/crates/katana/cli/src/node.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use std::path::PathBuf; use alloy_primitives::U256; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Parser; use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; use katana_core::service::messaging::MessagingConfig; @@ -19,9 +19,15 @@ use katana_primitives::chain_spec::{self, ChainSpec}; 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::utils::parse_seed; +use crate::options::*; +use crate::utils; +use crate::utils::{parse_seed, LogFormat}; + +pub(crate) const LOG_TARGET: &str = "katana::cli"; #[derive(Parser, Debug, Serialize, Deserialize, Default)] pub struct NodeArgs { @@ -107,6 +113,69 @@ pub struct NodeArgsConfig { } impl NodeArgs { + pub fn execute(&self) -> Result<()> { + self.init_logging()?; + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("failed to build tokio runtime")? + .block_on(self.start_node()) + } + + 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 { + utils::print_intro(self, &node.backend.chain_spec); + } + + // Launch the node + let handle = node.launch().await.context("failed to launch node")?; + + // Wait until an OS signal (ie SIGINT, SIGTERM) is received or the node is shutdown. + tokio::select! { + _ = dojo_utils::signal::wait_signals() => { + // Gracefully shutdown the node before exiting + handle.stop().await?; + }, + + _ = handle.stopped() => { } + } + + info!("Shutting down."); + + Ok(()) + } + + fn init_logging(&self) -> Result<()> { + const DEFAULT_LOG_FILTER: &str = "info,tasks=debug,executor=trace,forking::backend=trace,\ + blockifier=off,jsonrpsee_server=off,hyper=off,\ + messaging=debug,node=error"; + + let filter = if self.development.dev { + &format!("{DEFAULT_LOG_FILTER},server=debug") + } else { + DEFAULT_LOG_FILTER + }; + + LogTracer::init()?; + + // If the user has set the `RUST_LOG` environment variable, then we prioritize it. + // Otherwise, we use the default log filter. + // TODO: change env var to `KATANA_LOG`. + let filter = EnvFilter::try_from_default_env().or(EnvFilter::try_new(filter))?; + let builder = fmt::Subscriber::builder().with_env_filter(filter); + + let subscriber: Box = match self.logging.log_format { + LogFormat::Full => Box::new(builder.finish()), + LogFormat::Json => Box::new(builder.json().finish()), + }; + + Ok(tracing::subscriber::set_global_default(subscriber)?) + } + pub fn config(&self) -> Result { let db = self.db_config(); let rpc = self.rpc_config(); diff --git a/crates/katana/cli/src/utils.rs b/crates/katana/cli/src/utils.rs index 736781f147..751278a333 100644 --- a/crates/katana/cli/src/utils.rs +++ b/crates/katana/cli/src/utils.rs @@ -4,10 +4,22 @@ 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::node::LOG_TARGET; +use crate::NodeArgs; pub fn parse_seed(seed: &str) -> [u8; 32] { let seed = seed.as_bytes(); @@ -67,6 +79,118 @@ impl Display for LogFormat { } } +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::*; From 9f3ee843a1b384e6dbdb895dde29a1571e7b2eaa Mon Sep 17 00:00:00 2001 From: glihm Date: Tue, 12 Nov 2024 20:52:41 -0600 Subject: [PATCH 3/9] fix: add missing derive and conversion to config file --- crates/katana/cli/Cargo.toml | 3 +- crates/katana/cli/src/node.rs | 109 ++++++++++++++---- crates/katana/cli/src/options.rs | 6 + .../katana/core/src/service/messaging/mod.rs | 2 +- crates/torii/cli/Cargo.toml | 4 + crates/torii/cli/src/args.rs | 84 +++++++++++--- 6 files changed, 168 insertions(+), 40 deletions(-) diff --git a/crates/katana/cli/Cargo.toml b/crates/katana/cli/Cargo.toml index c5f247587c..89cc3e5ba1 100644 --- a/crates/katana/cli/Cargo.toml +++ b/crates/katana/cli/Cargo.toml @@ -25,5 +25,6 @@ assert_matches.workspace = true starknet.workspace = true [features] -default = [ "slot" ] +default = [ "slot", "server" ] slot = [ "dep:katana-slot-controller", "katana-primitives/slot" ] +server = [ ] diff --git a/crates/katana/cli/src/node.rs b/crates/katana/cli/src/node.rs index ea49e6d468..ea36693f0f 100644 --- a/crates/katana/cli/src/node.rs +++ b/crates/katana/cli/src/node.rs @@ -23,7 +23,8 @@ use serde::{Deserialize, Serialize}; use super::options::*; use crate::utils::parse_seed; -#[derive(Parser, Debug, Serialize, Deserialize, Default)] +#[derive(Parser, Debug, Serialize, Deserialize, Default, Clone)] +#[command(next_help_heading = "Node options")] pub struct NodeArgs { /// Don't print anything on startup. #[arg(long)] @@ -47,6 +48,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 @@ -59,9 +64,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, @@ -80,10 +87,6 @@ pub struct NodeArgs { #[cfg(feature = "slot")] #[command(flatten)] pub slot: SlotOptions, - - /// Configuration file - #[arg(long)] - config: Option, } #[derive(Debug, Serialize, Deserialize, Default)] @@ -94,18 +97,69 @@ pub struct NodeArgsConfig { 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 = "server")] + pub server: Option, + + #[cfg(feature = "server")] + pub metrics: Option, + #[cfg(feature = "slot")] pub slot: Option, } +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 { + silent: Some(args.silent), + no_mining: Some(args.no_mining), + 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 = "slot")] + { + node_config.slot = + if args.slot == SlotOptions::default() { None } else { Some(args.slot) }; + } + + #[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) + } +} + impl NodeArgs { pub fn config(&self) -> Result { let db = self.db_config(); @@ -132,12 +186,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() } } } @@ -222,11 +284,15 @@ 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, @@ -264,15 +330,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; + } } } diff --git a/crates/katana/cli/src/options.rs b/crates/katana/cli/src/options.rs index 83b90eb0b5..60658c3354 100644 --- a/crates/katana/cli/src/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/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 index fc67fbd3fd..0f8b708b33 100644 --- a/crates/torii/cli/Cargo.toml +++ b/crates/torii/cli/Cargo.toml @@ -18,3 +18,7 @@ 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 index dd5602b78d..d8d7f8b3dd 100644 --- a/crates/torii/cli/src/args.rs +++ b/crates/torii/cli/src/args.rs @@ -12,8 +12,9 @@ use super::options::*; pub 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)] +#[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")] @@ -42,8 +43,9 @@ pub struct ToriiArgs { #[arg(long, help = "Open World Explorer on the browser.")] pub explorer: bool, - #[command(flatten)] - pub metrics: MetricsOptions, + /// Configuration file + #[arg(long, help = "Configuration file to setup Torii.")] + pub config: Option, #[command(flatten)] pub indexing: IndexingOptions, @@ -51,15 +53,17 @@ pub struct ToriiArgs { #[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, - - /// Configuration file - #[arg(long, help = "Configuration file to setup Torii.")] - pub config: Option, } impl ToriiArgs { @@ -99,10 +103,6 @@ impl ToriiArgs { 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(); } @@ -111,12 +111,19 @@ impl ToriiArgs { self.events = config.events.unwrap_or_default(); } - if self.server == ServerOptions::default() { - self.server = config.server.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.relay == RelayOptions::default() { + self.relay = config.relay.unwrap_or_default(); + } + + if self.metrics == MetricsOptions::default() { + self.metrics = config.metrics.unwrap_or_default(); + } } Ok(self) @@ -130,13 +137,54 @@ pub struct ToriiArgsConfig { pub db_dir: Option, pub external_url: Option, pub explorer: Option, - pub metrics: 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}; From f9523ee8fcbd7da27c51baa1ea0f82e731794afb Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 13 Nov 2024 11:05:34 +0700 Subject: [PATCH 4/9] remove silent arg --- crates/katana/cli/src/node.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/katana/cli/src/node.rs b/crates/katana/cli/src/node.rs index 0c8e3d4cee..2eb121618a 100644 --- a/crates/katana/cli/src/node.rs +++ b/crates/katana/cli/src/node.rs @@ -97,7 +97,6 @@ pub struct NodeArgs { #[derive(Debug, Serialize, Deserialize, Default)] pub struct NodeArgsConfig { - pub silent: Option, pub no_mining: Option, pub block_time: Option, pub db_dir: Option, @@ -127,8 +126,7 @@ impl TryFrom for NodeArgsConfig { let args = args.with_config_file()?; let mut node_config = NodeArgsConfig { - silent: Some(args.silent), - no_mining: Some(args.no_mining), + no_mining: if args.no_mining { Some(true) } else { None }, block_time: args.block_time, db_dir: args.db_dir, messaging: args.messaging, @@ -377,10 +375,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(); } From d6b3b67508f66a65927143ac5213866d95d9f27e Mon Sep 17 00:00:00 2001 From: Nasr Date: Wed, 13 Nov 2024 11:17:10 +0700 Subject: [PATCH 5/9] create directory for db dir torii --- bin/torii/src/main.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index e4f7acec8e..dae312ad73 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -76,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) From 6d02f27c75a1c2fca55a249b4d0e7a97c1be23aa Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 13 Nov 2024 11:21:32 +0700 Subject: [PATCH 6/9] move to new module --- crates/katana/cli/src/file.rs | 73 +++++++++++++++++++++++++++++++ crates/katana/cli/src/lib.rs | 4 +- crates/katana/cli/src/node.rs | 81 ++--------------------------------- 3 files changed, 79 insertions(+), 79 deletions(-) create mode 100644 crates/katana/cli/src/file.rs diff --git a/crates/katana/cli/src/file.rs b/crates/katana/cli/src/file.rs new file mode 100644 index 0000000000..468f3c4db1 --- /dev/null +++ b/crates/katana/cli/src/file.rs @@ -0,0 +1,73 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use katana_core::service::messaging::MessagingConfig; +use serde::{Deserialize, Serialize}; + +use crate::{options::*, 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 index 116b49b24c..1692868d0b 100644 --- a/crates/katana/cli/src/lib.rs +++ b/crates/katana/cli/src/lib.rs @@ -1,8 +1,10 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] +pub mod file; pub mod node; pub mod options; pub mod utils; -pub use node::{NodeArgs, NodeArgsConfig}; +pub use file::NodeArgsConfig; +pub use node::NodeArgs; pub use options::*; diff --git a/crates/katana/cli/src/node.rs b/crates/katana/cli/src/node.rs index 2eb121618a..c2e5524f9b 100644 --- a/crates/katana/cli/src/node.rs +++ b/crates/katana/cli/src/node.rs @@ -23,6 +23,7 @@ use tracing::{info, Subscriber}; use tracing_log::LogTracer; use tracing_subscriber::{fmt, EnvFilter}; +use crate::file::NodeArgsConfig; use crate::options::*; use crate::utils; use crate::utils::{parse_seed, LogFormat}; @@ -95,75 +96,6 @@ pub struct NodeArgs { pub slot: SlotOptions, } -#[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, - - #[cfg(feature = "slot")] - pub slot: Option, -} - -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 = "slot")] - { - node_config.slot = - if args.slot == SlotOptions::default() { None } else { Some(args.slot) }; - } - - #[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) - } -} - impl NodeArgs { pub fn execute(&self) -> Result<()> { self.init_logging()?; @@ -365,8 +297,8 @@ impl NodeArgs { /// 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); }; @@ -423,13 +355,6 @@ impl NodeArgs { } } - #[cfg(feature = "slot")] - if self.slot == SlotOptions::default() { - if let Some(slot) = config.slot { - self.slot = slot; - } - } - Ok(self) } } From 6e91256788a3a66723cb9d09dc5508d011c5617c Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 13 Nov 2024 11:22:45 +0700 Subject: [PATCH 7/9] rename node -> args --- crates/katana/cli/src/{node.rs => args.rs} | 0 crates/katana/cli/src/lib.rs | 4 ++-- crates/katana/cli/src/utils.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename crates/katana/cli/src/{node.rs => args.rs} (100%) diff --git a/crates/katana/cli/src/node.rs b/crates/katana/cli/src/args.rs similarity index 100% rename from crates/katana/cli/src/node.rs rename to crates/katana/cli/src/args.rs diff --git a/crates/katana/cli/src/lib.rs b/crates/katana/cli/src/lib.rs index 1692868d0b..a53f26c0b0 100644 --- a/crates/katana/cli/src/lib.rs +++ b/crates/katana/cli/src/lib.rs @@ -1,10 +1,10 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] +pub mod args; pub mod file; -pub mod node; pub mod options; pub mod utils; +pub use args::NodeArgs; pub use file::NodeArgsConfig; -pub use node::NodeArgs; pub use options::*; diff --git a/crates/katana/cli/src/utils.rs b/crates/katana/cli/src/utils.rs index 751278a333..28cc0ffddc 100644 --- a/crates/katana/cli/src/utils.rs +++ b/crates/katana/cli/src/utils.rs @@ -18,7 +18,7 @@ use katana_primitives::genesis::Genesis; use serde::{Deserialize, Serialize}; use tracing::info; -use crate::node::LOG_TARGET; +use crate::args::LOG_TARGET; use crate::NodeArgs; pub fn parse_seed(seed: &str) -> [u8; 32] { From 43cebb0754598e216243f1426714aa9157c38f62 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 13 Nov 2024 11:23:12 +0700 Subject: [PATCH 8/9] dont re-export node config file --- crates/katana/cli/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/katana/cli/src/lib.rs b/crates/katana/cli/src/lib.rs index a53f26c0b0..b8a2a0a70b 100644 --- a/crates/katana/cli/src/lib.rs +++ b/crates/katana/cli/src/lib.rs @@ -6,5 +6,4 @@ pub mod options; pub mod utils; pub use args::NodeArgs; -pub use file::NodeArgsConfig; pub use options::*; From 8cb11197e6123026e3bd5f7aa5976f9b21df01a1 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 13 Nov 2024 11:27:13 +0700 Subject: [PATCH 9/9] fmt --- crates/katana/cli/src/file.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/katana/cli/src/file.rs b/crates/katana/cli/src/file.rs index 468f3c4db1..b9cd8a0c4c 100644 --- a/crates/katana/cli/src/file.rs +++ b/crates/katana/cli/src/file.rs @@ -4,7 +4,8 @@ use anyhow::Result; use katana_core::service::messaging::MessagingConfig; use serde::{Deserialize, Serialize}; -use crate::{options::*, NodeArgs}; +use crate::options::*; +use crate::NodeArgs; /// Node arguments configuration file. #[derive(Debug, Serialize, Deserialize, Default)]