Repository: dojoengine/dojo
Pull request #3085: feat(torii-indexer): transaction calls and outside calls
Author: login: Larkooo
Created: 2025-03-07T13:57:28Z | Merged: 2025-03-20T04:16:57Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced an enhanced caching mechanism to improve contract data retrieval speed.
  - Upgraded transaction processing to extract and store detailed function call information in a structured format.
  - Expanded the GraphQL API to support querying function call details, including pagination and ordering.
  - Added a new table to efficiently store and index transaction call records in the database.
  - Implemented support for complex SQL queries involving joins, enhancing data retrieval capabilities.
  - Added new functionality for retrieving entry point names from contract classes.
  - Introduced a structured representation for contract calls with new types for call details.
  - Enhanced error handling capabilities by integrating provider-related errors into the existing error structure.
  - Added a new dependency to facilitate project compilation and linking.
  - Improved modularity and error handling in token balance and transfer processing.
  - Enhanced transaction management capabilities within the executor module.
  - Introduced a new struct for managing transaction queries and improved message handling for transaction processing.
  - Added new methods and structures to support enhanced transaction processing logic and error handling.
  - Introduced new types for handling transaction and call data, improving the overall structure and maintainability of the code.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index b400b43458..3ee4e9614f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -15318,6 +15318,7 @@ dependencies = [
  "base64 0.21.7",
  "bitflags 2.6.0",
  "cainome 0.5.0",
+ "cainome-cairo-serde 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono",
  "crypto-bigint",
  "data-url",
diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs
index 3396743bf5..ac86adb9ad 100644
--- a/crates/torii/graphql/src/constants.rs
+++ b/crates/torii/graphql/src/constants.rs
@@ -8,6 +8,8 @@ pub const EVENT_TABLE: &str = "events";
 pub const EVENT_MESSAGE_TABLE: &str = "event_messages";
 pub const MODEL_TABLE: &str = "models";
 pub const TRANSACTION_TABLE: &str = "transactions";
+pub const TRANSACTION_CALLS_TABLE: &str = "transaction_calls";
+pub const TOKEN_TRANSFER_TABLE: &str = "token_transfers";
 pub const METADATA_TABLE: &str = "metadata";
 pub const CONTROLLER_TABLE: &str = "controllers";
 
@@ -30,6 +32,7 @@ pub const CONTENT_TYPE_NAME: &str = "World__Content";
 pub const METADATA_TYPE_NAME: &str = "World__Metadata";
 pub const PAGE_INFO_TYPE_NAME: &str = "World__PageInfo";
 pub const TRANSACTION_TYPE_NAME: &str = "World__Transaction";
+pub const CALL_TYPE_NAME: &str = "World__Call";
 pub const QUERY_TYPE_NAME: &str = "World__Query";
 pub const SUBSCRIPTION_TYPE_NAME: &str = "World__Subscription";
 pub const MODEL_ORDER_TYPE_NAME: &str = "World__ModelOrder";
diff --git a/crates/torii/graphql/src/mapping.rs b/crates/torii/graphql/src/mapping.rs
index e42dc3cd94..6a90dd16dc 100644
--- a/crates/torii/graphql/src/mapping.rs
+++ b/crates/torii/graphql/src/mapping.rs
@@ -64,6 +64,14 @@ lazy_static! {
             TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())),
         ),
     ]);
+    pub static ref CALL_MAPPING: TypeMapping = IndexMap::from([
+        (Name::new("transactionHash"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("contractAddress"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("entrypoint"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("calldata"), TypeData::Simple(TypeRef::named_list(TypeRef::STRING))),
+        (Name::new("callType"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("callerAddress"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+    ]);
     pub static ref TRANSACTION_MAPPING: TypeMapping = IndexMap::from([
         (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))),
         (
@@ -98,6 +106,10 @@ lazy_static! {
             Name::new("createdAt"),
             TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())),
         ),
+        (
+            Name::new("blockNumber"),
+            TypeData::Simple(TypeRef::named(TypeRef::STRING)),
+        )
     ]);
     pub static ref PAGE_INFO_TYPE_MAPPING: TypeMapping = TypeMapping::from([
         (Name::new("hasPreviousPage"), TypeData::Simple(TypeRef::named_nn(TypeRef::BOOLEAN))),
diff --git a/crates/torii/graphql/src/object/erc/token_balance.rs b/crates/torii/graphql/src/object/erc/token_balance.rs
index fb61d31bd5..6b9ea3d659 100644
--- a/crates/torii/graphql/src/object/erc/token_balance.rs
+++ b/crates/torii/graphql/src/object/erc/token_balance.rs
@@ -143,139 +143,20 @@ impl ResolvableObject for ErcBalanceObject {
                                         Err(_) => return None,
                                     };
 
-                                    let balance_value = match row
-                                        .contract_type
-                                        .to_lowercase()
-                                        .as_str()
-                                    {
-                                        "erc20" => {
-                                            let token_metadata = Erc20Token {
-                                                contract_address: row.contract_address,
-                                                name: row.name,
-                                                symbol: row.symbol,
-                                                decimals: row.decimals,
-                                                amount: row.balance,
-                                            };
-                                            ErcTokenType::Erc20(token_metadata)
-                                        }
-                                        "erc721" => {
-                                            let token_id =
-                                                row.token_id.split(':').collect::<Vec<&str>>();
-                                            assert!(token_id.len() == 2);
-
-                                            let metadata_str = row.metadata;
-                                            let (
-                                                metadata_str,
-                                                metadata_name,
-                                                metadata_description,
-                                                metadata_attributes,
-                                                image_path,
-                                            ) = if metadata_str.is_empty() {
-                                                (String::new(), None, None, None, String::new())
-                                            } else {
-                                                let metadata: serde_json::Value =
-                                                    serde_json::from_str(&metadata_str)
-                                                        .expect("metadata is always json");
-                                                let metadata_name = metadata.get("name").map(|v| {
-                                                    v.to_string().trim_matches('"').to_string()
-                                                });
-                                                let metadata_description =
-                                                    metadata.get("description").map(|v| {
-                                                        v.to_string().trim_matches('"').to_string()
-                                                    });
-                                                let metadata_attributes =
-                                                    metadata.get("attributes").map(|v| {
-                                                        v.to_string().trim_matches('"').to_string()
-                                                    });
-
-                                                let image_path =
-                                                    format!("{}/{}", token_id.join("/"), "image");
-
-                                                (
-                                                    metadata_str,
-                                                    metadata_name,
-                                                    metadata_description,
-                                                    metadata_attributes,
-                                                    image_path,
-                                                )
-                                            };
-
-                                            let token_metadata = Erc721Token {
-                                                name: row.name,
-                                                metadata: metadata_str,
-                                                contract_address: row.contract_address,
-                                                symbol: row.symbol,
-                                                token_id: token_id[1].to_string(),
-                                                metadata_name,
-                                                metadata_description,
-                                                metadata_attributes,
-                                                image_path,
-                                            };
-                                            ErcTokenType::Erc721(token_metadata)
+                                    // Use the extracted mapping function
+                                    match token_balance_mapping_from_row(&row) {
+                                        Ok(balance_value) => {
+                                            Some(Ok(FieldValue::owned_any(balance_value)))
                                         }
-                                        "erc1155" => {
-                                            let token_id =
-                                                row.token_id.split(':').collect::<Vec<&str>>();
-                                            assert!(token_id.len() == 2);
-
-                                            let metadata_str = row.metadata;
-                                            let (
-                                                metadata_str,
-                                                metadata_name,
-                                                metadata_description,
-                                                metadata_attributes,
-                                                image_path,
-                                            ) = if metadata_str.is_empty() {
-                                                (String::new(), None, None, None, String::new())
-                                            } else {
-                                                let metadata: serde_json::Value =
-                                                    serde_json::from_str(&metadata_str)
-                                                        .expect("metadata is always json");
-                                                let metadata_name = metadata.get("name").map(|v| {
-                                                    v.to_string().trim_matches('"').to_string()
-                                                });
-                                                let metadata_description =
-                                                    metadata.get("description").map(|v| {
-                                                        v.to_string().trim_matches('"').to_string()
-                                                    });
-                                                let metadata_attributes =
-                                                    metadata.get("attributes").map(|v| {
-                                                        v.to_string().trim_matches('"').to_string()
-                                                    });
-
-                                                let image_path =
-                                                    format!("{}/{}", token_id.join("/"), "image");
-
-                                                (
-                                                    metadata_str,
-                                                    metadata_name,
-                                                    metadata_description,
-                                                    metadata_attributes,
-                                                    image_path,
-                                                )
-                                            };
-
-                                            let token_metadata = Erc1155Token {
-                                                name: row.name,
-                                                metadata: metadata_str,
-                                                contract_address: row.contract_address,
-                                                symbol: row.symbol,
-                                                token_id: token_id[1].to_string(),
-                                                amount: row.balance,
-                                                metadata_name,
-                                                metadata_description,
-                                                metadata_attributes,
-                                                image_path,
-                                            };
-                                            ErcTokenType::Erc1155(token_metadata)
-                                        }
-                                        _ => {
-                                            warn!("Unknown contract type: {}", row.contract_type);
-                                            return None;
+                                        Err(err) => {
+                                            warn!(
+                                                "Failed to transform row to token balance in \
+                                                 subscription: {}",
+                                                err
+                                            );
+                                            None
                                         }
-                                    };
-
-                                    Some(Ok(FieldValue::owned_any(balance_value)))
+                                    }
                                 }
                             })
                             .filter_map(|result| result))
@@ -423,135 +304,151 @@ fn token_balances_connection_output<'a>(
         let row = BalanceQueryResultRaw::from_row(row)?;
         let cursor = cursor::encode(&row.id, &row.id);
 
-        let balance_value = match row.contract_type.to_lowercase().as_str() {
-            "erc20" => {
-                let token_metadata = Erc20Token {
-                    contract_address: row.contract_address,
-                    name: row.name,
-                    symbol: row.symbol,
-                    decimals: row.decimals,
-                    amount: row.balance,
-                };
+        match token_balance_mapping_from_row(&row) {
+            Ok(balance_value) => {
+                edges.push(ConnectionEdge { node: balance_value, cursor });
+            }
+            Err(err) => {
+                warn!("Failed to transform row to token balance: {}", err);
+                continue;
+            }
+        }
+    }
 
-                ErcTokenType::Erc20(token_metadata)
+    Ok(FieldValue::owned_any(Connection {
+        total_count,
+        edges,
+        page_info: PageInfoObject::value(page_info),
+    }))
+}
+
+/// Transforms a BalanceQueryResultRaw into an ErcTokenType
+fn token_balance_mapping_from_row(row: &BalanceQueryResultRaw) -> Result<ErcTokenType, String> {
+    match row.contract_type.to_lowercase().as_str() {
+        "erc20" => {
+            let token_metadata = Erc20Token {
+                contract_address: row.contract_address.clone(),
+                name: row.name.clone(),
+                symbol: row.symbol.clone(),
+                decimals: row.decimals,
+                amount: row.balance.clone(),
+            };
+
+            Ok(ErcTokenType::Erc20(token_metadata))
+        }
+        "erc721" => {
+            // contract_address:token_id
+            let token_id = row.token_id.split(':').collect::<Vec<&str>>();
+            if token_id.len() != 2 {
+                return Err(format!("Invalid token_id format: {}", row.token_id));
             }
-            "erc721" => {
-                // contract_address:token_id
-                let token_id = row.token_id.split(':').collect::<Vec<&str>>();
-                assert!(token_id.len() == 2);
-
-                let metadata_str = row.metadata;
-                let (
-                    metadata_str,
-                    metadata_name,
-                    metadata_description,
-                    metadata_attributes,
-                    image_path,
-                ) = if metadata_str.is_empty() {
-                    (String::new(), None, None, None, String::new())
-                } else {
-                    let metadata: serde_json::Value =
-                        serde_json::from_str(&metadata_str).expect("metadata is always json");
-                    let metadata_name =
-                        metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string());
-                    let metadata_description = metadata
-                        .get("description")
-                        .map(|v| v.to_string().trim_matches('"').to_string());
-                    let metadata_attributes = metadata
-                        .get("attributes")
-                        .map(|v| v.to_string().trim_matches('"').to_string());
-
-                    let image_path = format!("{}/{}", token_id.join("/"), "image");
-
-                    (
-                        metadata_str,
-                        metadata_name,
-                        metadata_description,
-                        metadata_attributes,
-                        image_path,
-                    )
-                };
 
-                let token_metadata = Erc721Token {
-                    name: row.name,
-                    metadata: metadata_str.to_owned(),
-                    contract_address: row.contract_address,
-                    symbol: row.symbol,
-                    token_id: token_id[1].to_string(),
-                    metadata_name,
-                    metadata_description,
-                    metadata_attributes,
-                    image_path,
+            let metadata_str = &row.metadata;
+            let (
+                metadata_str,
+                metadata_name,
+                metadata_description,
+                metadata_attributes,
+                image_path,
+            ) = if metadata_str.is_empty() {
+                (String::new(), None, None, None, String::new())
+            } else {
+                let metadata: serde_json::Value = match serde_json::from_str(metadata_str) {
+                    Ok(value) => value,
+                    Err(e) => return Err(format!("Failed to parse metadata as JSON: {}", e)),
                 };
 
-                ErcTokenType::Erc721(token_metadata)
-            }
-            "erc1155" => {
-                // contract_address:token_id
-                let token_id = row.token_id.split(':').collect::<Vec<&str>>();
-                assert!(token_id.len() == 2);
-
-                let metadata_str = row.metadata;
-                let (
-                    metadata_str,
+                let metadata_name =
+                    metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string());
+                let metadata_description = metadata
+                    .get("description")
+                    .map(|v| v.to_string().trim_matches('"').to_string());
+                let metadata_attributes =
+                    metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string());
+
+                let image_path = format!("{}/{}", token_id.join("/"), "image");
+
+                (
+                    metadata_str.to_owned(),
                     metadata_name,
                     metadata_description,
                     metadata_attributes,
                     image_path,
-                ) = if metadata_str.is_empty() {
-                    (String::new(), None, None, None, String::new())
-                } else {
-                    let metadata: serde_json::Value =
-                        serde_json::from_str(&metadata_str).expect("metadata is always json");
-                    let metadata_name =
-                        metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string());
-                    let metadata_description = metadata
-                        .get("description")
-                        .map(|v| v.to_string().trim_matches('"').to_string());
-                    let metadata_attributes = metadata
-                        .get("attributes")
-                        .map(|v| v.to_string().trim_matches('"').to_string());
-
-                    let image_path = format!("{}/{}", token_id.join("/"), "image");
-
-                    (
-                        metadata_str,
-                        metadata_name,
-                        metadata_description,
-                        metadata_attributes,
-                        image_path,
-                    )
+                )
+            };
+
+            let token_metadata = Erc721Token {
+                name: row.name.clone(),
+                metadata: metadata_str,
+                contract_address: row.contract_address.clone(),
+                symbol: row.symbol.clone(),
+                token_id: token_id[1].to_string(),
+                metadata_name,
+                metadata_description,
+                metadata_attributes,
+                image_path,
+            };
+
+            Ok(ErcTokenType::Erc721(token_metadata))
+        }
+        "erc1155" => {
+            // contract_address:token_id
+            let token_id = row.token_id.split(':').collect::<Vec<&str>>();
+            if token_id.len() != 2 {
+                return Err(format!("Invalid token_id format: {}", row.token_id));
+            }
+
+            let metadata_str = &row.metadata;
+            let (
+                metadata_str,
+                metadata_name,
+                metadata_description,
+                metadata_attributes,
+                image_path,
+            ) = if metadata_str.is_empty() {
+                (String::new(), None, None, None, String::new())
+            } else {
+                let metadata: serde_json::Value = match serde_json::from_str(metadata_str) {
+                    Ok(value) => value,
+                    Err(e) => return Err(format!("Failed to parse metadata as JSON: {}", e)),
                 };
 
-                let token_metadata = Erc1155Token {
-                    name: row.name,
-                    metadata: metadata_str.to_owned(),
-                    contract_address: row.contract_address,
-                    symbol: row.symbol,
-                    token_id: token_id[1].to_string(),
-                    amount: row.balance,
+                let metadata_name =
+                    metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string());
+                let metadata_description = metadata
+                    .get("description")
+                    .map(|v| v.to_string().trim_matches('"').to_string());
+                let metadata_attributes =
+                    metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string());
+
+                let image_path = format!("{}/{}", token_id.join("/"), "image");
+
+                (
+                    metadata_str.to_owned(),
                     metadata_name,
                     metadata_description,
                     metadata_attributes,
                     image_path,
-                };
-
-                ErcTokenType::Erc1155(token_metadata)
-            }
-            _ => {
-                warn!("Unknown contract type: {}", row.contract_type);
-                continue;
-            }
-        };
-
-        edges.push(ConnectionEdge { node: balance_value, cursor });
+                )
+            };
+
+            let token_metadata = Erc1155Token {
+                name: row.name.clone(),
+                metadata: metadata_str,
+                contract_address: row.contract_address.clone(),
+                symbol: row.symbol.clone(),
+                token_id: token_id[1].to_string(),
+                amount: row.balance.clone(),
+                metadata_name,
+                metadata_description,
+                metadata_attributes,
+                image_path,
+            };
+
+            Ok(ErcTokenType::Erc1155(token_metadata))
+        }
+        _ => Err(format!("Unknown contract type: {}", row.contract_type)),
     }
-
-    Ok(FieldValue::owned_any(Connection {
-        total_count,
-        edges,
-        page_info: PageInfoObject::value(page_info),
-    }))
 }
 
 // TODO: This would be required when subscriptions are needed
diff --git a/crates/torii/graphql/src/object/erc/token_transfer.rs b/crates/torii/graphql/src/object/erc/token_transfer.rs
index fe5a256571..f1edb58deb 100644
--- a/crates/torii/graphql/src/object/erc/token_transfer.rs
+++ b/crates/torii/graphql/src/object/erc/token_transfer.rs
@@ -18,7 +18,7 @@ use crate::object::connection::page_info::PageInfoObject;
 use crate::object::connection::{
     connection_arguments, cursor, parse_connection_arguments, ConnectionArguments,
 };
-use crate::object::erc::erc_token::Erc721Token;
+use crate::object::erc::erc_token::{Erc1155Token, Erc721Token};
 use crate::object::{BasicObject, ResolvableObject};
 use crate::query::order::{CursorDirection, Direction};
 use crate::types::TypeMapping;
@@ -240,72 +240,17 @@ fn token_transfers_connection_output<'a>(
 
     for row in data {
         let row = TransferQueryResultRaw::from_row(row)?;
-        let transaction_hash = get_transaction_hash_from_event_id(&row.id);
         let cursor = cursor::encode(&row.id, &row.id);
 
-        let transfer_node = match row.contract_type.to_lowercase().as_str() {
-            "erc20" => {
-                let token_metadata = ErcTokenType::Erc20(Erc20Token {
-                    contract_address: row.contract_address,
-                    name: row.name,
-                    symbol: row.symbol,
-                    decimals: row.decimals,
-                    amount: row.amount,
-                });
-
-                TokenTransferNode {
-                    from: row.from_address,
-                    to: row.to_address,
-                    executed_at: row.executed_at,
-                    token_metadata,
-                    transaction_hash,
-                }
-            }
-            "erc721" => {
-                // contract_address:token_id
-                let token_id = row.token_id.split(':').collect::<Vec<&str>>();
-                assert!(token_id.len() == 2);
-
-                let metadata_str = row.metadata;
-                let metadata: serde_json::Value =
-                    serde_json::from_str(&metadata_str).expect("metadata is always json");
-                let metadata_name =
-                    metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string());
-                let metadata_description = metadata
-                    .get("description")
-                    .map(|v| v.to_string().trim_matches('"').to_string());
-                let metadata_attributes =
-                    metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string());
-
-                let image_path = format!("{}/{}", token_id.join("/"), "image");
-
-                let token_metadata = ErcTokenType::Erc721(Erc721Token {
-                    name: row.name,
-                    metadata: metadata_str.to_owned(),
-                    contract_address: row.contract_address,
-                    symbol: row.symbol,
-                    token_id: token_id[1].to_string(),
-                    metadata_name,
-                    metadata_description,
-                    metadata_attributes,
-                    image_path,
-                });
-
-                TokenTransferNode {
-                    from: row.from_address,
-                    to: row.to_address,
-                    executed_at: row.executed_at,
-                    token_metadata,
-                    transaction_hash,
-                }
+        match token_transfer_mapping_from_row(&row) {
+            Ok(transfer_node) => {
+                edges.push(ConnectionEdge { node: transfer_node, cursor });
             }
-            _ => {
-                warn!("Unknown contract type: {}", row.contract_type);
+            Err(err) => {
+                warn!("Failed to transform row to TokenTransferNode: {}", err);
                 continue;
             }
-        };
-
-        edges.push(ConnectionEdge { node: transfer_node, cursor });
+        }
     }
 
     Ok(FieldValue::owned_any(Connection {
@@ -315,6 +260,128 @@ fn token_transfers_connection_output<'a>(
     }))
 }
 
+/// Transforms a TransferQueryResultRaw into a TokenTransferNode
+pub fn token_transfer_mapping_from_row(
+    row: &TransferQueryResultRaw,
+) -> Result<TokenTransferNode, String> {
+    let transaction_hash = get_transaction_hash_from_event_id(&row.id);
+
+    match row.contract_type.to_lowercase().as_str() {
+        "erc20" => {
+            let token_metadata = ErcTokenType::Erc20(Erc20Token {
+                contract_address: row.contract_address.clone(),
+                name: row.name.clone(),
+                symbol: row.symbol.clone(),
+                decimals: row.decimals,
+                amount: row.amount.clone(),
+            });
+
+            Ok(TokenTransferNode {
+                from: row.from_address.clone(),
+                to: row.to_address.clone(),
+                executed_at: row.executed_at.clone(),
+                token_metadata,
+                transaction_hash,
+            })
+        }
+        "erc721" => {
+            // contract_address:token_id
+            let token_id = row.token_id.split(':').collect::<Vec<&str>>();
+            if token_id.len() != 2 {
+                return Err(format!("Invalid token_id format: {}", row.token_id));
+            }
+
+            let metadata_str = &row.metadata;
+            let metadata: serde_json::Value = match serde_json::from_str(metadata_str) {
+                Ok(value) => value,
+                Err(e) => return Err(format!("Failed to parse metadata as JSON: {}", e)),
+            };
+
+            let metadata_name =
+                metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string());
+            let metadata_description =
+                metadata.get("description").map(|v| v.to_string().trim_matches('"').to_string());
+            let metadata_attributes =
+                metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string());
+
+            let image_path = format!("{}/{}", token_id.join("/"), "image");
+
+            let token_metadata = ErcTokenType::Erc721(Erc721Token {
+                name: row.name.clone(),
+                metadata: metadata_str.to_owned(),
+                contract_address: row.contract_address.clone(),
+                symbol: row.symbol.clone(),
+                token_id: token_id[1].to_string(),
+                metadata_name,
+                metadata_description,
+                metadata_attributes,
+                image_path,
+            });
+
+            Ok(TokenTransferNode {
+                from: row.from_address.clone(),
+                to: row.to_address.clone(),
+                executed_at: row.executed_at.clone(),
+                token_metadata,
+                transaction_hash,
+            })
+        }
+        "erc1155" => {
+            // contract_address:token_id
+            let token_id = row.token_id.split(':').collect::<Vec<&str>>();
+            if token_id.len() != 2 {
+                return Err(format!("Invalid token_id format: {}", row.token_id));
+            }
+
+            let metadata_str = &row.metadata;
+            let (metadata_name, metadata_description, metadata_attributes, image_path) =
+                if metadata_str.is_empty() {
+                    (None, None, None, String::new())
+                } else {
+                    let metadata: serde_json::Value = match serde_json::from_str(metadata_str) {
+                        Ok(value) => value,
+                        Err(e) => return Err(format!("Failed to parse metadata as JSON: {}", e)),
+                    };
+
+                    let metadata_name =
+                        metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string());
+                    let metadata_description = metadata
+                        .get("description")
+                        .map(|v| v.to_string().trim_matches('"').to_string());
+                    let metadata_attributes = metadata
+                        .get("attributes")
+                        .map(|v| v.to_string().trim_matches('"').to_string());
+
+                    let image_path = format!("{}/{}", token_id.join("/"), "image");
+
+                    (metadata_name, metadata_description, metadata_attributes, image_path)
+                };
+
+            let token_metadata = ErcTokenType::Erc1155(Erc1155Token {
+                name: row.name.clone(),
+                metadata: metadata_str.to_owned(),
+                contract_address: row.contract_address.clone(),
+                symbol: row.symbol.clone(),
+                token_id: token_id[1].to_string(),
+                amount: row.amount.clone(),
+                metadata_name,
+                metadata_description,
+                metadata_attributes,
+                image_path,
+            });
+
+            Ok(TokenTransferNode {
+                from: row.from_address.clone(),
+                to: row.to_address.clone(),
+                executed_at: row.executed_at.clone(),
+                token_metadata,
+                transaction_hash,
+            })
+        }
+        _ => Err(format!("Unknown contract type: {}", row.contract_type)),
+    }
+}
+
 // TODO: This would be required when subscriptions are needed
 // impl ErcTransferObject {
 //     pub fn value_mapping(entity: ErcBalance) -> ValueMapping {
@@ -325,7 +392,7 @@ fn token_transfers_connection_output<'a>(
 
 #[derive(FromRow, Deserialize, Debug, Clone)]
 #[serde(rename_all = "camelCase")]
-struct TransferQueryResultRaw {
+pub struct TransferQueryResultRaw {
     pub id: String,
     pub contract_address: String,
     pub from_address: String,
diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs
index 74df2b0239..11c6ca4002 100644
--- a/crates/torii/graphql/src/object/mod.rs
+++ b/crates/torii/graphql/src/object/mod.rs
@@ -28,7 +28,9 @@ use self::connection::{
 };
 use self::inputs::keys_input::parse_keys_argument;
 use self::inputs::order_input::parse_order_argument;
-use crate::query::data::{count_rows, fetch_multiple_rows, fetch_single_row};
+use crate::query::data::{
+    count_rows, fetch_multiple_rows, fetch_single_row, fetch_single_row_with_joins, JoinConfig,
+};
 use crate::query::value_mapping_from_row;
 use crate::types::{TypeMapping, ValueMapping};
 use crate::utils::extract;
@@ -270,6 +272,51 @@ pub fn resolve_one(
     .argument(argument)
 }
 
+// Resolves single object queries with joins, returns current object of type type_name with related
+// data
+pub fn resolve_one_with_joins(
+    table_name: &str,
+    id_column: &str,
+    field_name: &str,
+    type_name: &str,
+    type_mapping: &TypeMapping,
+    joins: Vec<JoinConfig>,
+    select_columns: Option<Vec<String>>,
+) -> Field {
+    let type_mapping = type_mapping.clone();
+    let table_name = table_name.to_owned();
+    let id_column = id_column.to_owned();
+    let joins = joins.to_owned();
+    let select_columns = select_columns.to_owned();
+    let argument = InputValue::new(id_column.to_case(Case::Camel), TypeRef::named_nn(TypeRef::ID));
+
+    Field::new(field_name, TypeRef::named_nn(type_name), move |ctx| {
+        let type_mapping = type_mapping.clone();
+        let table_name = table_name.to_owned();
+        let id_column = id_column.to_owned();
+        let joins = joins.to_owned();
+        let select_columns = select_columns.to_owned();
+
+        FieldFuture::new(async move {
+            let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+            let id: String =
+                extract::<String>(ctx.args.as_index_map(), &id_column.to_case(Case::Camel))?;
+            let data = fetch_single_row_with_joins(
+                &mut conn,
+                &table_name,
+                &id_column,
+                &id,
+                joins,
+                select_columns,
+            )
+            .await?;
+            let model = value_mapping_from_row(&data, &type_mapping, false, true)?;
+            Ok(Some(Value::Object(model)))
+        })
+    })
+    .argument(argument)
+}
+
 // Resolves plural object queries, returns type of {type_name}Connection (eg "PlayerConnection")
 pub fn resolve_many(
     table_name: &str,
diff --git a/crates/torii/graphql/src/object/transaction.rs b/crates/torii/graphql/src/object/transaction.rs
index b58b1671f6..1babfa02df 100644
--- a/crates/torii/graphql/src/object/transaction.rs
+++ b/crates/torii/graphql/src/object/transaction.rs
@@ -1,12 +1,47 @@
-use async_graphql::dynamic::Field;
+use std::str::FromStr;
 
-use super::{BasicObject, ResolvableObject, TypeMapping};
+use async_graphql::dynamic::{
+    Field, FieldFuture, FieldValue, InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef,
+};
+use async_graphql::{Name, Value};
+use sqlx::{FromRow, Pool, Sqlite};
+use starknet_crypto::Felt;
+use tokio_stream::StreamExt;
+use torii_sqlite::simple_broker::SimpleBroker;
+use torii_sqlite::types::Transaction;
+
+use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping};
 use crate::constants::{
-    ID_COLUMN, TRANSACTION_HASH_COLUMN, TRANSACTION_NAMES, TRANSACTION_TABLE, TRANSACTION_TYPE_NAME,
+    CALL_TYPE_NAME, ID_COLUMN, TOKEN_TRANSFER_TABLE, TOKEN_TRANSFER_TYPE_NAME,
+    TRANSACTION_CALLS_TABLE, TRANSACTION_HASH_COLUMN, TRANSACTION_NAMES, TRANSACTION_TABLE,
+    TRANSACTION_TYPE_NAME,
 };
-use crate::mapping::TRANSACTION_MAPPING;
+use crate::mapping::{CALL_MAPPING, TRANSACTION_MAPPING};
+use crate::object::erc::token_transfer::{token_transfer_mapping_from_row, TransferQueryResultRaw};
 use crate::object::{resolve_many, resolve_one};
+use crate::query::value_mapping_from_row;
+use crate::utils;
 
+#[derive(Debug)]
+pub struct CallObject;
+
+impl BasicObject for CallObject {
+    fn name(&self) -> (&str, &str) {
+        ("call", "calls")
+    }
+
+    fn type_name(&self) -> &str {
+        CALL_TYPE_NAME
+    }
+
+    fn type_mapping(&self) -> &TypeMapping {
+        &CALL_MAPPING
+    }
+
+    fn related_fields(&self) -> Option<Vec<Field>> {
+        None
+    }
+}
 #[derive(Debug)]
 pub struct TransactionObject;
 
@@ -22,6 +57,10 @@ impl BasicObject for TransactionObject {
     fn type_mapping(&self) -> &TypeMapping {
         &TRANSACTION_MAPPING
     }
+
+    fn related_fields(&self) -> Option<Vec<Field>> {
+        Some(vec![calls_field(), token_transfers_field()])
+    }
 }
 
 impl ResolvableObject for TransactionObject {
@@ -44,4 +83,154 @@ impl ResolvableObject for TransactionObject {
 
         vec![resolve_one, resolve_many]
     }
+
+    fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
+        Some(vec![
+            SubscriptionField::new("transaction", TypeRef::named_nn(self.type_name()), |ctx| {
+                SubscriptionFieldFuture::new(async move {
+                    let hash = match ctx.args.get("hash") {
+                        Some(hash) => Some(hash.string()?.to_string()),
+                        None => None,
+                    };
+
+                    let caller = match ctx.args.get("hasCaller") {
+                        Some(caller) => Some(caller.string()?.to_string()),
+                        None => None,
+                    };
+
+                    // if hash is None, then subscribe to all transactions
+                    // if hash is Some, then subscribe to only the transaction with that hash
+                    Ok(SimpleBroker::<Transaction>::subscribe().filter_map(
+                        move |transaction: Transaction| {
+                            if (hash.is_none()
+                                || hash == Some(transaction.transaction_hash.clone()))
+                                && (caller.is_none()
+                                    || transaction.calls.iter().any(|call| {
+                                        call.caller_address
+                                            == Felt::from_str(&caller.clone().unwrap()).unwrap()
+                                    }))
+                            {
+                                Some(Ok(Value::Object(TransactionObject::value_mapping(
+                                    transaction,
+                                ))))
+                            } else {
+                                // hash != transaction.transaction_hash, then don't send anything,
+                                // still listening
+                                None
+                            }
+                        },
+                    ))
+                })
+            })
+            .argument(InputValue::new("hash", TypeRef::named(TypeRef::ID)))
+            .argument(InputValue::new("hasCaller", TypeRef::named(TypeRef::STRING))),
+        ])
+    }
+}
+
+impl TransactionObject {
+    pub fn value_mapping(transaction: Transaction) -> ValueMapping {
+        async_graphql::dynamic::indexmap::IndexMap::from([
+            (Name::new("id"), Value::from(transaction.id)),
+            (Name::new("transactionHash"), Value::from(transaction.transaction_hash)),
+            (Name::new("senderAddress"), Value::from(transaction.sender_address)),
+            (
+                Name::new("calldata"),
+                Value::from(transaction.calldata.split("/").collect::<Vec<_>>()),
+            ),
+            (Name::new("maxFee"), Value::from(transaction.max_fee)),
+            (Name::new("signature"), Value::from(transaction.signature)),
+            (Name::new("nonce"), Value::from(transaction.nonce)),
+            (Name::new("executedAt"), Value::from(transaction.executed_at.to_rfc3339())),
+            (Name::new("createdAt"), Value::from(transaction.created_at.to_rfc3339())),
+            (Name::new("transactionType"), Value::from(transaction.transaction_type)),
+            (Name::new("blockNumber"), Value::from(transaction.block_number)),
+        ])
+    }
+}
+
+fn calls_field() -> Field {
+    Field::new("calls", TypeRef::named_list(CALL_TYPE_NAME), move |ctx| {
+        FieldFuture::new(async move {
+            match ctx.parent_value.try_to_value()? {
+                Value::Object(indexmap) => {
+                    let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+
+                    let transaction_hash = utils::extract::<String>(indexmap, "transactionHash")?;
+
+                    // Fetch all function calls for this transaction
+                    let query = &format!(
+                        "SELECT * FROM {TRANSACTION_CALLS_TABLE} WHERE transaction_hash = ?"
+                    );
+                    let rows =
+                        sqlx::query(query).bind(&transaction_hash).fetch_all(&mut *conn).await?;
+
+                    let results = rows
+                        .iter()
+                        .map(|row| {
+                            value_mapping_from_row(row, &CALL_MAPPING, false, true)
+                                .map(Value::Object)
+                        })
+                        .collect::<Result<Vec<_>, _>>()?;
+
+                    Ok(Some(Value::List(results)))
+                }
+                _ => Err("incorrect value, requires Value::Object".into()),
+            }
+        })
+    })
+}
+
+fn token_transfers_field() -> Field {
+    Field::new("tokenTransfers", TypeRef::named_list(TOKEN_TRANSFER_TYPE_NAME), move |ctx| {
+        FieldFuture::new(async move {
+            match ctx.parent_value.try_to_value()? {
+                Value::Object(indexmap) => {
+                    let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+
+                    let transaction_hash = utils::extract::<String>(indexmap, "transactionHash")?;
+
+                    // Fetch all token transfers for this transaction
+                    let query = format!(
+                        r#"
+                        SELECT 
+                            et.id,
+                            et.contract_address,
+                            et.from_address,
+                            et.to_address,
+                            et.amount,
+                            et.token_id,
+                            et.executed_at,
+                            t.name,
+                            t.symbol,
+                            t.decimals,
+                            c.contract_type,
+                            t.metadata
+                        FROM
+                            {TOKEN_TRANSFER_TABLE} et
+                        JOIN
+                            tokens t ON et.token_id = t.id
+                        JOIN
+                            contracts c ON t.contract_address = c.contract_address
+                        WHERE
+                            et.event_id LIKE '%:{transaction_hash}:%'
+                        "#
+                    );
+
+                    let rows =
+                        sqlx::query(&query).bind(&transaction_hash).fetch_all(&mut *conn).await?;
+
+                    let mut results = Vec::new();
+                    for row in &rows {
+                        let row = TransferQueryResultRaw::from_row(row)?;
+                        let result = token_transfer_mapping_from_row(&row)?;
+                        results.push(FieldValue::owned_any(result));
+                    }
+
+                    Ok(Some(FieldValue::list(results)))
+                }
+                _ => Err("incorrect value, requires Value::Object".into()),
+            }
+        })
+    })
 }
diff --git a/crates/torii/graphql/src/query/data.rs b/crates/torii/graphql/src/query/data.rs
index 83a5617621..ec2e37f77e 100644
--- a/crates/torii/graphql/src/query/data.rs
+++ b/crates/torii/graphql/src/query/data.rs
@@ -32,6 +32,33 @@ pub async fn fetch_world_address(conn: &mut SqliteConnection) -> Result<String>
     Ok(res.0)
 }
 
