diff --git a/Cargo.lock b/Cargo.lock index 27fb02c49d..bd10304343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15019,6 +15019,7 @@ dependencies = [ "hashlink", "ipfs-api-backend-hyper", "katana-runner", + "lazy_static", "num-traits 0.2.19", "once_cell", "reqwest 0.11.27", diff --git a/crates/torii/cli/src/options.rs b/crates/torii/cli/src/options.rs index 0746c9c952..22db9cd4dd 100644 --- a/crates/torii/cli/src/options.rs +++ b/crates/torii/cli/src/options.rs @@ -171,6 +171,15 @@ pub struct IndexingOptions { #[serde(default)] pub world_block: u64, + /// Whether or not to index Cartridge controllers. + #[arg( + long = "indexing.controllers", + default_value_t = false, + help = "Whether or not to index Cartridge controllers." + )] + #[serde(default)] + pub controllers: bool, + /// Whether or not to read models from the block number they were registered in. /// If false, models will be read from the latest block. #[arg( @@ -194,6 +203,7 @@ impl Default for IndexingOptions { max_concurrent_tasks: DEFAULT_MAX_CONCURRENT_TASKS, namespaces: vec![], world_block: 0, + controllers: false, strict_model_reader: false, } } @@ -238,6 +248,10 @@ impl IndexingOptions { self.world_block = other.world_block; } + if !self.controllers { + self.controllers = other.controllers; + } + if !self.strict_model_reader { self.strict_model_reader = other.strict_model_reader; } diff --git a/crates/torii/indexer/Cargo.toml b/crates/torii/indexer/Cargo.toml index 2a2e1ad22c..a82eca188b 100644 --- a/crates/torii/indexer/Cargo.toml +++ b/crates/torii/indexer/Cargo.toml @@ -38,6 +38,7 @@ ipfs-api-backend-hyper.workspace = true tokio-util.workspace = true tracing.workspace = true torii-sqlite.workspace = true +lazy_static.workspace = true [dev-dependencies] dojo-test-utils.workspace = true diff --git a/crates/torii/indexer/src/engine.rs b/crates/torii/indexer/src/engine.rs index 815849184d..42acd8e8e9 100644 --- a/crates/torii/indexer/src/engine.rs +++ b/crates/torii/indexer/src/engine.rs @@ -28,6 +28,7 @@ use torii_sqlite::{Cursors, Sql}; use tracing::{debug, error, info, trace, warn}; use crate::constants::LOG_TARGET; +use crate::processors::controller::ControllerProcessor; use crate::processors::erc20_legacy_transfer::Erc20LegacyTransferProcessor; use crate::processors::erc20_transfer::Erc20TransferProcessor; use crate::processors::erc721_legacy_transfer::Erc721LegacyTransferProcessor; @@ -39,6 +40,7 @@ use crate::processors::register_event::RegisterEventProcessor; use crate::processors::register_model::RegisterModelProcessor; use crate::processors::store_del_record::StoreDelRecordProcessor; use crate::processors::store_set_record::StoreSetRecordProcessor; +use crate::processors::store_transaction::StoreTransactionProcessor; use crate::processors::store_update_member::StoreUpdateMemberProcessor; use crate::processors::store_update_record::StoreUpdateRecordProcessor; use crate::processors::upgrade_event::UpgradeEventProcessor; @@ -62,7 +64,7 @@ impl Default for Processo fn default() -> Self { Self { block: vec![], - transaction: vec![], + transaction: vec![Box::new(StoreTransactionProcessor)], // We shouldn't have a catch all for now since the world doesn't forward raw events // anymore. catch_all_event: Box::new(RawEventProcessor) as Box>, @@ -105,6 +107,7 @@ impl Processors

{ Box::new(Erc721LegacyTransferProcessor) as Box>, ], ), + (ContractType::UDC, vec![Box::new(ControllerProcessor) as Box>]), ]; for (contract_type, processors) in event_processors { diff --git a/crates/torii/indexer/src/processors/controller.rs b/crates/torii/indexer/src/processors/controller.rs new file mode 100644 index 0000000000..de9ee84ef7 --- /dev/null +++ b/crates/torii/indexer/src/processors/controller.rs @@ -0,0 +1,124 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use anyhow::{Error, Result}; +use async_trait::async_trait; +use dojo_world::contracts::world::WorldContractReader; +use lazy_static::lazy_static; +use starknet::core::types::Event; +use starknet::core::utils::parse_cairo_short_string; +use starknet::macros::felt; +use starknet::providers::Provider; +use starknet_crypto::Felt; +use torii_sqlite::Sql; +use tracing::info; + +use super::{EventProcessor, EventProcessorConfig}; +use crate::task_manager::{TaskId, TaskPriority}; + +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::controller"; + +#[derive(Default, Debug)] +pub struct ControllerProcessor; + +lazy_static! { + // https://x.cartridge.gg/ + pub(crate) static ref CARTRIDGE_MAGIC: [Felt; 22] = [ + felt!("0x68"), + felt!("0x74"), + felt!("0x74"), + felt!("0x70"), + felt!("0x73"), + felt!("0x3a"), + felt!("0x2f"), + felt!("0x2f"), + felt!("0x78"), + felt!("0x2e"), + felt!("0x63"), + felt!("0x61"), + felt!("0x72"), + felt!("0x74"), + felt!("0x72"), + felt!("0x69"), + felt!("0x64"), + felt!("0x67"), + felt!("0x65"), + felt!("0x2e"), + felt!("0x67"), + felt!("0x67"), + ]; +} + +#[async_trait] +impl

EventProcessor

for ControllerProcessor +where + P: Provider + Send + Sync + std::fmt::Debug, +{ + fn event_key(&self) -> String { + "ContractDeployed".to_string() + } + + fn validate(&self, event: &Event) -> bool { + // ContractDeployed event has no keys and contains username in data + event.keys.len() == 1 && !event.data.is_empty() + } + + fn task_priority(&self) -> TaskPriority { + 3 + } + + fn task_identifier(&self, event: &Event) -> TaskId { + let mut hasher = DefaultHasher::new(); + // the contract address is the first felt in data + event.data[0].hash(&mut hasher); + hasher.finish() + } + + async fn process( + &self, + _world: &WorldContractReader

, + db: &mut Sql, + _block_number: u64, + block_timestamp: u64, + _event_id: &str, + event: &Event, + _config: &EventProcessorConfig, + ) -> Result<(), Error> { + // Address is the first felt in data + let address = event.data[0]; + + let calldata = event.data[5..].to_vec(); + // our calldata has to be more than 25 felts. + if calldata.len() < 25 { + return Ok(()); + } + // check for this sequence of felts + let cartridge_magic_len = calldata[2]; + // length has to be 22 + if cartridge_magic_len != Felt::from(22) { + return Ok(()); + } + + // this should never fail if since our len is 22 + let cartridge_magic: [Felt; 22] = calldata[3..25].try_into().unwrap(); + + // has to match with https://x.cartridge.gg/ + if !CARTRIDGE_MAGIC.eq(&cartridge_magic) { + return Ok(()); + } + + // Last felt in data is the salt which is the username encoded as short string + let username_felt = event.data[event.data.len() - 1]; + let username = parse_cairo_short_string(&username_felt)?; + + info!( + target: LOG_TARGET, + username = %username, + address = %format!("{address:#x}"), + "Controller deployed." + ); + + db.add_controller(&username, &format!("{address:#x}"), block_timestamp).await?; + + Ok(()) + } +} diff --git a/crates/torii/indexer/src/processors/erc20_legacy_transfer.rs b/crates/torii/indexer/src/processors/erc20_legacy_transfer.rs index 6783162419..500580aaa3 100644 --- a/crates/torii/indexer/src/processors/erc20_legacy_transfer.rs +++ b/crates/torii/indexer/src/processors/erc20_legacy_transfer.rs @@ -83,7 +83,7 @@ where block_number, ) .await?; - debug!(target: LOG_TARGET,from = ?from, to = ?to, value = ?value, "Legacy ERC20 Transfer"); + debug!(target: LOG_TARGET,from = ?from, to = ?to, value = ?value, "Legacy ERC20 Transfer."); Ok(()) } diff --git a/crates/torii/indexer/src/processors/erc20_transfer.rs b/crates/torii/indexer/src/processors/erc20_transfer.rs index 98fb907bb0..27afa51770 100644 --- a/crates/torii/indexer/src/processors/erc20_transfer.rs +++ b/crates/torii/indexer/src/processors/erc20_transfer.rs @@ -83,7 +83,7 @@ where block_number, ) .await?; - debug!(target: LOG_TARGET,from = ?from, to = ?to, value = ?value, "ERC20 Transfer"); + debug!(target: LOG_TARGET,from = ?from, to = ?to, value = ?value, "ERC20 Transfer."); Ok(()) } diff --git a/crates/torii/indexer/src/processors/erc721_legacy_transfer.rs b/crates/torii/indexer/src/processors/erc721_legacy_transfer.rs index e02304ed9c..9b55b1ac52 100644 --- a/crates/torii/indexer/src/processors/erc721_legacy_transfer.rs +++ b/crates/torii/indexer/src/processors/erc721_legacy_transfer.rs @@ -90,7 +90,7 @@ where block_number, ) .await?; - debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer"); + debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer."); Ok(()) } diff --git a/crates/torii/indexer/src/processors/erc721_transfer.rs b/crates/torii/indexer/src/processors/erc721_transfer.rs index 4a0383a4f1..805020e9b9 100644 --- a/crates/torii/indexer/src/processors/erc721_transfer.rs +++ b/crates/torii/indexer/src/processors/erc721_transfer.rs @@ -90,7 +90,7 @@ where block_number, ) .await?; - debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer"); + debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer."); Ok(()) } diff --git a/crates/torii/indexer/src/processors/mod.rs b/crates/torii/indexer/src/processors/mod.rs index 04f6c38150..96d2b250f2 100644 --- a/crates/torii/indexer/src/processors/mod.rs +++ b/crates/torii/indexer/src/processors/mod.rs @@ -9,6 +9,7 @@ use torii_sqlite::Sql; use crate::task_manager::{TaskId, TaskPriority}; +pub mod controller; pub mod erc20_legacy_transfer; pub mod erc20_transfer; pub mod erc721_legacy_transfer; @@ -25,7 +26,6 @@ pub mod store_update_member; pub mod store_update_record; pub mod upgrade_event; pub mod upgrade_model; - #[derive(Clone, Debug, Default)] pub struct EventProcessorConfig { pub historical_events: HashSet, diff --git a/crates/torii/migrations/20250128051146_controllers.sql b/crates/torii/migrations/20250128051146_controllers.sql new file mode 100644 index 0000000000..80c2c3dbdf --- /dev/null +++ b/crates/torii/migrations/20250128051146_controllers.sql @@ -0,0 +1,9 @@ +-- Cartridge controllers +CREATE TABLE controllers ( + id TEXT PRIMARY KEY NOT NULL, -- Username as primary key + username TEXT NOT NULL, -- Username + address TEXT NOT NULL, -- Wallet address + deployed_at TIMESTAMP NOT NULL -- Block timestamp of deployment +); + +CREATE INDEX idx_controllers_address ON controllers (address); diff --git a/crates/torii/runner/src/constants.rs b/crates/torii/runner/src/constants.rs new file mode 100644 index 0000000000..1c34815f3c --- /dev/null +++ b/crates/torii/runner/src/constants.rs @@ -0,0 +1,7 @@ +use starknet::macros::felt; +use starknet_crypto::Felt; + +pub(crate) const LOG_TARGET: &str = "torii:runner"; + +pub(crate) const UDC_ADDRESS: Felt = + felt!("0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf"); diff --git a/crates/torii/runner/src/lib.rs b/crates/torii/runner/src/lib.rs index ed54013a84..ccc74d6863 100644 --- a/crates/torii/runner/src/lib.rs +++ b/crates/torii/runner/src/lib.rs @@ -17,6 +17,7 @@ use std::sync::Arc; use std::time::Duration; use camino::Utf8PathBuf; +use constants::UDC_ADDRESS; use dojo_metrics::exporters::prometheus::PrometheusRecorder; use dojo_world::contracts::world::WorldContractReader; use sqlx::sqlite::{ @@ -31,7 +32,6 @@ use tokio::sync::broadcast::Sender; use tokio_stream::StreamExt; use torii_cli::ToriiArgs; use torii_indexer::engine::{Engine, EngineConfig, IndexingFlags, Processors}; -use torii_indexer::processors::store_transaction::StoreTransactionProcessor; use torii_indexer::processors::EventProcessorConfig; use torii_server::proxy::Proxy; use torii_sqlite::cache::ModelCache; @@ -43,7 +43,9 @@ use tracing::{error, info}; use tracing_subscriber::{fmt, EnvFilter}; use url::form_urlencoded; -pub(crate) const LOG_TARGET: &str = "torii:runner"; +mod constants; + +use crate::constants::LOG_TARGET; #[derive(Debug, Clone)] pub struct Runner { @@ -67,6 +69,13 @@ impl Runner { .contracts .push(Contract { address: world_address, r#type: ContractType::WORLD }); + if self.args.indexing.controllers { + self.args + .indexing + .contracts + .push(Contract { address: UDC_ADDRESS, r#type: ContractType::UDC }); + } + let filter_layer = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("info,hyper_reverse_proxy=off")); @@ -147,10 +156,7 @@ impl Runner { ) .await?; - let processors = Processors { - transaction: vec![Box::new(StoreTransactionProcessor)], - ..Processors::default() - }; + let processors = Processors::default(); let (block_tx, block_rx) = tokio::sync::mpsc::channel(100); diff --git a/crates/torii/sqlite/src/executor/erc.rs b/crates/torii/sqlite/src/executor/erc.rs index 5705b1bd64..f4c9bf210a 100644 --- a/crates/torii/sqlite/src/executor/erc.rs +++ b/crates/torii/sqlite/src/executor/erc.rs @@ -56,6 +56,7 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { let id = id_str.split(SQL_FELT_DELIMITER).collect::>(); match contract_type { ContractType::WORLD => unreachable!(), + ContractType::UDC => unreachable!(), ContractType::ERC721 => { // account_address/contract_address:id => ERC721 assert!(id.len() == 2); diff --git a/crates/torii/sqlite/src/lib.rs b/crates/torii/sqlite/src/lib.rs index fb9f268ce3..82f74ed267 100644 --- a/crates/torii/sqlite/src/lib.rs +++ b/crates/torii/sqlite/src/lib.rs @@ -820,6 +820,33 @@ impl Sql { self.executor.send(rollback)?; recv.await? } + + pub async fn add_controller( + &mut self, + username: &str, + address: &str, + block_timestamp: u64, + ) -> Result<()> { + let insert_controller = " + INSERT INTO controllers (id, username, address, deployed_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + username=EXCLUDED.username, + address=EXCLUDED.address, + deployed_at=EXCLUDED.deployed_at + RETURNING *"; + + let arguments = vec![ + Argument::String(username.to_string()), + Argument::String(username.to_string()), + Argument::String(address.to_string()), + Argument::String(utc_dt_string_from_timestamp(block_timestamp)), + ]; + + self.executor.send(QueryMessage::other(insert_controller.to_string(), arguments))?; + + Ok(()) + } } fn add_columns_recursive( diff --git a/crates/torii/sqlite/src/types.rs b/crates/torii/sqlite/src/types.rs index d56d33cb50..f01829427d 100644 --- a/crates/torii/sqlite/src/types.rs +++ b/crates/torii/sqlite/src/types.rs @@ -156,6 +156,7 @@ pub enum ContractType { WORLD, ERC20, ERC721, + UDC, } impl std::fmt::Display for Contract { @@ -172,6 +173,7 @@ impl FromStr for ContractType { "world" => Ok(ContractType::WORLD), "erc20" => Ok(ContractType::ERC20), "erc721" => Ok(ContractType::ERC721), + "udc" => Ok(ContractType::UDC), _ => Err(anyhow::anyhow!("Invalid ERC type: {}", input)), } } @@ -183,6 +185,7 @@ impl std::fmt::Display for ContractType { ContractType::WORLD => write!(f, "WORLD"), ContractType::ERC20 => write!(f, "ERC20"), ContractType::ERC721 => write!(f, "ERC721"), + ContractType::UDC => write!(f, "UDC"), } } }