diff --git a/bin/sozo/src/commands/events.rs b/bin/sozo/src/commands/events.rs index 5b7eee895d..3a19ab6a7d 100644 --- a/bin/sozo/src/commands/events.rs +++ b/bin/sozo/src/commands/events.rs @@ -1,8 +1,16 @@ +use std::collections::HashMap; +use std::sync::Arc; + use anyhow::Result; use clap::Args; +use colored::Colorize; +use dojo_world::contracts::abigen::world::{self, Event as WorldEvent}; +use dojo_world::diff::WorldDiff; use scarb::core::Config; -use sozo_ops::events; -use tracing::trace; +use sozo_ops::model; +use starknet::core::types::{BlockId, BlockTag, EventFilter, Felt}; +use starknet::core::utils::starknet_keccak; +use starknet::providers::Provider; use super::options::starknet::StarknetOptions; use super::options::world::WorldOptions; @@ -24,6 +32,7 @@ pub struct EventsArgs { #[arg(short, long)] #[arg(help = "Number of events to return per page")] + #[arg(default_value_t = 100)] pub chunk_size: u64, #[arg(long)] @@ -43,47 +52,325 @@ pub struct EventsArgs { impl EventsArgs { pub fn run(self, config: &Config) -> Result<()> { - let env_metadata = utils::load_metadata_from_config(config)?; - trace!(?env_metadata, "Metadata loaded from config."); - - let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; - trace!(ws_members_count = ws.members().count(), "Read workspace."); - - let project_dir = ws.manifest_path().parent().unwrap().to_path_buf(); - trace!(?project_dir, "Project directory defined from workspace."); - - let provider = self.starknet.provider(env_metadata.as_ref())?; - trace!(?provider, "Starknet RPC client provider."); - - let world_address = self.world.address(env_metadata.as_ref())?; - let event_filter = events::get_event_filter( - self.from_block, - self.to_block, - self.events, - Some(world_address), - ); - trace!( - from_block = self.from_block, - to_block = self.to_block, - chunk_size = self.chunk_size, - "Created event filter." - ); - let profile_name = - ws.current_profile().expect("Scarb profile expected at this point.").to_string(); - trace!(profile_name, "Current profile."); - config.tokio_handle().block_on(async { - trace!("Starting async event parsing."); - events::parse( - self.chunk_size, - provider, - self.continuation_token, - event_filter, - self.json, - &project_dir, - &profile_name, - ) - .await + let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; + + let (world_diff, provider, _) = + utils::get_world_diff_and_provider(self.starknet, self.world, &ws).await?; + + let provider = Arc::new(provider); + + let from_block = self.from_block.map(BlockId::Number); + let to_block = self.to_block.map(BlockId::Number); + let keys = self + .events + .map(|e| vec![e.iter().map(|event| starknet_keccak(event.as_bytes())).collect()]); + + let event_filter = EventFilter { + from_block, + to_block, + address: Some(world_diff.world_info.address), + keys, + }; + + let res = + provider.get_events(event_filter, self.continuation_token, self.chunk_size).await?; + + for event in &res.events { + match world::Event::try_from(event) { + Ok(ev) => { + match_event( + &ev, + &world_diff, + event.block_number, + event.transaction_hash, + &provider, + ) + .await + .unwrap_or_else(|e| { + tracing::error!(?e, "Failed to process event: {:?}", ev); + }); + } + Err(e) => { + tracing::error!( + ?e, + "Failed to parse remote world event which is supposed to be valid." + ); + } + } + } + + if let Some(continuation_token) = res.continuation_token { + println!("Continuation token: {:?}", continuation_token); + println!("----------------------------------------------"); + } + + Ok(()) }) } } + +/// Matches the event and prints it's content. +async fn match_event( + event: &WorldEvent, + world_diff: &WorldDiff, + block_number: Option, + transaction_hash: Felt, + provider: P, +) -> Result<()> { + // Get a mapping of all the known selectors and their addresses. + let contract_addresses_from_selector = world_diff.get_contracts_addresses(); + // Do a reverse mapping to retrieve a contract selector from it's address. + let contract_selectors_from_address: HashMap = + contract_addresses_from_selector.into_iter().map(|(s, a)| (a, s)).collect(); + // Finally, cache all the known tags by creating them once for each selector. + let mut tags = HashMap::new(); + for (s, r) in world_diff.resources.iter() { + tags.insert(s, r.tag()); + } + + let block_id = if let Some(block_number) = block_number { + BlockId::Number(block_number) + } else { + BlockId::Tag(BlockTag::Pending) + }; + + let (name, content) = match event { + WorldEvent::WorldSpawned(e) => ( + "World spawned".to_string(), + format!("Creator address: {:?}\nWorld class hash: {:#066x}", e.creator, e.class_hash.0), + ), + WorldEvent::WorldUpgraded(e) => { + ("World upgraded".to_string(), format!("World class hash: {:#066x}", e.class_hash.0)) + } + WorldEvent::NamespaceRegistered(e) => { + ("Namespace registered".to_string(), format!("Namespace: {}", e.namespace.to_string()?)) + } + WorldEvent::ModelRegistered(e) => ( + "Model registered".to_string(), + format!( + "Namespace: {}\nName: {}\nClass hash: {:#066x}\nAddress: {:#066x}", + e.namespace.to_string()?, + e.name.to_string()?, + e.class_hash.0, + e.address.0 + ), + ), + WorldEvent::EventRegistered(e) => ( + "Event registered".to_string(), + format!( + "Namespace: {}\nName: {}\nClass hash: {:#066x}\nAddress: {:#066x}", + e.namespace.to_string()?, + e.name.to_string()?, + e.class_hash.0, + e.address.0 + ), + ), + WorldEvent::ContractRegistered(e) => ( + "Contract registered".to_string(), + format!( + "Namespace: {}\nName: {}\nClass hash: {:#066x}\nAddress: {:#066x}\nSalt: {:#066x}", + e.namespace.to_string()?, + e.name.to_string()?, + e.class_hash.0, + e.address.0, + e.salt + ), + ), + WorldEvent::ModelUpgraded(e) => { + let tag = tags.get(&e.selector).unwrap(); + ( + format!("Model upgraded ({})", tag), + format!( + "Selector: {:#066x}\nClass hash: {:#066x}\nAddress: {:#066x}\nPrev address: \ + {:#066x}", + e.selector, e.class_hash.0, e.address.0, e.prev_address.0 + ), + ) + } + WorldEvent::EventUpgraded(e) => { + let tag = tags.get(&e.selector).unwrap(); + ( + format!("Event upgraded ({})", tag), + format!( + "Selector: {:#066x}\nClass hash: {:#066x}\nAddress: {:#066x}\nPrev address: \ + {:#066x}", + e.selector, e.class_hash.0, e.address.0, e.prev_address.0 + ), + ) + } + WorldEvent::ContractUpgraded(e) => { + let tag = tags.get(&e.selector).unwrap(); + ( + format!("Contract upgraded ({})", tag), + format!("Selector: {:#066x}\nClass hash: {:#066x}", e.selector, e.class_hash.0,), + ) + } + WorldEvent::ContractInitialized(e) => { + let tag = tags.get(&e.selector).unwrap(); + ( + format!("Contract initialized ({})", tag), + format!( + "Selector: {:#066x}\nInit calldata: {}", + e.selector, + e.init_calldata + .iter() + .map(|f| format!("{:#066x}", f)) + .collect::>() + .join(", ") + ), + ) + } + WorldEvent::WriterUpdated(e) => { + let tag = tags.get(&e.resource).unwrap(); + let grantee = + if let Some(selector) = contract_selectors_from_address.get(&e.contract.into()) { + tags.get(selector).unwrap().to_string() + } else { + format!("{:#066x}", e.contract.0) + }; + + ( + "Writer updated".to_string(), + format!("Target resource: {}\nContract: {}\nValue: {}", tag, grantee, e.value), + ) + } + WorldEvent::OwnerUpdated(e) => { + let tag = tags.get(&e.resource).unwrap(); + let grantee = + if let Some(selector) = contract_selectors_from_address.get(&e.contract.into()) { + tags.get(selector).unwrap().to_string() + } else { + format!("{:#066x}", e.contract.0) + }; + + ( + "Owner updated".to_string(), + format!("Target resource: {}\nContract: {}\nValue: {}", tag, grantee, e.value), + ) + } + WorldEvent::StoreSetRecord(e) => { + let tag = tags.get(&e.selector).unwrap(); + let (record, _, _) = model::model_get( + tag.clone(), + e.keys.clone(), + world_diff.world_info.address, + provider, + block_id, + ) + .await?; + + ( + format!("Store set record ({})", tag), + format!( + "Selector: {:#066x}\nEntity ID: {:#066x}\nKeys: {}\nValues: {}\nData:\n{}", + e.selector, + e.entity_id, + e.keys + .iter() + .map(|k| format!("{:#066x}", k)) + .collect::>() + .join(", "), + e.values + .iter() + .map(|v| format!("{:#066x}", v)) + .collect::>() + .join(", "), + record + ), + ) + } + WorldEvent::StoreUpdateRecord(e) => { + let tag = tags.get(&e.selector).unwrap(); + // TODO: model value impl + print. + ( + format!("Store update record ({})", tag), + format!( + "Selector: {:#066x}\nEntity ID: {:#066x}\nValues: {}", + e.selector, + e.entity_id, + e.values + .iter() + .map(|v| format!("{:#066x}", v)) + .collect::>() + .join(", "), + ), + ) + } + WorldEvent::StoreUpdateMember(e) => { + let tag = tags.get(&e.selector).unwrap(); + // TODO: pretty print of the value. + ( + format!("Store update member ({})", tag), + format!( + "Selector: {:#066x}\nEntity ID: {:#066x}\nMember selector: {:#066x}\nValues: \ + {}", + e.selector, + e.entity_id, + e.member_selector, + e.values + .iter() + .map(|v| format!("{:#066x}", v)) + .collect::>() + .join(", "), + ), + ) + } + WorldEvent::StoreDelRecord(e) => { + let tag = tags.get(&e.selector).unwrap(); + ( + format!("Store del record ({})", tag), + format!("Selector: {:#066x}\nEntity ID: {:#066x}", e.selector, e.entity_id,), + ) + } + WorldEvent::EventEmitted(e) => { + let tag = tags.get(&e.selector).unwrap(); + let contract_tag = if let Some(selector) = + contract_selectors_from_address.get(&e.system_address.into()) + { + tags.get(selector).unwrap().to_string() + } else { + format!("{:#066x}", e.system_address.0) + }; + + let (record, _, _) = model::model_get( + tag.clone(), + e.keys.clone(), + world_diff.world_info.address, + provider, + block_id, + ) + .await?; + + ( + format!("Event emitted ({})", tag), + format!( + "Selector: {:#066x}\nContract: {}\nHistorical: {}\nKeys: {}\nValues: \ + {}\nData:\n{}", + e.selector, + contract_tag, + e.historical, + e.keys + .iter() + .map(|k| format!("{:#066x}", k)) + .collect::>() + .join(", "), + e.values + .iter() + .map(|v| format!("{:#066x}", v)) + .collect::>() + .join(", "), + record + ), + ) + } + _ => ("Unprocessed event".to_string(), format!("Event: {:?}", event)), + }; + + let block_str = block_number.map(|n| n.to_string()).unwrap_or("pending".to_string()); + let ptr = format!("[block:{} / tx:{:#066x}]", block_str, transaction_hash).bright_black(); + + println!("> {name} {ptr}\n{content}\n-----\n"); + + Ok(()) +} diff --git a/bin/sozo/src/commands/mod.rs b/bin/sozo/src/commands/mod.rs index 92a82ba995..b1204be28c 100644 --- a/bin/sozo/src/commands/mod.rs +++ b/bin/sozo/src/commands/mod.rs @@ -2,6 +2,7 @@ use core::fmt; use anyhow::Result; use clap::Subcommand; +use events::EventsArgs; use scarb::core::{Config, Package, Workspace}; use tracing::info_span; @@ -9,6 +10,7 @@ pub(crate) mod build; pub(crate) mod call; pub(crate) mod calldata_decoder; pub(crate) mod clean; +pub(crate) mod events; pub(crate) mod execute; pub(crate) mod hash; pub(crate) mod init; @@ -52,6 +54,8 @@ pub enum Commands { Init(Box), #[command(about = "Inspect a model")] Model(Box), + #[command(about = "Inspect events emitted by the world")] + Events(Box), } impl fmt::Display for Commands { @@ -67,6 +71,7 @@ impl fmt::Display for Commands { Commands::Hash(_) => write!(f, "Hash"), Commands::Init(_) => write!(f, "Init"), Commands::Model(_) => write!(f, "Model"), + Commands::Events(_) => write!(f, "Events"), } } } @@ -90,6 +95,7 @@ pub fn run(command: Commands, config: &Config) -> Result<()> { Commands::Hash(args) => args.run().map(|_| ()), Commands::Init(args) => args.run(config), Commands::Model(args) => args.run(config), + Commands::Events(args) => args.run(config), } } diff --git a/bin/sozo/src/commands/model.rs b/bin/sozo/src/commands/model.rs index 01db01ee03..4458f940cc 100644 --- a/bin/sozo/src/commands/model.rs +++ b/bin/sozo/src/commands/model.rs @@ -4,7 +4,7 @@ use scarb::core::Config; use sozo_ops::model; use sozo_ops::resource_descriptor::ResourceDescriptor; use sozo_scarbext::WorkspaceExt; -use starknet::core::types::Felt; +use starknet::core::types::{BlockId, BlockTag, Felt}; use tracing::trace; use super::options::starknet::StarknetOptions; @@ -72,6 +72,12 @@ hashes, called 'hash' in the following documentation. #[command(flatten)] starknet: StarknetOptions, + + #[arg(short, long)] + #[arg( + help = "Block number at which to retrieve the model layout (pending block by default)" + )] + block: Option, }, #[command(about = "Retrieve the schema for a model")] @@ -88,6 +94,12 @@ hashes, called 'hash' in the following documentation. #[arg(short = 'j', long = "json")] #[arg(help_heading = "Display options")] to_json: bool, + + #[arg(short, long)] + #[arg( + help = "Block number at which to retrieve the model schema (pending block by default)" + )] + block: Option, }, #[command(about = "Get a models value for the provided key")] @@ -105,6 +117,10 @@ hashes, called 'hash' in the following documentation. #[command(flatten)] starknet: StarknetOptions, + + #[arg(short, long)] + #[arg(help = "Block number at which to retrieve the model data (pending block by default)")] + block: Option, }, } @@ -146,18 +162,27 @@ impl ModelArgs { .await?; Ok(()) } - ModelCommand::Layout { tag_or_name, starknet, world } => { + ModelCommand::Layout { tag_or_name, starknet, world, block } => { let tag = tag_or_name.ensure_namespace(&default_ns); + let block_id = + block.map(BlockId::Number).unwrap_or(BlockId::Tag(BlockTag::Pending)); let (world_diff, provider, _) = utils::get_world_diff_and_provider(starknet, world, &ws).await?; - model::model_layout(tag.to_string(), world_diff.world_info.address, &provider) - .await?; + model::model_layout( + tag.to_string(), + world_diff.world_info.address, + &provider, + block_id, + ) + .await?; Ok(()) } - ModelCommand::Schema { tag_or_name, to_json, starknet, world } => { + ModelCommand::Schema { tag_or_name, to_json, starknet, world, block } => { let tag = tag_or_name.ensure_namespace(&default_ns); + let block_id = + block.map(BlockId::Number).unwrap_or(BlockId::Tag(BlockTag::Pending)); let (world_diff, provider, _) = utils::get_world_diff_and_provider(starknet, world, &ws).await?; @@ -166,24 +191,31 @@ impl ModelArgs { tag.to_string(), world_diff.world_info.address, &provider, + block_id, to_json, ) .await?; Ok(()) } - ModelCommand::Get { tag_or_name, keys, starknet, world } => { + ModelCommand::Get { tag_or_name, keys, block, starknet, world } => { let tag = tag_or_name.ensure_namespace(&default_ns); + let block_id = + block.map(BlockId::Number).unwrap_or(BlockId::Tag(BlockTag::Pending)); let (world_diff, provider, _) = utils::get_world_diff_and_provider(starknet, world, &ws).await?; - model::model_get( + let (record, _, _) = model::model_get( tag.to_string(), keys, world_diff.world_info.address, &provider, + block_id, ) .await?; + + println!("{}", record); + Ok(()) } } diff --git a/crates/dojo/world/src/diff/resource.rs b/crates/dojo/world/src/diff/resource.rs index fc16320b54..b2402399c9 100644 --- a/crates/dojo/world/src/diff/resource.rs +++ b/crates/dojo/world/src/diff/resource.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use starknet::core::types::contract::AbiEntry; use starknet_crypto::Felt; use crate::local::ResourceLocal; @@ -107,4 +108,12 @@ impl ResourceDiff { ResourceDiff::Synced(_, remote) => remote.current_class_hash(), } } + + pub fn abi(&self) -> Vec { + match self { + ResourceDiff::Created(local) => local.abi(), + ResourceDiff::Updated(local, _) => local.abi(), + ResourceDiff::Synced(local, _) => local.abi(), + } + } } diff --git a/crates/dojo/world/src/local/resource.rs b/crates/dojo/world/src/local/resource.rs index f40b1fd54f..9822da1731 100644 --- a/crates/dojo/world/src/local/resource.rs +++ b/crates/dojo/world/src/local/resource.rs @@ -1,5 +1,5 @@ use dojo_types::naming; -use starknet::core::types::contract::SierraClass; +use starknet::core::types::contract::{AbiEntry, SierraClass}; use starknet::core::types::Felt; use crate::{DojoSelector, ResourceType}; @@ -100,6 +100,16 @@ impl ResourceLocal { } } + /// Returns the ABI of the resource. + pub fn abi(&self) -> Vec { + match self { + ResourceLocal::Contract(c) => c.common.class.abi.clone(), + ResourceLocal::Model(m) => m.common.class.abi.clone(), + ResourceLocal::Event(e) => e.common.class.abi.clone(), + _ => Vec::new(), + } + } + /// Returns the dojo selector of the resource. pub fn dojo_selector(&self) -> DojoSelector { match self { diff --git a/crates/sozo/ops/src/events.rs b/crates/sozo/ops/src/events.rs deleted file mode 100644 index 1bf378b101..0000000000 --- a/crates/sozo/ops/src/events.rs +++ /dev/null @@ -1,610 +0,0 @@ -use std::collections::{HashMap, VecDeque}; -use std::fs; - -use anyhow::{anyhow, Context, Result}; -use cainome::cairo_serde::{ByteArray, CairoSerde}; -use cainome::parser::tokens::{CompositeInner, CompositeInnerKind, CoreBasic, Token}; -use cainome::parser::AbiParser; -use camino::Utf8PathBuf; -use dojo_world::contracts::naming::get_filename_from_tag; -use dojo_world::manifest::{ - AbiFormat, DeploymentManifest, ManifestMethods, BASE_CONTRACT_TAG, DEPLOYMENT_DIR, - MANIFESTS_DIR, TARGET_DIR, WORLD_CONTRACT_TAG, -}; -use starknet::core::types::{BlockId, EventFilter, Felt}; -use starknet::core::utils::{parse_cairo_short_string, starknet_keccak}; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::{JsonRpcClient, Provider}; - -pub fn get_event_filter( - from_block: Option, - to_block: Option, - events: Option>, - world_address: Option, -) -> EventFilter { - let from_block = from_block.map(BlockId::Number); - let to_block = to_block.map(BlockId::Number); - // Currently dojo doesn't use custom keys for events. In future if custom keys are used this - // needs to be updated for granular queries. - let keys = - events.map(|e| vec![e.iter().map(|event| starknet_keccak(event.as_bytes())).collect()]); - - EventFilter { from_block, to_block, address: world_address, keys } -} - -pub async fn parse( - chunk_size: u64, - provider: JsonRpcClient, - continuation_token: Option, - event_filter: EventFilter, - json: bool, - project_dir: &Utf8PathBuf, - profile_name: &str, -) -> Result<()> { - let events_map = if !json { - let manifest_dir = project_dir.join(MANIFESTS_DIR).join(profile_name); - let target_dir = project_dir.join(TARGET_DIR).join(profile_name); - let deployed_manifest = - manifest_dir.join(DEPLOYMENT_DIR).join("manifest").with_extension("toml"); - - if !deployed_manifest.exists() { - return Err(anyhow!("Run scarb migrate before running this command")); - } - - Some(extract_events( - &DeploymentManifest::load_from_path(&deployed_manifest)?, - project_dir, - &target_dir, - )?) - } else { - None - }; - - let res = provider.get_events(event_filter, continuation_token, chunk_size).await?; - - if let Some(events_map) = events_map { - parse_and_print_events(res, events_map)?; - } else { - println!("{}", serde_json::to_string_pretty(&res).unwrap()); - } - Ok(()) -} - -fn is_event(token: &Token) -> bool { - match token { - Token::Composite(composite) => composite.is_event, - _ => false, - } -} - -fn extract_events( - manifest: &DeploymentManifest, - project_dir: &Utf8PathBuf, - target_dir: &Utf8PathBuf, -) -> Result>> { - fn process_abi( - events: &mut HashMap>, - full_abi_path: &Utf8PathBuf, - ) -> Result<()> { - let abi_str = fs::read_to_string(full_abi_path) - .with_context(|| format!("Failed to read ABI file at path: {}", full_abi_path))?; - - // TODO: add support for events emitted by world once its present in ABI - match AbiParser::tokens_from_abi_string(&abi_str, &HashMap::new()) { - Ok(tokens) => { - for token in tokens.structs { - if is_event(&token) { - let event_name = starknet_keccak(token.type_name().as_bytes()); - let vec = events.entry(event_name.to_string()).or_default(); - vec.push(token.clone()); - } - } - } - Err(e) => return Err(anyhow!("Error parsing events from ABI: {}", e)), - } - - Ok(()) - } - - let mut events_map = HashMap::new(); - - for contract in &manifest.contracts { - if let Some(AbiFormat::Path(abi_path)) = contract.inner.abi() { - let full_abi_path = project_dir.join(abi_path); - process_abi(&mut events_map, &full_abi_path)?; - } - } - - for model in &manifest.models { - if let Some(AbiFormat::Path(abi_path)) = model.inner.abi() { - let full_abi_path = project_dir.join(abi_path); - process_abi(&mut events_map, &full_abi_path)?; - } - } - - // Read the world and base ABI from scarb artifacts as the - // manifest does not include them (at least base is not included). - let world_abi_path = - target_dir.join(format!("{}.json", get_filename_from_tag(WORLD_CONTRACT_TAG))); - process_abi(&mut events_map, &world_abi_path)?; - - let base_abi_path = - target_dir.join(format!("{}.json", get_filename_from_tag(BASE_CONTRACT_TAG))); - process_abi(&mut events_map, &base_abi_path)?; - - Ok(events_map) -} - -fn parse_and_print_events( - res: starknet::core::types::EventsPage, - events_map: HashMap>, -) -> Result<()> { - println!("Continuation token: {:?}", res.continuation_token); - println!("----------------------------------------------"); - - for event in res.events { - let parsed_event = parse_event(event.clone(), &events_map); - - match parsed_event { - Ok(parsed_event) => { - println!("{parsed_event}"); - } - Err(e) => { - println!("{}", e); - println!("Event: {}\n", serde_json::to_string_pretty(&event).unwrap()); - } - } - } - Ok(()) -} - -fn parse_core_basic(cb: &CoreBasic, value: &Felt, include_felt_string: bool) -> Result { - match cb.type_name().as_str() { - "felt252" => { - let hex = format!("{:#x}", value); - match parse_cairo_short_string(value) { - Ok(parsed) if !parsed.is_empty() && (include_felt_string && parsed.is_ascii()) => { - Ok(format!("{} \"{}\"", hex, parsed)) - } - _ => Ok(hex.to_string()), - } - } - "bool" => { - if *value == Felt::ZERO { - Ok("false".to_string()) - } else { - Ok("true".to_string()) - } - } - "ClassHash" | "ContractAddress" => Ok(format!("{:#x}", value)), - "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" => { - Ok(value.to_string()) - } - _ => Err(anyhow!("Unsupported CoreBasic type: {}", cb.type_name())), - } -} - -fn parse_event( - event: starknet::core::types::EmittedEvent, - events_map: &HashMap>, -) -> Result { - let mut data = VecDeque::from(event.data.clone()); - let mut keys = VecDeque::from(event.keys.clone()); - let event_hash = keys.pop_front().ok_or(anyhow!("Event hash missing")).unwrap(); - - let events = events_map - .get(&event_hash.to_string()) - .ok_or(anyhow!("Events for hash not found: {:#x}", event_hash))?; - - for e in events { - if let Token::Composite(composite) = e { - let processed_inners = process_inners(&composite.inners, &mut data, &mut keys)?; - let ret = format!("Event name: {}\n{}", e.type_path(), processed_inners); - return Ok(ret); - } - } - - Err(anyhow!("No matching event found in tokens {:?}", event)) -} - -fn process_inners( - inners: &[CompositeInner], - data: &mut VecDeque, - keys: &mut VecDeque, -) -> Result { - let mut ret = String::new(); - - for inner in inners { - let value = match inner.kind { - CompositeInnerKind::Data => data.pop_front().ok_or(anyhow!("Missing data value")), - CompositeInnerKind::Key => keys.pop_front().ok_or(anyhow!("Missing key value")), - _ => Err(anyhow!("Unsupported inner kind encountered")), - }?; - - let formatted_value = match &inner.token { - Token::CoreBasic(ref cb) => parse_core_basic(cb, &value, true)?, - Token::Composite(c) => { - if c.type_path.eq("core::byte_array::ByteArray") { - data.push_front(value); - data.make_contiguous(); - let bytearray = ByteArray::cairo_deserialize(data.as_mut_slices().0, 0)?; - data.drain(0..ByteArray::cairo_serialized_size(&bytearray)); - ByteArray::to_string(&bytearray)? - } else { - return Err(anyhow!("Unhandled Composite token")); - } - } - Token::Array(ref array) => { - let length = value - .to_string() - .parse::() - .map_err(|_| anyhow!("Error parsing length to usize"))?; - - let cb = if let Token::CoreBasic(ref cb) = *array.inner { - cb - } else { - return Err(anyhow!("Inner token of array is not CoreBasic")); - }; - - let mut elements = Vec::new(); - for _ in 0..length { - if let Some(element_value) = data.pop_front() { - let element_str = parse_core_basic(cb, &element_value, false)?; - elements.push(element_str); - } else { - return Err(anyhow!("Missing array element value")); - } - } - - format!("[{}]", elements.join(", ")) - } - _ => return Err(anyhow!("Unsupported token type encountered")), - }; - ret.push_str(&format!("{}: {}\n", inner.name, formatted_value)); - } - - Ok(ret) -} - -#[cfg(test)] -mod tests { - use cainome::parser::tokens::{Array, Composite, CompositeInner, CompositeType}; - use camino::Utf8Path; - use dojo_world::manifest::WORLD_QUALIFIED_PATH; - use starknet::core::types::EmittedEvent; - - use super::*; - - #[test] - fn extract_events_work_as_expected() { - let profile_name = "dev"; - let project_dir = Utf8Path::new("../../../examples/spawn-and-move").to_path_buf(); - let manifest_dir = project_dir.join(MANIFESTS_DIR).join(profile_name); - println!("manifest_dir {:?}", manifest_dir); - let target_dir = project_dir.join(TARGET_DIR).join(profile_name); - println!("target dir {:?}", target_dir); - let manifest = DeploymentManifest::load_from_path( - &manifest_dir.join(DEPLOYMENT_DIR).join("manifest").with_extension("toml"), - ) - .unwrap(); - - let result = extract_events(&manifest, &project_dir, &target_dir).unwrap(); - - // we are just collecting all events from manifest file so just verifying count should work - assert_eq!(result.len(), 20); - } - - #[test] - fn test_core_basic() { - let composite = Composite { - type_path: format!("{WORLD_QUALIFIED_PATH}::TestEvent"), - inners: vec![ - CompositeInner { - index: 0, - name: "felt252".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_string() }), - }, - CompositeInner { - index: 1, - name: "bool".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { type_path: "core::bool".to_string() }), - }, - CompositeInner { - index: 2, - name: "u8".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u8".to_string(), - }), - }, - CompositeInner { - index: 3, - name: "u16".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u16".to_string(), - }), - }, - CompositeInner { - index: 4, - name: "u32".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u32".to_string(), - }), - }, - CompositeInner { - index: 5, - name: "u64".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u64".to_string(), - }), - }, - CompositeInner { - index: 6, - name: "u128".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u128".to_string(), - }), - }, - CompositeInner { - index: 7, - name: "usize".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::usize".to_string(), - }), - }, - CompositeInner { - index: 8, - name: "class_hash".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { type_path: "core::ClassHash".to_string() }), - }, - CompositeInner { - index: 9, - name: "contract_address".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::ContractAddress".to_string(), - }), - }, - ], - generic_args: vec![], - r#type: CompositeType::Struct, - is_event: true, - alias: None, - }; - let tokenized_composite = Token::Composite(composite); - - let mut events_map = HashMap::new(); - events_map - .insert(starknet_keccak("TestEvent".as_bytes()).to_string(), vec![tokenized_composite]); - - let event = EmittedEvent { - keys: vec![starknet_keccak("TestEvent".as_bytes())], - data: vec![ - Felt::from_hex("0x5465737431").unwrap(), - Felt::from(1u8), // bool true - Felt::from(1u8), - Felt::from(2u16), - Felt::from(3u32), - Felt::from(4u64), - Felt::from(5u128), - Felt::from(6usize), - Felt::from_hex("0x54657374").unwrap(), - Felt::from_hex("0x54657374").unwrap(), - ], - from_address: Felt::from_hex("0x123").unwrap(), - block_hash: Felt::from_hex("0x456").ok(), - block_number: Some(1), - transaction_hash: Felt::from_hex("0x789").unwrap(), - }; - - let expected_output = format!( - "Event name: {WORLD_QUALIFIED_PATH}::TestEvent\nfelt252: 0x5465737431 \ - \"Test1\"\nbool: true\nu8: 1\nu16: 2\nu32: 3\nu64: 4\nu128: 5\nusize: 6\nclass_hash: \ - 0x54657374\ncontract_address: 0x54657374\n" - ); - - let actual_output = parse_event(event, &events_map).expect("Failed to parse event"); - assert_eq!(actual_output, expected_output); - } - - #[test] - fn test_array() { - let composite = Composite { - type_path: format!("{WORLD_QUALIFIED_PATH}::StoreDelRecord"), - inners: vec![ - CompositeInner { - index: 0, - name: "table".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_string() }), - }, - CompositeInner { - index: 1, - name: "keys".to_string(), - kind: CompositeInnerKind::Data, - token: Token::Array(Array { - type_path: "core::array::Span::".to_string(), - inner: Box::new(Token::CoreBasic(CoreBasic { - type_path: "core::felt252".to_string(), - })), - is_legacy: false, - }), - }, - ], - generic_args: vec![], - r#type: CompositeType::Struct, - is_event: true, - alias: None, - }; - let tokenized_composite = Token::Composite(composite); - - let mut events_map = HashMap::new(); - events_map.insert( - starknet_keccak("StoreDelRecord".as_bytes()).to_string(), - vec![tokenized_composite], - ); - - let event = EmittedEvent { - keys: vec![starknet_keccak("StoreDelRecord".as_bytes())], - data: vec![ - Felt::from_hex("0x54657374").unwrap(), - Felt::from(3u128), - Felt::from_hex("0x5465737431").unwrap(), - Felt::from_hex("0x5465737432").unwrap(), - Felt::from_hex("0x5465737433").unwrap(), - ], - from_address: Felt::from_hex("0x123").unwrap(), - block_hash: Felt::from_hex("0x456").ok(), - block_number: Some(1), - transaction_hash: Felt::from_hex("0x789").unwrap(), - }; - - let expected_output = format!( - "Event name: {WORLD_QUALIFIED_PATH}::StoreDelRecord\ntable: 0x54657374 \ - \"Test\"\nkeys: [0x5465737431, 0x5465737432, 0x5465737433]\n" - ); - - let actual_output = parse_event(event, &events_map).expect("Failed to parse event"); - assert_eq!(actual_output, expected_output); - } - - #[test] - fn test_custom_event() { - let composite = Composite { - type_path: format!("{WORLD_QUALIFIED_PATH}::CustomEvent"), - inners: vec![ - CompositeInner { - index: 0, - name: "key_1".to_string(), - kind: CompositeInnerKind::Key, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u32".to_string(), - }), - }, - CompositeInner { - index: 1, - name: "key_2".to_string(), - kind: CompositeInnerKind::Key, - token: Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_string() }), - }, - CompositeInner { - index: 2, - name: "data_1".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u8".to_string(), - }), - }, - CompositeInner { - index: 3, - name: "data_2".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { - type_path: "core::integer::u8".to_string(), - }), - }, - ], - generic_args: vec![], - r#type: CompositeType::Struct, - is_event: true, - alias: None, - }; - let tokenized_composite = Token::Composite(composite); - - let mut events_map = HashMap::new(); - events_map.insert( - starknet_keccak("CustomEvent".as_bytes()).to_string(), - vec![tokenized_composite], - ); - - let event = EmittedEvent { - keys: vec![ - starknet_keccak("CustomEvent".as_bytes()), - Felt::from(3u128), - Felt::from_hex("0x5465737431").unwrap(), - ], - data: vec![Felt::from(1u128), Felt::from(2u128)], - from_address: Felt::from_hex("0x123").unwrap(), - block_hash: Felt::from_hex("0x456").ok(), - block_number: Some(1), - transaction_hash: Felt::from_hex("0x789").unwrap(), - }; - - let expected_output = format!( - "Event name: {WORLD_QUALIFIED_PATH}::CustomEvent\nkey_1: 3\nkey_2: 0x5465737431 \ - \"Test1\"\ndata_1: 1\ndata_2: 2\n" - ); - - let actual_output = parse_event(event, &events_map).expect("Failed to parse event"); - assert_eq!(actual_output, expected_output); - } - - #[test] - fn test_zero_felt() { - let composite = Composite { - type_path: format!("{WORLD_QUALIFIED_PATH}::StoreDelRecord"), - inners: vec![ - CompositeInner { - index: 0, - name: "table".to_string(), - kind: CompositeInnerKind::Data, - token: Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_string() }), - }, - CompositeInner { - index: 1, - name: "keys".to_string(), - kind: CompositeInnerKind::Data, - token: Token::Array(Array { - type_path: "core::array::Span::".to_string(), - inner: Box::new(Token::CoreBasic(CoreBasic { - type_path: "core::felt252".to_string(), - })), - is_legacy: false, - }), - }, - ], - generic_args: vec![], - r#type: CompositeType::Struct, - is_event: true, - alias: None, - }; - let tokenized_composite = Token::Composite(composite); - - let mut events_map = HashMap::new(); - events_map.insert( - starknet_keccak("StoreDelRecord".as_bytes()).to_string(), - vec![tokenized_composite], - ); - - let event = EmittedEvent { - keys: vec![starknet_keccak("StoreDelRecord".as_bytes())], - data: vec![ - Felt::from_hex("0x0").unwrap(), - Felt::from(3u128), - Felt::from_hex("0x0").unwrap(), - Felt::from_hex("0x1").unwrap(), - Felt::from_hex("0x2").unwrap(), - ], - from_address: Felt::from_hex("0x123").unwrap(), - block_hash: Felt::from_hex("0x456").ok(), - block_number: Some(1), - transaction_hash: Felt::from_hex("0x789").unwrap(), - }; - - let expected_output = format!( - "Event name: {WORLD_QUALIFIED_PATH}::StoreDelRecord\ntable: 0x0\nkeys: [0x0, 0x1, \ - 0x2]\n" - ); - - let actual_output = parse_event(event, &events_map).expect("Failed to parse event"); - assert_eq!(actual_output, expected_output); - } -} diff --git a/crates/sozo/ops/src/model.rs b/crates/sozo/ops/src/model.rs index bfdcc8e0db..d4d471eeb9 100644 --- a/crates/sozo/ops/src/model.rs +++ b/crates/sozo/ops/src/model.rs @@ -44,12 +44,17 @@ where Ok(model.contract_address()) } -pub async fn model_layout

(tag: String, world_address: Felt, provider: P) -> Result +pub async fn model_layout

( + tag: String, + world_address: Felt, + provider: P, + block_id: BlockId, +) -> Result where P: Provider + Send + Sync, { let mut world_reader = WorldContractReader::new(world_address, &provider); - world_reader.set_block(BlockId::Tag(BlockTag::Pending)); + world_reader.set_block(block_id); let model = world_reader.model_reader_with_tag(&tag).await?; let layout = match model.layout().await { @@ -70,13 +75,14 @@ pub async fn model_schema

( tag: String, world_address: Felt, provider: P, + block_id: BlockId, to_json: bool, ) -> Result where P: Provider + Send + Sync, { let mut world_reader = WorldContractReader::new(world_address, &provider); - world_reader.set_block(BlockId::Tag(BlockTag::Pending)); + world_reader.set_block(block_id); let model = world_reader.model_reader_with_tag(&tag).await?; let schema = model.schema().await?; @@ -95,7 +101,8 @@ pub async fn model_get

( keys: Vec, world_address: Felt, provider: P, -) -> Result<(Ty, Vec)> + block_id: BlockId, +) -> Result<(String, Ty, Vec)> where P: Provider + Send + Sync, { @@ -104,15 +111,13 @@ where } let mut world_reader = WorldContractReader::new(world_address, &provider); - world_reader.set_block(BlockId::Tag(BlockTag::Pending)); + world_reader.set_block(block_id); let model = world_reader.model_reader_with_tag(&tag).await?; let schema = model.schema().await?; let values = model.entity_storage(&keys).await?; - deep_print_record(&schema, &keys, &values); - - Ok((schema, values)) + Ok((format_deep_record(&schema, &keys, &values), schema, values)) } #[derive(Clone, Debug)] @@ -508,12 +513,12 @@ fn format_record_value( } // print the structured record values -fn deep_print_record(schema: &Ty, keys: &[Felt], values: &[Felt]) { +fn format_deep_record(schema: &Ty, keys: &[Felt], values: &[Felt]) -> String { let mut model_values = vec![]; model_values.extend(keys); model_values.extend(values); - println!("{}", format_record_value(schema, &mut model_values, 0, true)); + format_record_value(schema, &mut model_values, 0, true) } fn get_ty_repr(ty: &Ty) -> String {