+#[derive(Clone, Debug)]
+pub struct JoinConfig {
+    pub table: String,
+    pub alias: Option<String>,
+    pub on_condition: String,
+    pub join_type: JoinType,
+}
+
+#[derive(Clone, Debug)]
+pub enum JoinType {
+    Inner,
+    Left,
+    Right,
+    Full,
+}
+
+impl JoinType {
+    fn as_sql(&self) -> &'static str {
+        match self {
+            JoinType::Inner => "INNER JOIN",
+            JoinType::Left => "LEFT JOIN",
+            JoinType::Right => "LEFT JOIN", // SQLite doesn't support RIGHT JOIN, so we use LEFT
+            JoinType::Full => "LEFT JOIN",  // SQLite doesn't support FULL JOIN, so we use LEFT
+        }
+    }
+}
+
 pub async fn fetch_single_row(
     conn: &mut SqliteConnection,
     table_name: &str,
@@ -42,6 +69,42 @@ pub async fn fetch_single_row(
     sqlx::query(&query).fetch_one(conn).await
 }
 
+pub async fn fetch_single_row_with_joins(
+    conn: &mut SqliteConnection,
+    table_name: &str,
+    id_column: &str,
+    id: &str,
+    joins: Vec<JoinConfig>,
+    select_columns: Option<Vec<String>>,
+) -> sqlx::Result<SqliteRow> {
+    // Build the SELECT clause
+    let select = match select_columns {
+        Some(columns) => columns.join(", "),
+        None => format!("[{}].*", table_name),
+    };
+
+    // Build the JOIN clauses
+    let join_clauses = joins
+        .iter()
+        .map(|join| {
+            let table_ref = match &join.alias {
+                Some(alias) => format!("[{}] AS {}", join.table, alias),
+                None => format!("[{}]", join.table),
+            };
+            format!("{} {} ON {}", join.join_type.as_sql(), table_ref, join.on_condition)
+        })
+        .collect::<Vec<String>>()
+        .join(" ");
+
+    // Build the complete query
+    let query = format!(
+        "SELECT {} FROM [{}] {} WHERE [{}].{} = '{}'",
+        select, table_name, join_clauses, table_name, id_column, id
+    );
+
+    sqlx::query(&query).fetch_one(conn).await
+}
+
 #[allow(clippy::too_many_arguments)]
 pub async fn fetch_multiple_rows(
     conn: &mut SqliteConnection,
diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs
index fd37dfeb30..f3d77b9cdd 100644
--- a/crates/torii/graphql/src/schema.rs
+++ b/crates/torii/graphql/src/schema.rs
@@ -26,7 +26,7 @@ use crate::object::metadata::content::ContentObject;
 use crate::object::metadata::social::SocialObject;
 use crate::object::metadata::MetadataObject;
 use crate::object::model::ModelObject;
-use crate::object::transaction::TransactionObject;
+use crate::object::transaction::{CallObject, TransactionObject};
 use crate::object::ObjectVariant;
 use crate::query::build_type_mapping;
 
@@ -135,6 +135,7 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec<ObjectVariant>, Vec<Uni
         ObjectVariant::Basic(Box::new(Erc20TokenObject)),
         ObjectVariant::Basic(Box::new(Erc1155TokenObject)),
         ObjectVariant::Basic(Box::new(EmptyObject)),
+        ObjectVariant::Basic(Box::new(CallObject)),
     ];
 
     // model union object
diff --git a/crates/torii/indexer/Cargo.toml b/crates/torii/indexer/Cargo.toml
index a82eca188b..4be4185b5e 100644
--- a/crates/torii/indexer/Cargo.toml
+++ b/crates/torii/indexer/Cargo.toml
@@ -14,6 +14,7 @@ async-trait.workspace = true
 base64.workspace = true
 bitflags = "2.6.0"
 cainome.workspace = true
+cainome-cairo-serde.workspace = true
 chrono.workspace = true
 crypto-bigint.workspace = true
 data-url.workspace = true
diff --git a/crates/torii/indexer/src/engine.rs b/crates/torii/indexer/src/engine.rs
index 4173f2b5f0..21bbe12da0 100644
--- a/crates/torii/indexer/src/engine.rs
+++ b/crates/torii/indexer/src/engine.rs
@@ -23,6 +23,7 @@ use tokio::sync::mpsc::Sender as BoundedSender;
 use tokio::sync::Semaphore;
 use tokio::task::JoinSet;
 use tokio::time::{sleep, Instant};
+use torii_sqlite::cache::ContractClassCache;
 use torii_sqlite::types::{Contract, ContractType};
 use torii_sqlite::{Cursors, Sql};
 use tracing::{debug, error, info, trace, warn};
@@ -229,6 +230,7 @@ pub struct Engine<P: Provider + Send + Sync + std::fmt::Debug + 'static> {
     block_tx: Option<BoundedSender<u64>>,
     task_manager: TaskManager<P>,
     contracts: Arc<HashMap<Felt, ContractType>>,
+    contract_class_cache: Arc<ContractClassCache<P>>,
 }
 
 struct UnprocessedEvent {
@@ -255,11 +257,12 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> {
         let processors = Arc::new(processors);
         let max_concurrent_tasks = config.max_concurrent_tasks;
         let event_processor_config = config.event_processor_config.clone();
+        let provider = Arc::new(provider);
 
         Self {
             world: world.clone(),
             db: db.clone(),
-            provider: Arc::new(provider),
+            provider: provider.clone(),
             processors: processors.clone(),
             config,
             shutdown_tx,
@@ -272,6 +275,7 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> {
                 max_concurrent_tasks,
                 event_processor_config,
             ),
+            contract_class_cache: Arc::new(ContractClassCache::new(provider)),
         }
     }
 
@@ -772,6 +776,7 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> {
                     transaction_hash,
                     contract_addresses,
                     transaction,
+                    self.contract_class_cache.as_ref(),
                 )
                 .await?
         }
diff --git a/crates/torii/indexer/src/processors/mod.rs b/crates/torii/indexer/src/processors/mod.rs
index 4368ec606f..22b8f3d534 100644
--- a/crates/torii/indexer/src/processors/mod.rs
+++ b/crates/torii/indexer/src/processors/mod.rs
@@ -5,6 +5,7 @@ use async_trait::async_trait;
 use dojo_world::contracts::world::WorldContractReader;
 use starknet::core::types::{Event, Felt, Transaction};
 use starknet::providers::Provider;
+use torii_sqlite::cache::ContractClassCache;
 use torii_sqlite::Sql;
 
 use crate::task_manager::{TaskId, TaskPriority};
@@ -85,7 +86,7 @@ pub trait BlockProcessor<P: Provider + Sync>: Send + Sync {
 }
 
 #[async_trait]
