diff --git a/Cargo.lock b/Cargo.lock index f41033d65a..9d146f2f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8458,6 +8458,7 @@ dependencies = [ "tonic-build 0.10.2", "tonic-build 0.9.2", "tonic-web-wasm-client", + "torii-core", "tower", "tracing", "url", diff --git a/Cargo.toml b/Cargo.toml index 03682b4438..e935e5fc2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ serde = { version = "1.0.156", features = [ "derive" ] } serde_json = "1.0" serde_with = "2.3.1" smol_str = { version = "0.2.0", features = [ "serde" ] } +sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } starknet = "0.6.0" starknet-crypto = "0.6.0" starknet_api = { git = "https://github.com/starkware-libs/starknet-api", rev = "ecc9b6946ef13003da202838e4124a9ad2efabb0" } diff --git a/crates/dojo-world/src/contracts/model.rs b/crates/dojo-world/src/contracts/model.rs index f6c1ec6c84..b2fee105da 100644 --- a/crates/dojo-world/src/contracts/model.rs +++ b/crates/dojo-world/src/contracts/model.rs @@ -1,5 +1,6 @@ use std::vec; +use async_trait::async_trait; use dojo_types::packing::{parse_ty, unpack, PackingError, ParseError}; use dojo_types::primitive::PrimitiveError; use dojo_types::schema::Ty; @@ -46,7 +47,17 @@ pub enum ModelError

{ Packing(#[from] PackingError), } -pub struct ModelReader<'a, P> { +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait ModelReader { + fn class_hash(&self) -> FieldElement; + async fn schema(&self) -> Result; + async fn packed_size(&self) -> Result; + async fn unpacked_size(&self) -> Result; + async fn layout(&self) -> Result, E>; +} + +pub struct ModelRPCReader<'a, P: Sync + Send> { /// The name of the model name: FieldElement, /// The class hash of the model @@ -55,14 +66,14 @@ pub struct ModelReader<'a, P> { world_reader: &'a WorldContractReader

, } -impl<'a, P> ModelReader<'a, P> +impl<'a, P> ModelRPCReader<'a, P> where - P: Provider, + P: Provider + Sync + Send, { pub async fn new( name: &str, world: &'a WorldContractReader

, - ) -> Result, ModelError> { + ) -> Result, ModelError> { let name = cairo_short_string_to_felt(name)?; let class_hash = world @@ -88,11 +99,60 @@ where Ok(Self { world_reader: world, class_hash, name }) } - pub fn class_hash(&self) -> FieldElement { + pub async fn entity_storage( + &self, + keys: &[FieldElement], + ) -> Result, ModelError> { + let packed_size: u8 = + self.packed_size().await?.try_into().map_err(ParseError::ValueOutOfRange)?; + + let key = poseidon_hash_many(keys); + let key = poseidon_hash_many(&[short_string!("dojo_storage"), self.name, key]); + + let mut packed = Vec::with_capacity(packed_size as usize); + for slot in 0..packed_size { + let value = self + .world_reader + .provider() + .get_storage_at( + self.world_reader.address(), + key + slot.into(), + self.world_reader.block_id(), + ) + .await?; + + packed.push(value); + } + + Ok(packed) + } + + pub async fn entity(&self, keys: &[FieldElement]) -> Result> { + let mut schema = self.schema().await?; + + let layout = self.layout().await?; + let raw_values = self.entity_storage(keys).await?; + + let unpacked = unpack(raw_values, layout)?; + let mut keys_and_unpacked = [keys, &unpacked].concat(); + + schema.deserialize(&mut keys_and_unpacked)?; + + Ok(schema) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl<'a, P> ModelReader> for ModelRPCReader<'a, P> +where + P: Provider + Sync + Send, +{ + fn class_hash(&self) -> FieldElement { self.class_hash } - pub async fn schema(&self) -> Result> { + async fn schema(&self) -> Result> { let entrypoint = get_selector_from_name(SCHEMA_SELECTOR_STR).unwrap(); let res = self @@ -103,7 +163,7 @@ where Ok(parse_ty(&res[1..])?) } - pub async fn packed_size(&self) -> Result> { + async fn packed_size(&self) -> Result> { let entrypoint = get_selector_from_name(PACKED_SIZE_SELECTOR_STR).unwrap(); let res = self @@ -114,7 +174,7 @@ where Ok(res[1]) } - pub async fn unpacked_size(&self) -> Result> { + async fn unpacked_size(&self) -> Result> { let entrypoint = get_selector_from_name(UNPACKED_SIZE_SELECTOR_STR).unwrap(); let res = self @@ -125,7 +185,7 @@ where Ok(res[1]) } - pub async fn layout(&self) -> Result, ModelError> { + async fn layout(&self) -> Result, ModelError> { let entrypoint = get_selector_from_name(LAYOUT_SELECTOR_STR).unwrap(); let res = self @@ -135,46 +195,4 @@ where Ok(res[2..].into()) } - - pub async fn entity_storage( - &self, - keys: &[FieldElement], - ) -> Result, ModelError> { - let packed_size: u8 = - self.packed_size().await?.try_into().map_err(ParseError::ValueOutOfRange)?; - - let key = poseidon_hash_many(keys); - let key = poseidon_hash_many(&[short_string!("dojo_storage"), self.name, key]); - - let mut packed = Vec::with_capacity(packed_size as usize); - for slot in 0..packed_size { - let value = self - .world_reader - .provider() - .get_storage_at( - self.world_reader.address(), - key + slot.into(), - self.world_reader.block_id(), - ) - .await?; - - packed.push(value); - } - - Ok(packed) - } - - pub async fn entity(&self, keys: &[FieldElement]) -> Result> { - let mut schema = self.schema().await?; - - let layout = self.layout().await?; - let raw_values = self.entity_storage(keys).await?; - - let unpacked = unpack(raw_values, layout)?; - let mut keys_and_unpacked = [keys, &unpacked].concat(); - - schema.deserialize(&mut keys_and_unpacked)?; - - Ok(schema) - } } diff --git a/crates/dojo-world/src/contracts/model_test.rs b/crates/dojo-world/src/contracts/model_test.rs index 300ac6ede2..6b4b5e1981 100644 --- a/crates/dojo-world/src/contracts/model_test.rs +++ b/crates/dojo-world/src/contracts/model_test.rs @@ -7,6 +7,7 @@ use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; use starknet::accounts::ConnectedAccount; use starknet::core::types::FieldElement; +use crate::contracts::model::ModelReader; use crate::contracts::world::test::deploy_world; use crate::contracts::world::WorldContractReader; diff --git a/crates/dojo-world/src/contracts/world.rs b/crates/dojo-world/src/contracts/world.rs index 1f9440ff7a..9421d97bb9 100644 --- a/crates/dojo-world/src/contracts/world.rs +++ b/crates/dojo-world/src/contracts/world.rs @@ -11,7 +11,7 @@ use starknet::core::utils::{ use starknet::macros::selector; use starknet::providers::{Provider, ProviderError}; -use super::model::{ModelError, ModelReader}; +use super::model::{ModelError, ModelRPCReader}; #[cfg(test)] #[path = "world_test.rs"] @@ -177,7 +177,7 @@ where pub async fn model( &'a self, name: &str, - ) -> Result, ModelError<::Error>> + ) -> Result, ModelError<::Error>> { self.reader.model(name).await } @@ -340,9 +340,12 @@ where impl<'a, P> WorldContractReader

where - P: Provider, + P: Provider + Sync + Send, { - pub async fn model(&'a self, name: &str) -> Result, ModelError> { - ModelReader::new(name, self).await + pub async fn model( + &'a self, + name: &str, + ) -> Result, ModelError> { + ModelRPCReader::new(name, self).await } } diff --git a/crates/sozo/src/ops/model.rs b/crates/sozo/src/ops/model.rs index aba6fa548d..c22404dcec 100644 --- a/crates/sozo/src/ops/model.rs +++ b/crates/sozo/src/ops/model.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use dojo_world::contracts::model::ModelReader; use dojo_world::contracts::world::WorldContractReader; use dojo_world::metadata::Environment; use starknet::core::types::{BlockId, BlockTag}; diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index 9daf884848..829cddce9d 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -11,6 +11,7 @@ version.workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true +base64.workspace = true chrono.workspace = true dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world", features = [ "contracts", "manifest" ] } @@ -20,12 +21,12 @@ hex.workspace = true lazy_static.workspace = true log = "0.4.17" once_cell.workspace = true -reqwest = { version = "0.11.22", features = [ "blocking" ]} +reqwest = { version = "0.11.22", features = [ "blocking" ] } scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true slab = "0.4.2" -sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } +sqlx.workspace = true starknet-crypto.workspace = true starknet.workspace = true thiserror.workspace = true @@ -33,7 +34,6 @@ tokio = { version = "1.32.0", features = [ "sync" ], default-features = true } tokio-stream = "0.1.11" tokio-util = "0.7.7" tracing.workspace = true -base64.workspace = true [dev-dependencies] camino.workspace = true diff --git a/crates/torii/core/src/error.rs b/crates/torii/core/src/error.rs new file mode 100644 index 0000000000..5384512428 --- /dev/null +++ b/crates/torii/core/src/error.rs @@ -0,0 +1,18 @@ +use starknet::core::types::FromStrError; +use starknet::core::utils::CairoShortStringToFeltError; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("parsing error: {0}")] + Parse(#[from] ParseError), + #[error(transparent)] + Sql(#[from] sqlx::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error(transparent)] + FromStr(#[from] FromStrError), + #[error(transparent)] + CairoShortStringToFelt(#[from] CairoShortStringToFeltError), +} diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs index 6049e63d98..e4690cc5ec 100644 --- a/crates/torii/core/src/lib.rs +++ b/crates/torii/core/src/lib.rs @@ -4,6 +4,8 @@ use sqlx::FromRow; use crate::types::SQLFieldElement; pub mod engine; +pub mod error; +pub mod model; pub mod processors; pub mod simple_broker; pub mod sql; diff --git a/crates/torii/grpc/src/server/utils.rs b/crates/torii/core/src/model.rs similarity index 77% rename from crates/torii/grpc/src/server/utils.rs rename to crates/torii/core/src/model.rs index 5e8772806d..fad073317f 100644 --- a/crates/torii/grpc/src/server/utils.rs +++ b/crates/torii/core/src/model.rs @@ -1,4 +1,80 @@ +use async_trait::async_trait; use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; +use dojo_world::contracts::model::ModelReader; +use sqlx::{Pool, Sqlite}; +use starknet::core::types::FieldElement; + +use super::error::{self, Error}; + +pub struct ModelSQLReader { + /// The name of the model + name: String, + /// The class hash of the model + class_hash: FieldElement, + pool: Pool, + packed_size: FieldElement, + unpacked_size: FieldElement, + layout: Vec, +} + +impl ModelSQLReader { + pub async fn new(name: &str, pool: Pool) -> Result { + let (name, class_hash, packed_size, unpacked_size, layout): ( + String, + String, + u32, + u32, + String, + ) = sqlx::query_as( + "SELECT name, class_hash, packed_size, unpacked_size, layout FROM models WHERE id = ?", + ) + .bind(name) + .fetch_one(&pool) + .await?; + + let class_hash = + FieldElement::from_hex_be(&class_hash).map_err(error::ParseError::FromStr)?; + let packed_size = FieldElement::from(packed_size); + let unpacked_size = FieldElement::from(unpacked_size); + + let layout = hex::decode(layout).unwrap(); + let layout = layout.iter().map(|e| FieldElement::from(*e)).collect(); + + Ok(Self { name: name.clone(), class_hash, pool, packed_size, unpacked_size, layout }) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl ModelReader for ModelSQLReader { + fn class_hash(&self) -> FieldElement { + self.class_hash + } + + async fn schema(&self) -> Result { + let model_members: Vec = sqlx::query_as( + "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ + model_members WHERE model_id = ? ORDER BY model_idx ASC, member_idx ASC", + ) + .bind(self.name.clone()) + .fetch_all(&self.pool) + .await?; + + Ok(parse_sql_model_members(&self.name, &model_members)) + } + + async fn packed_size(&self) -> Result { + Ok(self.packed_size) + } + + async fn unpacked_size(&self) -> Result { + Ok(self.unpacked_size) + } + + async fn layout(&self) -> Result, Error> { + Ok(self.layout.clone()) + } +} #[allow(unused)] #[derive(Debug, sqlx::FromRow)] @@ -73,7 +149,7 @@ mod tests { use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; use super::SqlModelMember; - use crate::server::utils::parse_sql_model_members; + use crate::model::parse_sql_model_members; #[test] fn parse_simple_model_members_to_ty() { diff --git a/crates/torii/core/src/processors/register_model.rs b/crates/torii/core/src/processors/register_model.rs index 2d63014c93..f632f6e437 100644 --- a/crates/torii/core/src/processors/register_model.rs +++ b/crates/torii/core/src/processors/register_model.rs @@ -1,5 +1,6 @@ use anyhow::{Error, Ok, Result}; use async_trait::async_trait; +use dojo_world::contracts::model::ModelReader; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{BlockWithTxs, Event, InvokeTransactionReceipt}; use starknet::core::utils::parse_cairo_short_string; diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 8371cdfe1f..e428b833b1 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -21,7 +21,7 @@ lazy_static.workspace = true scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true -sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } +sqlx.workspace = true strum.workspace = true strum_macros.workspace = true thiserror.workspace = true diff --git a/crates/torii/grpc/Cargo.toml b/crates/torii/grpc/Cargo.toml index aa3376f1f3..fc19299146 100644 --- a/crates/torii/grpc/Cargo.toml +++ b/crates/torii/grpc/Cargo.toml @@ -15,6 +15,7 @@ rayon.workspace = true starknet-crypto.workspace = true starknet.workspace = true thiserror.workspace = true +torii-core = { path = "../core", optional = true } serde.workspace = true strum_macros.workspace = true @@ -22,10 +23,10 @@ strum_macros.workspace = true # server hex.workspace = true hyper = "0.14.27" +rand = "0.8.5" serde_json.workspace = true tower = "0.4.13" tracing.workspace = true -rand = "0.8.5" [target.'cfg(target_arch = "wasm32")'.dependencies] tonic-web-wasm-client.workspace = true @@ -34,7 +35,7 @@ wasm-tonic.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] prost.workspace = true -sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } +sqlx.workspace = true tokio-stream = "0.1.14" tokio.workspace = true tonic.workspace = true @@ -46,4 +47,4 @@ wasm-tonic-build.workspace = true [features] client = [ ] -server = [ ] # this feature can't be build on wasm32 +server = [ "dep:torii-core" ] # this feature can't be build on wasm32 diff --git a/crates/torii/grpc/src/server/error.rs b/crates/torii/grpc/src/server/error.rs index 5c3a99c729..539de8f527 100644 --- a/crates/torii/grpc/src/server/error.rs +++ b/crates/torii/grpc/src/server/error.rs @@ -1,27 +1,10 @@ -use starknet::core::types::FromStrError; -use starknet::core::utils::CairoShortStringToFeltError; use starknet::providers::{Provider, ProviderError}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("parsing error: {0}")] - Parse(#[from] ParseError), - #[error(transparent)] - Sql(#[from] sqlx::Error), -} - -#[derive(Debug, thiserror::Error)] -pub enum ParseError { - #[error(transparent)] - FromStr(#[from] FromStrError), - #[error(transparent)] - CairoShortStringToFelt(#[from] CairoShortStringToFeltError), -} +use torii_core::error::ParseError; #[derive(Debug, thiserror::Error)] pub enum SubscriptionError { #[error(transparent)] - Parse(#[from] super::error::ParseError), + Parse(#[from] ParseError), #[error(transparent)] Provider(ProviderError<

::Error>), } diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index 52a2ca734c..0fb13a61a7 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -1,7 +1,6 @@ pub mod error; pub mod logger; pub mod subscription; -pub mod utils; use std::pin::Pin; use std::str::FromStr; @@ -20,10 +19,10 @@ use starknet_crypto::FieldElement; use tokio::sync::mpsc::Receiver; use tokio_stream::wrappers::ReceiverStream; use tonic::{Request, Response, Status}; +use torii_core::error::{Error, ParseError}; +use torii_core::model::{parse_sql_model_members, SqlModelMember}; -use self::error::{Error, ParseError}; use self::subscription::SubscribeRequest; -use self::utils::{parse_sql_model_members, SqlModelMember}; use crate::protos::{self}; #[derive(Clone)] diff --git a/crates/torii/server/Cargo.toml b/crates/torii/server/Cargo.toml index d03afad9a9..2e266501ef 100644 --- a/crates/torii/server/Cargo.toml +++ b/crates/torii/server/Cargo.toml @@ -23,7 +23,7 @@ indexmap = "1.9.3" scarb.workspace = true serde.workspace = true serde_json.workspace = true -sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "uuid" ] } +sqlx.workspace = true starknet-crypto.workspace = true starknet.workspace = true tokio-stream = "0.1.11"