diff --git a/Cargo.lock b/Cargo.lock index c7c9a7e81c..1d58106900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14975,6 +14975,7 @@ name = "torii" version = "1.0.0-rc.2" dependencies = [ "anyhow", + "assert_matches", "async-trait", "base64 0.21.7", "camino", diff --git a/bin/katana/src/cli/options.rs b/bin/katana/src/cli/options.rs index ba5bffd04e..c915ce3118 100644 --- a/bin/katana/src/cli/options.rs +++ b/bin/katana/src/cli/options.rs @@ -63,21 +63,24 @@ pub struct ServerOptions { /// HTTP-RPC server listening interface. #[arg(long = "http.addr", value_name = "ADDRESS")] #[arg(default_value_t = DEFAULT_RPC_ADDR)] + #[serde(default = "default_http_addr")] pub http_addr: IpAddr, /// HTTP-RPC server listening port. #[arg(long = "http.port", value_name = "PORT")] #[arg(default_value_t = DEFAULT_RPC_PORT)] + #[serde(default = "default_http_port")] pub http_port: u16, /// Comma separated list of domains from which to accept cross origin requests. - #[arg(long = "http.corsdomain")] + #[arg(long = "http.cors_origins")] #[arg(value_delimiter = ',')] pub http_cors_origins: Option>, /// Maximum number of concurrent connections allowed. #[arg(long = "rpc.max-connections", value_name = "COUNT")] #[arg(default_value_t = DEFAULT_RPC_MAX_CONNECTIONS)] + #[serde(default = "default_max_connections")] pub max_connections: u32, } @@ -272,3 +275,15 @@ fn default_validate_max_steps() -> u32 { fn default_invoke_max_steps() -> u32 { DEFAULT_INVOCATION_MAX_STEPS } + +fn default_http_addr() -> IpAddr { + DEFAULT_RPC_ADDR +} + +fn default_http_port() -> u16 { + DEFAULT_RPC_PORT +} + +fn default_max_connections() -> u32 { + DEFAULT_RPC_MAX_CONNECTIONS +} diff --git a/bin/torii/Cargo.toml b/bin/torii/Cargo.toml index 71ff5ec916..c526e2481c 100644 --- a/bin/torii/Cargo.toml +++ b/bin/torii/Cargo.toml @@ -51,6 +51,7 @@ tempfile.workspace = true clap_config = "0.1.1" [dev-dependencies] +assert_matches.workspace = true camino.workspace = true [features] diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index d20b109066..fd130ce2f0 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -11,15 +11,14 @@ //! for more info. use std::cmp; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use anyhow::Context; -use clap::{ArgAction, CommandFactory, FromArgMatches, Parser}; -use clap_config::ClapConfig; +use anyhow::Result; +use clap::Parser; use dojo_metrics::exporters::prometheus::PrometheusRecorder; use dojo_utils::parse::parse_url; use dojo_world::contracts::world::WorldContractReader; @@ -47,163 +46,143 @@ 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(ClapConfig, Parser, Debug)] +#[derive(Parser, Debug)] #[command(name = "torii", author, version, about, long_about = None)] -struct Args { +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 = ":5050", value_parser = parse_url)] + #[arg(long, value_name = "URL", default_value = DEFAULT_RPC_URL, value_parser = parse_url)] rpc: Url, - #[command(flatten)] - server: ServerOptions, - /// Database filepath (ex: indexer.db). If specified file doesn't exist, it will be - /// created. Defaults to in-memory database - #[arg(short, long, default_value = "")] - database: String, + /// 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, - /// Port to serve Libp2p TCP & UDP Quic transports - #[arg(long, value_name = "PORT", default_value = "9090")] - relay_port: u16, + /// 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, - /// Port to serve Libp2p WebRTC transport - #[arg(long, value_name = "PORT", default_value = "9091")] - relay_webrtc_port: u16, + /// Open World Explorer on the browser. + #[arg(long, help = "Open World Explorer on the browser.")] + explorer: bool, - /// Port to serve Libp2p WebRTC transport - #[arg(long, value_name = "PORT", default_value = "9092")] - relay_websocket_port: u16, + #[command(flatten)] + metrics: MetricsOptions, - /// Path to a local identity key file. If not specified, a new identity will be generated - #[arg(long, value_name = "PATH")] - relay_local_key_path: Option, + #[command(flatten)] + indexing: IndexingOptions, - /// Path to a local certificate file. If not specified, a new certificate will be generated - /// for WebRTC connections - #[arg(long, value_name = "PATH")] - relay_cert_path: Option, + #[command(flatten)] + events: EventsOptions, - /// The external url of the server, used for configuring the GraphQL Playground in a hosted - /// environment - #[arg(long, value_parser = parse_url)] - external_url: Option, + #[command(flatten)] + server: ServerOptions, #[command(flatten)] - metrics: MetricsOptions, + relay: RelayOptions, - /// Open World Explorer on the browser. - #[arg(long)] - explorer: bool, + /// Configuration file + #[arg(long, help = "Configuration file to setup Torii.")] + config: Option, +} - /// Chunk size of the events page when indexing using events - #[arg(long, default_value = "1024")] - events_chunk_size: u64, +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); + }; - /// Number of blocks to process before commiting to DB - #[arg(long, default_value = "10240")] - blocks_chunk_size: u64, + // 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. - /// Enable indexing pending blocks - #[arg(long, action = ArgAction::Set, default_value_t = true)] - index_pending: bool, + if self.world_address.is_none() { + self.world_address = config.world_address; + } - /// Polling interval in ms - #[arg(long, default_value = "500")] - polling_interval: u64, + if self.rpc == Url::parse(DEFAULT_RPC_URL).unwrap() { + if let Some(rpc) = config.rpc { + self.rpc = rpc; + } + } - /// Max concurrent tasks - #[arg(long, default_value = "100")] - max_concurrent_tasks: usize, + if self.db_dir.is_none() { + self.db_dir = config.db_dir; + } - /// Whether or not to index world transactions - #[arg(long, action = ArgAction::Set, default_value_t = false)] - index_transactions: bool, + if self.external_url.is_none() { + self.external_url = config.external_url; + } - /// Whether or not to index raw events - #[arg(long, action = ArgAction::Set, default_value_t = true)] - index_raw_events: bool, + // Currently the comparison it's only at the top level. + // Need to make it more granular. - /// ERC contract addresses to index - #[arg(long, value_delimiter = ',', value_parser = parse_erc_contract)] - contracts: Vec, + if !self.explorer { + self.explorer = config.explorer.unwrap_or_default(); + } - /// Event messages that are going to be treated as historical - /// A list of the model tags (namespace-name) - #[arg(long, value_delimiter = ',')] - historical_events: Vec, + if self.metrics == MetricsOptions::default() { + self.metrics = config.metrics.unwrap_or_default(); + } - /// Configuration file - #[arg(long)] - #[clap_config(skip)] - config: Option, -} + if self.indexing == IndexingOptions::default() { + self.indexing = config.indexing.unwrap_or_default(); + } -/// Metrics server default address. -const DEFAULT_METRICS_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); -/// Torii metrics server default port. -const DEFAULT_METRICS_PORT: u16 = 9200; - -#[derive(Debug, clap::Args, Clone, Serialize, Deserialize)] -#[command(next_help_heading = "Metrics options")] -struct MetricsOptions { - /// Enable metrics. - /// - /// For now, metrics will still be collected even if this flag is not set. This only - /// controls whether the metrics server is started or not. - #[arg(long)] - metrics: bool, - - /// The metrics will be served at the given address. - #[arg(requires = "metrics")] - #[arg(long = "metrics.addr", value_name = "ADDRESS")] - #[arg(default_value_t = DEFAULT_METRICS_ADDR)] - metrics_addr: IpAddr, - - /// The metrics will be served at the given port. - #[arg(requires = "metrics")] - #[arg(long = "metrics.port", value_name = "PORT")] - #[arg(default_value_t = DEFAULT_METRICS_PORT)] - metrics_port: u16, + 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) + } } -const DEFAULT_HTTP_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); -const DEFAULT_HTTP_PORT: u16 = 8080; - -#[derive(Debug, clap::Args, Clone, Serialize, Deserialize, PartialEq)] -#[command(next_help_heading = "Server options")] -struct ServerOptions { - /// HTTP server listening interface. - #[arg(long = "http.addr", value_name = "ADDRESS")] - #[arg(default_value_t = DEFAULT_HTTP_ADDR)] - http_addr: IpAddr, - - /// HTTP server listening port. - #[arg(long = "http.port", value_name = "PORT")] - #[arg(default_value_t = DEFAULT_HTTP_PORT)] - http_port: u16, - - /// Comma separated list of domains from which to accept cross origin requests. - #[arg(long = "http.corsdomain")] - #[arg(value_delimiter = ',')] - http_cors_origins: Vec, +#[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 matches = ::command().get_matches(); - let mut args = if let Some(path) = matches.get_one::("config") { - let config: ArgsConfig = toml::from_str(&std::fs::read_to_string(path)?)?; - Args::from_merged(matches, Some(config)) - } else { - Args::from_arg_matches(&matches)? - }; + let mut args = ToriiArgs::parse().with_config_file()?; let world_address = if let Some(world_address) = args.world_address { world_address @@ -212,7 +191,7 @@ async fn main() -> anyhow::Result<()> { }; // let mut contracts = parse_erc_contracts(&args.contracts)?; - args.contracts.push(Contract { address: world_address, r#type: ContractType::WORLD }); + args.indexing.contracts.push(Contract { address: world_address, r#type: ContractType::WORLD }); let filter_layer = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("info,hyper_reverse_proxy=off")); @@ -234,10 +213,11 @@ async fn main() -> anyhow::Result<()> { let tempfile = NamedTempFile::new()?; let database_path = - if args.database.is_empty() { tempfile.path().to_str().unwrap() } else { &args.database }; + if let Some(db_dir) = args.db_dir { db_dir } else { tempfile.path().to_path_buf() }; - let mut options = - SqliteConnectOptions::from_str(database_path)?.create_if_missing(true).with_regexp(); + let mut options = SqliteConnectOptions::from_str(&database_path.to_string_lossy())? + .create_if_missing(true) + .with_regexp(); // Performance settings options = options.auto_vacuum(SqliteAutoVacuum::None); @@ -263,7 +243,7 @@ async fn main() -> anyhow::Result<()> { executor.run().await.unwrap(); }); - let db = Sql::new(pool.clone(), sender.clone(), &args.contracts).await?; + let db = Sql::new(pool.clone(), sender.clone(), &args.indexing.contracts).await?; let processors = Processors { transaction: vec![Box::new(StoreTransactionProcessor)], @@ -273,10 +253,10 @@ async fn main() -> anyhow::Result<()> { let (block_tx, block_rx) = tokio::sync::mpsc::channel(100); let mut flags = IndexingFlags::empty(); - if args.index_transactions { + if args.indexing.index_transactions { flags.insert(IndexingFlags::TRANSACTIONS); } - if args.index_raw_events { + if args.events.raw { flags.insert(IndexingFlags::RAW_EVENTS); } @@ -286,20 +266,20 @@ async fn main() -> anyhow::Result<()> { provider.clone(), processors, EngineConfig { - max_concurrent_tasks: args.max_concurrent_tasks, + max_concurrent_tasks: args.indexing.max_concurrent_tasks, start_block: 0, - blocks_chunk_size: args.blocks_chunk_size, - events_chunk_size: args.events_chunk_size, - index_pending: args.index_pending, - polling_interval: Duration::from_millis(args.polling_interval), + blocks_chunk_size: args.indexing.blocks_chunk_size, + events_chunk_size: args.indexing.events_chunk_size, + index_pending: args.indexing.index_pending, + polling_interval: Duration::from_millis(args.indexing.polling_interval), flags, event_processor_config: EventProcessorConfig { - historical_events: args.historical_events.into_iter().collect(), + historical_events: args.events.historical.unwrap_or_default().into_iter().collect(), }, }, shutdown_tx.clone(), Some(block_tx), - &args.contracts, + &args.indexing.contracts, ); let shutdown_rx = shutdown_tx.subscribe(); @@ -310,22 +290,18 @@ async fn main() -> anyhow::Result<()> { let mut libp2p_relay_server = torii_relay::server::Relay::new( db, provider.clone(), - args.relay_port, - args.relay_webrtc_port, - args.relay_websocket_port, - args.relay_local_key_path, - args.relay_cert_path, + args.relay.port, + args.relay.webrtc_port, + args.relay.websocket_port, + args.relay.local_key_path, + args.relay.cert_path, ) .expect("Failed to start libp2p relay server"); let addr = SocketAddr::new(args.server.http_addr, args.server.http_port); let proxy_server = Arc::new(Proxy::new( addr, - if args.server.http_cors_origins.is_empty() { - None - } else { - Some(args.server.http_cors_origins) - }, + args.server.http_cors_origins.filter(|cors_origins| !cors_origins.is_empty()), Some(grpc_addr), None, )); @@ -403,23 +379,125 @@ async fn spawn_rebuilding_graphql_server( } } -// Parses clap cli argument which is expected to be in the format: -// - erc_type:address:start_block -// - address:start_block (erc_type defaults to ERC20) -fn parse_erc_contract(part: &str) -> anyhow::Result { - match part.split(':').collect::>().as_slice() { - [r#type, address] => { - let r#type = r#type.parse::()?; - if r#type == ContractType::WORLD { - return Err(anyhow::anyhow!( - "World address cannot be specified as an ERC contract" - )); - } +#[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()); + } - let address = Felt::from_str(address) - .with_context(|| format!("Expected address, found {}", address))?; - Ok(Contract { address, r#type }) - } - _ => Err(anyhow::anyhow!("Invalid contract format")), + #[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/bin/torii/src/options.rs b/bin/torii/src/options.rs new file mode 100644 index 0000000000..be1636c16a --- /dev/null +++ b/bin/torii/src/options.rs @@ -0,0 +1,315 @@ +use std::net::{IpAddr, Ipv4Addr}; +use std::str::FromStr; + +use anyhow::Context; +use clap::ArgAction; +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; + +#[derive(Debug, clap::Args, Clone, Serialize, Deserialize, PartialEq)] +#[command(next_help_heading = "Relay options")] +pub struct RelayOptions { + /// Port to serve Libp2p TCP & UDP Quic transports + #[arg( + long = "relay.port", + value_name = "PORT", + default_value_t = DEFAULT_RELAY_PORT, + help = "Port to serve Libp2p TCP & UDP Quic transports." + )] + #[serde(default = "default_relay_port")] + pub port: u16, + + /// Port to serve Libp2p WebRTC transport + #[arg( + long = "relay.webrtc_port", + value_name = "PORT", + default_value_t = DEFAULT_RELAY_WEBRTC_PORT, + help = "Port to serve Libp2p WebRTC transport." + )] + #[serde(default = "default_relay_webrtc_port")] + pub webrtc_port: u16, + + /// Port to serve Libp2p WebRTC transport + #[arg( + long = "relay.websocket_port", + value_name = "PORT", + default_value_t = DEFAULT_RELAY_WEBSOCKET_PORT, + help = "Port to serve Libp2p WebRTC transport." + )] + #[serde(default = "default_relay_websocket_port")] + pub websocket_port: u16, + + /// Path to a local identity key file. If not specified, a new identity will be generated + #[arg( + long = "relay.local_key_path", + value_name = "PATH", + help = "Path to a local identity key file. If not specified, a new identity will be \ + generated." + )] + pub local_key_path: Option, + + /// Path to a local certificate file. If not specified, a new certificate will be generated + /// for WebRTC connections + #[arg( + long = "relay.cert_path", + value_name = "PATH", + help = "Path to a local certificate file. If not specified, a new certificate will be \ + generated for WebRTC connections." + )] + pub cert_path: Option, +} + +impl Default for RelayOptions { + fn default() -> Self { + Self { + port: DEFAULT_RELAY_PORT, + webrtc_port: DEFAULT_RELAY_WEBRTC_PORT, + websocket_port: DEFAULT_RELAY_WEBSOCKET_PORT, + local_key_path: None, + cert_path: None, + } + } +} + +#[derive(Debug, clap::Args, Clone, Serialize, Deserialize, PartialEq)] +#[command(next_help_heading = "Indexing options")] +pub struct IndexingOptions { + /// Chunk size of the events page when indexing using events + #[arg(long = "indexing.events_chunk_size", default_value_t = DEFAULT_EVENTS_CHUNK_SIZE, help = "Chunk size of the events page to fetch from the sequencer.")] + #[serde(default = "default_events_chunk_size")] + pub events_chunk_size: u64, + + /// Number of blocks to process before commiting to DB + #[arg(long = "indexing.blocks_chunk_size", default_value_t = DEFAULT_BLOCKS_CHUNK_SIZE, help = "Number of blocks to process before commiting to DB.")] + #[serde(default = "default_blocks_chunk_size")] + pub blocks_chunk_size: u64, + + /// Enable indexing pending blocks + #[arg(long = "indexing.pending", action = ArgAction::Set, default_value_t = true, help = "Whether or not to index pending blocks.")] + pub index_pending: bool, + + /// Polling interval in ms + #[arg( + long = "indexing.polling_interval", + default_value_t = DEFAULT_POLLING_INTERVAL, + help = "Polling interval in ms for Torii to check for new events." + )] + #[serde(default = "default_polling_interval")] + pub polling_interval: u64, + + /// Max concurrent tasks + #[arg( + long = "indexing.max_concurrent_tasks", + default_value_t = DEFAULT_MAX_CONCURRENT_TASKS, + help = "Max concurrent tasks used to parallelize indexing." + )] + #[serde(default = "default_max_concurrent_tasks")] + pub max_concurrent_tasks: usize, + + /// Whether or not to index world transactions + #[arg( + long = "indexing.transactions", + action = ArgAction::Set, + default_value_t = false, + help = "Whether or not to index world transactions and keep them in the database." + )] + pub index_transactions: bool, + + /// ERC contract addresses to index + #[arg( + long = "indexing.contracts", + value_delimiter = ',', + value_parser = parse_erc_contract, + help = "ERC contract addresses to index. You may only specify ERC20 or ERC721 contracts." + )] + #[serde(deserialize_with = "deserialize_contracts")] + pub contracts: Vec, +} + +impl Default for IndexingOptions { + fn default() -> Self { + Self { + events_chunk_size: DEFAULT_EVENTS_CHUNK_SIZE, + blocks_chunk_size: DEFAULT_BLOCKS_CHUNK_SIZE, + index_pending: true, + index_transactions: false, + contracts: vec![], + polling_interval: DEFAULT_POLLING_INTERVAL, + max_concurrent_tasks: DEFAULT_MAX_CONCURRENT_TASKS, + } + } +} + +#[derive(Debug, clap::Args, Clone, Serialize, Deserialize, PartialEq)] +#[command(next_help_heading = "Events indexing options")] +pub struct EventsOptions { + /// Whether or not to index raw events + #[arg(long = "events.raw", action = ArgAction::Set, default_value_t = true, help = "Whether or not to index raw events.")] + pub raw: bool, + + /// Event messages that are going to be treated as historical + /// A list of the model tags (namespace-name) + #[arg( + long = "events.historical", + value_delimiter = ',', + help = "Event messages that are going to be treated as historical during indexing." + )] + pub historical: Option>, +} + +impl Default for EventsOptions { + fn default() -> Self { + Self { raw: true, historical: None } + } +} + +#[derive(Debug, clap::Args, Clone, Serialize, Deserialize, PartialEq)] +#[command(next_help_heading = "HTTP server options")] +pub struct ServerOptions { + /// HTTP server listening interface. + #[arg(long = "http.addr", value_name = "ADDRESS")] + #[arg(default_value_t = DEFAULT_HTTP_ADDR)] + #[serde(default = "default_http_addr")] + pub http_addr: IpAddr, + + /// HTTP server listening port. + #[arg(long = "http.port", value_name = "PORT")] + #[arg(default_value_t = DEFAULT_HTTP_PORT)] + #[serde(default = "default_http_port")] + pub http_port: u16, + + /// Comma separated list of domains from which to accept cross origin requests. + #[arg(long = "http.cors_origins")] + #[arg(value_delimiter = ',')] + pub http_cors_origins: Option>, +} + +impl Default for ServerOptions { + fn default() -> Self { + Self { http_addr: DEFAULT_HTTP_ADDR, http_port: DEFAULT_HTTP_PORT, http_cors_origins: None } + } +} + +#[derive(Debug, clap::Args, Clone, Serialize, Deserialize, PartialEq)] +#[command(next_help_heading = "Metrics options")] +pub struct MetricsOptions { + /// Enable metrics. + /// + /// For now, metrics will still be collected even if this flag is not set. This only + /// controls whether the metrics server is started or not. + #[arg(long)] + pub metrics: bool, + + /// The metrics will be served at the given address. + #[arg(requires = "metrics")] + #[arg(long = "metrics.addr", value_name = "ADDRESS")] + #[arg(default_value_t = DEFAULT_METRICS_ADDR)] + #[serde(default = "default_metrics_addr")] + pub metrics_addr: IpAddr, + + /// The metrics will be served at the given port. + #[arg(requires = "metrics")] + #[arg(long = "metrics.port", value_name = "PORT")] + #[arg(default_value_t = DEFAULT_METRICS_PORT)] + #[serde(default = "default_metrics_port")] + pub metrics_port: u16, +} + +impl Default for MetricsOptions { + fn default() -> Self { + Self { + metrics: false, + metrics_addr: DEFAULT_METRICS_ADDR, + metrics_port: DEFAULT_METRICS_PORT, + } + } +} + +// Parses clap cli argument which is expected to be in the format: +// - erc_type:address:start_block +// - address:start_block (erc_type defaults to ERC20) +fn parse_erc_contract(part: &str) -> anyhow::Result { + match part.split(':').collect::>().as_slice() { + [r#type, address] => { + let r#type = r#type.parse::()?; + if r#type == ContractType::WORLD { + return Err(anyhow::anyhow!( + "World address cannot be specified as an ERC contract" + )); + } + + let address = Felt::from_str(address) + .with_context(|| format!("Expected address, found {}", address))?; + Ok(Contract { address, r#type }) + } + _ => Err(anyhow::anyhow!("Invalid contract format")), + } +} + +// Add this function to handle TOML deserialization +fn deserialize_contracts<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let contracts: Vec = Vec::deserialize(deserializer)?; + contracts.iter().map(|s| parse_erc_contract(s).map_err(serde::de::Error::custom)).collect() +} + +// ** Default functions to setup serde of the configuration file ** +fn default_http_addr() -> IpAddr { + DEFAULT_HTTP_ADDR +} + +fn default_http_port() -> u16 { + DEFAULT_HTTP_PORT +} + +fn default_metrics_addr() -> IpAddr { + DEFAULT_METRICS_ADDR +} + +fn default_metrics_port() -> u16 { + DEFAULT_METRICS_PORT +} + +fn default_events_chunk_size() -> u64 { + DEFAULT_EVENTS_CHUNK_SIZE +} + +fn default_blocks_chunk_size() -> u64 { + DEFAULT_BLOCKS_CHUNK_SIZE +} + +fn default_polling_interval() -> u64 { + DEFAULT_POLLING_INTERVAL +} + +fn default_max_concurrent_tasks() -> usize { + DEFAULT_MAX_CONCURRENT_TASKS +} + +fn default_relay_port() -> u16 { + DEFAULT_RELAY_PORT +} + +fn default_relay_webrtc_port() -> u16 { + DEFAULT_RELAY_WEBRTC_PORT +} + +fn default_relay_websocket_port() -> u16 { + DEFAULT_RELAY_WEBSOCKET_PORT +} diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index 96d2f68ca4..be120e50ad 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -121,7 +121,7 @@ pub struct Event { pub executed_at: DateTime, pub created_at: DateTime, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] pub struct Contract { pub address: Felt, pub r#type: ContractType, diff --git a/examples/simple/manifest_dev.json b/examples/simple/manifest_dev.json index dcf12f67a9..ec085d7fce 100644 --- a/examples/simple/manifest_dev.json +++ b/examples/simple/manifest_dev.json @@ -1,7 +1,7 @@ { "world": { - "class_hash": "0x2f92b70bd2b5a40ddef12c55257f245176870b25c7eb0bd7a60cf1f1f2fbf0e", - "address": "0x30e907536f553a4b90bbd008df0785061073e432f20779e0c3549923dd67576", + "class_hash": "0x79d9ce84b97bcc2a631996c3100d57966fc2f5b061fb1ec4dfd0040976bcac6", + "address": "0x1b2e50266b9b673eb82e68249a1babde860f6414737b4a36ff7b29411a64666", "seed": "simple", "name": "simple", "entrypoints": [ @@ -1243,8 +1243,8 @@ }, "contracts": [ { - "address": "0xb03e5934197257ff6e03833bf34989930cd0fa49acd441995e5f40b08e96bc", - "class_hash": "0x758181d4a28c2a580c7ef9e963d4bcd5b0712320dc68a174c4f3ff4ad3eaae0", + "address": "0x366a86098f39b0c3e026067c846367e83111c727b88fa036597120d154d44f5", + "class_hash": "0x340e197b0fac61961591acdd872a89b0cb862893272ab72455356f5534baa7e", "abi": [ { "type": "impl", @@ -1501,7 +1501,7 @@ ] }, { - "address": "0x4664cdf8f8b36a99f441440a92252e40b4885a9973e0d0b38ca204ef2b3c4a1", + "address": "0x53fbb8640694994275d8a1b31ce290ec13e0dd433077775e185a9c31f054008", "class_hash": "0x2a400df88b0add6c980d281dc96354a5cfc2b886547e9ed621b097e21842ee6", "abi": [ { @@ -1677,7 +1677,7 @@ ] }, { - "address": "0x151981aac48998f52e5d3189cf0fb0819ba642158425275a939d915e66bd2a3", + "address": "0x40c69a07b5a9b64f581176511c8f8eac9008b48cb3e3c543eac2bf7466c57e3", "class_hash": "0x7cc8d15e576873d544640f7fd124bd430bd19c0f31e203fb069b4fc2f5c0ab9", "abi": [ { @@ -1853,8 +1853,8 @@ ] }, { - "address": "0x7b320d6a08aa96dd4bdcae3e031d6313e0628239c332e37584ed3ebaea133dd", - "class_hash": "0x758181d4a28c2a580c7ef9e963d4bcd5b0712320dc68a174c4f3ff4ad3eaae0", + "address": "0x1302b12ead2cdce8cb0a47f461ecd9d53629e62e8d38327f6452066298381b5", + "class_hash": "0x340e197b0fac61961591acdd872a89b0cb862893272ab72455356f5534baa7e", "abi": [ { "type": "impl", @@ -2128,13 +2128,13 @@ "events": [ { "members": [], - "class_hash": "0x5c72f78327d896e16f3c9fe7ee5e136ad9fc4cda89fba1ce7e665877d477cd5", + "class_hash": "0x943620824729c411797e6be26c3078924893be417ab08789489532d9c6aebb", "tag": "ns-E", "selector": "0x260e0511a6fa454a7d4ed8bea5fa52fc80fc588e33ba4cb58c65bbeeadf7565" }, { "members": [], - "class_hash": "0x408fc887363634ee0d261cc26606574ce2bc433bedbcef4bb88f8fbc69a1e43", + "class_hash": "0x5ed10e08c25eb6a1cb7e221209eac3baab14be36a3ea0b55f789d8302712310", "tag": "ns-EH", "selector": "0x4c6c7772b19b700cf97d078d02a419670d11d2b689a7a3647eac311b2817ced" }