-pub trait TransactionProcessor<P: Provider + Sync>: Send + Sync {
+pub trait TransactionProcessor<P: Provider + Sync + std::fmt::Debug>: Send + Sync {
     #[allow(clippy::too_many_arguments)]
     async fn process(
         &self,
@@ -96,5 +97,6 @@ pub trait TransactionProcessor<P: Provider + Sync>: Send + Sync {
         transaction_hash: Felt,
         contract_addresses: &HashSet<Felt>,
         transaction: &Transaction,
+        contract_class_cache: &ContractClassCache<P>,
     ) -> Result<(), Error>;
 }
diff --git a/crates/torii/indexer/src/processors/store_transaction.rs b/crates/torii/indexer/src/processors/store_transaction.rs
index f451b26da9..abbe38cb2e 100644
--- a/crates/torii/indexer/src/processors/store_transaction.rs
+++ b/crates/torii/indexer/src/processors/store_transaction.rs
@@ -1,18 +1,55 @@
 use std::collections::HashSet;
 
-use anyhow::{Error, Ok, Result};
 use async_trait::async_trait;
-use starknet::core::types::{Felt, Transaction};
+use cainome::cairo_serde_derive::CairoSerde;
+use cainome_cairo_serde::CairoSerde;
+use starknet::core::types::{BlockId, BlockTag, Felt, InvokeTransaction, Transaction};
 use starknet::providers::Provider;
+use torii_sqlite::cache::{get_entrypoint_name_from_class, ContractClassCache};
+use torii_sqlite::types::{CallType, ParsedCall};
 use torii_sqlite::Sql;
 
 use super::TransactionProcessor;
 
+#[derive(CairoSerde, Debug, Clone)]
+pub struct ExecuteCall {
+    pub contract_address: Felt,
+    pub selector: Felt,
+    pub calldata: Vec<Felt>,
+}
+
+#[derive(CairoSerde, Debug, Clone)]
+pub struct LegacyExecuteCall {
+    pub contract_address: Felt,
+    pub selector: Felt,
+    pub data_offset: usize,
+    pub data_length: usize,
+}
+
+#[derive(CairoSerde, Debug, Clone)]
+pub struct ExecuteTransaction {
+    pub calls: Vec<ExecuteCall>,
+}
+
+#[derive(CairoSerde, Debug, Clone)]
+pub struct LegacyExecuteTransaction {
+    pub calls: Vec<LegacyExecuteCall>,
+    pub calldata: Vec<Felt>,
+}
+
+#[derive(CairoSerde, Debug, Clone)]
+pub enum Execute {
+    Legacy(LegacyExecuteTransaction),
+    Execute(ExecuteTransaction),
+}
+
 #[derive(Default, Debug)]
 pub struct StoreTransactionProcessor;
 
 #[async_trait]
-impl<P: Provider + Sync + std::fmt::Debug> TransactionProcessor<P> for StoreTransactionProcessor {
+impl<P: Provider + Send + Sync + std::fmt::Debug> TransactionProcessor<P>
+    for StoreTransactionProcessor
+{
     async fn process(
         &self,
         db: &mut Sql,
@@ -22,8 +59,173 @@ impl<P: Provider + Sync + std::fmt::Debug> TransactionProcessor<P> for StoreTran
         _transaction_hash: Felt,
         contract_addresses: &HashSet<Felt>,
         transaction: &Transaction,
-    ) -> Result<(), Error> {
-        db.store_transaction(transaction, block_number, contract_addresses, block_timestamp)?;
+        contract_class_cache: &ContractClassCache<P>,
+    ) -> Result<(), anyhow::Error> {
+        let transaction_type = match transaction {
+            Transaction::Invoke(_) => "INVOKE",
+            Transaction::L1Handler(_) => "L1_HANDLER",
+            _ => return Ok(()),
+        };
+
+        let (transaction_hash, sender_address, calldata, max_fee, signature, nonce) =
+            match transaction {
+                Transaction::Invoke(InvokeTransaction::V3(invoke_v3_transaction)) => (
+                    invoke_v3_transaction.transaction_hash,
+                    invoke_v3_transaction.sender_address,
+                    &invoke_v3_transaction.calldata,
+                    Felt::ZERO, // has no max_fee
+                    &invoke_v3_transaction.signature,
+                    invoke_v3_transaction.nonce,
+                ),
+                Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => (
+                    invoke_v1_transaction.transaction_hash,
+                    invoke_v1_transaction.sender_address,
+                    &invoke_v1_transaction.calldata,
+                    invoke_v1_transaction.max_fee,
+                    &invoke_v1_transaction.signature,
+                    invoke_v1_transaction.nonce,
+                ),
+                Transaction::L1Handler(l1_handler_transaction) => (
+                    l1_handler_transaction.transaction_hash,
+                    l1_handler_transaction.contract_address,
+                    &l1_handler_transaction.calldata,
+                    Felt::ZERO, // has no max_fee
+                    &vec![],    // has no signature
+                    l1_handler_transaction.nonce.into(),
+                ),
+                _ => return Ok(()),
+            };
+
+        let mut calls: Vec<ParsedCall> = vec![];
+
+        if transaction_type == "INVOKE" {
+            let execute = if let Ok(execute) = ExecuteTransaction::cairo_deserialize(calldata, 0) {
+                Some(Execute::Execute(execute))
+            } else if let Ok(execute) = LegacyExecuteTransaction::cairo_deserialize(calldata, 0) {
+                Some(Execute::Legacy(execute))
+            } else {
+                None
+            };
+
+            match execute {
+                Some(Execute::Execute(execute)) => {
+                    for call in execute.calls {
+                        let contract_class = contract_class_cache
+                            .get(call.contract_address, BlockId::Tag(BlockTag::Pending))
+                            .await?;
+                        let entrypoint =
+                            get_entrypoint_name_from_class(&contract_class, call.selector)
+                                .unwrap_or(format!("{:#x}", call.selector));
+
+                        calls.push(ParsedCall {
+                            contract_address: call.contract_address,
+                            entrypoint,
+                            calldata: call.calldata.clone(),
+                            call_type: CallType::Execute,
+                            caller_address: sender_address,
+                        });
+                    }
+                }
+                Some(Execute::Legacy(execute)) => {
+                    for call in execute.calls {
+                        let contract_class = contract_class_cache
+                            .get(call.contract_address, BlockId::Tag(BlockTag::Pending))
+                            .await?;
+                        let entrypoint =
+                            get_entrypoint_name_from_class(&contract_class, call.selector)
+                                .unwrap_or(format!("{:#x}", call.selector));
+
+                        calls.push(ParsedCall {
+                            contract_address: call.contract_address,
+                            entrypoint,
+                            calldata: execute.calldata
+                                [call.data_offset..call.data_offset + call.data_length]
+                                .to_vec(),
+                            call_type: CallType::Execute,
+                            caller_address: sender_address,
+                        });
+                    }
+                }
+                _ => {}
+            }
+
+            for call in calls.clone() {
+                if call.entrypoint == "execute_from_outside_v3" {
+                    let outside_calls_len: usize = call.calldata[5].try_into().unwrap();
+                    for _ in 0..outside_calls_len {
+                        let to_offset = 6;
+                        let selector_offset = to_offset + 1;
+                        let calldata_offset = selector_offset + 2;
+                        let calldata_len: usize =
+                            call.calldata[selector_offset + 1].try_into().unwrap();
+                        let contract_address = call.calldata[to_offset];
+                        let contract_class = contract_class_cache
+                            .get(contract_address, BlockId::Tag(BlockTag::Pending))
+                            .await?;
+                        let entrypoint = get_entrypoint_name_from_class(
+                            &contract_class,
+                            call.calldata[selector_offset],
+                        )
+                        .unwrap_or(format!("{:#x}", call.calldata[selector_offset]));
+
+                        let outside_call = ParsedCall {
+                            contract_address,
+                            entrypoint,
+                            calldata: call.calldata
+                                [calldata_offset..calldata_offset + calldata_len]
+                                .to_vec(),
+                            call_type: CallType::ExecuteFromOutside,
+                            caller_address: call.contract_address,
+                        };
+                        calls.push(outside_call);
+                    }
+                } else if call.entrypoint == "execute_from_outside_v2" {
+                    // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
+                    let outside_calls_len: usize = call.calldata[4].try_into().unwrap();
+                    for _ in 0..outside_calls_len {
+                        let to_offset = 5;
+                        let selector_offset = to_offset + 1;
+                        let calldata_offset = selector_offset + 2;
+                        let calldata_len: usize =
+                            call.calldata[selector_offset + 1].try_into().unwrap();
+                        let contract_address = call.calldata[to_offset];
+                        let contract_class = contract_class_cache
+                            .get(contract_address, BlockId::Tag(BlockTag::Pending))
+                            .await?;
+                        let entrypoint = get_entrypoint_name_from_class(
+                            &contract_class,
+                            call.calldata[selector_offset],
+                        )
+                        .unwrap_or(format!("{:#x}", call.calldata[selector_offset]));
+
+                        let outside_call = ParsedCall {
+                            contract_address,
+                            entrypoint,
+                            calldata: call.calldata
+                                [calldata_offset..calldata_offset + calldata_len]
+                                .to_vec(),
+                            call_type: CallType::ExecuteFromOutside,
+                            caller_address: call.contract_address,
+                        };
+                        calls.push(outside_call);
+                    }
+                }
+            }
+        }
+
+        db.store_transaction(
+            transaction_hash,
+            sender_address,
+            calldata,
+            max_fee,
+            signature,
+            nonce,
+            block_number,
+            contract_addresses,
+            transaction_type,
+            block_timestamp,
+            &calls,
+        )?;
         Ok(())
     }
 }
diff --git a/crates/torii/migrations/20250317023025_transaction_calls.sql b/crates/torii/migrations/20250317023025_transaction_calls.sql
new file mode 100644
index 0000000000..2afcb0d28b
--- /dev/null
+++ b/crates/torii/migrations/20250317023025_transaction_calls.sql
@@ -0,0 +1,15 @@
+-- Create a new table to store the transaction and contract addresses
+CREATE TABLE IF NOT EXISTS transaction_calls (
+    transaction_hash TEXT NOT NULL,
+    contract_address TEXT NOT NULL,
+    entrypoint TEXT NOT NULL,
+    calldata TEXT NOT NULL,
+    call_type TEXT NOT NULL DEFAULT 'EXECUTE',
+    caller_address TEXT NOT NULL,
+    FOREIGN KEY (transaction_hash) REFERENCES transactions(id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_transaction_calls_transaction_hash ON transaction_calls (transaction_hash);
+CREATE INDEX IF NOT EXISTS idx_transaction_calls_contract_address ON transaction_calls (contract_address);
+CREATE INDEX IF NOT EXISTS idx_transaction_calls_entrypoint ON transaction_calls (entrypoint);
+CREATE INDEX IF NOT EXISTS idx_transaction_calls_caller_address ON transaction_calls (caller_address);
\ No newline at end of file
diff --git a/crates/torii/sqlite/src/cache.rs b/crates/torii/sqlite/src/cache.rs
index 3d6eea38b8..cc61a34635 100644
--- a/crates/torii/sqlite/src/cache.rs
+++ b/crates/torii/sqlite/src/cache.rs
@@ -1,8 +1,15 @@
 use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
 
 use dojo_types::schema::Ty;
 use dojo_world::contracts::abigen::model::Layout;
 use sqlx::{Pool, Sqlite, SqlitePool};
+use starknet::core::types::contract::AbiEntry;
+use starknet::core::types::{
+    BlockId, BlockTag, ContractClass, EntryPointsByType, LegacyContractAbiEntry, StarknetError,
+};
+use starknet::core::utils::get_selector_from_name;
+use starknet::providers::{Provider, ProviderError};
 use starknet_crypto::Felt;
 use tokio::sync::RwLock;
 
@@ -147,3 +154,100 @@ impl LocalCache {
         self.token_id_registry.write().await.insert(token_id);
     }
 }
+
+#[derive(Debug, Clone)]
+pub enum ClassAbi {
+    Sierra((EntryPointsByType, Vec<AbiEntry>)),
+    Legacy(Vec<LegacyContractAbiEntry>),
+}
+
+#[derive(Debug)]
+pub struct ContractClassCache<P: Provider + Sync + std::fmt::Debug> {
+    pub classes: RwLock<HashMap<Felt, (Felt, ClassAbi)>>,
+    pub provider: Arc<P>,
+}
+
+impl<P: Provider + Sync + std::fmt::Debug> ContractClassCache<P> {
+    pub fn new(provider: Arc<P>) -> Self {
+        Self { classes: RwLock::new(HashMap::new()), provider }
+    }
+
+    pub async fn get(
+        &self,
+        contract_address: Felt,
+        mut block_id: BlockId,
+    ) -> Result<ClassAbi, Error> {
+        {
+            let classes = self.classes.read().await;
+            if let Some(class) = classes.get(&contract_address) {
+                return Ok(class.1.clone());
+            }
+        }
+
+        let class_hash = match self.provider.get_class_hash_at(block_id, contract_address).await {
+            Ok(class_hash) => class_hash,
+            Err(e) => match e {
+                // if we got a block not found error, we probably are in a pending block.
+                ProviderError::StarknetError(StarknetError::BlockNotFound) => {
+                    block_id = BlockId::Tag(BlockTag::Pending);
+                    self.provider.get_class_hash_at(block_id, contract_address).await?
+                }
+                _ => return Err(Error::ProviderError(e)),
+            },
+        };
+        let class = match self.provider.get_class_at(block_id, contract_address).await? {
+            ContractClass::Sierra(sierra) => {
+                let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi).unwrap();
+                let functions: Vec<AbiEntry> = flatten_abi_funcs_recursive(&abi);
+                ClassAbi::Sierra((sierra.entry_points_by_type, functions))
+            }
+            ContractClass::Legacy(legacy) => ClassAbi::Legacy(legacy.abi.unwrap_or_default()),
+        };
+        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
+        Ok(class)
+    }
+}
+
+fn flatten_abi_funcs_recursive(abi: &[AbiEntry]) -> Vec<AbiEntry> {
+    abi.iter()
+        .flat_map(|entry| match entry {
+            AbiEntry::Function(_) | AbiEntry::L1Handler(_) | AbiEntry::Constructor(_) => {
+                vec![entry.clone()]
+            }
+            AbiEntry::Interface(interface) => flatten_abi_funcs_recursive(&interface.items),
+            _ => vec![],
+        })
+        .collect()
+}
+
+pub fn get_entrypoint_name_from_class(class: &ClassAbi, selector: Felt) -> Option<String> {
+    match class {
+        ClassAbi::Sierra((entrypoints, abi)) => {
+            let entrypoint_idx = match entrypoints
+                .external
+                .iter()
+                .chain(entrypoints.l1_handler.iter())
+                .chain(entrypoints.constructor.iter())
+                .find(|entrypoint| entrypoint.selector == selector)
+            {
+                Some(entrypoint) => entrypoint.function_idx,
+                None => return None,
+            };
+
+            abi.get(entrypoint_idx as usize).and_then(|function| match function {
+                AbiEntry::Function(function) => Some(function.name.clone()),
+                AbiEntry::L1Handler(l1_handler) => Some(l1_handler.name.clone()),
+                AbiEntry::Constructor(constructor) => Some(constructor.name.clone()),
+                _ => None,
+            })
+        }
+        ClassAbi::Legacy(abi) => abi.iter().find_map(|entry| match entry {
+            LegacyContractAbiEntry::Function(function)
+                if get_selector_from_name(&function.name).unwrap() == selector =>
+            {
+                Some(function.name.clone())
+            }
+            _ => None,
+        }),
+    }
+}
diff --git a/crates/torii/sqlite/src/error.rs b/crates/torii/sqlite/src/error.rs
index 2c33e5193f..c07ea09dda 100644
--- a/crates/torii/sqlite/src/error.rs
+++ b/crates/torii/sqlite/src/error.rs
@@ -4,6 +4,7 @@ use dojo_types::primitive::PrimitiveError;
 use dojo_types::schema::EnumError;
 use starknet::core::types::FromStrError;
 use starknet::core::utils::{CairoShortStringToFeltError, NonAsciiNameError};
+use starknet::providers::ProviderError;
 
 #[derive(Debug, thiserror::Error)]
 pub enum Error {
@@ -17,6 +18,8 @@ pub enum Error {
     PrimitiveError(#[from] PrimitiveError),
     #[error(transparent)]
     EnumError(#[from] EnumError),
+    #[error(transparent)]
+    ProviderError(#[from] ProviderError),
 }
 
 #[derive(Debug, thiserror::Error)]
diff --git a/crates/torii/sqlite/src/executor/mod.rs b/crates/torii/sqlite/src/executor/mod.rs
index 22b6046e41..486423c923 100644
--- a/crates/torii/sqlite/src/executor/mod.rs
+++ b/crates/torii/sqlite/src/executor/mod.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::mem;
 use std::str::FromStr;
 use std::sync::Arc;
@@ -8,7 +8,7 @@ use anyhow::{Context, Result};
 use cainome::cairo_serde::{ByteArray, CairoSerde};
 use dojo_types::schema::{Struct, Ty};
 use erc::{RegisterNftTokenMetadata, UpdateNftMetadataQuery};
-use sqlx::{FromRow, Pool, Sqlite, Transaction};
+use sqlx::{FromRow, Pool, Sqlite, Transaction as SqlxTransaction};
 use starknet::core::types::{BlockId, BlockTag, Felt, FunctionCall};
 use starknet::core::utils::{get_selector_from_name, parse_cairo_short_string};
 use starknet::providers::Provider;
@@ -24,9 +24,9 @@ use crate::simple_broker::SimpleBroker;
 use crate::types::{
     ContractCursor, ContractType, Entity as EntityUpdated, Event as EventEmitted,
     EventMessage as EventMessageUpdated, Model as ModelRegistered, OptimisticEntity,
-    OptimisticEventMessage, Token, TokenBalance,
+    OptimisticEventMessage, ParsedCall, Token, TokenBalance, Transaction,
 };
-use crate::utils::{felt_to_sql_string, I256};
+use crate::utils::{felt_to_sql_string, felts_to_sql_string, I256};
 
 pub mod erc;
 pub use erc::{RegisterErc20TokenQuery, RegisterNftTokenQuery};
@@ -51,6 +51,7 @@ pub enum BrokerMessage {
     EventEmitted(EventEmitted),
     TokenRegistered(Token),
     TokenBalanceUpdated(TokenBalance),
+    Transaction(Transaction),
 }
 
 #[derive(Debug, Clone)]
@@ -104,6 +105,12 @@ pub struct EventMessageQuery {
     pub ty: Ty,
 }
 
+#[derive(Debug, Clone)]
+pub struct StoreTransactionQuery {
+    pub contract_addresses: HashSet<Felt>,
+    pub calls: Vec<ParsedCall>,
+}
+
 #[derive(Debug, Clone)]
 pub struct EntityQuery {
     pub entity_id: String,
@@ -117,6 +124,7 @@ pub struct EntityQuery {
 
 #[derive(Debug, Clone)]
 pub enum QueryType {
+    StoreTransaction(StoreTransactionQuery),
     SetHead(SetHeadQuery),
     ResetCursors(ResetCursorsQuery),
     UpdateCursors(UpdateCursorsQuery),
@@ -141,7 +149,7 @@ pub struct Executor<'c, P: Provider + Sync + Send + 'static> {
     // Queries should use `transaction` instead of `pool`
     // This `pool` is only used to create a new `transaction`
     pool: Pool<Sqlite>,
-    transaction: Transaction<'c, Sqlite>,
+    transaction: SqlxTransaction<'c, Sqlite>,
     publish_queue: Vec<BrokerMessage>,
     rx: UnboundedReceiver<QueryMessage>,
     shutdown_rx: Receiver<()>,
@@ -451,6 +459,48 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> {
                     self.publish_queue.push(BrokerMessage::SetHead(cursor.clone()));
                 }
             }
+            QueryType::StoreTransaction(store_transaction) => {
+                let row = query.fetch_one(&mut **tx).await.with_context(|| {
+                    format!(
+                        "Failed to execute query: {:?}, args: {:?}",
+                        query_message.statement, query_message.arguments
+                    )
+                })?;
+                let mut transaction = Transaction::from_row(&row)?;
+
+                for contract_address in &store_transaction.contract_addresses {
+                    sqlx::query(
+                        "INSERT OR IGNORE INTO transaction_contract (transaction_hash, \
+                         contract_address) VALUES (?, ?)",
+                    )
+                    .bind(&transaction.transaction_hash)
+                    .bind(felt_to_sql_string(contract_address))
+                    .execute(&mut **tx)
+                    .await?;
+                }
+
+                // Store each call in the transaction_calls table
+                for call in &store_transaction.calls {
+                    sqlx::query(
+                        "INSERT OR IGNORE INTO transaction_calls (transaction_hash, \
+                         contract_address, entrypoint, calldata, call_type, caller_address) \
+                         VALUES (?, ?, ?, ?, ?, ?)",
+                    )
+                    .bind(&transaction.transaction_hash)
+                    .bind(felt_to_sql_string(&call.contract_address))
+                    .bind(call.entrypoint.clone())
+                    .bind(felts_to_sql_string(&call.calldata))
+                    .bind(call.call_type.to_string())
+                    .bind(felt_to_sql_string(&call.caller_address))
+                    .execute(&mut **tx)
+                    .await?;
+                }
+
+                transaction.contract_addresses = store_transaction.contract_addresses;
+                transaction.calls = store_transaction.calls;
+
+                self.publish_queue.push(BrokerMessage::Transaction(transaction));
+            }
             QueryType::SetEntity(entity) => {
                 let row = query.fetch_one(&mut **tx).await.with_context(|| {
                     format!(
@@ -901,5 +951,6 @@ fn send_broker_message(message: BrokerMessage) {
         BrokerMessage::EventEmitted(event) => SimpleBroker::publish(event),
         BrokerMessage::TokenRegistered(token) => SimpleBroker::publish(token),
         BrokerMessage::TokenBalanceUpdated(token_balance) => SimpleBroker::publish(token_balance),
+        BrokerMessage::Transaction(transaction) => SimpleBroker::publish(transaction),
     }
 }
diff --git a/crates/torii/sqlite/src/lib.rs b/crates/torii/sqlite/src/lib.rs
index 9ea33558a0..af77328ab7 100644
--- a/crates/torii/sqlite/src/lib.rs
+++ b/crates/torii/sqlite/src/lib.rs
@@ -10,11 +10,12 @@ use dojo_types::schema::{Struct, Ty};
 use dojo_world::config::WorldMetadata;
 use dojo_world::contracts::abigen::model::Layout;
 use dojo_world::contracts::naming::compute_selector_from_names;
-use executor::EntityQuery;
+use executor::{EntityQuery, StoreTransactionQuery};
 use sqlx::{Pool, Sqlite};
-use starknet::core::types::{Event, Felt, InvokeTransaction, Transaction};
+use starknet::core::types::{Event, Felt};
 use starknet_crypto::poseidon_hash_many;
 use tokio::sync::mpsc::UnboundedSender;
+use types::ParsedCall;
 use utils::felts_to_sql_string;
 
 use crate::constants::SQL_FELT_DELIMITER;
@@ -534,76 +535,45 @@ impl Sql {
         self.model_cache.model(&selector).await.map_err(|e| e.into())
     }
 
+    #[allow(clippy::too_many_arguments)]
     pub fn store_transaction(
         &mut self,
-        transaction: &Transaction,
+        transaction_hash: Felt,
+        sender_address: Felt,
+        calldata: &[Felt],
+        max_fee: Felt,
+        signature: &[Felt],
+        nonce: Felt,
         block_number: u64,
         contract_addresses: &HashSet<Felt>,
+        transaction_type: &str,
         block_timestamp: u64,
+        calls: &[ParsedCall],
     ) -> Result<()> {
-        let transaction_type = match transaction {
-            Transaction::Invoke(_) => "INVOKE",
-            Transaction::L1Handler(_) => "L1_HANDLER",
-            _ => return Ok(()),
-        };
-
-        let (transaction_hash, sender_address, calldata, max_fee, signature, nonce) =
-            match transaction {
-                Transaction::Invoke(InvokeTransaction::V3(invoke_v3_transaction)) => (
-                    Argument::FieldElement(invoke_v3_transaction.transaction_hash),
-                    Argument::FieldElement(invoke_v3_transaction.sender_address),
-                    Argument::String(felts_to_sql_string(&invoke_v3_transaction.calldata)),
-                    Argument::FieldElement(Felt::ZERO), // has no max_fee
-                    Argument::String(felts_to_sql_string(&invoke_v3_transaction.signature)),
-                    Argument::FieldElement(invoke_v3_transaction.nonce),
-                ),
-                Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => (
-                    Argument::FieldElement(invoke_v1_transaction.transaction_hash),
-                    Argument::FieldElement(invoke_v1_transaction.sender_address),
-                    Argument::String(felts_to_sql_string(&invoke_v1_transaction.calldata)),
-                    Argument::FieldElement(invoke_v1_transaction.max_fee),
-                    Argument::String(felts_to_sql_string(&invoke_v1_transaction.signature)),
-                    Argument::FieldElement(invoke_v1_transaction.nonce),
-                ),
-                Transaction::L1Handler(l1_handler_transaction) => (
-                    Argument::FieldElement(l1_handler_transaction.transaction_hash),
-                    Argument::FieldElement(l1_handler_transaction.contract_address),
-                    Argument::String(felts_to_sql_string(&l1_handler_transaction.calldata)),
-                    Argument::FieldElement(Felt::ZERO), // has no max_fee
-                    Argument::String("".to_string()),   // has no signature
-                    Argument::FieldElement((l1_handler_transaction.nonce).into()),
-                ),
-                _ => return Ok(()),
-            };
-
-        self.executor.send(QueryMessage::other(
+        // Store the transaction in the transactions table
+        self.executor.send(QueryMessage::new(
             "INSERT OR IGNORE INTO transactions (id, transaction_hash, sender_address, calldata, \
              max_fee, signature, nonce, transaction_type, executed_at, block_number) VALUES (?, \
-             ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+             ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *"
                 .to_string(),
             vec![
-                transaction_hash.clone(),
-                transaction_hash.clone(),
-                sender_address,
-                calldata,
-                max_fee,
-                signature,
-                nonce,
+                Argument::FieldElement(transaction_hash),
+                Argument::FieldElement(transaction_hash),
+                Argument::FieldElement(sender_address),
+                Argument::String(felts_to_sql_string(calldata)),
+                Argument::FieldElement(max_fee),
+                Argument::String(felts_to_sql_string(signature)),
+                Argument::FieldElement(nonce),
                 Argument::String(transaction_type.to_string()),
                 Argument::String(utc_dt_string_from_timestamp(block_timestamp)),
                 Argument::String(block_number.to_string()),
             ],
+            QueryType::StoreTransaction(StoreTransactionQuery {
+                contract_addresses: contract_addresses.clone(),
+                calls: calls.to_vec(),
+            }),
         ))?;
 
-        for contract_address in contract_addresses {
-            self.executor.send(QueryMessage::other(
-                "INSERT OR IGNORE INTO transaction_contract (transaction_hash, contract_address) \
-                 VALUES (?, ?)"
-                    .to_string(),
-                vec![transaction_hash.clone(), Argument::FieldElement(*contract_address)],
-            ))?;
-        }
-
         Ok(())
     }
 
diff --git a/crates/torii/sqlite/src/types.rs b/crates/torii/sqlite/src/types.rs
index db306a3ac6..8a5b4cbd8d 100644
--- a/crates/torii/sqlite/src/types.rs
+++ b/crates/torii/sqlite/src/types.rs
@@ -1,4 +1,5 @@
 use core::fmt;
+use std::collections::HashSet;
 use std::str::FromStr;
 
 use chrono::{DateTime, Utc};
@@ -223,6 +224,51 @@ pub struct ContractCursor {
     pub last_pending_block_contract_tx: Option<String>,
 }
 
+#[derive(Debug, Clone, Deserialize)]
+pub enum CallType {
+    Execute,
+    ExecuteFromOutside,
+}
+
+impl std::fmt::Display for CallType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            CallType::Execute => write!(f, "EXECUTE"),
+            CallType::ExecuteFromOutside => write!(f, "EXECUTE_FROM_OUTSIDE"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct ParsedCall {
+    pub contract_address: Felt,
+    pub entrypoint: String,
+    pub calldata: Vec<Felt>,
+    pub call_type: CallType,
+    pub caller_address: Felt,
+}
+
+#[derive(FromRow, Deserialize, Debug, Clone, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct Transaction {
+    pub id: String,
+    pub transaction_hash: String,
+    pub sender_address: String,
+    pub calldata: String,
+    pub max_fee: String,
+    pub signature: String,
+    pub nonce: String,
+    pub executed_at: DateTime<Utc>,
+    pub created_at: DateTime<Utc>,
+    pub transaction_type: String,
+    pub block_number: u64,
+
+    #[sqlx(skip)]
+    pub calls: Vec<ParsedCall>,
+    #[sqlx(skip)]
+    pub contract_addresses: HashSet<Felt>,
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 pub struct ModelIndices {
     pub model_tag: String,


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (1)</summary><blockquote>

`165-179`: **Error handling needs improvement in the `get` method.**

Sensei, while the cache implementation is solid, I noticed you're using `unwrap()` on lines 175 and 176 which could cause panics if the provider operations fail. Consider proper error handling instead.


```diff
- let class_hash = self.provider.get_class_hash_at(block_id, contract_address).await.unwrap();
- let class = self.provider.get_class(block_id, class_hash).await.unwrap();
+ let class_hash = self.provider.get_class_hash_at(block_id, contract_address).await?;
+ let class = self.provider.get_class(block_id, class_hash).await?;
```

</blockquote></details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (1)</summary><blockquote>

`565-596`: **Consider extracting outside call parsing into a helper function.**

Ohayo, sensei! The outside call parsing logic is duplicated for both v2 and v3 cases. Consider extracting this into a helper function to avoid code duplication.


```diff
+ fn parse_outside_calls(calldata: &[Felt], calldata_offset: usize, has_v3_formatting: bool) -> Vec<FunctionCall> {
+     let mut outside_calls = vec![];
+     let outside_calls_idx = if has_v3_formatting { calldata_offset + 5 } else { calldata_offset + 4 };
+     let outside_calls_len: usize = calldata[outside_calls_idx].try_into().unwrap_or(0);
+     
+     let offset_start = if has_v3_formatting { calldata_offset + 6 } else { calldata_offset + 5 };
+     
+     for _ in 0..outside_calls_len {
+         let to_offset = offset_start;
+         let selector_offset = to_offset + 1;
+         let calldata_offset = selector_offset + 2;
+         let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap_or(0);
+         
+         let outside_call = FunctionCall {
+             contract_address: calldata[to_offset],
+             entry_point_selector: calldata[selector_offset],
+             calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+         };
+         outside_calls.push(outside_call);
+     }
+     
+     outside_calls
+ }

// Then in the code:
- if call.entry_point_selector == selector!("execute_from_outside_v3") {
-     let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
-     for _ in 0..outside_calls_len {
-         let to_offset = calldata_offset + 6;
-         let selector_offset = to_offset + 1;
-         let calldata_offset = selector_offset + 2;
-         let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
-         let outside_call = FunctionCall {
-             contract_address: calldata[to_offset],
-             entry_point_selector: calldata[selector_offset],
-             calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
-         };
-         outside_calls.push(outside_call);
-     }
- } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
-     // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
-     let outside_calls_len: usize = calldata[calldata_offset + 4].try_into().unwrap();
-     for _ in 0..outside_calls_len {
-         let to_offset = calldata_offset + 5;
-         let selector_offset = to_offset + 1;
-         let calldata_offset = selector_offset + 2;
-         let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
-         let outside_call = FunctionCall {
-             contract_address: calldata[to_offset],
-             entry_point_selector: calldata[selector_offset],
-             calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
-         };
-         outside_calls.push(outside_call);
-     }
+ if call.entry_point_selector == selector!("execute_from_outside_v3") {
+     outside_calls.extend(parse_outside_calls(calldata, calldata_offset, true));
+ } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
+     outside_calls.extend(parse_outside_calls(calldata, calldata_offset, false));
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5b832955ea5e4a04315e495b7830b0190b4824bf and ec9920345069ebd4920aee37fdb7fd844b8e7f25.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `crates/torii/migrations/20250307134638_transaction-calls.sql` (1 hunks)
* `crates/torii/sqlite/src/cache.rs` (2 hunks)
* `crates/torii/sqlite/src/lib.rs` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: ensure-windows
* GitHub Check: test-hurl
* GitHub Check: ensure-wasm
* GitHub Check: dojo-core-test
* GitHub Check: docs
* GitHub Check: clippy
* GitHub Check: dojo-examples-test

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>crates/torii/migrations/20250307134638_transaction-calls.sql (1)</summary>

`1-7`: **LGTM! The migration adds necessary columns for transaction analysis.**

Ohayo, sensei! This migration looks well-structured, adding TEXT columns for storing parsed calls from transaction calldata and outside calls. This aligns perfectly with the PR objective to enhance transaction handling functionality.

</details>
<details>
<summary>crates/torii/sqlite/src/cache.rs (1)</summary>

`154-163`: **Well-structured contract class cache implementation.**

Ohayo, sensei! The `ContractClassCache` struct looks clean and follows good design patterns with proper generics constraints. Nice job using RwLock for thread safety!

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (3)</summary>

`14-15`: **LGTM! Imports updated for the new functionality.**

Ohayo, sensei! The imports look good with the addition of `FunctionCall` and the `selector` macro.

---

`524-532`: **LGTM! Updated transaction field access.**

Sensei, you've correctly modified this section to directly use the calldata fields instead of converting them to strings right away.

---

`602-614`: **LGTM! SQL query updated to handle new transaction data.**

Nice work adding the new columns to the SQL query and properly serializing the calls data as JSON, sensei!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T11:03:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/mapping.rs (1)</summary><blockquote>

`67-71`: **Consider using camelCase for field names!**

Ohayo sensei! The new `FUNCTION_CALL_MAPPING` fields use snake_case (`contract_address`, `entry_point_selector`, `calldata`), but GraphQL conventions and other mappings in this file use camelCase. Consider renaming these fields to maintain consistency:

```diff
-        (Name::new("contract_address"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
-        (Name::new("entry_point_selector"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("contractAddress"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("entryPointSelector"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ec9920345069ebd4920aee37fdb7fd844b8e7f25 and e3bdbb6918239a9bf64bbbae101731084d1a5722.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `crates/torii/graphql/src/constants.rs` (1 hunks)
* `crates/torii/graphql/src/mapping.rs` (3 hunks)
* `crates/torii/graphql/src/object/transaction.rs` (1 hunks)
* `crates/torii/graphql/src/schema.rs` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/constants.rs (1)</summary>

`33-33`: **LGTM! New type name constant follows convention.**

Ohayo! The constant `FUNCTION_CALL_TYPE_NAME` is properly defined and follows the same naming pattern as other type names in this file. Good consistency, sensei!

</details>
<details>
<summary>crates/torii/graphql/src/schema.rs (2)</summary>

`29-29`: **Import looks good!**

Ohayo! The import for `FunctionCallObject` alongside `TransactionObject` is properly structured. Clean change, sensei!

---

`138-138`: **Excellent addition to the objects list!**

The `FunctionCallObject` is properly added to the predefined objects list following the same pattern as other `Basic` objects. This ensures it will be registered in the GraphQL schema correctly.

</details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (3)</summary>

`5-6`: **Import pattern looks good!**

Ohayo! The constant imports are well-organized with the addition of `FUNCTION_CALL_TYPE_NAME`. Nicely done, sensei!

---

`7-7`: **Proper mapping import!**

Adding the import for `FUNCTION_CALL_MAPPING` follows the established patterns. Clean addition, sensei!

---

`10-25`: **New FunctionCallObject implementation looks good!**

Ohayo! The implementation of `FunctionCallObject` follows the existing pattern of other objects in this codebase. The naming is consistent with GraphQL conventions using camelCase for "functionCall".

</details>
<details>
<summary>crates/torii/graphql/src/mapping.rs (2)</summary>

`7-7`: **Import addition is correct!**

Ohayo! The import for `FUNCTION_CALL_TYPE_NAME` is properly added to the existing imports. Good organization, sensei!

---

`86-93`: **LGTM! New fields for transaction calls look good!**

Ohayo! The additions of `calls` and `outsideCalls` fields to the `TRANSACTION_MAPPING` correctly use the `FUNCTION_CALL_MAPPING` for their nested types. The naming uses camelCase which follows GraphQL conventions. Excellent work, sensei!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T01:27:14Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/lib.rs (2)</summary><blockquote>

`582-597`: _⚠️ Potential issue_

**Add safeguards against out-of-bounds access.**

Sensei, this section of code calculates various offsets and accesses elements by index without verifying array bounds first. This could lead to panics if called with unexpected data.



Consider adding checks before accessing array elements:

```diff
  for _ in 0..calls_len {
      let to_offset = offset + 1;
      let selector_offset = to_offset + 1;
      let calldata_offset = selector_offset + 2;
+     
+     if selector_offset + 1 >= calldata.len() {
+         break; // or return an error
+     }
      let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+     
+     if calldata_offset + calldata_len > calldata.len() {
+         break; // or return an error
+     }

      let call = FunctionCall {
          contract_address: calldata[to_offset],
          entry_point_selector: calldata[selector_offset],
          calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
      };
```

---

`582-634`: _⚠️ Potential issue_

**Transaction calldata parsing logic needs better error handling.**

Ohayo, sensei! The transaction parsing logic is intricate, but I'm concerned about the error handling. You're using `unwrap()` for array conversions and accessing elements without bounds checking, which could lead to runtime panics.



```diff
- let calls_len: usize = calldata[0].try_into().unwrap();
+ let calls_len: usize = calldata.get(0)
+     .ok_or_else(|| anyhow!("Calldata is empty"))?
+     .try_into()
+     .map_err(|_| anyhow!("Failed to parse calls length"))?;

// Add similar error handling for other unwraps and array accesses
```

Also, add checks before accessing array indices to ensure they're within bounds:

```diff
+ if calldata.len() <= selector_offset + 1 {
+     return Err(anyhow!("Calldata too short for selector_offset"));
+ }
  let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+ if calldata.len() < calldata_offset + calldata_len {
+     return Err(anyhow!("Calldata too short for calldata_len"));
+ }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/lib.rs (1)</summary><blockquote>

`599-614`: **Replace magic numbers with named constants.**

Ohayo! The code uses magic numbers like `calldata_offset + 5` for offsets, making it difficult to understand the structure of the calldata. Consider defining named constants for these offsets to improve readability.

```diff
+     // Offsets in execute_from_outside_v3 calldata
+     const OUTSIDE_CALLS_LEN_OFFSET_V3: usize = 5;
+     const OUTSIDE_CALLS_START_OFFSET_V3: usize = 6;

      if call.entry_point_selector == selector!("execute_from_outside_v3") {
-         let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
+         let outside_calls_len: usize = calldata[calldata_offset + OUTSIDE_CALLS_LEN_OFFSET_V3].try_into().unwrap();
          for _ in 0..outside_calls_len {
-             let to_offset = calldata_offset + 6;
+             let to_offset = calldata_offset + OUTSIDE_CALLS_START_OFFSET_V3;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 30423c9d979416853ddd399c41c2d485746b06c8 and 270235f91c08ddf8ecec0dcb86b6b9ac9e32b5b2.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `crates/torii/migrations/20250317023025_transaction_calls.sql` (1 hunks)
* `crates/torii/sqlite/src/lib.rs` (3 hunks)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>crates/torii/migrations/20250317023025_transaction_calls.sql (2)</summary>

`1-9`: **Well-structured table design with appropriate constraints.**

The table structure is well designed with the appropriate foreign key constraint linking transaction calls to transactions. The default value for `call_type` is a nice touch to maintain consistent data.

---

`11-14`: **Good indexing strategy for query optimization.**

You've created indexes on all the important columns that will be used in WHERE clauses. This will ensure optimal query performance when filtering by transaction hash, contract address, entry point selector, or call type.

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (6)</summary>

`14-15`: **LGTM! Appropriate imports added.**

The additions of `FunctionCall` from `starknet::core::types` and the `selector` macro are necessary for the new transaction call parsing functionality.

---

`524-525`: **Direct calldata handling improves data integrity.**

Ohayo, sensei! By passing the raw calldata directly rather than converting it to a string representation, you're preserving the original data structure for later parsing. This is a good improvement for data integrity.



Also applies to: 532-533, 540-541

---

`558-559`: **Good, still using string representation for storage.**

While you're processing the raw calldata for the call parsing, you correctly maintain the string representation for the main transactions table. This ensures backward compatibility with existing queries.

---

`577-580`: **Clean control flow for transaction type filtering.**

The early return for non-INVOKE transactions keeps the code clean and avoids unnecessary processing.

---

`637-650`: **LGTM! Clean insertion of transaction calls.**

The insertion of call data into the transaction_calls table is well-implemented with proper SQL queries and argument handling.

---

`652-666`: **LGTM! Good differentiation between call types.**

Setting 'EXECUTE_FROM_OUTSIDE' as the call_type for outside calls helps distinguish them from regular calls in queries, which is valuable for filtering and analysis.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:45:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/lib.rs (2)</summary><blockquote>

`582-587`: _🛠️ Refactor suggestion_

**Add error handling for calldata parsing**

Sensei, the current implementation uses `unwrap()` which could panic if the calldata is malformed. Consider adding proper error handling here.



```diff
-let calls_len: usize = calldata[0].try_into().unwrap();
+let calls_len: usize = calldata.get(0)
+    .ok_or_else(|| anyhow!("Calldata is empty"))?
+    .try_into()
+    .map_err(|_| anyhow!("Failed to parse calls length"))?;
```

---

`588-598`: _⚠️ Potential issue_

**Add safeguards against out-of-bounds access**

Ohayo, sensei! This section calculates various offsets and accesses elements without verifying array bounds, which could lead to panics with unexpected data.



```diff
for _ in 0..calls_len {
    let to_offset = offset + 1;
    let selector_offset = to_offset + 1;
    let calldata_offset = selector_offset + 2;
+     
+    if selector_offset + 1 >= calldata.len() {
+        return Err(anyhow!("Calldata too short for selector_offset"));
+    }
    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+     
+    if calldata_offset + calldata_len > calldata.len() {
+        return Err(anyhow!("Calldata too short for calldata_len"));
+    }

    let call = FunctionCall {
        contract_address: calldata[to_offset],
        entry_point_selector: calldata[selector_offset],
        calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
    };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/mapping.rs (1)</summary><blockquote>

`89-96`: **Ohayo sensei! Confirm consistent usage of these new fields.**
Adding both `calls` and `outsideCalls` is clear, but be sure the rest of the code—like resolvers and database queries—handles them in a way that clearly differentiates the two. If usage might overlap, consider a more descriptive naming convention.

</blockquote></details>
<details>
<summary>crates/torii/migrations/20250317023025_transaction_calls.sql (1)</summary><blockquote>

`12-15`: **Ohayo sensei! Indexes look solid.**
If queries frequently combine these columns, consider multi-column indexes in a future iteration to improve performance for more complex queries.

</blockquote></details>
<details>
<summary>crates/torii/graphql/src/query/data.rs (2)</summary><blockquote>

`35-41`: **Ohayo sensei! Comprehensive join configuration struct.**
In the future, you might add validations (e.g., verifying `on_condition` syntax) to catch potential errors early.

---

`72-106`: **Ohayo sensei! Good unified approach to fetch a single row with joins.**
1. Watch out for SQL injection: the dynamic string interpolation is fine here if all user inputs are validated.  
2. The pipeline flagged formatting issues at lines 82 and 95. Please run `rustfmt` to clear them up.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 82-82: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 95-95: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

</blockquote></details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (2)</summary><blockquote>

`156-159`: **Improve error handling for pagination parameters**

Sensei, you're using `unwrap_or` and `unwrap_or_else` for pagination parameters, which is good for providing defaults. However, it would be even better to validate these parameters to ensure they're within reasonable ranges.


```diff
-let limit: u64 = extract(ctx.args.as_index_map(), "limit").unwrap_or(50);
-let offset: u64 = extract(ctx.args.as_index_map(), "offset").unwrap_or(0);
+let limit: u64 = extract(ctx.args.as_index_map(), "limit").unwrap_or(50).min(100); // Cap at 100
+let offset: u64 = extract(ctx.args.as_index_map(), "offset").unwrap_or(0);

-let order_by: String = extract(ctx.args.as_index_map(), "orderBy").unwrap_or_else(|_| "executed_at".to_string());
-let order_direction: String = extract(ctx.args.as_index_map(), "orderDirection").unwrap_or_else(|_| "DESC".to_string());
+// Validate order_by is a valid column name to prevent SQL injection
+let order_by: String = {
+    let column = extract(ctx.args.as_index_map(), "orderBy").unwrap_or_else(|_| "executed_at".to_string());
+    match column.as_str() {
+        "executed_at" | "transaction_hash" | "sender_address" | "block_number" => column,
+        _ => "executed_at".to_string(), // Default to a safe column if invalid
+    }
+};
+
+// Validate order_direction is either ASC or DESC
+let order_direction: String = {
+    let direction = extract(ctx.args.as_index_map(), "orderDirection").unwrap_or_else(|_| "DESC".to_string());
+    match direction.as_str() {
+        "ASC" | "DESC" => direction,
+        _ => "DESC".to_string(), // Default to DESC if invalid
+    }
+};
```

---

`1-213`: **Fix Rust formatting issues**

Ohayo, sensei! The pipeline has flagged multiple Rust formatting issues. Please run `rustfmt` on this file to resolve these formatting issues.


```shell
rustfmt crates/torii/graphql/src/object/transaction.rs
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 1-1: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 8-8: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 63-63: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 72-72: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 94-94: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 103-103: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 113-113: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 122-122: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 151-151: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 270235f91c08ddf8ecec0dcb86b6b9ac9e32b5b2 and 88db0a8032bf42cdede3b6241d0309d7b558b897.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `crates/torii/graphql/src/constants.rs` (2 hunks)
* `crates/torii/graphql/src/mapping.rs` (3 hunks)
* `crates/torii/graphql/src/object/mod.rs` (2 hunks)
* `crates/torii/graphql/src/object/transaction.rs` (2 hunks)
* `crates/torii/graphql/src/query/data.rs` (2 hunks)
* `crates/torii/migrations/20250317023025_transaction_calls.sql` (1 hunks)
* `crates/torii/sqlite/src/lib.rs` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* crates/torii/graphql/src/constants.rs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

<details>
<summary>crates/torii/graphql/src/object/mod.rs</summary>

[error] 28-28: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 35-35: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 271-271: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs</summary>

[warning] 580-580: Rust formatting issues detected. Please run 'rustfmt' to format the code.

---

[warning] 669-669: Rust formatting issues detected. Please run 'rustfmt' to format the code.

---

[warning] 686-686: Rust formatting issues detected. Please run 'rustfmt' to format the code.

</details>
<details>
<summary>crates/torii/graphql/src/query/data.rs</summary>

[error] 82-82: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 95-95: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs</summary>

[error] 1-1: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 8-8: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 63-63: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 72-72: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 94-94: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 103-103: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 113-113: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 122-122: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 151-151: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/mapping.rs (1)</summary>

`7-9`: **Ohayo sensei! Nice addition of the new imports.**
They align well with the rest of the file’s constants.

</details>
<details>
<summary>crates/torii/graphql/src/object/mod.rs (2)</summary>

`31-31`: **Ohayo sensei! Good import alignment.**
These additional imports are consistent with the new join-based functionality introduced in the codebase.

---

`35-35`: **Ohayo sensei! JoinConfig import looks correct.**
Everything is properly set up to handle advanced join scenarios.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 35-35: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

</details>
<details>
<summary>crates/torii/migrations/20250317023025_transaction_calls.sql (1)</summary>

`1-10`: **Ohayo sensei! Double-check the foreign key referencing.**
`transaction_hash` references `transactions(id)`—verify that the `transactions` table indeed uses `id` as a primary key for these hashes. If the primary key in `transactions` is actually `transaction_hash`, point your foreign key to the matching column.

</details>
<details>
<summary>crates/torii/graphql/src/query/data.rs (2)</summary>

`43-49`: **Ohayo sensei! Creative approach for unsupported SQLite joins.**
Mapping `RIGHT JOIN` and `FULL JOIN` to `LEFT JOIN` is practical. Just ensure the rest of the code and queries handle potential data disparities if you rely on right/full semantics.

---

`51-60`: **Ohayo sensei! The `as_sql` method is straightforward.**
If you add more join types or need more robust cross-database support, consider an adapter pattern for better maintainability.

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (6)</summary>

`14-15`: **New imports for improved code structure**

Ohayo, sensei! Good job adding the `FunctionCall` type to the imports and including the `selector` macro. This properly organizes the dependencies needed for the new transaction call parsing functionality.

---

`524-540`: **Calldata handling improvement**

Nice optimization to pass calldata references directly instead of converting them to strings prematurely. This approach is more efficient and preserves the original data structure for proper parsing.

---

`577-580`: **Early return for non-INVOKE transactions**

Good pattern to check transaction type and return early if processing isn't needed. This makes the code path clearer and more efficient.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[warning] 580-580: Rust formatting issues detected. Please run 'rustfmt' to format the code.

</details>

</details>

---

`600-631`: **Refactor duplicated outside call parsing logic**

The logic for parsing outside calls is duplicated between v2 and v3 handling with only small differences in offsets. This reduces maintainability.



```diff
+fn parse_outside_calls(
+    calldata: &[Felt],
+    calldata_offset: usize,
+    calls_len_offset: usize,
+    calls_start_offset: usize,
+) -> Result<Vec<(Felt, FunctionCall)>> {
+    let mut outside_calls = Vec::new();
+    let outside_calls_len: usize = calldata.get(calldata_offset + calls_len_offset)
+        .ok_or_else(|| anyhow!("Calldata too short"))?
+        .try_into()
+        .map_err(|_| anyhow!("Failed to parse outside calls length"))?;
+    
+    let mut offset = 0;
+    for _ in 0..outside_calls_len {
+        let to_offset = calldata_offset + calls_start_offset + offset;
+        let selector_offset = to_offset + 1;
+        let calldata_offset = selector_offset + 2;
+        
+        if selector_offset + 1 >= calldata.len() {
+            return Err(anyhow!("Calldata too short for outside call selector_offset"));
+        }
+        let calldata_len: usize = calldata[selector_offset + 1].try_into()
+            .map_err(|_| anyhow!("Failed to parse outside call calldata length"))?;
+        
+        if calldata_offset + calldata_len > calldata.len() {
+            return Err(anyhow!("Calldata too short for outside call calldata_len"));
+        }
+        
+        let outside_call = FunctionCall {
+            contract_address: calldata[to_offset],
+            entry_point_selector: calldata[selector_offset],
+            calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+        };
+        outside_calls.push((calldata[0], outside_call));
+        offset += 3 + calldata_len;
+    }
+    
+    Ok(outside_calls)
+}

if call.entry_point_selector == selector!("execute_from_outside_v3") {
-    let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
-    for _ in 0..outside_calls_len {
-        // ... duplicated logic ...
-    }
+    outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 5, 6)?);
} else if call.entry_point_selector == selector!("execute_from_outside_v2") {
-    // ... duplicated logic ...
+    outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 4, 5)?);
}
```

---

`637-652`: **Good separation of transaction call storage**

Ohayo! I like how you've separated the storage of the main calls into their own loop. This improves code readability and maintainability.


Some minor formatting issues were flagged in the pipeline. Remember to run `rustfmt` on the final code.

---

`654-669`: **Good handling of outside calls with appropriate call type**

Nice work differentiating between regular calls and outside calls by using the "EXECUTE_FROM_OUTSIDE" call type. This will be helpful for filtering and querying later.


Remember to fix the formatting issues flagged by the pipeline by running `rustfmt`.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[warning] 669-669: Rust formatting issues detected. Please run 'rustfmt' to format the code.

</details>

</details>

</details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (3)</summary>

`1-14`: **Updated imports for new functionality**

Ohayo, sensei! Good work organizing all necessary imports for the new GraphQL functionality. The inclusion of HashMap and Row imports will be useful for the new query processing.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 1-1: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 8-8: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

---

`15-30`: **Well-structured FunctionCallObject implementation**

Nice job implementing the new `FunctionCallObject` struct with clear method names and references to constants. This follows the same pattern as other GraphQL objects in your system.

---

`49-57`: **Updated resolvers to include new function call methods**

Good refactoring to include the new resolver methods for transactions with calls. This maintains a clean API structure.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T09:39:30Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/lib.rs (3)</summary><blockquote>

`587-598`: _⚠️ Potential issue_

**Implement bounds checking for calldata access.**

Ohayo, sensei! This section is still vulnerable to out-of-bounds accesses, just like in the previous review. The code accesses array elements without verifying they exist first.



```diff
  for _ in 0..calls_len {
      let to_offset = offset + 1;
      let selector_offset = to_offset + 1;
      let calldata_offset = selector_offset + 2;
+     
+     if to_offset >= calldata.len() || selector_offset >= calldata.len() {
+         break; // or return an error
+     }
      let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+     
+     if calldata_offset + calldata_len > calldata.len() {
+         break; // or return an error
+     }

      let call = FunctionCall {
          contract_address: calldata[to_offset],
          entry_point_selector: calldata[selector_offset],
          calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
      };
```

---

`600-631`: _🛠️ Refactor suggestion_

**Refactor duplicated outside call parsing logic.**

Ohayo, sensei! There's significant duplication between the parsing logic for v2 and v3 outside calls. This is the same issue noted in the previous review.



Extract the common logic into a helper function:

```diff
+     fn parse_outside_calls(
+         calldata: &[Felt],
+         calldata_offset: usize,
+         calls_len_offset: usize,
+         calls_start_offset: usize,
+     ) -> Vec<(Felt, FunctionCall)> {
+         let mut outside_calls = Vec::new();
+         let outside_calls_len: usize = calldata[calldata_offset + calls_len_offset].try_into().unwrap();
+         
+         let mut offset = 0;
+         for _ in 0..outside_calls_len {
+             let to_offset = calldata_offset + calls_start_offset + offset;
+             let selector_offset = to_offset + 1;
+             let calldata_offset = selector_offset + 2;
+             let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+             
+             let outside_call = FunctionCall {
+                 contract_address: calldata[to_offset],
+                 entry_point_selector: calldata[selector_offset],
+                 calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+             };
+             outside_calls.push((call.contract_address, outside_call));
+             offset += 3 + calldata_len;
+         }
+         
+         outside_calls
+     }

      if call.entry_point_selector == selector!("execute_from_outside_v3") {
-         let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
-         for _ in 0..outside_calls_len {
-             // ... duplicated logic ...
-         }
+         outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 5, 6));
      } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
-         // ... duplicated logic ...
+         outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 4, 5));
      }
```

---

`582-635`: _🛠️ Refactor suggestion_

**Improve error handling in transaction calls parsing.**

Ohayo, sensei! The transaction parsing logic needs better error handling. You're using `unwrap()` for conversions and not handling potential errors.



```diff
- let calls_len: usize = calldata[0].try_into().unwrap();
+ let calls_len: usize = calldata.get(0)
+     .ok_or_else(|| anyhow!("Calldata is empty"))?
+     .try_into()
+     .map_err(|_| anyhow!("Failed to parse calls length"))?;

// Similarly for other unwraps:
- let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+ let calldata_len: usize = calldata.get(selector_offset + 1)
+     .ok_or_else(|| anyhow!("Missing calldata length"))?
+     .try_into()
+     .map_err(|_| anyhow!("Failed to parse calldata length"))?;
```

This would make the function return meaningful errors instead of panicking.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 88db0a8032bf42cdede3b6241d0309d7b558b897 and 701d4ccb4023a8c6f3a81fd274285259fdf4eea6.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `crates/torii/sqlite/src/lib.rs` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

<details>
<summary>crates/torii/sqlite/src/lib.rs</summary>

[error] 580-580: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 669-669: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 686-686: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/lib.rs (4)</summary>

`14-15`: **Added the new FunctionCall import and selector! macro.**

Ohayo, sensei! I see you've added the `FunctionCall` type to your imports and included the `selector!` macro. These additions align perfectly with your new transaction call parsing feature.

---

`524-540`: **Improved parameter handling for calldata.**

Nice improvement here! You've changed from converting the calldata to a string to directly passing it as a reference, which is much more efficient for the parsing approach you're implementing.

---

`582-586`: **Added variables for tracking function calls.**

You're correctly initializing the needed data structures for parsing and storing both direct calls and outside calls.

---

`638-652`: **Good implementation of transaction calls storage.**

The SQLite insertion for regular function calls looks good. You've correctly mapped all necessary fields and used appropriate types.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T09:53:42Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 701d4ccb4023a8c6f3a81fd274285259fdf4eea6 and 094bdd8a91b91c8b4d9ee11bc28ae07de9216d91.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `crates/torii/indexer/src/engine.rs` (5 hunks)
* `crates/torii/indexer/src/processors/mod.rs` (3 hunks)
* `crates/torii/indexer/src/processors/store_transaction.rs` (2 hunks)
* `crates/torii/migrations/20250317023025_transaction_calls.sql` (1 hunks)
* `crates/torii/sqlite/src/cache.rs` (2 hunks)
* `crates/torii/sqlite/src/lib.rs` (3 hunks)
* `crates/torii/sqlite/src/types.rs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* crates/torii/migrations/20250317023025_transaction_calls.sql

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/types.rs (2)</summary>

`230-243`: **Ohayo! Clean implementation of CallType enum.**

The CallType enum with Display implementation follows best practices and provides a clear way to distinguish between different types of contract calls.

---

`245-252`: **Well-structured ParsedCall struct, sensei!**

This struct provides all the necessary fields to represent a contract call in a structured way. Good job including the caller_address which will be helpful for call attribution.

</details>
<details>
<summary>crates/torii/indexer/src/engine.rs (6)</summary>

`26-26`: **Ohayo! Adding ContractClassCache import.**

This import properly brings in the new caching functionality we're implementing.

---

`233-233`: **Nice field addition for caching, sensei!**

Adding the contract_class_cache field to the Engine struct will help optimize transaction processing by avoiding redundant contract class retrievals.

---

`260-261`: **Clean provider initialization.**

Wrapping the provider in Arc directly without redundant code.

---

`265-266`: **Using cloned provider consistently.**

Good practice to use the already Arc-wrapped provider.

---

`278-279`: **Proper cache initialization, sensei!**

Correctly initializing the contract_class_cache with the provider.

---

`779-780`: **Passing cache reference to processor.**

Correctly passing the contract class cache to the transaction processor.

</details>
<details>
<summary>crates/torii/indexer/src/processors/mod.rs (4)</summary>

`2-2`: **Adding Arc import for shared caching.**

Good addition of the Arc import needed for the contract class cache.

---

`9-9`: **Importing the cache module.**

Proper import of the ContractClassCache component.

---

`95-95`: **Adding Debug trait bound, nice work sensei!**

Adding std::fmt::Debug trait bound will help with debugging issues related to transaction processing.

---

`106-107`: **Adding contract_class_cache parameter to the process method.**

This change enables the TransactionProcessor to access cached contract class data, which will help with parsing and interpreting transaction calls.

</details>
<details>
<summary>crates/torii/sqlite/src/cache.rs (1)</summary>

`157-166`: **Ohayo! Well-designed ContractClassCache struct.**

The cache implementation with RwLock provides thread-safe access to cached contract classes, which is important in an asynchronous environment.

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (3)</summary>

`14-15`: **Ohayo sensei, imports look good!**
No issues spotted.  



Also applies to: 18-18

---

`546-551`: **Ohayo sensei, storing associated contracts looks good!**
This insertion uses two placeholders for two arguments, properly matching columns.

---

`553-566`: **Ohayo sensei, storing calls in `transaction_calls` is well-structured!**
The placeholders match the arguments. No issues spotted.

</details>
<details>
<summary>crates/torii/indexer/src/processors/store_transaction.rs (5)</summary>

`5-10`: **Ohayo sensei, the new imports align perfectly with usage!**
They are referenced properly within the file.

---

`29-35`: **Ohayo sensei, good check on transaction type!**
Skipping non-`Invoke` or non-`L1Handler` transactions ensures the rest of the code won’t process irrelevant data.

---

`37-64`: **Ohayo sensei, the expanded match arms are clear, but watch for missing boundary checks.**
If `calldata` is empty in `Transaction::Invoke(V3)`, calling `calldata[0]` triggers a panic.


We previously advised adding out-of-bounds checks for `calldata` usage. Please ensure the code checks `calldata.len()` before indexing.

---

`66-144`: **Ohayo sensei, calls parsing logic looks good but needs boundary checks.**
Parsing `calls_len` and iterating could cause panics if `calldata` lacks sufficient elements. This has already been flagged in a past review.

---

`146-158`: **Ohayo sensei, storing the transaction with associated calls is properly invoked.**
No further concerns here.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T11:12:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>crates/torii/indexer/src/processors/store_transaction.rs (2)</summary><blockquote>

`66-80`: **Good implementation of calldata parsing!**

The implementation correctly extracts each call from the calldata array by calculating offsets and lengths. Using the `contract_class_cache` to fetch contract classes and resolve entrypoint names is a great approach to make the data more readable and useful.

Consider adding error handling for the `try_into()` unwrap operations to gracefully handle malformed calldata.

```diff
-        let calls_len: usize = calldata[0].try_into().unwrap();
+        let calls_len: usize = calldata[0].try_into().unwrap_or_else(|_| {
+            log::warn!("Failed to parse calls_len from calldata, defaulting to 0");
+            0
+        });
```

---

`89-114`: **Ohayo! Nice handling of execute_from_outside_v3 calls, sensei!**

The special handling for `execute_from_outside_v3` calls correctly extracts the nested calls, which is crucial for tracking all transaction interactions. The code is well-structured with clear offset calculations.

However, there's some duplication between this block and the v2 handler. Consider refactoring to reduce duplication.

You could extract the common outside call parsing logic into a helper function to reduce duplication between v2 and v3 handlers:

```rust
fn parse_outside_calls(
    calls: &mut Vec<ParsedCall>,
    calldata: &[Felt],
    base_offset: usize,
    calls_len_offset: usize,
    caller_address: Felt,
    block_number: u64,
    contract_class_cache: &ContractClassCache<P>
) -> Result<(), Error> {
    let outside_calls_len: usize = calldata[base_offset + calls_len_offset].try_into().unwrap();
    let start_offset = base_offset + calls_len_offset + 1;
    
    for _ in 0..outside_calls_len {
        // Extract and push outside calls (existing logic)
        // ...
    }
    
    Ok(())
}
```

Then call it with appropriate offsets for v3 and v2.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 094bdd8a91b91c8b4d9ee11bc28ae07de9216d91 and c62243c1db560b751a012b0cbacdd40f3c3d516c.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `crates/torii/indexer/src/processors/store_transaction.rs` (3 hunks)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>crates/torii/indexer/src/processors/store_transaction.rs (8)</summary>

`5-10`: **Ohayo! Nice job with the imports, sensei!**

The new imports are well-organized and clearly reflect the new functionality being added. The addition of ContractClassCache and related imports aligns perfectly with the PR objective of implementing transaction calls parsing.

---

`19-30`: **Method signature update looks good, sensei!**

Adding the `contract_class_cache` parameter to the process method ensures that contract class information can be retrieved efficiently during transaction processing, which is essential for resolving entrypoint names from selectors.

---

`31-35`: **Transaction type matching is well-implemented!**

The matching of transaction types to identify "INVOKE" and "L1_HANDLER" transactions is clean and provides early return for unsupported types, which is a good programming practice.

---

`37-64`: **Excellent transaction data extraction, sensei!**

You've handled all the different transaction types (InvokeV3, InvokeV1, L1Handler) thoroughly, extracting the necessary fields from each. The pattern matching is clean and comprehensive.

I particularly appreciate the explicit handling of fields that don't exist in certain transaction types (using Felt::ZERO for missing max_fee and empty vector for missing signature).

---

`81-88`: **ParsedCall struct creation is well-structured!**

Creating a `ParsedCall` object with the extracted information is a clean approach. The fields are properly populated with the relevant data from the transaction.

---

`114-140`: **Good handling of execute_from_outside_v2 calls!**

The implementation correctly handles the v2 format which has a different offset structure. The comment explaining the difference is helpful for understanding the code.

As mentioned for the v3 handler, consider refactoring to reduce duplication with the v3 logic.

---

`141-144`: **Logical call addition and offset update, sensei!**

Adding the main call to the calls vector after processing any outside calls is the correct approach. The offset calculation for moving to the next call in the calldata is accurate.

---

`146-158`: **Store transaction call looks good!**

The method signature for `db.store_transaction` has been updated to include the new `calls` parameter, ensuring the parsed call data is stored in the database.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T11:15:51Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>crates/torii/indexer/src/processors/store_transaction.rs (3)</summary><blockquote>

`94-151`: **Be careful with hard-coded offsets in outside calls parsing, sensei.**

While the implementation for both `execute_from_outside_v3` and `execute_from_outside_v2` looks thorough, the hard-coded offsets (like `calldata_offset + 5` and `calldata_offset + 4`) make the code somewhat brittle. Consider extracting these magic numbers into named constants or calculating them based on the calldata structure to improve maintainability.


```diff
- let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
+ // V3 structure: [max_amount, nonce_low, nonce_high, block_timestamp, expiration_timestamp, calls_len, ...]
+ const V3_CALLS_LEN_OFFSET: usize = 5;
+ let outside_calls_len: usize = calldata[calldata_offset + V3_CALLS_LEN_OFFSET].try_into().unwrap();
```

Similar refactoring could be applied to other offsets in the code.

---

`152-156`: **Be mindful of the loop structure and offset calculation, sensei.**

The offset calculation seems accurate but can be error-prone when the calldata structure changes. It would be good to add a comment explaining the calldata structure or the logic behind the calculation for future reference.

---

`94-151`: **Consider refactoring the duplicate code in handling outside calls, sensei.**

The code for handling `execute_from_outside_v3` and `execute_from_outside_v2` is very similar with only slight differences in offsets. This could be refactored into a helper function to reduce duplication and improve maintainability.


```rust
fn process_outside_calls(
    calldata: &[Felt],
    base_offset: usize,
    calls_len_offset: usize,
    start_offset: usize,
    block_number: u64,
    caller_address: Felt,
    contract_class_cache: &ContractClassCache<P>,
    calls: &mut Vec<ParsedCall>,
) -> Result<(), Error> {
    let outside_calls_len: usize = calldata[base_offset + calls_len_offset].try_into().unwrap();
    for _ in 0..outside_calls_len {
        let to_offset = base_offset + start_offset;
        let selector_offset = to_offset + 1;
        // Rest of the implementation...
    }
    Ok(())
}

// Then call it like:
if call.entrypoint == "execute_from_outside_v3" {
    process_outside_calls(calldata, calldata_offset, 5, 6, block_number, call.contract_address, contract_class_cache, &mut calls)?;
} else if call.entrypoint == "execute_from_outside_v2" {
    process_outside_calls(calldata, calldata_offset, 4, 5, block_number, call.contract_address, contract_class_cache, &mut calls)?;
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 064a74f951c9bb0435de0ad77abbe10931b95f1c and dc0d65798d4739cf6cd06c83ca92301497ec4c4d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `crates/torii/indexer/src/processors/store_transaction.rs` (3 hunks)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>crates/torii/indexer/src/processors/store_transaction.rs (4)</summary>

`5-7`: **Ohayo! The updated imports and method signature look good.**

The new parameter `contract_class_cache` along with the necessary imports for `ContractClassCache`, `get_entrypoint_name_from_class`, `ParsedCall`, and other types are well organized and match the implementation needs.



Also applies to: 9-10, 19-21, 31-32

---

`33-67`: **Nice match pattern implementation for transaction types, sensei!**

The extraction of common transaction fields is well-structured, with proper handling of different transaction variants. I like how you've organized the code to handle both `InvokeTransaction` (V1 and V3) and `L1Handler` transactions, with appropriate default values for missing fields.

---

`68-93`: **The function call parsing logic is well implemented!**

The implementation correctly handles the calldata structure for invoke transactions, calculating offsets properly and extracting contract addresses and entrypoints. Using the contract class cache to resolve the human-readable entrypoint name is a nice touch for improved debugging and readability.

---

`158-170`: **The updates to db.store_transaction look good!**

The transaction storage function is now called with the parsed calls, which aligns with the purpose of this PR. This allows storing structured function calls instead of just raw calldata.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T14:49:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (1)</summary><blockquote>

`168-195`: **Ohayo sensei!** This `get` method handles caching effectively, but beware of races where multiple tasks might concurrently fetch the same contract address before insertion into the cache. If needed, consider a double-checked locking pattern to avoid redundant provider calls.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dc0d65798d4739cf6cd06c83ca92301497ec4c4d and 92f9ad9046f98560df011b567234d22520f5c871.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `crates/torii/sqlite/src/cache.rs` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (5)</summary>

`2-2`: **Ohayo sensei!** The addition of `Arc` for the provider instance is a solid concurrency choice, ensuring thread-safe usage. Nicely done!

---

`7-11`: **Ohayo sensei!** These new imports neatly expand functionality for StarkNet types and providers, aligning well with the caching logic. Looks great!

---

`156-162`: **Ohayo sensei!** The `ContractClassCache` struct uses `RwLock` to synchronize access to contract classes, which is a well-chosen concurrency approach.

---

`163-167`: **Ohayo sensei!** The constructor for `ContractClassCache` is straightforward and clear. Fantastic job on keeping it simple.

---

`197-268`: **Ohayo sensei!** This function still uses `.unwrap()` when parsing the Sierra ABI (line 224), which could panic on malformed JSON. Please consider gracefully handling or propagating the error.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T02:07:51Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (1)</summary><blockquote>

`164-196`: **Consider verifying cache consistency across concurrent calls.**  
The `get` method properly acquires read locks and falls back to fetching data when missing. However, there might be a brief gap between releasing the read lock and acquiring the write lock where multiple tasks fetch the same class. If performance is sensitive, you can consider a more sophisticated concurrency scheme (like a “double-checked locking” approach) to avoid multiple fetches for the same address.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 92f9ad9046f98560df011b567234d22520f5c871 and 60a908d854bc9997e400f3f9106e740ba5da08fa.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `crates/torii/sqlite/src/cache.rs` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (7)</summary>

`2-2`: **Ohayo sensei! Good addition of `Arc`.**  
This import makes sense for proper reference counting of the provider in the cache.

---

`7-10`: **Ohayo sensei! Imports look solid.**  
These imports from `starknet::core::types` correctly provide what’s needed to handle contract classes, ABIs, and block references.

---

`11-12`: **Ohayo sensei! New utilities are in good use.**  
`get_selector_from_name` and the `Provider` trait align well with your `ContractClassCache` usage.

---

`157-162`: **Ohayo sensei! `ContractClassCache` struct is well-defined.**  
The design of storing `(Felt, ContractClass)` for each address and using an `Arc<P>` provider looks clean and straightforward for concurrent reads.

---

`198-209`: **Ohayo sensei! Nice recursive flattening logic.**  
The `flatten_abi_funcs_recursive` approach is a neat way to handle nested ABI definitions, especially with interfaces.

---

`225-225`: **Ohayo sensei! Avoid `.unwrap()` calls to prevent panics.**  
These lines repeat a past concern: if parsing JSON or deriving selectors fails, the code could panic. Handling these errors gracefully would improve stability.  



Also applies to: 249-249

---

`239-255`: **Ohayo sensei! Legacy ABI handling logic is well-structured.**  
The code correctly identifies function entries based on matching selectors. Consider the same error-handling refinements if `get_selector_from_name` might fail.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T02:42:14Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>crates/torii/indexer/src/processors/store_transaction.rs (2)</summary><blockquote>

`14-44`: **Consider centralizing code for the different transaction data structures.**

Your new `ExecuteCall`, `LegacyExecuteCall`, `ExecuteTransaction`, `LegacyExecuteTransaction`, and `Execute` types look correct. However, keep an eye on duplication between “legacy” and “non-legacy” data structures. If their fields diverge significantly, you are good. If they remain similar, you could factor out common elements into a shared struct or trait to reduce repetitive logic.

---

`101-109`: **Graceful fallback for parse attempts.**

You try the newer `ExecuteTransaction` parse first, then `LegacyExecuteTransaction`. If both fail, you store `None`. Consider logging a warning or returning an informative error instead of silently ignoring a parse failure.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 60a908d854bc9997e400f3f9106e740ba5da08fa and 3a54560cf363586ed5bee5772f1eac41d0e908bc.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `crates/torii/indexer/Cargo.toml` (1 hunks)
* `crates/torii/indexer/src/processors/store_transaction.rs` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>crates/torii/indexer/Cargo.toml (1)</summary>

`17-17`: **Ohayo sensei! Good addition of the `cainome-cairo-serde` workspace dependency.**

This inclusion ensures the derived implementation macros are accessible, streamlining serialization functionality for Cairo types. No issues observed here.

</details>
<details>
<summary>crates/torii/indexer/src/processors/store_transaction.rs (11)</summary>

`50-52`: **Your generic bounds on the Provider trait seem correct.**

Implementing `TransactionProcessor<P>` with `P: Provider + Send + Sync + std::fmt::Debug` is idiomatic for async operations. No immediate concerns here.

---

`62-63`: **Compact parameter set for contract cache usage.**

Adding `contract_class_cache` to the method signature provides direct access for fetching contract class data. This is a clean approach to expand your indexing logic without overcomplicating the function arguments.

---

`64-68`: **Neat check for unsupported transaction types.**

Returning early if the transaction isn’t `Invoke` or `L1Handler` is an efficient guard. No concerns here.

---

`70-98`: **Efficient destructuring of transaction fields.**

This approach is clean; each transaction variant is handled separately, clarifying differences in max fee or signature. Good job, sensei!

---

`99-100`: **Initializing parsed calls vector.**

This is straightforward. No issues here.

---

`110-128`: **Handling the “Execute” variant successfully.**

Looping over each call, fetching the contract class from the cache, and deriving the entrypoint name is a solid approach. The fallback to the hex string is user-friendly. Great job, sensei!

---

`152-182`: **Potential out-of-bounds indexing.**  

At lines 154, 160, etc., you use `call.calldata[...]` without verifying `call.calldata.len()` is sufficient. This was noted in previous reviews. For safety, either ensure minimal length or handle errors gracefully.

---

`183-212`: **Bounds checks repeated for “execute_from_outside_v2”.**

Similar to the v3 flow, lines 184, 190, etc., are at risk of out-of-bounds panics. Consider wrapping in safe `.get()` calls or validating lengths.

---

`216-228`: **Storing the transaction with parsed calls.**

This final storage step appears correct: passing all parameters, including the new `calls` vector. Make sure to handle the possibility of empty or partially-parsed calls if deserialization fails.

---

`4-9`: <details>
<summary>✅ Verification successful</summary>

**Ohayo sensei! Ensure there's no conflict in import usage.**

You're importing `CairoSerde` from two crates: `cainome::cairo_serde_derive::CairoSerde` and `cainome_cairo_serde::CairoSerde`. Confirm each is being used for distinct purposes (macro vs. trait or function) to avoid confusion and potential conflicts.

---

🏁 Script executed:

```shell
#!/bin/bash
# Searching for references to both imports to verify they're each being used.
rg --context 3 'CairoSerde' crates/torii/indexer/src/processors/store_transaction.rs
```

Length of output: 1147

---



</details>

**Ohayo sensei!** After checking, both `CairoSerde` imports are indeed being used as intended. The derive macro on your structs leverages the procedural macro from `cainome::cairo_serde_derive`, while the trait or associated implementations come from `cainome_cairo_serde`. There’s no indication of a conflict in usage. If you feel it might be clearer for future maintainers, consider adding a short inline comment on each import to specify its purpose.

---

`129-150`: <details>
<summary>❓ Verification inconclusive</summary>

**Handling the “Legacy” variant.**

Similar approach to the “Execute” variant, with offset-based call data extraction. This is well-structured. Just be mindful of potential out-of-range slicing in line 142 if `data_offset + data_length` exceeds `execute.calldata.len()`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Searching for legacy call slicing usage to confirm indexes stay in range.
rg --context 3 "execute\.calldata\[call\.data_offset\.\.call\.data_offset \+ call\.data_length\]"
```

Length of output: 99

---



</details>

**Ohayo, Sensei: Verify Slicing Safety in Legacy Execute Calls**

Your handling of the Legacy variant correctly mimics the Execute variant by slicing the calldata. However, please manually verify that the slicing operation  
```rust
execute.calldata[call.data_offset..call.data_offset + call.data_length]
```  
is safe—that is, ensure that `call.data_offset + call.data_length` does not exceed `execute.calldata.len()`. The automated shell search didn't return any output, so a manual check is recommended.

- Confirm that bounds are properly validated or that invalid offsets are appropriately handled.
- If necessary, consider adding runtime checks to avoid potential out-of-range errors.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T05:17:22Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`114-166`: _⚠️ Potential issue_

**Ohayo sensei, watch out for SQL injection in `LIKE` clause!**  
Embedding `{transaction_hash}` directly in the string can be risky. Consider parameterizing the pattern (e.g., using `et.event_id LIKE '%' || ? || '%'`) for safety.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/erc/token_transfer.rs (1)</summary><blockquote>

`263-382`: **Ohayo sensei, commendable modular design!**  
`token_transfer_mapping_from_row` unifies parsing logic and error handling across all token types. However, consider splitting out sub-logic (e.g., metadata parsing) into helper functions for improved readability.

</blockquote></details>
<details>
<summary>crates/torii/graphql/src/object/erc/token_balance.rs (1)</summary><blockquote>

`319-448`: **Ohayo sensei, robust token balance mapping!**  
`token_balance_mapping_from_row` handles multiple ERC types elegantly. If it grows further, consider extracting metadata parsing into helper methods for clarity.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 68831455f3ec0f55d9bf77ebf612624fc1bf5ec3 and 0dd0bc017080bd8afa4d1d37ac44a7c558ef2afd.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `crates/torii/graphql/src/constants.rs` (2 hunks)
* `crates/torii/graphql/src/object/erc/token_balance.rs` (2 hunks)
* `crates/torii/graphql/src/object/erc/token_transfer.rs` (4 hunks)
* `crates/torii/graphql/src/object/transaction.rs` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* crates/torii/graphql/src/constants.rs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>crates/torii/graphql/src/object/erc/token_transfer.rs (4)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/erc/token_balance.rs (2) (2)</summary>

* `row` (335:335)
* `row` (391:391)

</details>
<details>
<summary>crates/torii/indexer/src/engine.rs (1) (1)</summary>

* `get_transaction_hash_from_event_id` (929:931)

</details>
<details>
<summary>crates/torii/sqlite/src/executor/erc.rs (2) (2)</summary>

* `token_id` (72:72)
* `token_id` (109:109)

</details>
<details>
<summary>crates/torii/sqlite/src/types.rs (1) (1)</summary>

* `from_str` (195:204)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/erc/token_transfer.rs (3)</summary>

`21-21`: **Ohayo sensei, nice import alignment!**  
Bringing in `Erc1155Token` and `Erc721Token` is helpful for handling multiple ERC token types with clarity.

---

`245-253`: **Ohayo sensei, great error-handling approach!**  
Continuing after a failed row transformation ensures partial data is still processed while logging issues for debugging.

---

`394-394`: **Ohayo sensei, good call on making `TransferQueryResultRaw` public!**  
This change improves cross-module accessibility and reuse without duplicating structs.

</details>
<details>
<summary>crates/torii/graphql/src/object/erc/token_balance.rs (2)</summary>

`301-310`: **Ohayo sensei, nice integration of the mapping function!**  
Invoking `token_balance_mapping_from_row` here streamlines the transformation and centralizes error handling.

---

`312-317`: **Ohayo sensei, succinct connection construction!**  
Returning early with the connection object is straightforward and reduces clutter in the calling code.

</details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (3)</summary>

`19-20`: **Ohayo sensei, neat introduction of `CallObject`!**  
Defining a dedicated struct keeps calls organized and flexible for future expansions.

---

`22-38`: **Ohayo sensei, well-structured `BasicObject` implementation!**  
Providing `related_fields` as `None` keeps `CallObject` lean and consistent until additional relationships are needed.

---

`55-55`: **Ohayo sensei, good combination of fields!**  
Exposing `calls` and `tokenTransfers` at the transaction level enhances discoverability of contextual data.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:25:33Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/types.rs (1)</summary><blockquote>

`254-268`: **Consider a numeric type for `block_number` instead of `String`.**

Currently, storing `block_number` as a `String` might introduce overhead in parsing and data validation. Converting it to an integer-based type (e.g., `i64`) could be helpful, if feasible.

</blockquote></details>
<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (1)</summary><blockquote>

`451-476`: **Ohayo sensei, storing transaction data and calls in a single transaction is sound.**

One optimization to consider is a bulk insert if there are many calls, which can improve performance. Otherwise, logic is straightforward and correct.

</blockquote></details>
<details>
<summary>crates/torii/sqlite/src/cache.rs (2)</summary><blockquote>

`157-162`: **Ohayo sensei, consider adding bounds to your cache.**  
Currently, the cache can grow unbounded, potentially leading to large memory usage in production. An eviction or size-limiting strategy could help.

---

`164-196`: **Check handling of multiple block IDs, sensei.**  
Because the cache stores classes keyed only by address, subsequent calls with different `block_id` values for the same address might reuse outdated data. Ensure that this fallback to `BlockTag::Pending` and ignoring block ID variations is intentional for your scenario.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0dd0bc017080bd8afa4d1d37ac44a7c558ef2afd and 2980f7e146ef2ec2f41ba3ec168658da5cfffb21.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `crates/torii/sqlite/src/cache.rs` (2 hunks)
* `crates/torii/sqlite/src/executor/mod.rs` (8 hunks)
* `crates/torii/sqlite/src/lib.rs` (2 hunks)
* `crates/torii/sqlite/src/types.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>crates/torii/sqlite/src/lib.rs (1)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/utils.rs (2) (2)</summary>

* `felts_to_sql_string` (32-35)
* `utc_dt_string_from_timestamp` (28-30)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (19)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/types.rs (2)</summary>

`229-244`: **Ohayo sensei, this enum addition looks great!**

Nice and concise implementation with matching `Display` variants. No issues spotted here.

---

`245-252`: **Ohayo sensei, this struct is clear and straightforward!**

The fields cover all necessary data for a parsed call, and the design is readable.

</details>
<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (9)</summary>

`1-1`: **Ohayo sensei, new import looks fine!**

---

`11-11`: **Ohayo sensei, no concerns about this addition.**

---

`27-27`: **Ohayo sensei, importing `ParsedCall` and `Transaction` is appropriate.**

---

`29-29`: **No issues with these utility imports.**

---

`54-54`: **BrokerMessage variant inclusion looks good!**

---

`108-113`: **Ohayo sensei, this struct effectively captures new transaction data!**

---

`116-116`: **Enum variant addition is consistent with the new struct.**

---

`141-141`: **Renaming to avoid conflicts is a good call.**

---

`874-874`: **Publishing the transaction as a broker message is clearly integrated.**

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (5)</summary>

`13-13`: **Ohayo sensei, this import aligns perfectly with the new transaction logic.**

---

`15-15`: **No issues with this import.**

---

`18-18`: **Importing `ParsedCall` is consistent with usage in `store_transaction`.**

---

`507-520`: **Function signature is cohesive and well-defined.**

The chosen parameters cover all crucial transaction fields.

---

`522-544`: **Ohayo sensei, the query logic is correctly aligned with the arguments.**

The column-argument mapping matches, preventing accidental shifts. Good job!

</details>
<details>
<summary>crates/torii/sqlite/src/cache.rs (3)</summary>

`2-2`: **Ohayo sensei, nice usage of Arc!**  
This import facilitates shared ownership of your provider instance across async tasks.

---

`7-12`: **Ohayo sensei, these imports look solid for contract interactions.**  
They neatly cover the essentials for fetching contract classes and handling provider errors.

---

`198-257`: **Ohayo sensei, `unwrap()` usage here can still cause panics.**  
This duplicates a prior comment regarding unwrapping at lines 225 and 249. Consider graceful error handling or returning `None` if parsing fails.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T03:00:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/erc/token_balance.rs (1)</summary><blockquote>

`326-452`: **Ohayo sensei! Well-structured token mapping function with robust error handling.**

The extracted `token_balance_mapping_from_row` function provides a centralized place for token type conversion logic. The error handling is thorough, especially for metadata parsing. 

One small improvement could be to use more descriptive variable names for clarity in the token_id splitting operations, particularly for ERC721 and ERC1155 sections.

```diff
- let token_id = row.token_id.split(':').collect::<Vec<&str>>();
- if token_id.len() != 2 {
-     return Err(format!("Invalid token_id format: {}", row.token_id));
- }
+ let token_id_parts = row.token_id.split(':').collect::<Vec<&str>>();
+ if token_id_parts.len() != 2 {
+     return Err(format!("Invalid token_id format (expected 'contract:id'): {}", row.token_id));
+ }
+ let contract_part = token_id_parts[0];
+ let id_part = token_id_parts[1];
```

</blockquote></details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`143-145`: **Consider using a prepared statement for consistency**

Ohayo, sensei! While this query isn't vulnerable to SQL injection since it only uses constants, consider using a style consistent with other queries for better maintainability.


```diff
-                    let query = &format!(
-                        "SELECT * FROM {TRANSACTION_CALLS_TABLE} WHERE transaction_hash = ?"
-                    );
+                    let query = &format!(
+                        "SELECT * FROM [{}] WHERE transaction_hash = ?", TRANSACTION_CALLS_TABLE
+                    );
```

This makes the query format consistent with other queries in the codebase using table name in square brackets.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2980f7e146ef2ec2f41ba3ec168658da5cfffb21 and 61102d347d2f09ccbc569eeab65f93a2f1b5d000.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `crates/torii/graphql/src/mapping.rs` (1 hunks)
* `crates/torii/graphql/src/object/erc/token_balance.rs` (2 hunks)
* `crates/torii/graphql/src/object/erc/token_transfer.rs` (4 hunks)
* `crates/torii/graphql/src/object/mod.rs` (2 hunks)
* `crates/torii/graphql/src/object/transaction.rs` (3 hunks)
* `crates/torii/graphql/src/query/data.rs` (2 hunks)
* `crates/torii/sqlite/src/cache.rs` (2 hunks)
* `crates/torii/sqlite/src/executor/mod.rs` (8 hunks)
* `crates/torii/sqlite/src/lib.rs` (2 hunks)
* `crates/torii/sqlite/src/types.rs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* crates/torii/graphql/src/object/erc/token_transfer.rs
* crates/torii/sqlite/src/types.rs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (4)</summary>

<details>
<summary>crates/torii/graphql/src/object/mod.rs (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/query/data.rs (11) (11)</summary>

* `data` (197-197)
* `data` (198-198)
* `data` (223-223)
* `data` (224-224)
* `data` (227-227)
* `data` (228-228)
* `count_rows` (11-26)
* `fetch_multiple_rows` (109-244)
* `fetch_single_row` (62-70)
* `fetch_single_row_with_joins` (72-106)
* `joins` (87-96)

</details>

</blockquote></details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (2)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (2) (2)</summary>

* `new` (166-168)
* `new` (241-267)

</details>
<details>
<summary>crates/torii/sqlite/src/utils.rs (2) (2)</summary>

* `felts_to_sql_string` (32-35)
* `utc_dt_string_from_timestamp` (28-30)

</details>

</blockquote></details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (2)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/erc/token_transfer.rs (8) (8)</summary>

* `token_transfer_mapping_from_row` (264-383)
* `name` (31-33)
* `type_name` (35-37)
* `type_mapping` (39-41)
* `ctx` (57-57)
* `extract` (59-59)
* `row` (289-289)
* `row` (331-331)

</details>
<details>
<summary>crates/torii/graphql/src/object/mod.rs (16) (16)</summary>

* `resolve_many` (321-374)
* `resolve_one` (246-273)
* `name` (46-46)
* `type_name` (49-49)
* `type_mapping` (52-52)
* `ctx` (84-84)
* `ctx` (109-109)
* `ctx` (123-123)
* `ctx` (148-148)
* `ctx` (161-161)
* `ctx` (190-190)
* `ctx` (264-264)
* `ctx` (301-301)
* `ctx` (339-339)
* `extract` (266-266)
* `extract` (303-303)

</details>

</blockquote></details>
<details>
<summary>crates/torii/graphql/src/object/erc/token_balance.rs (8)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/erc/token_transfer.rs (2) (2)</summary>

* `row` (289-289)
* `row` (331-331)

</details>
<details>
<summary>crates/torii/graphql/src/object/erc/erc_token.rs (2) (2)</summary>

* `row` (597-597)
* `row` (639-639)

</details>
<details>
<summary>crates/torii/graphql/src/object/connection/mod.rs (2) (2)</summary>

* `row` (138-138)
* `row` (139-139)

</details>
<details>
<summary>crates/torii/graphql/src/query/mod.rs (7) (7)</summary>

* `row` (123-123)
* `row` (125-125)
* `row` (310-310)
* `row` (312-312)
* `row` (319-320)
* `row` (326-326)
* `e` (67-81)

</details>
<details>
<summary>crates/torii/grpc/src/server/mod.rs (3) (3)</summary>

* `row` (1130-1130)
* `row` (1131-1135)
* `value` (121-121)

</details>
<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (2) (2)</summary>

* `None` (300-300)
* `None` (848-848)

</details>
<details>
<summary>crates/torii/sqlite/src/executor/erc.rs (2) (2)</summary>

* `token_id` (72-72)
* `token_id` (109-109)

</details>
<details>
<summary>crates/torii/sqlite/src/types.rs (1) (1)</summary>

* `from_str` (195-204)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (29)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/mapping.rs (1)</summary>

`67-74`: **Ohayo sensei! Consider using felt-like fields for contract addresses.**

Currently, `contractAddress` and `entrypoint` are typed as strings. If these values represent FELT-252 addresses/selectors, you could store them as numeric or typed FELT strings to maintain consistency across the codebase (like you did in other mappings such as TRANSACTION_MAPPING).

```diff
-        (Name::new("contractAddress"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
-        (Name::new("entrypoint"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("contractAddress"), TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string()))),
+        (Name::new("entrypoint"), TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string()))),
```

</details>
<details>
<summary>crates/torii/graphql/src/object/mod.rs (1)</summary>

`275-318`: **Ohayo sensei! The implementation looks good but consider refactoring out common parts with `resolve_one`.**

The new function `resolve_one_with_joins` largely duplicates logic from `resolve_one`. Consider extracting shared parts (like argument handling or field creation) into helper functions to improve maintainability and reduce potential inconsistencies.

</details>
<details>
<summary>crates/torii/graphql/src/query/data.rs (1)</summary>

`35-60`: **Ohayo sensei! Clean implementation of join types and configuration.**

The `JoinType` enum and `JoinConfig` struct are well-designed and provide a flexible way to configure SQL joins. I appreciate the handling of SQLite's limitations for RIGHT and FULL joins in the `as_sql` method. This makes the API more forgiving for users who might not be aware of SQLite's limitations.

</details>
<details>
<summary>crates/torii/graphql/src/object/erc/token_balance.rs (2)</summary>

`147-159`: **Ohayo sensei! Good refactoring of token balance logic for subscriptions.**

The refactoring to use the centralized `token_balance_mapping_from_row` function improves code maintainability and consistency. The error handling with warning logs is also beneficial for debugging.

---

`307-316`: **Ohayo sensei! Good refactoring of token balance output logic.**

Extracting the token mapping logic into a separate function makes the code more maintainable and easier to understand. The improved error handling with warning logs helps debugging potential issues.

</details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (5)</summary>

`22-41`: **Ohayo! New CallObject implementation looks good!**

The new `CallObject` struct and its implementation of the `BasicObject` trait follow the established pattern in the codebase. The type mappings are properly referenced from constants.

---

`58-60`: **Related fields implementation looks clean, sensei!**

Good addition of both call and token transfer fields to the transaction object, making the GraphQL API more feature-rich.

---

`84-112`: **Nice subscription implementation!**

The subscription field for transactions with optional hash filtering is well implemented. The filter_map pattern allows for efficient filtering of transaction events.

---

`115-131`: **Complete value mapping for transaction fields!**

All the necessary transaction fields are correctly mapped to GraphQL field names with appropriate types.

---

`133-163`: **Good calls field implementation, sensei!**

The implementation fetches and maps transaction calls correctly. The query uses proper parameter binding with the `?` placeholder, which is secure against SQL injection.

</details>
<details>
<summary>crates/torii/sqlite/src/cache.rs (6)</summary>

`2-2`: **Ohayo sensei, this import looks correct.**  
No concerns here.

---

`7-12`: **Ohayo sensei, these new imports are well-organized.**  
Everything seems in order.

---

`157-163`: **Ohayo sensei, nice concurrency setup.**  
Storing `(Felt, ContractClass)` in an `RwLock<HashMap<...>>` is straightforward. Consider memory usage if the cache grows large.

---

`164-168`: **Ohayo sensei, neat constructor for `ContractClassCache`.**  
It cleanly injects the provider and read/write lock.

---

`198-254`: **Ohayo sensei, consider handling `.unwrap()` safely.**  
Calls to `.unwrap()` can panic on invalid JSON or selector mismatch. Returning `None` or an error is safer.  

```diff
- let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi).unwrap();
+ let abi: Vec<AbiEntry> = match serde_json::from_str(&sierra.abi) {
+     Ok(abi) => abi,
+     Err(_) => return None,
+ };

- if get_selector_from_name(&function.name).unwrap() == selector
+ if get_selector_from_name(&function.name).ok()? == selector
```

---

`169-195`: <details>
<summary>❓ Verification inconclusive</summary>

**Ohayo sensei, verify cache key usage with different blocks.**  
The `get` method stores contract classes keyed only by contract address, ignoring `block_id`. This may lead to returning stale data if a contract address differs across blocks.  

Here’s a script to see if references exist regarding multi-block caching strategy:

---

🏁 Script executed:

```shell
#!/bin/bash
rg --context 5 "multi-block" 
```

Length of output: 28

---



</details>

**Ohayo, sensei! Quick heads-up on the caching strategy:**  
The `get` method currently caches contract classes using only the contract address and ignores the `block_id`. This design may lead to serving stale data in cases where a contract’s class could differ between blocks. The grep search for "multi-block" didn't yield any supporting evidence for a multi-block caching strategy elsewhere in the codebase, which suggests this might be an oversight.

- Please double-check whether the intention was to cache solely based on contract address.
- If contract variants across blocks are valid, consider incorporating `block_id` into the cache key to avoid stale data.

</details>
<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (8)</summary>

`1-1`: **Ohayo sensei, this addition looks good!**  
No concerns with importing `HashSet`. It's neatly used for storing distinct contract addresses.

---

`11-11`: **Ohayo sensei, no issues found here!**  
Aliasing the `Transaction` type to `SqlxTransaction` helps avoid naming collisions. Continue.

---

`27-27`: **No immediate concerns with the new imports.**  
Introducing `ParsedCall` is a welcome addition for structured call data handling.

---

`29-29`: **Neat usage of helpers.**  
`felt_to_sql_string` and `felts_to_sql_string` are consistent with the existing code. Good to see them used here.

---

`108-112`: **Ohayo sensei, nice addition of `StoreTransactionQuery`.**  
Encapsulating the set of contract addresses and parsed calls in one struct clarifies the code’s intent. Consider including other relevant transaction metadata if needed in future expansions.

---

`116-116`: **Enum variant addition looks good.**  
Adding `StoreTransaction(StoreTransactionQuery)` is consistent with the rest of the `QueryType` enum usage.

---

`141-141`: **Smooth transition to `SqlxTransaction`.**  
Renaming prevents confusion with your internal `Transaction` type while preserving clarity.

---

`451-489`: **Ohayo sensei, solid handling of the new `StoreTransaction` variant!**  
1. The logic for inserting into `transaction_contract` and `transaction_calls` is cohesive and straightforward.  
2. Using `INSERT OR IGNORE` prevents duplicates, but confirm that ignoring collisions is desired behavior. If updates are ever needed, consider using `INSERT ... ON CONFLICT ... DO UPDATE`.  
3. Fetching and using the newly inserted transaction row is well-managed. The error handling with `.with_context(...)` is helpful for debugging.  
4. Pushing `BrokerMessage::Transaction` to the queue is a good approach for decoupling.  

Looking great overall!  

Do you want to confirm that collisions for the tables `transaction_contract` and `transaction_calls` are never expected to require subsequent updates? If so, verifying no manual updates are needed might be wise.

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (5)</summary>

`13-13`: **Imports look neat.**  
No issues with referencing `StoreTransactionQuery`, `Felt`, and the new `ParsedCall`.  



Also applies to: 15-15, 18-18

---

`509-519`: **Ohayo sensei, good signature for your new function parameters!**  
Explicit fields like `transaction_hash`, `sender_address`, and `nonce` make everything more transparent. This design is more flexible than passing a single `Transaction` object.

---

`521-522`: **Nice comment clarifying the insertion.**  
The purposeful approach of ignoring existing records might be fine if the transaction data won't change.

---

`528-538`: **Double usage of `transaction_hash` for both `id` and `transaction_hash` columns is now aligned.**  
1. The argument count matches the number of columns.  
2. Reusing the same `transaction_hash` for both columns can be intentional if they are effectively the same ID. Confirm this design meets your business requirements.  

Do you want me to check for references to a separate schema where `id` could differ from `transaction_hash`?

---

`539-542`: **Linking `StoreTransactionQuery` in the final step is consistent with your design.**  
You're properly encapsulating the contract addresses and calls in the query. Looks good!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T03:05:30Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`175-199`: _⚠️ Potential issue_

**Potential SQL injection in token transfers query!**

Ohayo, sensei! The SQL query is vulnerable to injection because you're directly interpolating the transaction_hash into the LIKE pattern instead of using a parameterized query.


Fix this by using parameters properly:

```diff
-                        WHERE
-                            et.event_id LIKE '%:{transaction_hash}:%'
+                        WHERE
+                            et.event_id LIKE '%:' || ? || ':%'
```

This ensures the transaction hash is properly escaped and handles any special characters safely.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/types.rs (2)</summary><blockquote>

`230-234`: **Consider deriving more traits for CallType enum**

Ohayo, sensei! The `CallType` enum looks good but could benefit from deriving additional traits for better functionality:

```diff
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub enum CallType {
     Execute,
     ExecuteFromOutside,
 }
```

Adding these traits would enable easy comparison, use in HashMap/HashSet collections, and serialization/deserialization - making the enum more versatile throughout the codebase.

---

`245-252`: **ParsedCall struct could benefit from serialization traits**

Ohayo sensei! The `ParsedCall` struct looks well-structured for representing calls. For consistency with other types in this file, consider adding serialization traits:

```diff
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct ParsedCall {
     pub contract_address: Felt,
     pub entrypoint: String,
     pub calldata: Vec<Felt>,
     pub call_type: CallType,
     pub caller_address: Felt,
 }
```

This would make it easier to serialize/deserialize this struct when needed, especially for API responses or persistence.

</blockquote></details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`204-211`: **Consider optimizing token transfer data processing**

The current approach processes each row individually. For better performance with large result sets, consider using batch processing or parallelization.


```diff
-                    let mut results = Vec::new();
-                    for row in &rows {
-                        let row = TransferQueryResultRaw::from_row(row)?;
-                        let result = token_transfer_mapping_from_row(&row)?;
-                        results.push(FieldValue::owned_any(result));
-                    }
+                    let results = rows.iter()
+                        .map(|row| -> Result<FieldValue, async_graphql::Error> {
+                            let row = TransferQueryResultRaw::from_row(row)?;
+                            let result = token_transfer_mapping_from_row(&row)?;
+                            Ok(FieldValue::owned_any(result))
+                        })
+                        .collect::<Result<Vec<_>, _>>()?;
```

This functional approach is more concise and makes the error handling more explicit.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 74590a212f667e8e6bbcd7a9314e0ca922656ad9 and 2e1aed21985bf00f3620c4f43a49091d69a1d3af.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `crates/torii/graphql/src/mapping.rs` (2 hunks)
* `crates/torii/graphql/src/object/transaction.rs` (3 hunks)
* `crates/torii/sqlite/src/types.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (2)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/erc/token_transfer.rs (8) (8)</summary>

* `token_transfer_mapping_from_row` (264-383)
* `name` (31-33)
* `type_name` (35-37)
* `type_mapping` (39-41)
* `ctx` (57-57)
* `extract` (59-59)
* `row` (289-289)
* `row` (331-331)

</details>
<details>
<summary>crates/torii/graphql/src/object/mod.rs (16) (16)</summary>

* `resolve_many` (321-374)
* `resolve_one` (246-273)
* `name` (46-46)
* `type_name` (49-49)
* `type_mapping` (52-52)
* `ctx` (84-84)
* `ctx` (109-109)
* `ctx` (123-123)
* `ctx` (148-148)
* `ctx` (161-161)
* `ctx` (190-190)
* `ctx` (264-264)
* `ctx` (301-301)
* `ctx` (339-339)
* `extract` (266-266)
* `extract` (303-303)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/types.rs (2)</summary>

`236-243`: **Display implementation looks good**

The `Display` trait implementation is clear and follows the naming convention consistently. Nice work!

---

`254-268`: **Transaction struct implementation looks good**

The `Transaction` struct is well-defined with appropriate fields and trait derivations. The camelCase serialization annotation is consistent with other structs in this file, and the inclusion of `FromRow` makes it compatible with SQLx for database operations.

</details>
<details>
<summary>crates/torii/graphql/src/mapping.rs (2)</summary>

`67-74`: **Ohayo sensei! Consider using felt-like fields for contract addresses.**

Currently, `contractAddress`, `entrypoint`, and `callerAddress` are typed as strings in your new `CALL_MAPPING`. If these values represent FELT-252 addresses/selectors, it would be more consistent to use `TypeRef::named(Primitive::Felt252(None).to_string())` like you've done in other mappings, rather than simple strings.

---

`109-112`: **Looks good! Adding blockNumber to transaction mapping.**

The blockNumber field is properly added to the transaction mapping. Using STRING is appropriate here since block numbers are often represented in both decimal and hex formats in client applications.

</details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (6)</summary>

`22-41`: **Ohayo, the CallObject implementation looks good!**

The BasicObject implementation for CallObject is clean and follows the established pattern in the codebase. It properly references the constants and mappings defined elsewhere.

---

`58-60`: **Good addition of related fields to TransactionObject!**

Adding calls and token transfers as related fields to transactions provides a nice GraphQL structure that allows clients to fetch this data in a single query.

---

`84-112`: **Nice implementation of transaction subscriptions!**

The subscription implementation allows clients to receive real-time updates about transactions, with the option to filter by transaction hash. The filter logic is clean and efficient.

---

`115-130`: **Clean value mapping implementation for transactions.**

This method properly maps all transaction fields to their GraphQL representation. The calldata splitting on "/" is an interesting approach - make sure this matches how calldata is stored in the database.

---

`133-163`: **Ohayo sensei! The calls_field implementation looks good.**

The implementation correctly fetches call data associated with a transaction. You're properly using parameterized queries to prevent SQL injection.

---

`202-202`: **Make sure SQL bindings match query parameters**

The query uses string interpolation for transaction_hash (which is unsafe) but then also tries to bind it as a parameter. When fixing the LIKE clause, ensure you're using the bound parameter correctly.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T04:17:27Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`178-205`: _⚠️ Potential issue_

**SQL injection vulnerability in token_transfers query!**

Ohayo, sensei! The query is vulnerable to SQL injection because you're directly interpolating the transaction_hash into the LIKE pattern instead of using a parameterized query. Additionally, you're binding a parameter on line 205, but there's no corresponding placeholder in the query.



Fix this by using parameters properly:

```diff
                    let query = format!(
                        r#"
                        SELECT 
                            et.id,
                            et.contract_address,
                            et.from_address,
                            et.to_address,
                            et.amount,
                            et.token_id,
                            et.executed_at,
                            t.name,
                            t.symbol,
                            t.decimals,
                            c.contract_type,
                            t.metadata
                        FROM
                            {TOKEN_TRANSFER_TABLE} et
                        JOIN
                            tokens t ON et.token_id = t.id
                        JOIN
                            contracts c ON t.contract_address = c.contract_address
                        WHERE
-                            et.event_id LIKE '%:{transaction_hash}:%'
+                            et.event_id LIKE '%:' || ? || ':%'
                        "#
                    );

                    let rows =
                        sqlx::query(&query).bind(&transaction_hash).fetch_all(&mut *conn).await?;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`115-134`: **Clean implementation of value_mapping**

This helper function efficiently transforms the Transaction struct into a GraphQL-compatible value mapping. The use of `split("/")` on line 123 for calldata works, but consider documenting the expected format to avoid future confusion.


```diff
            (
                Name::new("calldata"),
-                Value::from(transaction.calldata.split("/").collect::<Vec<_>>()),
+                // Split calldata on "/" as it's stored as slash-separated values
+                Value::from(transaction.calldata.split("/").collect::<Vec<_>>()),
            ),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2e1aed21985bf00f3620c4f43a49091d69a1d3af and bdf06681cc11e41d33e46fe35ae45af56ea0d954.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `crates/torii/graphql/src/object/transaction.rs` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (5)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/lib.rs (3) (3)</summary>

* `sqlx` (87-87)
* `sqlx` (175-175)
* `sqlx` (182-182)

</details>
<details>
<summary>crates/torii/graphql/src/object/erc/token_transfer.rs (5) (5)</summary>

* `name` (31-33)
* `type_name` (35-37)
* `type_mapping` (39-41)
* `row` (289-289)
* `row` (331-331)

</details>
<details>
<summary>crates/torii/graphql/src/object/mod.rs (5) (5)</summary>

* `name` (46-46)
* `type_name` (49-49)
* `type_mapping` (52-52)
* `related_fields` (55-57)
* `subscriptions` (216-218)

</details>
<details>
<summary>crates/torii/graphql/src/object/erc/token_balance.rs (6) (6)</summary>

* `name` (37-39)
* `type_name` (41-43)
* `type_mapping` (45-47)
* `subscriptions` (94-168)
* `row` (341-341)
* `row` (396-396)

</details>
<details>
<summary>crates/torii/graphql/src/query/data.rs (1) (1)</summary>

* `list` (291-298)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (5)</summary>

`22-41`: **Good implementation of CallObject struct, ohayo sensei!**

The new `CallObject` structure is well-implemented with all required trait methods. It follows the same pattern as other objects in the codebase, making the code consistent and maintainable.

---

`58-60`: **LGTM! Nice enhancement to related_fields**

Adding related fields for calls and token transfers improves the GraphQL schema by enabling clients to fetch transaction-related data in a single query.

---

`84-113`: **Good implementation of GraphQL subscriptions, sensei!**

The subscription implementation allows clients to subscribe to transaction updates by hash or to all transactions. The filtering logic is clean and effective.

---

`136-166`: **Nice implementation of calls_field, ohayo!**

The function correctly fetches calls for a transaction from the database. Good use of parameterized queries to prevent SQL injection!

---

`207-219`: **LGTM! Good result handling logic**

The code correctly processes the query results by mapping database rows to the expected GraphQL output format.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T04:28:45Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`194-218`: _⚠️ Potential issue_

**Potential SQL injection vulnerability in token transfers query!**

Ohayo, sensei! The SQL query is vulnerable to injection because you're directly interpolating the transaction_hash into the LIKE pattern instead of using a parameterized query.



Fix this by using parameters properly:

```diff
-                    let query = format!(
-                        r#"
-                        SELECT 
-                            et.id,
-                            et.contract_address,
-                            et.from_address,
-                            et.to_address,
-                            et.amount,
-                            et.token_id,
-                            et.executed_at,
-                            t.name,
-                            t.symbol,
-                            t.decimals,
-                            c.contract_type,
-                            t.metadata
-                        FROM
-                            {TOKEN_TRANSFER_TABLE} et
-                        JOIN
-                            tokens t ON et.token_id = t.id
-                        JOIN
-                            contracts c ON t.contract_address = c.contract_address
-                        WHERE
-                            et.event_id LIKE '%:{transaction_hash}:%'
-                        "#
-                    );
+                    let query = format!(
+                        r#"
+                        SELECT 
+                            et.id,
+                            et.contract_address,
+                            et.from_address,
+                            et.to_address,
+                            et.amount,
+                            et.token_id,
+                            et.executed_at,
+                            t.name,
+                            t.symbol,
+                            t.decimals,
+                            c.contract_type,
+                            t.metadata
+                        FROM
+                            {TOKEN_TRANSFER_TABLE} et
+                        JOIN
+                            tokens t ON et.token_id = t.id
+                        JOIN
+                            contracts c ON t.contract_address = c.contract_address
+                        WHERE
+                            et.event_id LIKE '%:' || ? || ':%'
+                        "#
+                    );
```

This ensures the transaction hash is properly escaped and prevents SQL injection.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>crates/torii/graphql/src/object/transaction.rs (1)</summary><blockquote>

`139-140`: **Consider using collect with type annotation for clarity.**

Sensei, the code works but could be slightly clearer with an explicit type annotation:


```diff
- Value::from(transaction.calldata.split("/").collect::<Vec<_>>()),
+ Value::from(transaction.calldata.split("/").collect::<Vec<&str>>()),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bdf06681cc11e41d33e46fe35ae45af56ea0d954 and 476a98a897e23244ca766994e1fda8e5572d0c7c.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `crates/torii/graphql/src/object/transaction.rs` (3 hunks)
* `crates/torii/sqlite/src/executor/mod.rs` (8 hunks)
* `crates/torii/sqlite/src/types.rs` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* crates/torii/sqlite/src/types.rs

</details>

<details>
<summary>🔇 Additional comments (16)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (8)</summary>

`1-1`: **Ohayo! Good addition of HashSet import.**

The HashSet is appropriately imported for efficient storage of unique contract addresses in the new transaction storage mechanism.

---

`11-11`: **Nice type renaming to avoid name conflicts, sensei!**

Renaming `Transaction` to `SqlxTransaction` is a smart move to prevent confusion with the domain `Transaction` type from your types module.

---

`27-27`: **Good additions to support structured transaction calls.**

Adding `ParsedCall` to the imports and the `felts_to_sql_string` utility function properly sets up the structure needed for the new transaction call handling mechanism.



Also applies to: 29-29

---

`108-112`: **Well-designed struct for transaction query!**

The `StoreTransactionQuery` struct elegantly captures both the contract addresses and function calls associated with a transaction. Using a HashSet for addresses ensures uniqueness.

---

`116-116`: **Clean enum addition, sensei!**

The new `StoreTransaction` variant in the `QueryType` enum follows the existing pattern and integrates well with the codebase.

---

`141-141`: **Consistent field type update.**

The `transaction` field type is correctly updated to match the renamed `SqlxTransaction` type, maintaining consistency throughout the codebase.

---

`451-492`: **Excellent implementation of transaction storage with structured calls!**

The implementation nicely handles:
1. Retrieving the transaction from the database
2. Storing contract addresses in the transaction_contract table
3. Storing structured calls in the transaction_calls table 
4. Publishing the transaction message

The code properly uses parameterized queries to prevent SQL injection and includes proper error handling.

---

`890-890`: **Updated broker message handling appropriately.**

The `send_broker_message` function is correctly updated to handle the new transaction message type.

</details>
<details>
<summary>crates/torii/graphql/src/object/transaction.rs (8)</summary>

`3-5`: **Ohayo, sensei! Good addition of SubscriptionField types.**

I see you've added imports for supporting the new subscription functionality. Clear and thoughtful inclusion of the necessary dynamic types.

---

`15-17`: **Constants added appropriately for the new functionality.**

The constants for CALL_TYPE_NAME and TRANSACTION_CALLS_TABLE are properly imported, maintaining consistency with the project's pattern of centralizing constants.

---

`19-23`: **Great job importing the CALL_MAPPING!**

Adding the CALL_MAPPING import aligns well with how you're handling other type mappings in the system.

---

`25-44`: **Nice implementation of the CallObject struct!**

The implementation follows the established pattern for GraphQL objects in the system. Clean and consistent with other object implementations.

---

`61-63`: **Well-structured related_fields implementation.**

You've properly updated the related_fields method to include both calls and token transfers, which creates a good GraphQL schema structure.

---

`131-149`: **Clean value_mapping implementation!**

The method properly converts Transaction fields to GraphQL values. I like how you're using IndexMap for predictable field ordering.

---

`152-182`: **Well implemented calls_field function!**

The function correctly fetches and maps function calls data. Good job using parameterized queries to prevent SQL injection.

---

`221-221`: **Make sure SQL bindings match query parameters.**

The query uses string interpolation for transaction_hash (which is unsafe) but then also tries to bind it as a parameter. When fixing the LIKE clause, ensure you're using the bound parameter correctly.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T04:40:24Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (1)</summary><blockquote>

`462-503`: **Ohayo sensei! Add error handling for calls collection processing.**

The implementation for handling StoreTransaction queries looks solid overall, but I'm concerned about the error handling when processing the calls collection. If there's an issue with the format of any call data, the current code might panic.


Consider adding error checking before accessing call data elements, especially when processing large call collections:

```diff
  for call in &store_transaction.calls {
+     // Validate call data before inserting
+     if call.calldata.is_empty() {
+         tracing::warn!(target: LOG_TARGET, "Empty calldata for call to {} with entrypoint {}", &call.contract_address, &call.entrypoint);
+     }
+     
      sqlx::query(
          "INSERT OR IGNORE INTO transaction_calls (transaction_hash, \
           contract_address, entrypoint, calldata, call_type, caller_address) \
           VALUES (?, ?, ?, ?, ?, ?)",
      )
      // ...
  }
```

</blockquote></details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (1)</summary><blockquote>

`538-551`: **Ohayo! Updated function signature looks good but needs documentation.**

The function signature change from accepting a Transaction object to individual parameters makes the function more flexible but could benefit from documentation to explain the purpose and expected values of each parameter.


Consider adding a docstring to explain the function parameters:

```diff
+    /// Stores a transaction and its associated calls in the database
+    /// 
+    /// # Arguments
+    /// * `transaction_hash` - The hash of the transaction
+    /// * `sender_address` - The address of the transaction sender
+    /// * `calldata` - The transaction calldata
+    /// * `max_fee` - The maximum fee for the transaction
+    /// * `signature` - The transaction signature
+    /// * `nonce` - The transaction nonce
+    /// * `block_number` - The block number where the transaction was included
+    /// * `contract_addresses` - Set of contract addresses associated with this transaction
+    /// * `transaction_type` - The type of transaction (e.g., "INVOKE")
+    /// * `block_timestamp` - The timestamp of the block
+    /// * `calls` - The parsed calls contained in this transaction
     pub fn store_transaction(
         &mut self,
         transaction_hash: Felt,
         sender_address: Felt,
         calldata: &[Felt],
         max_fee: Felt,
         signature: &[Felt],
         nonce: Felt,
         block_number: u64,
         contract_addresses: &HashSet<Felt>,
         transaction_type: &str,
         block_timestamp: u64,
         calls: &[ParsedCall],
     ) -> Result<()> {
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 476a98a897e23244ca766994e1fda8e5572d0c7c and 58aed92bcf8580f7f4f2b794697186044b013fb8.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `crates/torii/indexer/src/processors/mod.rs` (3 hunks)
* `crates/torii/sqlite/src/executor/mod.rs` (9 hunks)
* `crates/torii/sqlite/src/lib.rs` (2 hunks)
* `crates/torii/sqlite/src/types.rs` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* crates/torii/indexer/src/processors/mod.rs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>crates/torii/sqlite/src/lib.rs (2)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (2) (2)</summary>

* `new` (177-179)
* `new` (252-278)

</details>
<details>
<summary>crates/torii/sqlite/src/utils.rs (2) (2)</summary>

* `felts_to_sql_string` (35-38)
* `utc_dt_string_from_timestamp` (31-33)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (16)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/types.rs (4)</summary>

`227-231`: **Ohayo, nice enum implementation for call types!**

Clean and straightforward implementation with the two call types. The naming is descriptive and follows Rust conventions.

---

`233-240`: **Display implementation looks good, sensei.**

The Display trait implementation correctly formats both variants as uppercase strings, which will be useful for storing in the database.

---

`242-249`: **Solid ParsedCall struct design.**

This struct neatly encapsulates all the essential information about a contract call. The fields are aptly named and the field types are appropriate.

---

`251-270`: **Transaction struct looks good with the new fields!**

I like how you've added the new fields for `calls` and `contract_addresses` with the `#[sqlx(skip)]` attribute to ensure they're not included in database operations. This separation of concerns makes sense given the complex nature of these fields.

</details>
<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (8)</summary>

`1-1`: **Nice HashSet addition to the imports.**

Adding HashSet to the existing HashMap import is clean and makes sense for the new contract_addresses field.

---

`11-11`: **Good rename to avoid naming conflict, sensei!**

Renaming `Transaction` to `SqlxTransaction` avoids confusion with your new Transaction struct.

---

`27-27`: **Clean import addition for ParsedCall.**

Adding ParsedCall to the existing imports from types.rs is an efficient way to manage the imports.

---

`29-29`: **Well-organized utility function import.**

Adding the felts_to_sql_string utility shows good code organization.

---

`54-54`: **BrokerMessage transaction variant looks good.**

The Transaction variant in BrokerMessage enum aligns with your other changes.

---

`109-112`: **Clean design for StoreTransactionQuery struct.**

The struct has a simple and clear design with just two fields: contract_addresses and calls, which aligns well with the requirement to store transaction calls.

---

`127-127`: **QueryType enum addition looks good.**

Adding the StoreTransaction variant to QueryType enum is a necessary and clean extension.

---

`954-955`: **Transaction broker message publishing looks good.**

You've correctly updated the send_broker_message function to handle Transaction messages consistently with other message types.

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (4)</summary>

`13-14`: **Good update to imports.**

Adding StoreTransactionQuery to the imports is appropriate for the changes.

---

`15-15`: **Clean up of import list.**

Removing Transaction from the starknet imports since you're now using your own Transaction struct.

---

`18-19`: **Nice additions to imports.**

Adding ParsedCall and felts_to_sql_string shows good attention to dependencies.

---

`552-574`: **Ohayo sensei! Transaction storage query looks solid but check the ID column.**

The implementation for storing transactions is clean and follows a consistent pattern with other parts of the codebase. However, I noticed you're using the transaction_hash as both the ID and transaction_hash columns.



Make sure this is intentional - if the ID column is meant to be a unique identifier separate from the transaction hash, you might want to generate a different value. If using the transaction hash as ID is intentional (which is common and reasonable), then this is perfect.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:22:17Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (1)</summary><blockquote>

`164-196`: **Consider locking strategy to avoid duplicate fetches, sensei.**  
Your double-checked locking approach is fine, but if multiple tasks request the same contract concurrently, you could overlap fetches. A small improvement would be to store an “in-progress” marker or use a future-based cache.

</blockquote></details>
<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (1)</summary><blockquote>

`463-469`: **Ohayo sensei! Verifying fetch with `INSERT OR IGNORE`.**  
While retrieving the newly inserted row with `fetch_one` can work, keep in mind that "INSERT OR IGNORE" might not insert a row if it already exists, potentially making `fetch_one` return zero rows. Consider handling the scenario where the record already exists to avoid runtime errors.  

You could, for example, switch to a pattern like:
```diff
- INSERT OR IGNORE INTO transactions (...) VALUES (...) RETURNING *
+ INSERT INTO transactions (...) VALUES (...) ON CONFLICT(id) DO UPDATE SET ...
  RETURNING *;
```
Then proceed with `fetch_one` to reliably get the current row.

</blockquote></details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (1)</summary><blockquote>

`38-38`: **Ohayo sensei! `#[allow(clippy::too_many_arguments)]`.**  
Acceptable if needed, though consider grouping arguments into a builder struct in the future to avoid passing so many parameters.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 58aed92bcf8580f7f4f2b794697186044b013fb8 and 65d80a3ec2c34b218fccc6e78e337052502fc080.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `crates/torii/sqlite/src/cache.rs` (2 hunks)
* `crates/torii/sqlite/src/executor/mod.rs` (9 hunks)
* `crates/torii/sqlite/src/lib.rs` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>crates/torii/sqlite/src/lib.rs (3)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (8) (8)</summary>

* `sqlx` (321-321)
* `sqlx` (520-520)
* `sqlx` (611-611)
* `sqlx` (651-651)
* `sqlx` (720-720)
* `sqlx` (803-803)
* `new` (177-179)
* `new` (252-278)

</details>
<details>
<summary>crates/torii/sqlite/src/model.rs (1) (1)</summary>

* `new` (36-70)

</details>
<details>
<summary>crates/torii/sqlite/src/utils.rs (2) (2)</summary>

* `felts_to_sql_string` (35-38)
* `utc_dt_string_from_timestamp` (31-33)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (25)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (5)</summary>

`2-2`: **Ohayo sensei, nice Arc usage!**  
This import is appropriate for synchronous concurrency, well done.

---

`7-12`: **Ohayo sensei, these imports fit the new caching flow well.**  
They cleanly provide the types you need for contract interactions. Great job!

---

`157-162`: **Ohayo sensei, good approach for caching contract classes!**  
The struct fields make sense. Storing both class hash and `ContractClass` together can help avoid repeated lookups.

---

`225-225`: **Ohayo sensei, avoid `.unwrap()` for JSON deserialization.**  
This `.unwrap()` could panic on malformed JSON. Use a safe error-handling pattern (e.g. `?`) instead.

---

`242-242`: **Ohayo sensei, watch out for `.unwrap()` on `get_selector_from_name`.**  
If this function fails on an invalid name, the code will panic. Graceful error handling is preferable.

</details>
<details>
<summary>crates/torii/sqlite/src/executor/mod.rs (13)</summary>

`1-1`: **Ohayo sensei! New import for HashSet.**  
This import enables using `HashSet` for handling unique collections, which is beneficial for avoiding duplicates in transaction-related logic.

---

`11-11`: **Ohayo sensei! Introducing `SqlxTransaction` alias.**  
Renaming or aliasing the transaction type helps clarify the domain-specific usage here. Ensure that all references and doc comments are updated to reflect this new naming.

---

`27-27`: **Ohayo sensei! `ParsedCall` import recognized.**  
Including `ParsedCall` in this import list indicates it will be used downstream for transaction calls. The addition is consistent with your expanded transaction processing logic.

---

`29-29`: **Ohayo sensei! Utility imports look good.**  
Bringing in `felts_to_sql_string` and `felt_to_sql_string` is consistent with the new transaction calls logic.

---

`54-54`: **Ohayo sensei! New message variant for transactions.**  
Adding `Transaction(Transaction)` to `BrokerMessage` ensures transaction data can be published across the system. LGTM!

---

`108-112`: **Ohayo sensei! Introducing `StoreTransactionQuery`.**  
This struct captures the contract addresses and parsed calls for a transaction. Structuring them in a dedicated query object is clear and maintainable.

---

`127-127`: **Ohayo sensei! Added `StoreTransaction` enum variant.**  
This is a natural extension to handle storing transactions. The naming is intuitive, matching the new struct.

---

`152-152`: **Ohayo sensei! Transaction field uses `SqlxTransaction`.**  
Replacing the older naming with `SqlxTransaction` can help prevent confusion with different transaction abstractions.

---

`462-462`: **Ohayo sensei! Implementing `StoreTransaction` branch.**  
The new branch in `handle_query_message` systematically processes stored transactions. This integration point looks consistent.

---

`471-480`: **Ohayo sensei! `transaction_contract` insertion logic.**  
You're inserting contract addresses into the `transaction_contract` table for every relevant transaction. Check that ignoring duplicates is desired—if so, this is fine. Otherwise, consider `UPDATE` or `REPLACE` if data might change over time.  

Would you like me to generate a script to check for existing code references that rely on updating existing rows in `transaction_contract`?

---

`482-497`: **Ohayo sensei! `transaction_calls` insertion loop.**  
Storing each parsed call with `INSERT OR IGNORE` is consistent. If you want to track call updates (e.g., updated `entrypoint` or `calldata` after a re-run), consider an `ON CONFLICT` upsert strategy.

---

`499-503`: **Ohayo sensei! Finalizing the transaction object.**  
Appending `contract_addresses` and `calls` to the `transaction` struct before publishing is a clear approach and ensures a consistent, enriched `BrokerMessage::Transaction`.

---

`954-954`: **Ohayo sensei! Transaction variant broker publish.**  
Publishing the `Transaction` variant completes the pipeline for storing and broadcasting transaction data. Good addition!

</details>
<details>
<summary>crates/torii/sqlite/src/lib.rs (7)</summary>

`13-13`: **Ohayo sensei! Adding `StoreTransactionQuery` import.**  
This import aligns with the new transaction-handling logic. Looks consistent with the rest of the file’s usage.

---

`15-15`: **Ohayo sensei! `Felt` type usage.**  
We see `Felt` is central to StarkNet calls; bringing its symbol into scope here is consistent with the store logic.

---

`18-18`: **Ohayo sensei! `ParsedCall` now in scope.**  
This newly imported type suggests you’ll parse calls individually. Appropriately used for capturing call details.

---

`541-546`: **Ohayo sensei! Extended store_transaction parameters.**  
Adding these parameters (`transaction_hash`, `sender_address`, etc.) clarifies the transaction shape. This is a good step toward modularizing the logic.

---

`549-549`: **Ohayo sensei! `transaction_type` parameter.**  
Allowing injection of the transaction type string might help differentiate behaviors if needed.

---

`551-551`: **Ohayo sensei! Accepting `calls` slice.**  
This design paves the way for storing multiple function calls within a single transaction.

---

`553-574`: **Ohayo sensei! Transaction insertion with new fields.**  
Storing the extra fields in `transactions` followed by a `StoreTransactionQuery` ensures the subsequent insertion of calls and contract addresses flows seamlessly in the executor.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T01:07:49Z
user: login: glihm
body: A small comment that could help on performance and reducing the memory overhead of the classes that could be big. And a Dojo project may have bunch of classes.

Not mandatory though, can be optimized later by removing the byte code of classes and focusing only on `selectors<>entrypoint name` pairs.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-20T02:59:50Z
user: login: Larkooo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T03:01:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (1)</summary><blockquote>

`244-252`: _⚠️ Potential issue_

**Remove unwrap in Legacy ABI selector lookup, sensei.**

The Legacy ABI lookup uses `unwrap()` on line 246 when getting a selector from a function name, which could panic.


```diff
-            LegacyContractAbiEntry::Function(function)
-                if get_selector_from_name(&function.name).unwrap() == selector =>
+            LegacyContractAbiEntry::Function(function)
+                if get_selector_from_name(&function.name).ok() == Some(selector) =>
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 91e4bbaaa35ef47d14088069864244898fbe032f and 65cb243ccce6cb499cb8a5aea1df9cba08d9a6b6.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `crates/torii/sqlite/src/cache.rs` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>crates/torii/sqlite/src/cache.rs (7)</summary>

`158-162`: **Ohayo, sensei! Clean enum definition for ClassAbi.**

This enum provides a nice separation between Sierra and Legacy ABI types, making it easier to handle different contract classes consistently.

---

`164-168`: **Good use of generics and thread-safe caching, sensei!**

The `ContractClassCache` design with generics over a Provider type and RwLock for thread-safety is appropriate. The struct stores only the class hash and ABI data instead of the entire contract class, which is memory-efficient.

---

`170-173`: **Simple and clean constructor implementation.**

The `new` function properly initializes the cache with an empty HashMap and stores the provider reference.

---

`175-197`: **Improved error handling for provider operations, sensei!**

The implementation correctly handles the block not found error by falling back to the pending block. This is a good improvement from the previous implementation that was using `unwrap()`.

---

`206-206`: **Consider implementing cache size limits, sensei.**

The cache currently grows unbounded as new contract classes are accessed. Consider adding a maximum size limit or an LRU eviction policy to prevent potential memory issues.

---

`211-221`: **Nice recursive function to flatten ABI functions.**

This helper function efficiently extracts function entries from nested ABI definitions, which is useful for searching across all function definitions later.

---

`223-243`: **Good entrypoint lookup for Sierra ABI.**

The implementation correctly handles the lookup of entrypoint names from Sierra ABIs by first finding the matching selector in the entrypoints list and then looking up the function details in the ABI.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:17:42Z

Review comments:
diff_hunk: @@ -520,40 +521,96 @@ impl Sql {
                 Transaction::Invoke(InvokeTransaction::V3(invoke_v3_transaction)) => (
                     Argument::FieldElement(invoke_v3_transaction.transaction_hash),
                     Argument::FieldElement(invoke_v3_transaction.sender_address),
-                    Argument::String(felts_to_sql_string(&invoke_v3_transaction.calldata)),
+                    &invoke_v3_transaction.calldata,
                     Argument::FieldElement(Felt::ZERO), // has no max_fee
                     Argument::String(felts_to_sql_string(&invoke_v3_transaction.signature)),
                     Argument::FieldElement(invoke_v3_transaction.nonce),
                 ),
                 Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => (
                     Argument::FieldElement(invoke_v1_transaction.transaction_hash),
                     Argument::FieldElement(invoke_v1_transaction.sender_address),
-                    Argument::String(felts_to_sql_string(&invoke_v1_transaction.calldata)),
+                    &invoke_v1_transaction.calldata,
                     Argument::FieldElement(invoke_v1_transaction.max_fee),
                     Argument::String(felts_to_sql_string(&invoke_v1_transaction.signature)),
                     Argument::FieldElement(invoke_v1_transaction.nonce),
                 ),
                 Transaction::L1Handler(l1_handler_transaction) => (
                     Argument::FieldElement(l1_handler_transaction.transaction_hash),
                     Argument::FieldElement(l1_handler_transaction.contract_address),
-                    Argument::String(felts_to_sql_string(&l1_handler_transaction.calldata)),
+                    &l1_handler_transaction.calldata,
                     Argument::FieldElement(Felt::ZERO), // has no max_fee
                     Argument::String("".to_string()),   // has no signature
                     Argument::FieldElement((l1_handler_transaction.nonce).into()),
                 ),
                 _ => return Ok(()),
             };
 
+        let calls_len: usize = calldata[0].try_into().unwrap();
+        let mut calls: Vec<FunctionCall> = vec![];
+        let mut outside_calls: Vec<FunctionCall> = vec![];
+
+        let mut offset = 0;
+        for _ in 0..calls_len {
+            let to_offset = offset + 1;
+            let selector_offset = to_offset + 1;
+            let calldata_offset = selector_offset + 2;
+            let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+
+            let call = FunctionCall {
+                contract_address: calldata[to_offset],
+                entry_point_selector: calldata[selector_offset],
+                calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+            };
path: crates/torii/sqlite/src/lib.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add safeguards against out-of-bounds access.**

Sensei, this section of code calculates various offsets and accesses elements by index without verifying array bounds first. This could lead to panics if called with unexpected data.


Consider adding checks before accessing array elements:

```diff
  for _ in 0..calls_len {
      let to_offset = offset + 1;
      let selector_offset = to_offset + 1;
      let calldata_offset = selector_offset + 2;
+     
+     if selector_offset + 1 >= calldata.len() {
+         break; // or return an error
+     }
      let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+     
+     if calldata_offset + calldata_len > calldata.len() {
+         break; // or return an error
+     }

      let call = FunctionCall {
          contract_address: calldata[to_offset],
          entry_point_selector: calldata[selector_offset],
          calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
      };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        for _ in 0..calls_len {
            let to_offset = offset + 1;
            let selector_offset = to_offset + 1;
            let calldata_offset = selector_offset + 2;
            
            if selector_offset + 1 >= calldata.len() {
                break; // or return an error
            }
            let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
            
            if calldata_offset + calldata_len > calldata.len() {
                break; // or return an error
            }

            let call = FunctionCall {
                contract_address: calldata[to_offset],
                entry_point_selector: calldata[selector_offset],
                calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
            };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T11:03:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -520,40 +521,96 @@
                 Transaction::Invoke(InvokeTransaction::V3(invoke_v3_transaction)) => (
                     Argument::FieldElement(invoke_v3_transaction.transaction_hash),
                     Argument::FieldElement(invoke_v3_transaction.sender_address),
-                    Argument::String(felts_to_sql_string(&invoke_v3_transaction.calldata)),
+                    &invoke_v3_transaction.calldata,
                     Argument::FieldElement(Felt::ZERO), // has no max_fee
                     Argument::String(felts_to_sql_string(&invoke_v3_transaction.signature)),
                     Argument::FieldElement(invoke_v3_transaction.nonce),
                 ),
                 Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => (
                     Argument::FieldElement(invoke_v1_transaction.transaction_hash),
                     Argument::FieldElement(invoke_v1_transaction.sender_address),
-                    Argument::String(felts_to_sql_string(&invoke_v1_transaction.calldata)),
+                    &invoke_v1_transaction.calldata,
                     Argument::FieldElement(invoke_v1_transaction.max_fee),
                     Argument::String(felts_to_sql_string(&invoke_v1_transaction.signature)),
                     Argument::FieldElement(invoke_v1_transaction.nonce),
                 ),
                 Transaction::L1Handler(l1_handler_transaction) => (
                     Argument::FieldElement(l1_handler_transaction.transaction_hash),
                     Argument::FieldElement(l1_handler_transaction.contract_address),
-                    Argument::String(felts_to_sql_string(&l1_handler_transaction.calldata)),
+                    &l1_handler_transaction.calldata,
                     Argument::FieldElement(Felt::ZERO), // has no max_fee
                     Argument::String("".to_string()),   // has no signature
                     Argument::FieldElement((l1_handler_transaction.nonce).into()),
                 ),
                 _ => return Ok(()),
             };
 
+        let calls_len: usize = calldata[0].try_into().unwrap();
+        let mut calls: Vec<FunctionCall> = vec![];
+        let mut outside_calls: Vec<FunctionCall> = vec![];
+
+        let mut offset = 0;
+        for _ in 0..calls_len {
+            let to_offset = offset + 1;
+            let selector_offset = to_offset + 1;
+            let calldata_offset = selector_offset + 2;
+            let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+
+            let call = FunctionCall {
+                contract_address: calldata[to_offset],
+                entry_point_selector: calldata[selector_offset],
+                calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+            };
+
+            if call.entry_point_selector == selector!("execute_from_outside_v3") {
+                let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
+                for _ in 0..outside_calls_len {
+                    let to_offset = calldata_offset + 6;
+                    let selector_offset = to_offset + 1;
+                    let calldata_offset = selector_offset + 2;
+                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                    let outside_call = FunctionCall {
+                        contract_address: calldata[to_offset],
+                        entry_point_selector: calldata[selector_offset],
+                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                            .to_vec(),
+                    };
+                    outside_calls.push(outside_call);
+                }
+            } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
+                // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
+                let outside_calls_len: usize = calldata[calldata_offset + 4].try_into().unwrap();
+                for _ in 0..outside_calls_len {
+                    let to_offset = calldata_offset + 5;
+                    let selector_offset = to_offset + 1;
+                    let calldata_offset = selector_offset + 2;
+                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                    let outside_call = FunctionCall {
+                        contract_address: calldata[to_offset],
+                        entry_point_selector: calldata[selector_offset],
+                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                            .to_vec(),
+                    };
+                    outside_calls.push(outside_call);
+                }
+            }
+
+            calls.push(call);
+            offset += 3 + calldata_len;
+        }
path: crates/torii/sqlite/src/lib.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Transaction calldata parsing logic needs better error handling.**

Ohayo, sensei! The transaction parsing logic is intricate, but I'm concerned about the error handling. You're using `unwrap()` for array conversions and accessing elements without bounds checking, which could lead to runtime panics.


```diff
- let calls_len: usize = calldata[0].try_into().unwrap();
+ let calls_len: usize = calldata.get(0)
+     .ok_or_else(|| anyhow!("Calldata is empty"))?
+     .try_into()
+     .map_err(|_| anyhow!("Failed to parse calls length"))?;

// Add similar error handling for other unwraps and array accesses
```

Also, add checks before accessing array indices to ensure they're within bounds:

```diff
+ if calldata.len() <= selector_offset + 1 {
+     return Err(anyhow!("Calldata too short for selector_offset"));
+ }
  let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+ if calldata.len() < calldata_offset + calldata_len {
+     return Err(anyhow!("Calldata too short for calldata_len"));
+ }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        let calls_len: usize = calldata.get(0)
            .ok_or_else(|| anyhow!("Calldata is empty"))?
            .try_into()
            .map_err(|_| anyhow!("Failed to parse calls length"))?;
        let mut calls: Vec<FunctionCall> = vec![];
        let mut outside_calls: Vec<FunctionCall> = vec![];

        let mut offset = 0;
        for _ in 0..calls_len {
            let to_offset = offset + 1;
            let selector_offset = to_offset + 1;
            let calldata_offset = selector_offset + 2;
            if calldata.len() <= selector_offset + 1 {
                return Err(anyhow!("Calldata too short for selector_offset"));
            }
            let calldata_len: usize = calldata[selector_offset + 1]
                .try_into()
                .map_err(|_| anyhow!("Failed to parse calldata length"))?;
            if calldata.len() < calldata_offset + calldata_len {
                return Err(anyhow!("Calldata too short for calldata_len"));
            }

            let call = FunctionCall {
                contract_address: calldata[to_offset],
                entry_point_selector: calldata[selector_offset],
                calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
            };

            if call.entry_point_selector == selector!("execute_from_outside_v3") {
                let outside_calls_len: usize = calldata[calldata_offset + 5]
                    .try_into()
                    .unwrap();
                for _ in 0..outside_calls_len {
                    let to_offset = calldata_offset + 6;
                    let selector_offset = to_offset + 1;
                    let calldata_offset = selector_offset + 2;
                    let calldata_len: usize = calldata[selector_offset + 1]
                        .try_into()
                        .unwrap();
                    let outside_call = FunctionCall {
                        contract_address: calldata[to_offset],
                        entry_point_selector: calldata[selector_offset],
                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
                            .to_vec(),
                    };
                    outside_calls.push(outside_call);
                }
            } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
                // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
                let outside_calls_len: usize = calldata[calldata_offset + 4]
                    .try_into()
                    .unwrap();
                for _ in 0..outside_calls_len {
                    let to_offset = calldata_offset + 5;
                    let selector_offset = to_offset + 1;
                    let calldata_offset = selector_offset + 2;
                    let calldata_len: usize = calldata[selector_offset + 1]
                        .try_into()
                        .unwrap();
                    let outside_call = FunctionCall {
                        contract_address: calldata[to_offset],
                        entry_point_selector: calldata[selector_offset],
                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
                            .to_vec(),
                    };
                    outside_calls.push(outside_call);
                }
            }

            calls.push(call);
            offset += 3 + calldata_len;
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 094bdd8
created_at: 2025-03-14T11:03:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+-- Create a new table to store the transaction and contract addresses
+CREATE TABLE IF NOT EXISTS transaction_calls (
+    transaction_hash TEXT NOT NULL,
+    contract_address TEXT NOT NULL,
+    entry_point_selector TEXT NOT NULL,
+    calldata TEXT NOT NULL,
+    call_type TEXT NOT NULL DEFAULT 'EXECUTE',
+    FOREIGN KEY (transaction_hash) REFERENCES transactions(id),
+);
path: crates/torii/migrations/20250317023025_transaction_calls.sql
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix SQL syntax error: remove trailing comma.**

Ohayo, sensei! There's a syntax error in your SQL migration. The trailing comma after the foreign key constraint on line 9 should be removed, as most SQL engines don't allow trailing commas in the final element of a definition list.

```diff
-    FOREIGN KEY (transaction_hash) REFERENCES transactions(id),
+    FOREIGN KEY (transaction_hash) REFERENCES transactions(id)
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    FOREIGN KEY (transaction_hash) REFERENCES transactions(id)
);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 88db0a8
created_at: 2025-03-17T02:45:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -572,6 +574,97 @@ impl Sql {
             ))?;
         }
 
+        // Parse and store calls for INVOKE transactions
+        if transaction_type != "INVOKE" {
+            return Ok(());
+        }
+
+        let calls_len: usize = calldata[0].try_into().unwrap();
+        let mut calls: Vec<FunctionCall> = vec![];
+        let mut outside_calls: Vec<FunctionCall> = vec![];
+
+        let mut offset = 0;
+        for _ in 0..calls_len {
+            let to_offset = offset + 1;
+            let selector_offset = to_offset + 1;
+            let calldata_offset = selector_offset + 2;
+            let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+
+            let call = FunctionCall {
+                contract_address: calldata[to_offset],
+                entry_point_selector: calldata[selector_offset],
+                calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+            };
+
+            if call.entry_point_selector == selector!("execute_from_outside_v3") {
+                let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
+                for _ in 0..outside_calls_len {
+                    let to_offset = calldata_offset + 6;
+                    let selector_offset = to_offset + 1;
+                    let calldata_offset = selector_offset + 2;
+                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                    let outside_call = FunctionCall {
+                        contract_address: calldata[to_offset],
+                        entry_point_selector: calldata[selector_offset],
+                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                            .to_vec(),
+                    };
+                    outside_calls.push(outside_call);
+                }
+            } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
+                // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
+                let outside_calls_len: usize = calldata[calldata_offset + 4].try_into().unwrap();
+                for _ in 0..outside_calls_len {
+                    let to_offset = calldata_offset + 5;
+                    let selector_offset = to_offset + 1;
+                    let calldata_offset = selector_offset + 2;
+                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                    let outside_call = FunctionCall {
+                        contract_address: calldata[to_offset],
+                        entry_point_selector: calldata[selector_offset],
+                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                            .to_vec(),
+                    };
+                    outside_calls.push(outside_call);
+                }
+            }
+
+            calls.push(call);
+            offset += 3 + calldata_len;
+        }
path: crates/torii/sqlite/src/lib.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Refactor duplicated outside call parsing logic.**

The logic for parsing outside calls is duplicated between v2 and v3 handling, with only small differences in offsets. Consider extracting this to a helper function to avoid code duplication.

```diff
+     fn parse_outside_calls(
+         calldata: &[Felt],
+         calldata_offset: usize,
+         calls_len_offset: usize,
+         calls_start_offset: usize,
+     ) -> Vec<FunctionCall> {
+         let mut outside_calls = Vec::new();
+         let outside_calls_len: usize = calldata[calldata_offset + calls_len_offset].try_into().unwrap();
+         
+         let mut offset = 0;
+         for _ in 0..outside_calls_len {
+             let to_offset = calldata_offset + calls_start_offset + offset;
+             let selector_offset = to_offset + 1;
+             let calldata_offset = selector_offset + 2;
+             let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+             
+             let outside_call = FunctionCall {
+                 contract_address: calldata[to_offset],
+                 entry_point_selector: calldata[selector_offset],
+                 calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+             };
+             outside_calls.push(outside_call);
+             offset += 3 + calldata_len;
+         }
+         
+         outside_calls
+     }

      if call.entry_point_selector == selector!("execute_from_outside_v3") {
-         let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
-         for _ in 0..outside_calls_len {
-             // ... duplicated logic ...
-         }
+         outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 5, 6));
      } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
-         // ... duplicated logic ...
+         outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 4, 5));
      }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            // New helper function to parse outside calls.
            fn parse_outside_calls(
                calldata: &[Felt],
                calldata_offset: usize,
                calls_len_offset: usize,
                calls_start_offset: usize,
            ) -> Vec<FunctionCall> {
                let mut outside_calls = Vec::new();
                let outside_calls_len: usize =
                    calldata[calldata_offset + calls_len_offset].try_into().unwrap();
                
                let mut offset = 0;
                for _ in 0..outside_calls_len {
                    let to_offset = calldata_offset + calls_start_offset + offset;
                    let selector_offset = to_offset + 1;
                    let calldata_offset = selector_offset + 2;
                    let calldata_len: usize =
                        calldata[selector_offset + 1].try_into().unwrap();
                    
                    let outside_call = FunctionCall {
                        contract_address: calldata[to_offset],
                        entry_point_selector: calldata[selector_offset],
                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
                            .to_vec(),
                    };
                    outside_calls.push(outside_call);
                    offset += 3 + calldata_len;
                }
                
                outside_calls
            }

            if call.entry_point_selector == selector!("execute_from_outside_v3") {
-                let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
-                for _ in 0..outside_calls_len {
-                    let to_offset = calldata_offset + 6;
-                    let selector_offset = to_offset + 1;
-                    let calldata_offset = selector_offset + 2;
-                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
-                    let outside_call = FunctionCall {
-                        contract_address: calldata[to_offset],
-                        entry_point_selector: calldata[selector_offset],
-                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
-                            .to_vec(),
-                    };
-                    outside_calls.push(outside_call);
-                }
+                outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 5, 6));
            } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
-                // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
-                let outside_calls_len: usize = calldata[calldata_offset + 4].try_into().unwrap();
-                for _ in 0..outside_calls_len {
-                    let to_offset = calldata_offset + 5;
-                    let selector_offset = to_offset + 1;
-                    let calldata_offset = selector_offset + 2;
-                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
-                    let outside_call = FunctionCall {
-                        contract_address: calldata[to_offset],
-                        entry_point_selector: calldata[selector_offset],
-                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
-                            .to_vec(),
-                    };
-                    outside_calls.push(outside_call);
-                }
+                // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
+                outside_calls.extend(parse_outside_calls(calldata, calldata_offset, 4, 5));
            }

            calls.push(call);
            offset += 3 + calldata_len;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 094bdd8
created_at: 2025-03-17T02:45:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,6 +66,12 @@ lazy_static! {
             TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())),
         ),
     ]);
+    pub static ref FUNCTION_CALL_MAPPING: TypeMapping = IndexMap::from([
+        (Name::new("contractAddress"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("entryPointSelector"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+        (Name::new("calldata"), TypeData::Simple(TypeRef::named_list(TypeRef::STRING))),
+        (Name::new("callType"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
+    ]);
path: crates/torii/graphql/src/mapping.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Ohayo sensei! Consider using felt-like fields for contract addresses.**
Currently, `contractAddress` and `entryPointSelector` are typed as strings. If these values represent FELT-252 addresses/selectors, you could store them as numeric or typed FELT strings to maintain consistency across the codebase and possibly prevent accidental misuse.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-17T09:39:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,6 +271,49 @@ pub fn resolve_one(
     .argument(argument)
 }
 
+// Resolves single object queries with joins, returns current object of type type_name with related data
+pub fn resolve_one_with_joins(
+    table_name: &str,
+    id_column: &str,
+    field_name: &str,
+    type_name: &str,
+    type_mapping: &TypeMapping,
+    joins: Vec<JoinConfig>,
+    select_columns: Option<Vec<String>>,
+) -> Field {
+    let type_mapping = type_mapping.clone();
+    let table_name = table_name.to_owned();
+    let id_column = id_column.to_owned();
+    let joins = joins.to_owned();
+    let select_columns = select_columns.to_owned();
+    let argument = InputValue::new(id_column.to_case(Case::Camel), TypeRef::named_nn(TypeRef::ID));
+
+    Field::new(field_name, TypeRef::named_nn(type_name), move |ctx| {
+        let type_mapping = type_mapping.clone();
+        let table_name = table_name.to_owned();
+        let id_column = id_column.to_owned();
+        let joins = joins.to_owned();
+        let select_columns = select_columns.to_owned();
+
+        FieldFuture::new(async move {
+            let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+            let id: String =
+                extract::<String>(ctx.args.as_index_map(), &id_column.to_case(Case::Camel))?;
+            let data = fetch_single_row_with_joins(
+                &mut conn, 
+                &table_name, 
+                &id_column, 
+                &id, 
+                joins,
+                select_columns,
+            ).await?;
+            let model = value_mapping_from_row(&data, &type_mapping, false, true)?;
+            Ok(Some(Value::Object(model)))
+        })
+    })
+    .argument(argument)
+}
+
path: crates/torii/graphql/src/object/mod.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Ohayo sensei! Consider refactoring out common parts with `resolve_one`.**
`resolve_one_with_joins` largely duplicates logic from `resolve_one`. Extracting shared parts (like argument handling or row retrieval) into helper functions could improve maintainability and reduce potential inconsistencies.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-17T09:39:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,22 +48,165 @@ impl BasicObject for TransactionObject {
 
 impl ResolvableObject for TransactionObject {
     fn resolvers(&self) -> Vec<Field> {
-        let resolve_one = resolve_one(
-            TRANSACTION_TABLE,
-            TRANSACTION_HASH_COLUMN,
-            self.name().0,
-            self.type_name(),
-            self.type_mapping(),
-        );
-
-        let resolve_many = resolve_many(
-            TRANSACTION_TABLE,
-            ID_COLUMN,
-            self.name().1,
-            self.type_name(),
-            self.type_mapping(),
-        );
-
-        vec![resolve_one, resolve_many]
+        let resolve_one_with_calls = self.resolve_transaction_with_calls();
+        let resolve_many_with_calls = self.resolve_transactions_with_calls();
+
+        vec![resolve_one_with_calls, resolve_many_with_calls]
+    }
+}
+
+impl TransactionObject {
+    fn resolve_transaction_with_calls(&self) -> Field {
+        let type_mapping = self.type_mapping().clone();
+        let function_call_mapping = FUNCTION_CALL_MAPPING.clone();
+        let table_name = TRANSACTION_TABLE.to_owned();
+        let id_column = TRANSACTION_HASH_COLUMN.to_owned();
+        let field_name = self.name().0.to_owned();
+        let type_name = self.type_name().to_owned();
+        
+        Field::new(field_name, TypeRef::named_nn(type_name), move |ctx| {
+            let type_mapping = type_mapping.clone();
+            let function_call_mapping = function_call_mapping.clone();
+            let table_name = table_name.clone();
+            let id_column = id_column.clone();
+
+            FieldFuture::new(async move {
+                let pool = ctx.data::<Pool<Sqlite>>()?;
+                
+                // Extract the transaction hash from arguments
+                let tx_hash: String = extract(ctx.args.as_index_map(), "transactionHash")?;
+                
+                // First, fetch the transaction
+                let transaction_query = format!(
+                    "SELECT * FROM [{}] WHERE {} = '{}'", 
+                    table_name, id_column, tx_hash
+                );
+                let transaction = sqlx::query(&transaction_query)
+                    .fetch_one(pool)
+                    .await?;
+                
+                // Convert transaction row to GraphQL object
+                let mut transaction_obj = value_mapping_from_row(&transaction, &type_mapping, false, true)?;
+                
+                // Then, fetch all calls for this transaction
+                let calls_query = format!(
+                    "SELECT * FROM [{}] WHERE transaction_hash = '{}'",
+                    TRANSACTION_CALLS_TABLE, tx_hash
+                );
+                let calls = sqlx::query(&calls_query)
+                    .fetch_all(pool)
+                    .await?;
+                
+                // Convert each call row to a GraphQL object and add to a list
+                let mut calls_list = Vec::new();
+                for call in calls {
+                    let call_obj = value_mapping_from_row(&call, &function_call_mapping, false, true)?;
+                    calls_list.push(Value::Object(call_obj));
+                }
+                
+                // Add the calls list as a field in the transaction object
+                transaction_obj.insert(Name::new("calls"), Value::List(calls_list));
+                
+                Ok(Some(Value::Object(transaction_obj)))
+            })
+        })
+        .argument(async_graphql::dynamic::InputValue::new(
+            "transactionHash", 
+            TypeRef::named_nn(TypeRef::ID)
+        ))
+    }
path: crates/torii/graphql/src/object/transaction.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Potential SQL injection in transaction query**

Sensei, I noticed you're constructing SQL queries using string formatting, which could lead to SQL injection if the `tx_hash` contains malicious characters.


```diff
-let transaction_query = format!(
-    "SELECT * FROM [{}] WHERE {} = '{}'", 
-    table_name, id_column, tx_hash
-);
-let transaction = sqlx::query(&transaction_query)
-    .fetch_one(pool)
-    .await?;

+let transaction = sqlx::query("SELECT * FROM [?] WHERE ? = ?")
+    .bind(table_name)
+    .bind(id_column)
+    .bind(tx_hash)
+    .fetch_one(pool)
+    .await?;
```

Apply similar changes to the calls query as well.


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 63-63: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 72-72: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 94-94: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 103-103: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 113-113: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 6883145
created_at: 2025-03-17T09:39:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,22 +48,165 @@
 
 impl ResolvableObject for TransactionObject {
     fn resolvers(&self) -> Vec<Field> {
-        let resolve_one = resolve_one(
-            TRANSACTION_TABLE,
-            TRANSACTION_HASH_COLUMN,
-            self.name().0,
-            self.type_name(),
-            self.type_mapping(),
-        );
-
-        let resolve_many = resolve_many(
-            TRANSACTION_TABLE,
-            ID_COLUMN,
-            self.name().1,
-            self.type_name(),
-            self.type_mapping(),
-        );
-
-        vec![resolve_one, resolve_many]
+        let resolve_one_with_calls = self.resolve_transaction_with_calls();
+        let resolve_many_with_calls = self.resolve_transactions_with_calls();
+
+        vec![resolve_one_with_calls, resolve_many_with_calls]
+    }
+}
+
+impl TransactionObject {
+    fn resolve_transaction_with_calls(&self) -> Field {
+        let type_mapping = self.type_mapping().clone();
+        let function_call_mapping = FUNCTION_CALL_MAPPING.clone();
+        let table_name = TRANSACTION_TABLE.to_owned();
+        let id_column = TRANSACTION_HASH_COLUMN.to_owned();
+        let field_name = self.name().0.to_owned();
+        let type_name = self.type_name().to_owned();
+        
+        Field::new(field_name, TypeRef::named_nn(type_name), move |ctx| {
+            let type_mapping = type_mapping.clone();
+            let function_call_mapping = function_call_mapping.clone();
+            let table_name = table_name.clone();
+            let id_column = id_column.clone();
+
+            FieldFuture::new(async move {
+                let pool = ctx.data::<Pool<Sqlite>>()?;
+                
+                // Extract the transaction hash from arguments
+                let tx_hash: String = extract(ctx.args.as_index_map(), "transactionHash")?;
+                
+                // First, fetch the transaction
+                let transaction_query = format!(
+                    "SELECT * FROM [{}] WHERE {} = '{}'", 
+                    table_name, id_column, tx_hash
+                );
+                let transaction = sqlx::query(&transaction_query)
+                    .fetch_one(pool)
+                    .await?;
+                
+                // Convert transaction row to GraphQL object
+                let mut transaction_obj = value_mapping_from_row(&transaction, &type_mapping, false, true)?;
+                
+                // Then, fetch all calls for this transaction
+                let calls_query = format!(
+                    "SELECT * FROM [{}] WHERE transaction_hash = '{}'",
+                    TRANSACTION_CALLS_TABLE, tx_hash
+                );
+                let calls = sqlx::query(&calls_query)
+                    .fetch_all(pool)
+                    .await?;
+                
+                // Convert each call row to a GraphQL object and add to a list
+                let mut calls_list = Vec::new();
+                for call in calls {
+                    let call_obj = value_mapping_from_row(&call, &function_call_mapping, false, true)?;
+                    calls_list.push(Value::Object(call_obj));
+                }
+                
+                // Add the calls list as a field in the transaction object
+                transaction_obj.insert(Name::new("calls"), Value::List(calls_list));
+                
+                Ok(Some(Value::Object(transaction_obj)))
+            })
+        })
+        .argument(async_graphql::dynamic::InputValue::new(
+            "transactionHash", 
+            TypeRef::named_nn(TypeRef::ID)
+        ))
+    }
+
+    fn resolve_transactions_with_calls(&self) -> Field {
+        let type_mapping = self.type_mapping().clone();
+        let function_call_mapping = FUNCTION_CALL_MAPPING.clone();
+        let table_name = TRANSACTION_TABLE.to_owned();
+        let field_name = self.name().1.to_owned();
+        let type_name = self.type_name().to_owned();
+        
+        // Add pagination arguments
+        let limit_arg = async_graphql::dynamic::InputValue::new(
+            "limit", 
+            TypeRef::named(TypeRef::INT)
+        ).default_value(50);
+        
+        let offset_arg = async_graphql::dynamic::InputValue::new(
+            "offset", 
+            TypeRef::named(TypeRef::INT)
+        ).default_value(0);
+
+        let order_by_arg = async_graphql::dynamic::InputValue::new(
+            "orderBy", 
+            TypeRef::named(TypeRef::STRING)
+        ).default_value("executed_at");
+
+        let order_direction_arg = async_graphql::dynamic::InputValue::new(
+            "orderDirection", 
+            TypeRef::named(TypeRef::STRING)
+        ).default_value("DESC");
+        
+        Field::new(field_name, TypeRef::named_list(type_name), move |ctx| {
+            let type_mapping = type_mapping.clone();
+            let function_call_mapping = function_call_mapping.clone();
+            let table_name = table_name.clone();
+
+            FieldFuture::new(async move {
+                let pool = ctx.data::<Pool<Sqlite>>()?;
+                
+                // Extract pagination parameters
+                let limit: u64 = extract(ctx.args.as_index_map(), "limit").unwrap_or(50);
+                let offset: u64 = extract(ctx.args.as_index_map(), "offset").unwrap_or(0);
+                let order_by: String = extract(ctx.args.as_index_map(), "orderBy").unwrap_or_else(|_| "executed_at".to_string());
+                let order_direction: String = extract(ctx.args.as_index_map(), "orderDirection").unwrap_or_else(|_| "DESC".to_string());
+                
+                // Fetch transactions with pagination
+                let transactions_query = format!(
+                    "SELECT * FROM [{}] ORDER BY {} {} LIMIT {} OFFSET {}", 
+                    table_name, order_by, order_direction, limit, offset
+                );
+                let transactions = sqlx::query(&transactions_query)
+                    .fetch_all(pool)
+                    .await?;
+                
+                // Create a list to hold all transaction objects
+                let mut transaction_objects = Vec::new();
+                
+                // Process each transaction
+                for transaction in transactions {
+                    // Convert transaction row to GraphQL object
+                    let mut transaction_obj = value_mapping_from_row(&transaction, &type_mapping, false, true)?;
+                    
+                    // Get the transaction hash for this transaction
+                    let tx_hash = transaction.get::<String, _>(TRANSACTION_HASH_COLUMN);
+                    
+                    // Fetch all calls for this transaction
+                    let calls_query = format!(
+                        "SELECT * FROM [{}] WHERE transaction_hash = '{}'",
+                        TRANSACTION_CALLS_TABLE, tx_hash
+                    );
+                    let calls = sqlx::query(&calls_query)
+                        .fetch_all(pool)
+                        .await?;
+                    
+                    // Convert each call row to a GraphQL object and add to a list
+                    let mut calls_list = Vec::new();
+                    for call in calls {
+                        let call_obj = value_mapping_from_row(&call, &function_call_mapping, false, true)?;
+                        calls_list.push(Value::Object(call_obj));
+                    }
+                    
+                    // Add the calls list as a field in the transaction object
+                    transaction_obj.insert(Name::new("calls"), Value::List(calls_list));
+                    
+                    // Add the transaction to our list
+                    transaction_objects.push(Value::Object(transaction_obj));
+                }
+                
+                Ok(Some(Value::List(transaction_objects)))
+            })
+        })
+        .argument(limit_arg)
+        .argument(offset_arg)
+        .argument(order_by_arg)
+        .argument(order_direction_arg)
     }
path: crates/torii/graphql/src/object/transaction.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Implement proper pagination with parameterized queries**

While the pagination implementation is good, there are two issues to address:
1. SQL injection vulnerability in string-formatted queries
2. N+1 query problem when fetching calls for each transaction



```diff
// Fetch transactions with pagination
-let transactions_query = format!(
-    "SELECT * FROM [{}] ORDER BY {} {} LIMIT {} OFFSET {}", 
-    table_name, order_by, order_direction, limit, offset
-);
-let transactions = sqlx::query(&transactions_query)
-    .fetch_all(pool)
-    .await?;

+let transactions = sqlx::query("SELECT * FROM [?] ORDER BY ? ? LIMIT ? OFFSET ?")
+    .bind(table_name)
+    .bind(order_by)
+    .bind(order_direction)
+    .bind(limit)
+    .bind(offset)
+    .fetch_all(pool)
+    .await?;

// Later, instead of fetching calls one transaction at a time:
+// Get all transaction hashes
+let tx_hashes: Vec<String> = transactions.iter()
+    .map(|tx| tx.get::<String, _>(TRANSACTION_HASH_COLUMN))
+    .collect();
+
+// Fetch all calls in one query
+let calls_query = format!(
+    "SELECT * FROM [{}] WHERE transaction_hash IN ({})",
+    TRANSACTION_CALLS_TABLE, 
+    tx_hashes.iter().map(|h| format!("'{}'", h)).collect::<Vec<_>>().join(",")
+);
+let all_calls = sqlx::query(&calls_query)
+    .fetch_all(pool)
+    .await?;
+
+// Group calls by transaction hash
+let mut calls_by_tx: HashMap<String, Vec<sqlx::sqlite::SqliteRow>> = HashMap::new();
+for call in all_calls {
+    let tx_hash = call.get::<String, _>("transaction_hash");
+    calls_by_tx.entry(tx_hash).or_default().push(call);
+}
```

Then use `calls_by_tx` when processing each transaction.


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 122-122: Rust formatting check failed. Please run 'rustfmt' to format the code.

---

[error] 151-151: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 6883145
created_at: 2025-03-17T09:39:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -572,6 +574,100 @@ impl Sql {
             ))?;
         }
 
+        // Parse and store calls for INVOKE transactions
+        if transaction_type != "INVOKE" {
+            return Ok(());
+        }
+
+        let calls_len: usize = calldata[0].try_into().unwrap();
+        let mut calls: Vec<FunctionCall> = vec![];
+        // (our outside sender address, the outside call)
+        let mut outside_calls: Vec<(Felt, FunctionCall)> = vec![];
+
+        let mut offset = 0;
+        for _ in 0..calls_len {
+            let to_offset = offset + 1;
+            let selector_offset = to_offset + 1;
+            let calldata_offset = selector_offset + 2;
+            let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+
+            let call = FunctionCall {
+                contract_address: calldata[to_offset],
+                entry_point_selector: calldata[selector_offset],
+                calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+            };
+
+            if call.entry_point_selector == selector!("execute_from_outside_v3") {
+                let outside_calls_len: usize = calldata[calldata_offset + 5].try_into().unwrap();
+                for _ in 0..outside_calls_len {
+                    let to_offset = calldata_offset + 6;
+                    let selector_offset = to_offset + 1;
+                    let calldata_offset = selector_offset + 2;
+                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                    let outside_call = FunctionCall {
+                        contract_address: calldata[to_offset],
+                        entry_point_selector: calldata[selector_offset],
+                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                            .to_vec(),
+                    };
+                    outside_calls.push((call.contract_address, outside_call));
+                }
+            } else if call.entry_point_selector == selector!("execute_from_outside_v2") {
+                // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
+                let outside_calls_len: usize = calldata[calldata_offset + 4].try_into().unwrap();
+                for _ in 0..outside_calls_len {
+                    let to_offset = calldata_offset + 5;
+                    let selector_offset = to_offset + 1;
+                    let calldata_offset = selector_offset + 2;
+                    let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                    let outside_call = FunctionCall {
+                        contract_address: calldata[to_offset],
+                        entry_point_selector: calldata[selector_offset],
+                        calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                            .to_vec(),
+                    };
+                    outside_calls.push((call.contract_address, outside_call));
+                }
+            }
+
+            calls.push(call);
+            offset += 3 + calldata_len;
+        }
+
+        // Store each call in the transaction_calls table
+        for call in calls {
+            self.executor.send(QueryMessage::other(
+                "INSERT OR IGNORE INTO transaction_calls (transaction_hash, contract_address, entry_point_selector, calldata, call_type, caller_address) \
+                 VALUES (?, ?, ?, ?, ?, ?)"
+                    .to_string(),
+                vec![
+                    transaction_hash.clone(),
+                    Argument::FieldElement(call.contract_address),
+                    Argument::FieldElement(call.entry_point_selector),
+                    Argument::String(felts_to_sql_string(&call.calldata)),
+                    Argument::String("EXECUTE".to_string()),
+                    sender_address.clone(),
+                ],
+            ))?;
+        }
+
+        // Store each outside call in the transaction_calls table
+        for outside_call in outside_calls {
+            self.executor.send(QueryMessage::other(
+                "INSERT OR IGNORE INTO transaction_calls (transaction_hash, contract_address, entry_point_selector, calldata, call_type, caller_address) \
+                 VALUES (?, ?, ?, ?, ?, ?)"
+                    .to_string(),
+                vec![
+                    transaction_hash.clone(),
+                    Argument::FieldElement(outside_call.1.contract_address),
+                    Argument::FieldElement(outside_call.1.entry_point_selector),
+                    Argument::String(felts_to_sql_string(&outside_call.1.calldata)),
+                    Argument::String("EXECUTE_FROM_OUTSIDE".to_string()),
+                    Argument::FieldElement(outside_call.0),
+                ],
+            ))?;
+        }
path: crates/torii/sqlite/src/lib.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Need to fix formatting in outside calls storage.**

The implementation for storing outside calls is correct, but the pipeline reports a formatting issue.


Run `rustfmt` on the file to fix the formatting issues on line 669.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 669-669: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-17T09:53:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -572,6 +574,100 @@
             ))?;
         }
 
+        // Parse and store calls for INVOKE transactions
+        if transaction_type != "INVOKE" {
+            return Ok(());
+        }
path: crates/torii/sqlite/src/lib.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Logic added to process only INVOKE transactions.**

Good check to only process INVOKE transactions for call parsing. However, there's a formatting issue reported by the pipeline.


```diff
-        // Parse and store calls for INVOKE transactions
-        if transaction_type != "INVOKE" {
-            return Ok(());
-        }
+        // Parse and store calls for INVOKE transactions
+        if transaction_type != "INVOKE" {
+            return Ok(());
+        }
```

Run `rustfmt` to fix the formatting issues.


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: ci</summary>

[error] 580-580: Rust formatting check failed. Please run 'rustfmt' to format the code.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-17T09:53:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,3 +153,65 @@ impl LocalCache {
         self.token_id_registry.write().await.insert(token_id);
     }
 }
+
+#[derive(Debug)]
+pub struct ContractClassCache<P: Provider + Sync + std::fmt::Debug> {
+    pub classes: RwLock<HashMap<Felt, (Felt, ContractClass)>>,
+    pub provider: Arc<P>,
+}
+
+impl<P: Provider + Sync + std::fmt::Debug> ContractClassCache<P> {
+    pub fn new(provider: Arc<P>) -> Self {
+        Self { classes: RwLock::new(HashMap::new()), provider }
+    }
+
+    pub async fn get(
+        &self,
+        contract_address: Felt,
+        block_id: BlockId,
+    ) -> Result<ContractClass, Error> {
+        let classes = self.classes.read().await;
+        if let Some(class) = classes.get(&contract_address) {
+            return Ok(class.1.clone());
+        }
+
+        let class_hash = self.provider.get_class_hash_at(block_id, contract_address).await.unwrap();
+        let class = self.provider.get_class(block_id, class_hash).await.unwrap();
+        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
+        Ok(class)
+    }
+}
path: crates/torii/sqlite/src/cache.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Error handling could be improved in get method, sensei.**

The method currently uses `.unwrap()` on provider operations which could lead to panics if external operations fail.


Consider propagating errors with the ? operator instead of using unwrap:

```diff
pub async fn get(
    &self,
    contract_address: Felt,
    block_id: BlockId,
) -> Result<ContractClass, Error> {
    let classes = self.classes.read().await;
    if let Some(class) = classes.get(&contract_address) {
        return Ok(class.1.clone());
    }

-    let class_hash = self.provider.get_class_hash_at(block_id, contract_address).await.unwrap();
-    let class = self.provider.get_class(block_id, class_hash).await.unwrap();
+    let class_hash = self.provider.get_class_hash_at(block_id, contract_address).await?;
+    let class = self.provider.get_class(block_id, class_hash).await?;
    self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
    Ok(class)
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    pub async fn get(
        &self,
        contract_address: Felt,
        block_id: BlockId,
    ) -> Result<ContractClass, Error> {
        let classes = self.classes.read().await;
        if let Some(class) = classes.get(&contract_address) {
            return Ok(class.1.clone());
        }
    
        let class_hash = self.provider.get_class_hash_at(block_id, contract_address).await?;
        let class = self.provider.get_class(block_id, class_hash).await?;
        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
        Ok(class)
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 064a74f
created_at: 2025-03-17T11:12:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,3 +153,65 @@ impl LocalCache {
         self.token_id_registry.write().await.insert(token_id);
     }
 }
+
+#[derive(Debug)]
+pub struct ContractClassCache<P: Provider + Sync + std::fmt::Debug> {
+    pub classes: RwLock<HashMap<Felt, (Felt, ContractClass)>>,
+    pub provider: Arc<P>,
+}
+
+impl<P: Provider + Sync + std::fmt::Debug> ContractClassCache<P> {
+    pub fn new(provider: Arc<P>) -> Self {
+        Self { classes: RwLock::new(HashMap::new()), provider }
+    }
+
+    pub async fn get(
+        &self,
+        contract_address: Felt,
+        block_id: BlockId,
+    ) -> Result<ContractClass, Error> {
+        let classes = self.classes.read().await;
+        if let Some(class) = classes.get(&contract_address) {
+            return Ok(class.1.clone());
+        }
+
+        let class_hash = self.provider.get_class_hash_at(block_id, contract_address).await.unwrap();
+        let class = self.provider.get_class(block_id, class_hash).await.unwrap();
+        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
+        Ok(class)
+    }
+}
+
+pub fn get_entrypoint_name_from_class(class: &ContractClass, selector: Felt) -> Option<String> {
+    match class {
+        ContractClass::Sierra(sierra) => {
+            let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi).unwrap();
+            abi.iter().find_map(|entry| match entry {
+                AbiEntry::Function(function) => {
+                    if get_selector_from_name(&function.name).unwrap() == selector {
+                        Some(function.name.clone())
+                    } else {
+                        None
+                    }
+                }
+                _ => None,
+            })
+        }
+        ContractClass::Legacy(legacy) => {
+            if let Some(abi) = &legacy.abi {
+                abi.iter().find_map(|entry| match entry {
+                    LegacyContractAbiEntry::Function(function) => {
+                        if get_selector_from_name(&function.name).unwrap() == selector {
+                            Some(function.name.clone())
+                        } else {
+                            None
+                        }
+                    }
+                    _ => None,
+                })
+            } else {
+                None
+            }
+        }
+    }
+}
path: crates/torii/sqlite/src/cache.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve error handling in ABI parsing, sensei.**

The function uses `.unwrap()` when parsing the Sierra ABI, which could panic on invalid JSON.


Consider handling the parsing error gracefully:

```diff
pub fn get_entrypoint_name_from_class(class: &ContractClass, selector: Felt) -> Option<String> {
    match class {
        ContractClass::Sierra(sierra) => {
-            let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi).unwrap();
+            let abi: Vec<AbiEntry> = match serde_json::from_str(&sierra.abi) {
+                Ok(abi) => abi,
+                Err(_) => return None,
+            };
            abi.iter().find_map(|entry| match entry {
                AbiEntry::Function(function) => {
-                    if get_selector_from_name(&function.name).unwrap() == selector {
+                    if get_selector_from_name(&function.name).ok()? == selector {
                        Some(function.name.clone())
                    } else {
                        None
                    }
                }
                _ => None,
            })
        }
        ContractClass::Legacy(legacy) => {
            if let Some(abi) = &legacy.abi {
                abi.iter().find_map(|entry| match entry {
                    LegacyContractAbiEntry::Function(function) => {
-                        if get_selector_from_name(&function.name).unwrap() == selector {
+                        if get_selector_from_name(&function.name).ok()? == selector {
                            Some(function.name.clone())
                        } else {
                            None
                        }
                    }
                    _ => None,
                })
            } else {
                None
            }
        }
    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
pub fn get_entrypoint_name_from_class(class: &ContractClass, selector: Felt) -> Option<String> {
    match class {
        ContractClass::Sierra(sierra) => {
            let abi: Vec<AbiEntry> = match serde_json::from_str(&sierra.abi) {
                Ok(abi) => abi,
                Err(_) => return None,
            };
            abi.iter().find_map(|entry| match entry {
                AbiEntry::Function(function) => {
                    if get_selector_from_name(&function.name).ok()? == selector {
                        Some(function.name.clone())
                    } else {
                        None
                    }
                }
                _ => None,
            })
        }
        ContractClass::Legacy(legacy) => {
            if let Some(abi) = &legacy.abi {
                abi.iter().find_map(|entry| match entry {
                    LegacyContractAbiEntry::Function(function) => {
                        if get_selector_from_name(&function.name).ok()? == selector {
                            Some(function.name.clone())
                        } else {
                            None
                        }
                    }
                    _ => None,
                })
            } else {
                None
            }
        }
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 064a74f
created_at: 2025-03-17T11:12:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -504,59 +506,32 @@ impl Sql {
 
     pub fn store_transaction(
         &mut self,
-        transaction: &Transaction,
+        transaction_hash: Felt,
+        sender_address: Felt,
+        calldata: &[Felt],
+        max_fee: Felt,
+        signature: &[Felt],
+        nonce: Felt,
         block_number: u64,
         contract_addresses: &HashSet<Felt>,
+        transaction_type: &str,
         block_timestamp: u64,
+        calls: &[ParsedCall],
path: crates/torii/sqlite/src/lib.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Ohayo sensei, verify the column-to-argument mapping in the INSERT statement!**

The query at lines 524-526 includes 10 columns – the first column is `id` – but only 9 arguments are passed. This mismatch can inadvertently shift the arguments, causing incorrect data to be inserted.



Below is a suggested fix to explicitly provide the `id` argument. If you intend for `transaction_hash` to serve as both `id` and `transaction_hash` columns, pass it twice:

```diff
 "INSERT OR IGNORE INTO transactions (id, transaction_hash, sender_address, calldata, \
 max_fee, signature, nonce, transaction_type, executed_at, block_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
 ...
 vec![
+    Argument::FieldElement(transaction_hash), // Use transaction_hash as the 'id' column
     Argument::FieldElement(transaction_hash),
     Argument::FieldElement(sender_address),
     Argument::String(felts_to_sql_string(calldata)),
     Argument::FieldElement(max_fee),
     Argument::String(felts_to_sql_string(signature)),
     Argument::FieldElement(nonce),
     Argument::String(transaction_type.to_string()),
     Argument::String(utc_dt_string_from_timestamp(block_timestamp)),
     Argument::String(block_number.to_string()),
 ]
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
"INSERT OR IGNORE INTO transactions (id, transaction_hash, sender_address, calldata, \
max_fee, signature, nonce, transaction_type, executed_at, block_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
...
vec![
    Argument::FieldElement(transaction_hash), // Use transaction_hash as the 'id' column
    Argument::FieldElement(transaction_hash),
    Argument::FieldElement(sender_address),
    Argument::String(felts_to_sql_string(calldata)),
    Argument::FieldElement(max_fee),
    Argument::String(felts_to_sql_string(signature)),
    Argument::FieldElement(nonce),
    Argument::String(transaction_type.to_string()),
    Argument::String(utc_dt_string_from_timestamp(block_timestamp)),
    Argument::String(block_number.to_string()),
]
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-17T11:12:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,8 +28,146 @@ impl<P: Provider + Sync + std::fmt::Debug> TransactionProcessor<P> for StoreTran
         _transaction_hash: Felt,
         contract_addresses: &HashSet<Felt>,
         transaction: &Transaction,
+        contract_class_cache: &ContractClassCache<P>,
     ) -> Result<(), Error> {
-        db.store_transaction(transaction, block_number, contract_addresses, block_timestamp)?;
+        let transaction_type = match transaction {
+            Transaction::Invoke(_) => "INVOKE",
+            Transaction::L1Handler(_) => "L1_HANDLER",
+            _ => return Ok(()),
+        };
+
+        let (transaction_hash, sender_address, calldata, max_fee, signature, nonce) =
+            match transaction {
+                Transaction::Invoke(InvokeTransaction::V3(invoke_v3_transaction)) => (
+                    invoke_v3_transaction.transaction_hash,
+                    invoke_v3_transaction.sender_address,
+                    &invoke_v3_transaction.calldata,
+                    Felt::ZERO, // has no max_fee
+                    &invoke_v3_transaction.signature,
+                    invoke_v3_transaction.nonce,
+                ),
+                Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => (
+                    invoke_v1_transaction.transaction_hash,
+                    invoke_v1_transaction.sender_address,
+                    &invoke_v1_transaction.calldata,
+                    invoke_v1_transaction.max_fee,
+                    &invoke_v1_transaction.signature,
+                    invoke_v1_transaction.nonce,
+                ),
+                Transaction::L1Handler(l1_handler_transaction) => (
+                    l1_handler_transaction.transaction_hash,
+                    l1_handler_transaction.contract_address,
+                    &l1_handler_transaction.calldata,
+                    Felt::ZERO, // has no max_fee
+                    &vec![],    // has no signature
+                    l1_handler_transaction.nonce.into(),
+                ),
+                _ => return Ok(()),
+            };
+
+        let mut calls: Vec<ParsedCall> = vec![];
+
+        if transaction_type == "INVOKE" {
+            let calls_len: usize = calldata[0].try_into().unwrap();
+            let mut offset = 0;
+            for _ in 0..calls_len {
+                let to_offset = offset + 1;
+                let selector_offset = to_offset + 1;
+                let calldata_offset = selector_offset + 2;
+                let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                let contract_address = calldata[to_offset];
+                let contract_class = contract_class_cache
+                    .get(contract_address, BlockId::Number(block_number))
+                    .await?;
+                let entrypoint =
+                    get_entrypoint_name_from_class(&contract_class, calldata[selector_offset])
+                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));
+
+                let call = ParsedCall {
+                    contract_address,
+                    entrypoint,
+                    calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+                    call_type: CallType::Execute,
+                    caller_address: sender_address,
+                };
+
+                if call.entrypoint == "execute_from_outside_v3" {
+                    let outside_calls_len: usize =
+                        calldata[calldata_offset + 5].try_into().unwrap();
+                    for _ in 0..outside_calls_len {
+                        let to_offset = calldata_offset + 6;
+                        let selector_offset = to_offset + 1;
+                        let calldata_offset = selector_offset + 2;
+                        let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                        let contract_address = calldata[to_offset];
+                        let contract_class = contract_class_cache
+                            .get(contract_address, BlockId::Number(block_number))
+                            .await?;
+                        let entrypoint = get_entrypoint_name_from_class(
+                            &contract_class,
+                            calldata[selector_offset],
+                        )
+                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));
+
+                        let outside_call = ParsedCall {
+                            contract_address,
+                            entrypoint,
+                            calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                                .to_vec(),
+                            call_type: CallType::ExecuteFromOutside,
+                            caller_address: call.contract_address,
+                        };
+                        calls.push(outside_call);
+                    }
+                } else if call.entrypoint == "execute_from_outside_v2" {
+                    // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
+                    let outside_calls_len: usize =
+                        calldata[calldata_offset + 4].try_into().unwrap();
+                    for _ in 0..outside_calls_len {
+                        let to_offset = calldata_offset + 5;
+                        let selector_offset = to_offset + 1;
+                        let calldata_offset = selector_offset + 2;
+                        let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                        let contract_address = calldata[to_offset];
+                        let contract_class = contract_class_cache
+                            .get(contract_address, BlockId::Number(block_number))
+                            .await?;
+                        let entrypoint = get_entrypoint_name_from_class(
+                            &contract_class,
+                            calldata[selector_offset],
+                        )
+                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));
+
+                        let outside_call = ParsedCall {
+                            contract_address,
+                            entrypoint,
+                            calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                                .to_vec(),
+                            call_type: CallType::ExecuteFromOutside,
+                            caller_address: call.contract_address,
+                        };
+                        calls.push(outside_call);
+                    }
+                }
+
+                calls.push(call);
+                offset += 3 + calldata_len;
+            }
+        }
path: crates/torii/indexer/src/processors/store_transaction.rs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Good implementation of transaction call parsing, but needs bounds checking, sensei.**

The current implementation accesses array elements directly without bounds checking, which could lead to panics if the calldata is malformed or shorter than expected. Consider adding bounds checking before accessing array elements or use safe access methods like `get()`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any existing error handling for calldata access in the codebase
rg "unwrap\(\)" --type rust crates/torii/indexer/src/processors/ -A 1 -B 1
```

Length of output: 4535

---



</details>

**Ohayo sensei, let's add those bounds checks!**

The transaction call parsing logic in `crates/torii/indexer/src/processors/store_transaction.rs` is structured nicely. However, as it stands, it directly indexes into the `calldata` array without verifying that the indices are within range. This approach (using constructs like `calldata[0].try_into().unwrap()`) could lead to panics if the input `calldata` is malformed or shorter than expected. While other modules in the codebase also use similar `unwrap` calls, enhancing safety here would be beneficial for robustness.

- **Please consider:**
  - Adding explicit bounds checking before each array access (or using safe methods like `.get()`).
  - Revisiting the offset calculations in both the primary and nested loops to ensure they don't exceed `calldata`’s length.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-17T14:49:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,8 +28,146 @@ impl<P: Provider + Sync + std::fmt::Debug> TransactionProcessor<P> for StoreTran
         _transaction_hash: Felt,
         contract_addresses: &HashSet<Felt>,
         transaction: &Transaction,
+        contract_class_cache: &ContractClassCache<P>,
     ) -> Result<(), Error> {
-        db.store_transaction(transaction, block_number, contract_addresses, block_timestamp)?;
+        let transaction_type = match transaction {
+            Transaction::Invoke(_) => "INVOKE",
+            Transaction::L1Handler(_) => "L1_HANDLER",
+            _ => return Ok(()),
+        };
+
+        let (transaction_hash, sender_address, calldata, max_fee, signature, nonce) =
+            match transaction {
+                Transaction::Invoke(InvokeTransaction::V3(invoke_v3_transaction)) => (
+                    invoke_v3_transaction.transaction_hash,
+                    invoke_v3_transaction.sender_address,
+                    &invoke_v3_transaction.calldata,
+                    Felt::ZERO, // has no max_fee
+                    &invoke_v3_transaction.signature,
+                    invoke_v3_transaction.nonce,
+                ),
+                Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => (
+                    invoke_v1_transaction.transaction_hash,
+                    invoke_v1_transaction.sender_address,
+                    &invoke_v1_transaction.calldata,
+                    invoke_v1_transaction.max_fee,
+                    &invoke_v1_transaction.signature,
+                    invoke_v1_transaction.nonce,
+                ),
+                Transaction::L1Handler(l1_handler_transaction) => (
+                    l1_handler_transaction.transaction_hash,
+                    l1_handler_transaction.contract_address,
+                    &l1_handler_transaction.calldata,
+                    Felt::ZERO, // has no max_fee
+                    &vec![],    // has no signature
+                    l1_handler_transaction.nonce.into(),
+                ),
+                _ => return Ok(()),
+            };
+
+        let mut calls: Vec<ParsedCall> = vec![];
+
+        if transaction_type == "INVOKE" {
+            let calls_len: usize = calldata[0].try_into().unwrap();
+            let mut offset = 0;
+            for _ in 0..calls_len {
+                let to_offset = offset + 1;
+                let selector_offset = to_offset + 1;
+                let calldata_offset = selector_offset + 2;
+                let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                let contract_address = calldata[to_offset];
+                let contract_class = contract_class_cache
+                    .get(contract_address, BlockId::Number(block_number))
+                    .await?;
+                let entrypoint =
+                    get_entrypoint_name_from_class(&contract_class, calldata[selector_offset])
+                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));
+
+                let call = ParsedCall {
+                    contract_address,
+                    entrypoint,
+                    calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
+                    call_type: CallType::Execute,
+                    caller_address: sender_address,
+                };
+
+                if call.entrypoint == "execute_from_outside_v3" {
+                    let outside_calls_len: usize =
+                        calldata[calldata_offset + 5].try_into().unwrap();
+                    for _ in 0..outside_calls_len {
+                        let to_offset = calldata_offset + 6;
+                        let selector_offset = to_offset + 1;
+                        let calldata_offset = selector_offset + 2;
+                        let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                        let contract_address = calldata[to_offset];
+                        let contract_class = contract_class_cache
+                            .get(contract_address, BlockId::Number(block_number))
+                            .await?;
+                        let entrypoint = get_entrypoint_name_from_class(
+                            &contract_class,
+                            calldata[selector_offset],
+                        )
+                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));
+
+                        let outside_call = ParsedCall {
+                            contract_address,
+                            entrypoint,
+                            calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                                .to_vec(),
+                            call_type: CallType::ExecuteFromOutside,
+                            caller_address: call.contract_address,
+                        };
+                        calls.push(outside_call);
+                    }
+                } else if call.entrypoint == "execute_from_outside_v2" {
+                    // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
+                    let outside_calls_len: usize =
+                        calldata[calldata_offset + 4].try_into().unwrap();
+                    for _ in 0..outside_calls_len {
+                        let to_offset = calldata_offset + 5;
+                        let selector_offset = to_offset + 1;
+                        let calldata_offset = selector_offset + 2;
+                        let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                        let contract_address = calldata[to_offset];
+                        let contract_class = contract_class_cache
+                            .get(contract_address, BlockId::Number(block_number))
+                            .await?;
+                        let entrypoint = get_entrypoint_name_from_class(
+                            &contract_class,
+                            calldata[selector_offset],
+                        )
+                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));
+
+                        let outside_call = ParsedCall {
+                            contract_address,
+                            entrypoint,
+                            calldata: calldata[calldata_offset..calldata_offset + calldata_len]
+                                .to_vec(),
+                            call_type: CallType::ExecuteFromOutside,
+                            caller_address: call.contract_address,
+                        };
+                        calls.push(outside_call);
+                    }
+                }
+
+                calls.push(call);
+                offset += 3 + calldata_len;
+            }
path: crates/torii/indexer/src/processors/store_transaction.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding error handling for malformed calldata, sensei.**

The current implementation uses `unwrap()` in several places when parsing calldata structures. This could lead to runtime panics if the calldata format doesn't match expectations, which might happen with malicious transactions or protocol changes.


I'd recommend adding proper error handling for malformed calldata:

```diff
- let calls_len: usize = calldata[0].try_into().unwrap();
+ let calls_len: usize = calldata.get(0)
+     .ok_or_else(|| anyhow::anyhow!("Missing calls length in calldata"))?
+     .try_into()
+     .map_err(|_| anyhow::anyhow!("Invalid calls length in calldata"))?;

- let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+ let calldata_len: usize = calldata.get(selector_offset + 1)
+     .ok_or_else(|| anyhow::anyhow!("Missing calldata length"))?
+     .try_into()
+     .map_err(|_| anyhow::anyhow!("Invalid calldata length"))?;
```

This approach would provide more helpful error messages and prevent application crashes.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        if transaction_type == "INVOKE" {
-            let calls_len: usize = calldata[0].try_into().unwrap();
+            let calls_len: usize = calldata.get(0)
+                .ok_or_else(|| anyhow::anyhow!("Missing calls length in calldata"))?
+                .try_into()
+                .map_err(|_| anyhow::anyhow!("Invalid calls length in calldata"))?;
            let mut offset = 0;
            for _ in 0..calls_len {
                let to_offset = offset + 1;
                let selector_offset = to_offset + 1;
                let calldata_offset = selector_offset + 2;
-                let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
+                let calldata_len: usize = calldata.get(selector_offset + 1)
+                    .ok_or_else(|| anyhow::anyhow!("Missing calldata length"))?
+                    .try_into()
+                    .map_err(|_| anyhow::anyhow!("Invalid calldata length"))?;
                let contract_address = calldata[to_offset];
                let contract_class = contract_class_cache
                    .get(contract_address, BlockId::Number(block_number))
                    .await?;
                let entrypoint =
                    get_entrypoint_name_from_class(&contract_class, calldata[selector_offset])
                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));

                let call = ParsedCall {
                    contract_address,
                    entrypoint,
                    calldata: calldata[calldata_offset..calldata_offset + calldata_len].to_vec(),
                    call_type: CallType::Execute,
                    caller_address: sender_address,
                };

                if call.entrypoint == "execute_from_outside_v3" {
                    let outside_calls_len: usize =
                        calldata[calldata_offset + 5].try_into().unwrap();
                    for _ in 0..outside_calls_len {
                        let to_offset = calldata_offset + 6;
                        let selector_offset = to_offset + 1;
                        let calldata_offset = selector_offset + 2;
                        let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
                        let contract_address = calldata[to_offset];
                        let contract_class = contract_class_cache
                            .get(contract_address, BlockId::Number(block_number))
                            .await?;
                        let entrypoint = get_entrypoint_name_from_class(
                            &contract_class,
                            calldata[selector_offset],
                        )
                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));

                        let outside_call = ParsedCall {
                            contract_address,
                            entrypoint,
                            calldata: calldata[calldata_offset..calldata_offset + calldata_len]
                                .to_vec(),
                            call_type: CallType::ExecuteFromOutside,
                            caller_address: call.contract_address,
                        };
                        calls.push(outside_call);
                    }
                } else if call.entrypoint == "execute_from_outside_v2" {
                    // the execute_from_outside_v2 nonce is only a felt, thus we have a 4 offset
                    let outside_calls_len: usize =
                        calldata[calldata_offset + 4].try_into().unwrap();
                    for _ in 0..outside_calls_len {
                        let to_offset = calldata_offset + 5;
                        let selector_offset = to_offset + 1;
                        let calldata_offset = selector_offset + 2;
                        let calldata_len: usize = calldata[selector_offset + 1].try_into().unwrap();
                        let contract_address = calldata[to_offset];
                        let contract_class = contract_class_cache
                            .get(contract_address, BlockId::Number(block_number))
                            .await?;
                        let entrypoint = get_entrypoint_name_from_class(
                            &contract_class,
                            calldata[selector_offset],
                        )
                        .unwrap_or(format!("{:#x}", calldata[selector_offset]));

                        let outside_call = ParsedCall {
                            contract_address,
                            entrypoint,
                            calldata: calldata[calldata_offset..calldata_offset + calldata_len]
                                .to_vec(),
                            call_type: CallType::ExecuteFromOutside,
                            caller_address: call.contract_address,
                        };
                        calls.push(outside_call);
                    }
                }

                calls.push(call);
                offset += 3 + calldata_len;
            }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 3a54560
created_at: 2025-03-17T14:49:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,6 +69,42 @@ pub async fn fetch_single_row(
     sqlx::query(&query).fetch_one(conn).await
 }
 
+pub async fn fetch_single_row_with_joins(
+    conn: &mut SqliteConnection,
+    table_name: &str,
+    id_column: &str,
+    id: &str,
+    joins: Vec<JoinConfig>,
+    select_columns: Option<Vec<String>>,
+) -> sqlx::Result<SqliteRow> {
+    // Build the SELECT clause
+    let select = match select_columns {
+        Some(columns) => columns.join(", "),
+        None => format!("[{}].*", table_name),
+    };
+
+    // Build the JOIN clauses
+    let join_clauses = joins
+        .iter()
+        .map(|join| {
+            let table_ref = match &join.alias {
+                Some(alias) => format!("[{}] AS {}", join.table, alias),
+                None => format!("[{}]", join.table),
+            };
+            format!("{} {} ON {}", join.join_type.as_sql(), table_ref, join.on_condition)
+        })
+        .collect::<Vec<String>>()
+        .join(" ");
+
+    // Build the complete query
+    let query = format!(
+        "SELECT {} FROM [{}] {} WHERE [{}].{} = '{}'",
+        select, table_name, join_clauses, table_name, id_column, id
+    );
+
+    sqlx::query(&query).fetch_one(conn).await
+}
path: crates/torii/graphql/src/query/data.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Ohayo sensei! Be cautious about potential SQL injection in the join query builder.**

While the function builds SQL queries dynamically, it directly interpolates values from the `JoinConfig` struct into the query string. Consider parameterizing these values where possible or implementing additional sanitization to prevent potential SQL injection attacks.

```diff
- let query = format!(
-     "SELECT {} FROM [{}] {} WHERE [{}].{} = '{}'",
-     select, table_name, join_clauses, table_name, id_column, id
- );
+ let query = format!(
+     "SELECT {} FROM [{}] {} WHERE [{}].{} = ?",
+     select, table_name, join_clauses, table_name, id_column
+ );
+ sqlx::query(&query).bind(id).fetch_one(conn).await
```




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
pub async fn fetch_single_row_with_joins(
    conn: &mut SqliteConnection,
    table_name: &str,
    id_column: &str,
    id: &str,
    joins: Vec<JoinConfig>,
    select_columns: Option<Vec<String>>,
) -> sqlx::Result<SqliteRow> {
    // Build the SELECT clause
    let select = match select_columns {
        Some(columns) => columns.join(", "),
        None => format!("[{}].*", table_name),
    };

    // Build the JOIN clauses
    let join_clauses = joins
        .iter()
        .map(|join| {
            let table_ref = match &join.alias {
                Some(alias) => format!("[{}] AS {}", join.table, alias),
                None => format!("[{}]", join.table),
            };
            format!("{} {} ON {}", join.join_type.as_sql(), table_ref, join.on_condition)
        })
        .collect::<Vec<String>>()
        .join(" ");

    // Build the complete query
    let query = format!(
        "SELECT {} FROM [{}] {} WHERE [{}].{} = ?",
        select, table_name, join_clauses, table_name, id_column
    );

    sqlx::query(&query).bind(id).fetch_one(conn).await
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T03:05:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,4 +80,138 @@ impl ResolvableObject for TransactionObject {
 
         vec![resolve_one, resolve_many]
     }
+
+    fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
+        Some(vec![
+            SubscriptionField::new("transaction", TypeRef::named_nn(self.type_name()), |ctx| {
+                SubscriptionFieldFuture::new(async move {
+                    let hash = match ctx.args.get("hash") {
+                        Some(hash) => Some(hash.string()?.to_string()),
+                        None => None,
+                    };
+                    // if hash is None, then subscribe to all transactions
+                    // if hash is Some, then subscribe to only the transaction with that hash
+                    Ok(SimpleBroker::<Transaction>::subscribe().filter_map(
+                        move |transaction: Transaction| {
+                            if hash.is_none() || hash == Some(transaction.transaction_hash.clone())
+                            {
+                                Some(Ok(Value::Object(TransactionObject::value_mapping(
+                                    transaction,
+                                ))))
+                            } else {
+                                // hash != transaction.transaction_hash, then don't send anything,
+                                // still listening
+                                None
+                            }
+                        },
+                    ))
+                })
+            })
+            .argument(InputValue::new("hash", TypeRef::named(TypeRef::ID))),
+        ])
+    }
+}
+
+impl TransactionObject {
+    pub fn value_mapping(transaction: Transaction) -> ValueMapping {
+        async_graphql::dynamic::indexmap::IndexMap::from([
+            (Name::new("id"), Value::from(transaction.id)),
+            (Name::new("transactionHash"), Value::from(transaction.transaction_hash)),
+            (Name::new("senderAddress"), Value::from(transaction.sender_address)),
+            (Name::new("calldata"), Value::from(transaction.calldata)),
+            (Name::new("maxFee"), Value::from(transaction.max_fee)),
+            (Name::new("signature"), Value::from(transaction.signature)),
+            (Name::new("nonce"), Value::from(transaction.nonce)),
+            (Name::new("executedAt"), Value::from(transaction.executed_at.to_rfc3339())),
+            (Name::new("createdAt"), Value::from(transaction.created_at.to_rfc3339())),
+            (Name::new("transactionType"), Value::from(transaction.transaction_type)),
+            (Name::new("blockNumber"), Value::from(transaction.block_number)),
+        ])
+    }
+}
+
+fn calls_field() -> Field {
+    Field::new("calls", TypeRef::named_list(CALL_TYPE_NAME), move |ctx| {
+        FieldFuture::new(async move {
+            match ctx.parent_value.try_to_value()? {
+                Value::Object(indexmap) => {
+                    let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+
+                    let transaction_hash = utils::extract::<String>(indexmap, "transactionHash")?;
+
+                    // Fetch all function calls for this transaction
+                    let query = &format!(
+                        "SELECT * FROM {TRANSACTION_CALLS_TABLE} WHERE transaction_hash = ?"
+                    );
+                    let rows =
+                        sqlx::query(query).bind(&transaction_hash).fetch_all(&mut *conn).await?;
+
+                    let results = rows
+                        .iter()
+                        .map(|row| {
+                            value_mapping_from_row(&row, &CALL_MAPPING, false, true)
+                                .map(|value| Value::Object(value))
+                        })
+                        .collect::<Result<Vec<_>, _>>()?;
+
+                    Ok(Some(Value::List(results)))
+                }
+                _ => Err("incorrect value, requires Value::Object".into()),
+            }
+        })
+    })
+}
+
+fn token_transfers_field() -> Field {
+    Field::new("tokenTransfers", TypeRef::named_list(TOKEN_TRANSFER_TYPE_NAME), move |ctx| {
+        FieldFuture::new(async move {
+            match ctx.parent_value.try_to_value()? {
+                Value::Object(indexmap) => {
+                    let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+
+                    let transaction_hash = utils::extract::<String>(indexmap, "transactionHash")?;
+
+                    // Fetch all token transfers for this transaction
+                    let query = format!(
+                        r#"
+                        SELECT 
+                            et.id,
+                            et.contract_address,
+                            et.from_address,
+                            et.to_address,
+                            et.amount,
+                            et.token_id,
+                            et.executed_at,
+                            t.name,
+                            t.symbol,
+                            t.decimals,
+                            c.contract_type,
+                            t.metadata
+                        FROM
+                            {TOKEN_TRANSFER_TABLE} et
+                        JOIN
+                            tokens t ON et.token_id = t.id
+                        JOIN
+                            contracts c ON t.contract_address = c.contract_address
+                        WHERE
+                            et.event_id LIKE '%:{transaction_hash}:%'
+                        "#
+                    );
+
+                    let rows =
+                        sqlx::query(&query).bind(&transaction_hash).fetch_all(&mut *conn).await?;
path: crates/torii/graphql/src/object/transaction.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Make sure SQL bindings match query parameters**

The query uses string interpolation for transaction_hash (which is unsafe) but then also tries to bind it as a parameter. When fixing the LIKE clause, ensure you're using the bound parameter correctly.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T03:05:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,4 +80,138 @@ impl ResolvableObject for TransactionObject {
 
         vec![resolve_one, resolve_many]
     }
+
+    fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
+        Some(vec![
+            SubscriptionField::new("transaction", TypeRef::named_nn(self.type_name()), |ctx| {
+                SubscriptionFieldFuture::new(async move {
+                    let hash = match ctx.args.get("hash") {
+                        Some(hash) => Some(hash.string()?.to_string()),
+                        None => None,
+                    };
+                    // if hash is None, then subscribe to all transactions
+                    // if hash is Some, then subscribe to only the transaction with that hash
+                    Ok(SimpleBroker::<Transaction>::subscribe().filter_map(
+                        move |transaction: Transaction| {
+                            if hash.is_none() || hash == Some(transaction.transaction_hash.clone())
+                            {
+                                Some(Ok(Value::Object(TransactionObject::value_mapping(
+                                    transaction,
+                                ))))
+                            } else {
+                                // hash != transaction.transaction_hash, then don't send anything,
+                                // still listening
+                                None
+                            }
+                        },
+                    ))
+                })
+            })
+            .argument(InputValue::new("hash", TypeRef::named(TypeRef::ID))),
+        ])
+    }
+}
+
+impl TransactionObject {
+    pub fn value_mapping(transaction: Transaction) -> ValueMapping {
+        async_graphql::dynamic::indexmap::IndexMap::from([
+            (Name::new("id"), Value::from(transaction.id)),
+            (Name::new("transactionHash"), Value::from(transaction.transaction_hash)),
+            (Name::new("senderAddress"), Value::from(transaction.sender_address)),
+            (Name::new("calldata"), Value::from(transaction.calldata)),
+            (Name::new("maxFee"), Value::from(transaction.max_fee)),
+            (Name::new("signature"), Value::from(transaction.signature)),
+            (Name::new("nonce"), Value::from(transaction.nonce)),
+            (Name::new("executedAt"), Value::from(transaction.executed_at.to_rfc3339())),
+            (Name::new("createdAt"), Value::from(transaction.created_at.to_rfc3339())),
+            (Name::new("transactionType"), Value::from(transaction.transaction_type)),
+            (Name::new("blockNumber"), Value::from(transaction.block_number)),
+        ])
+    }
+}
+
+fn calls_field() -> Field {
+    Field::new("calls", TypeRef::named_list(CALL_TYPE_NAME), move |ctx| {
+        FieldFuture::new(async move {
+            match ctx.parent_value.try_to_value()? {
+                Value::Object(indexmap) => {
+                    let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+
+                    let transaction_hash = utils::extract::<String>(indexmap, "transactionHash")?;
+
+                    // Fetch all function calls for this transaction
+                    let query = &format!(
+                        "SELECT * FROM {TRANSACTION_CALLS_TABLE} WHERE transaction_hash = ?"
+                    );
+                    let rows =
+                        sqlx::query(query).bind(&transaction_hash).fetch_all(&mut *conn).await?;
+
+                    let results = rows
+                        .iter()
+                        .map(|row| {
+                            value_mapping_from_row(&row, &CALL_MAPPING, false, true)
+                                .map(|value| Value::Object(value))
+                        })
+                        .collect::<Result<Vec<_>, _>>()?;
+
+                    Ok(Some(Value::List(results)))
+                }
+                _ => Err("incorrect value, requires Value::Object".into()),
+            }
+        })
+    })
+}
+
+fn token_transfers_field() -> Field {
+    Field::new("tokenTransfers", TypeRef::named_list(TOKEN_TRANSFER_TYPE_NAME), move |ctx| {
+        FieldFuture::new(async move {
+            match ctx.parent_value.try_to_value()? {
+                Value::Object(indexmap) => {
+                    let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?;
+
+                    let transaction_hash = utils::extract::<String>(indexmap, "transactionHash")?;
+
+                    // Fetch all token transfers for this transaction
+                    let query = format!(
+                        r#"
+                        SELECT 
+                            et.id,
+                            et.contract_address,
+                            et.from_address,
+                            et.to_address,
+                            et.amount,
+                            et.token_id,
+                            et.executed_at,
+                            t.name,
+                            t.symbol,
+                            t.decimals,
+                            c.contract_type,
+                            t.metadata
+                        FROM
+                            {TOKEN_TRANSFER_TABLE} et
+                        JOIN
+                            tokens t ON et.token_id = t.id
+                        JOIN
+                            contracts c ON t.contract_address = c.contract_address
+                        WHERE
+                            et.event_id LIKE '%:{transaction_hash}:%'
+                        "#
+                    );
path: crates/torii/graphql/src/object/transaction.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Potential SQL injection in token transfers query!**

Ohayo, sensei! The SQL query is vulnerable to injection because you're directly interpolating the transaction_hash into the LIKE pattern instead of using a parameterized query.


Fix this by using parameters properly:

```diff
-                    let query = format!(
-                        r#"
-                        SELECT 
-                            et.id,
-                            et.contract_address,
-                            et.from_address,
-                            et.to_address,
-                            et.amount,
-                            et.token_id,
-                            et.executed_at,
-                            t.name,
-                            t.symbol,
-                            t.decimals,
-                            c.contract_type,
-                            t.metadata
-                        FROM
-                            {TOKEN_TRANSFER_TABLE} et
-                        JOIN
-                            tokens t ON et.token_id = t.id
-                        JOIN
-                            contracts c ON t.contract_address = c.contract_address
-                        WHERE
-                            et.event_id LIKE '%:{transaction_hash}:%'
-                        "#
-                    );
+                    let query = format!(
+                        r#"
+                        SELECT 
+                            et.id,
+                            et.contract_address,
+                            et.from_address,
+                            et.to_address,
+                            et.amount,
+                            et.token_id,
+                            et.executed_at,
+                            t.name,
+                            t.symbol,
+                            t.decimals,
+                            c.contract_type,
+                            t.metadata
+                        FROM
+                            {TOKEN_TRANSFER_TABLE} et
+                        JOIN
+                            tokens t ON et.token_id = t.id
+                        JOIN
+                            contracts c ON t.contract_address = c.contract_address
+                        WHERE
+                            et.event_id LIKE '%:' || ? || ':%'
+                        "#
+                    );
```

This ensures the transaction hash is properly escaped and handles any special characters safely.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                    let query = format!(
                        r#"
                        SELECT 
                            et.id,
                            et.contract_address,
                            et.from_address,
                            et.to_address,
                            et.amount,
                            et.token_id,
                            et.executed_at,
                            t.name,
                            t.symbol,
                            t.decimals,
                            c.contract_type,
                            t.metadata
                        FROM
                            {TOKEN_TRANSFER_TABLE} et
                        JOIN
                            tokens t ON et.token_id = t.id
                        JOIN
                            contracts c ON t.contract_address = c.contract_address
                        WHERE
                            et.event_id LIKE '%:' || ? || ':%'
                        "#
                    );
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T03:05:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,4 +83,154 @@ impl ResolvableObject for TransactionObject {
 
         vec![resolve_one, resolve_many]
     }
+
+    fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
+        Some(vec![
+            SubscriptionField::new("transaction", TypeRef::named_nn(self.type_name()), |ctx| {
+                SubscriptionFieldFuture::new(async move {
+                    let hash = match ctx.args.get("hash") {
+                        Some(hash) => Some(hash.string()?.to_string()),
+                        None => None,
+                    };
+
+                    let caller = match ctx.args.get("hasCaller") {
+                        Some(caller) => Some(caller.string()?.to_string()),
+                        None => None,
+                    };
+
+                    // if hash is None, then subscribe to all transactions
+                    // if hash is Some, then subscribe to only the transaction with that hash
+                    Ok(SimpleBroker::<Transaction>::subscribe().filter_map(
+                        move |transaction: Transaction| {
+                            if (hash.is_none()
+                                || hash == Some(transaction.transaction_hash.clone()))
+                                && (caller.is_none()
+                                    || transaction.calls.iter().any(|call| {
+                                        call.caller_address
+                                            == Felt::from_str(&caller.clone().unwrap()).unwrap()
+                                    }))
+                            {
+                                Some(Ok(Value::Object(TransactionObject::value_mapping(
+                                    transaction,
+                                ))))
+                            } else {
+                                // hash != transaction.transaction_hash, then don't send anything,
+                                // still listening
+                                None
+                            }
+                        },
+                    ))
+                })
+            })
+            .argument(InputValue::new("hash", TypeRef::named(TypeRef::ID)))
+            .argument(InputValue::new("hasCaller", TypeRef::named(TypeRef::STRING))),
+        ])
+    }
path: crates/torii/graphql/src/object/transaction.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Be careful with unwrapping in the subscriptions filter!**

Sensei, I noticed some potential issues in the subscription implementation:

1. The unwrapping of `caller.clone().unwrap()` on line 110 could cause a panic if caller is None but somehow passes the earlier check
2. The `Felt::from_str(...).unwrap()` call could panic if the caller string isn't a valid Felt value



```diff
- call.caller_address == Felt::from_str(&caller.clone().unwrap()).unwrap()
+ caller.as_ref().and_then(|c| Felt::from_str(c).ok())
+     .map_or(false, |addr| call.caller_address == addr)
```

This pattern safely handles both the unwrapping of the Option and the Result from parsing.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
        Some(vec![
            SubscriptionField::new("transaction", TypeRef::named_nn(self.type_name()), |ctx| {
                SubscriptionFieldFuture::new(async move {
                    let hash = match ctx.args.get("hash") {
                        Some(hash) => Some(hash.string()?.to_string()),
                        None => None,
                    };

                    let caller = match ctx.args.get("hasCaller") {
                        Some(caller) => Some(caller.string()?.to_string()),
                        None => None,
                    };

                    // if hash is None, then subscribe to all transactions
                    // if hash is Some, then subscribe to only the transaction with that hash
                    Ok(SimpleBroker::<Transaction>::subscribe().filter_map(
                        move |transaction: Transaction| {
                            if (hash.is_none()
                                || hash == Some(transaction.transaction_hash.clone()))
                                && (caller.is_none()
                                    || transaction.calls.iter().any(|call| {
                                        caller.as_ref().and_then(|c| Felt::from_str(c).ok())
                                            .map_or(false, |addr| call.caller_address == addr)
                                    }))
                            {
                                Some(Ok(Value::Object(TransactionObject::value_mapping(
                                    transaction,
                                ))))
                            } else {
                                // hash != transaction.transaction_hash, then don't send anything,
                                // still listening
                                None
                            }
                        },
                    ))
                })
            })
            .argument(InputValue::new("hash", TypeRef::named(TypeRef::ID)))
            .argument(InputValue::new("hasCaller", TypeRef::named(TypeRef::STRING))),
        ])
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T04:40:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,3 +154,98 @@ impl LocalCache {
         self.token_id_registry.write().await.insert(token_id);
     }
 }
+
+#[derive(Debug)]
+pub struct ContractClassCache<P: Provider + Sync + std::fmt::Debug> {
+    pub classes: RwLock<HashMap<Felt, (Felt, ContractClass)>>,
+    pub provider: Arc<P>,
+}
+
+impl<P: Provider + Sync + std::fmt::Debug> ContractClassCache<P> {
+    pub fn new(provider: Arc<P>) -> Self {
+        Self { classes: RwLock::new(HashMap::new()), provider }
+    }
+
+    pub async fn get(
+        &self,
+        contract_address: Felt,
+        mut block_id: BlockId,
+    ) -> Result<ContractClass, Error> {
+        {
+            let classes = self.classes.read().await;
+            if let Some(class) = classes.get(&contract_address) {
+                return Ok(class.1.clone());
+            }
+        }
+
+        let class_hash = match self.provider.get_class_hash_at(block_id, contract_address).await {
+            Ok(class_hash) => class_hash,
+            Err(e) => match e {
+                // if we got a block not found error, we probably are in a pending block.
+                ProviderError::StarknetError(StarknetError::BlockNotFound) => {
+                    block_id = BlockId::Tag(BlockTag::Pending);
+                    self.provider.get_class_hash_at(block_id, contract_address).await?
+                }
+                _ => return Err(Error::ProviderError(e)),
+            },
+        };
+        let class = self.provider.get_class_at(block_id, contract_address).await?;
+        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
path: crates/torii/sqlite/src/cache.rs
user: login: glihm
body: If it mostly used for ABI lookup, storing the ABI only instead of the whole class with byte code could result in less memory being used.

Also, if the usage is mostly mapping a selector to a name, the mapping could be done the first time the class is fetched instead of storing the raw ABI.
created_at: 2025-03-20T02:52:50Z
author_association: COLLABORATOR
diff_hunk: @@ -147,3 +154,98 @@ impl LocalCache {
         self.token_id_registry.write().await.insert(token_id);
     }
 }
+
+#[derive(Debug)]
+pub struct ContractClassCache<P: Provider + Sync + std::fmt::Debug> {
+    pub classes: RwLock<HashMap<Felt, (Felt, ContractClass)>>,
+    pub provider: Arc<P>,
+}
+
+impl<P: Provider + Sync + std::fmt::Debug> ContractClassCache<P> {
+    pub fn new(provider: Arc<P>) -> Self {
+        Self { classes: RwLock::new(HashMap::new()), provider }
+    }
+
+    pub async fn get(
+        &self,
+        contract_address: Felt,
+        mut block_id: BlockId,
+    ) -> Result<ContractClass, Error> {
+        {
+            let classes = self.classes.read().await;
+            if let Some(class) = classes.get(&contract_address) {
+                return Ok(class.1.clone());
+            }
+        }
+
+        let class_hash = match self.provider.get_class_hash_at(block_id, contract_address).await {
+            Ok(class_hash) => class_hash,
+            Err(e) => match e {
+                // if we got a block not found error, we probably are in a pending block.
+                ProviderError::StarknetError(StarknetError::BlockNotFound) => {
+                    block_id = BlockId::Tag(BlockTag::Pending);
+                    self.provider.get_class_hash_at(block_id, contract_address).await?
+                }
+                _ => return Err(Error::ProviderError(e)),
+            },
+        };
+        let class = self.provider.get_class_at(block_id, contract_address).await?;
+        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
path: crates/torii/sqlite/src/cache.rs
user: login: Larkooo
body: Oh nice catch yes!
created_at: 2025-03-20T03:01:35Z
author_association: COLLABORATOR
diff_hunk: @@ -147,3 +154,100 @@ impl LocalCache {
         self.token_id_registry.write().await.insert(token_id);
     }
 }
+
+#[derive(Debug, Clone)]
+pub enum ClassAbi {
+    Sierra((EntryPointsByType, Vec<AbiEntry>)),
+    Legacy(Vec<LegacyContractAbiEntry>),
+}
+
+#[derive(Debug)]
+pub struct ContractClassCache<P: Provider + Sync + std::fmt::Debug> {
+    pub classes: RwLock<HashMap<Felt, (Felt, ClassAbi)>>,
+    pub provider: Arc<P>,
+}
+
+impl<P: Provider + Sync + std::fmt::Debug> ContractClassCache<P> {
+    pub fn new(provider: Arc<P>) -> Self {
+        Self { classes: RwLock::new(HashMap::new()), provider }
+    }
+
+    pub async fn get(
+        &self,
+        contract_address: Felt,
+        mut block_id: BlockId,
+    ) -> Result<ClassAbi, Error> {
+        {
+            let classes = self.classes.read().await;
+            if let Some(class) = classes.get(&contract_address) {
+                return Ok(class.1.clone());
+            }
+        }
+
+        let class_hash = match self.provider.get_class_hash_at(block_id, contract_address).await {
+            Ok(class_hash) => class_hash,
+            Err(e) => match e {
+                // if we got a block not found error, we probably are in a pending block.
+                ProviderError::StarknetError(StarknetError::BlockNotFound) => {
+                    block_id = BlockId::Tag(BlockTag::Pending);
+                    self.provider.get_class_hash_at(block_id, contract_address).await?
+                }
+                _ => return Err(Error::ProviderError(e)),
+            },
+        };
+        let class = match self.provider.get_class_at(block_id, contract_address).await? {
+            ContractClass::Sierra(sierra) => {
+                let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi).unwrap();
+                let functions: Vec<AbiEntry> = flatten_abi_funcs_recursive(&abi);
+                ClassAbi::Sierra((sierra.entry_points_by_type, functions))
+            }
+            ContractClass::Legacy(legacy) => ClassAbi::Legacy(legacy.abi.unwrap_or_default()),
+        };
+        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
+        Ok(class)
+    }
path: crates/torii/sqlite/src/cache.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Potential panic with unwrap on JSON parsing, sensei.**

There's still an `unwrap()` on line 200 when parsing the Sierra ABI JSON. This could panic if the ABI is malformed.


```diff
-                let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi).unwrap();
+                let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi)
+                    .map_err(|e| Error::ParseError(ParseError::FromJsonStr(e)))?;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        let class = match self.provider.get_class_at(block_id, contract_address).await? {
            ContractClass::Sierra(sierra) => {
-                let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi).unwrap();
+                let abi: Vec<AbiEntry> = serde_json::from_str(&sierra.abi)
+                    .map_err(|e| Error::ParseError(ParseError::FromJsonStr(e)))?;
                let functions: Vec<AbiEntry> = flatten_abi_funcs_recursive(&abi);
                ClassAbi::Sierra((sierra.entry_points_by_type, functions))
            }
            ContractClass::Legacy(legacy) => ClassAbi::Legacy(legacy.abi.unwrap_or_default()),
        };
        self.classes.write().await.insert(contract_address, (class_hash, class.clone()));
        Ok(class)
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-20T03:17:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-14T11:03:31Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
Ohayo, sensei! This pull request introduces a caching mechanism for contract classes in the SQLite module via a new `ContractClassCache` struct. It also updates the transaction storage logic to directly parse calldata into structured function calls rather than converting it into a string. Additionally, the GraphQL schema is expanded with a new function call object and corresponding mapping, and a new SQL migration file is added to create a dedicated table for transaction calls.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `crates/torii/sqlite/src/cache.rs`<br/>`crates/torii/sqlite/src/lib.rs` | Introduces `ContractClassCache` with methods `new` and `get` for caching contract classes. Updates `store_transaction` to accept multiple parameters instead of a `Transaction` object, allowing for direct handling of transaction data. |
| `crates/torii/graphql/src/constants.rs`<br/>`crates/torii/graphql/src/mapping.rs`<br/>`crates/torii/graphql/src/object/transaction.rs`<br/>`crates/torii/graphql/src/schema.rs` | Adds new constants and mappings for function calls. Introduces `CallObject` with methods for resolving transactions and their associated calls. Updates import statements and GraphQL object registration to include the new function call object. |
| `crates/torii/migrations/20250317023025_transaction_calls.sql` | Adds a new SQL migration file that creates the `transaction_calls` table with defined columns and indexes to optimize query performance. |
| `crates/torii/sqlite/src/types.rs` | Introduces `CallType` enum and `ParsedCall` struct to represent contract call data. |
| `crates/torii/indexer/src/engine.rs`<br/>`crates/torii/indexer/src/processors/mod.rs`<br/>`crates/torii/indexer/src/processors/store_transaction.rs` | Updates `Engine` struct to include `contract_class_cache`. Modifies `TransactionProcessor` trait and `StoreTransactionProcessor` to utilize the cache for processing transactions, enhancing transaction handling logic. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant C as Caller
    participant Cache as ContractClassCache
    participant P as Provider

    C ->> Cache: get(contract_address, block_id)
    alt Entry exists in cache
        Cache -->> C: Return cached ContractClass
    else Entry missing in cache
        Cache ->> P: Fetch contract hash for contract_address
        P -->> Cache: Return contract hash
        Cache ->> P: Fetch ContractClass using contract hash
        P -->> Cache: Return ContractClass
        Cache ->> Cache: Store ContractClass in cache
        Cache -->> C: Return ContractClass
    end
```

```mermaid
sequenceDiagram
    participant T as Transaction
    participant S as store_transaction Method
    participant CP as Call Parser
    participant DB as Database

    T ->> S: Invoke store_transaction(transaction_hash, sender_address, ...)
    S ->> CP: Parse calldata to extract FunctionCall details
    CP -->> S: Return lists of calls and outsideCalls
    S ->> DB: Insert transaction record with JSON serialized calls
    DB -->> S: Acknowledge insertion
```

## Possibly related PRs
- dojoengine/dojo#3055: The changes in the main PR introduce a new caching mechanism for contract classes and enhance transaction processing, while the retrieved PR modifies transaction processing methods to include contract addresses, indicating a direct relationship through the handling of contract-related data in transaction processing.

## Suggested reviewers
- glihm

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dojoengine/dojo&utm_content=3085):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-14T11:13:19Z
body: ## [Codecov](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) Report
Attention: Patch coverage is `4.70263%` with `689 lines` in your changes missing coverage. Please review.
> Project coverage is 56.82%. Comparing base [(`5e0de11`)](https://app.codecov.io/gh/dojoengine/dojo/commit/5e0de118533a00267895e5e58cf86ae10d844fb8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) to head [(`65cb243`)](https://app.codecov.io/gh/dojoengine/dojo/commit/65cb243ccce6cb499cb8a5aea1df9cba08d9a6b6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) | Patch % | Lines |
|---|---|---|
| [.../torii/indexer/src/processors/store\_transaction.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Findexer%2Fsrc%2Fprocessors%2Fstore_transaction.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL2luZGV4ZXIvc3JjL3Byb2Nlc3NvcnMvc3RvcmVfdHJhbnNhY3Rpb24ucnM=) | 0.00% | [153 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [...ates/torii/graphql/src/object/erc/token\_balance.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fgraphql%2Fsrc%2Fobject%2Ferc%2Ftoken_balance.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL2dyYXBocWwvc3JjL29iamVjdC9lcmMvdG9rZW5fYmFsYW5jZS5ycw==) | 0.00% | [119 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [...tes/torii/graphql/src/object/erc/token\_transfer.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fgraphql%2Fsrc%2Fobject%2Ferc%2Ftoken_transfer.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL2dyYXBocWwvc3JjL29iamVjdC9lcmMvdG9rZW5fdHJhbnNmZXIucnM=) | 0.00% | [112 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [crates/torii/graphql/src/object/transaction.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fgraphql%2Fsrc%2Fobject%2Ftransaction.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL2dyYXBocWwvc3JjL29iamVjdC90cmFuc2FjdGlvbi5ycw==) | 20.61% | [104 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [crates/torii/sqlite/src/cache.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fsqlite%2Fsrc%2Fcache.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL3NxbGl0ZS9zcmMvY2FjaGUucnM=) | 4.91% | [58 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [crates/torii/graphql/src/object/mod.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fgraphql%2Fsrc%2Fobject%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL2dyYXBocWwvc3JjL29iamVjdC9tb2QucnM=) | 0.00% | [42 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [crates/torii/graphql/src/query/data.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fgraphql%2Fsrc%2Fquery%2Fdata.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL2dyYXBocWwvc3JjL3F1ZXJ5L2RhdGEucnM=) | 0.00% | [37 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [crates/torii/sqlite/src/executor/mod.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fsqlite%2Fsrc%2Fexecutor%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL3NxbGl0ZS9zcmMvZXhlY3V0b3IvbW9kLnJz) | 0.00% | [36 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [crates/torii/sqlite/src/lib.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fsqlite%2Fsrc%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL3NxbGl0ZS9zcmMvbGliLnJz) | 0.00% | [22 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| [crates/torii/sqlite/src/types.rs](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&filepath=crates%2Ftorii%2Fsqlite%2Fsrc%2Ftypes.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine#diff-Y3JhdGVzL3RvcmlpL3NxbGl0ZS9zcmMvdHlwZXMucnM=) | 0.00% | [5 Missing :warning: ](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) |
| ... and [1 more](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine) | |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3085      +/-   ##
==========================================
- Coverage   57.14%   56.82%   -0.33%     
==========================================
  Files         442      442              
  Lines       61142    61561     +419     
==========================================
+ Hits        34942    34979      +37     
- Misses      26200    26582     +382     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/dojoengine/dojo/pull/3085?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dojoengine).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE