From 2884ef6b1b08a848966a6452d7f8b4d6b7145df7 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Mon, 2 Oct 2023 15:19:37 -0400 Subject: [PATCH] Support Ty values, fix sql set entity (#947) --- Cargo.lock | 4 +- crates/dojo-types/src/core.rs | 152 --------- crates/dojo-types/src/lib.rs | 2 +- crates/dojo-types/src/primitive.rs | 201 ++++++++++++ crates/dojo-types/src/schema.rs | 107 +++++-- crates/torii/client/src/contract/model.rs | 8 +- .../torii/client/src/contract/model_test.rs | 15 +- crates/torii/core/Cargo.toml | 3 + .../core/src/processors/store_set_record.rs | 12 +- crates/torii/core/src/sql.rs | 217 +++++++------ crates/torii/core/src/sql_test.rs | 128 +++++++- crates/torii/graphql/Cargo.toml | 1 - .../torii/graphql/src/tests/entities_test.rs | 25 +- crates/torii/graphql/src/tests/mod.rs | 294 ++++++++++++------ 14 files changed, 748 insertions(+), 421 deletions(-) delete mode 100644 crates/dojo-types/src/core.rs create mode 100644 crates/dojo-types/src/primitive.rs diff --git a/Cargo.lock b/Cargo.lock index 39a0c6cbb2..b7f5845e06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7875,6 +7875,7 @@ dependencies = [ "async-trait", "camino", "chrono", + "dojo-test-utils", "dojo-types", "dojo-world", "futures-channel", @@ -7883,9 +7884,11 @@ dependencies = [ "lazy_static", "log", "once_cell", + "scarb-ui", "serde", "serde_json", "slab", + "sozo", "sqlx", "starknet", "starknet-crypto 0.6.0", @@ -7914,7 +7917,6 @@ dependencies = [ "scarb-ui", "serde", "serde_json", - "sozo", "sqlx", "starknet", "starknet-crypto 0.6.0", diff --git a/crates/dojo-types/src/core.rs b/crates/dojo-types/src/core.rs deleted file mode 100644 index ddeb4005d0..0000000000 --- a/crates/dojo-types/src/core.rs +++ /dev/null @@ -1,152 +0,0 @@ -use ethabi::ethereum_types::U256; -use serde::{Deserialize, Serialize}; -use starknet::core::types::{FieldElement, ValueOutOfRangeError}; -use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; - -#[derive( - AsRefStr, Display, EnumIter, EnumString, Clone, Debug, Serialize, Deserialize, PartialEq, -)] -#[strum(serialize_all = "lowercase")] -pub enum CairoType { - U8(Option), - U16(Option), - U32(Option), - U64(Option), - U128(Option), - U256(Option), - USize(Option), - Bool(Option), - Felt252(Option), - #[strum(serialize = "ClassHash")] - ClassHash(Option), - #[strum(serialize = "ContractAddress")] - ContractAddress(Option), -} - -#[derive(Debug, thiserror::Error)] -pub enum CairoTypeError { - #[error("Value must have at least one FieldElement")] - MissingFieldElement, - #[error("Not enough FieldElements for U256")] - NotEnoughFieldElements, - #[error("Unsupported CairoType for SQL formatting")] - UnsupportedType, - #[error(transparent)] - ValueOutOfRange(#[from] ValueOutOfRangeError), -} - -impl CairoType { - pub fn to_sql_type(&self) -> String { - match self { - CairoType::U8(_) - | CairoType::U16(_) - | CairoType::U32(_) - | CairoType::U64(_) - | CairoType::USize(_) - | CairoType::Bool(_) => "INTEGER".to_string(), - CairoType::U128(_) - | CairoType::U256(_) - | CairoType::ContractAddress(_) - | CairoType::ClassHash(_) - | CairoType::Felt252(_) => "TEXT".to_string(), - } - } - - pub fn format_for_sql(&self, value: Vec<&FieldElement>) -> Result { - if value.is_empty() { - return Err(CairoTypeError::MissingFieldElement); - } - - match self { - CairoType::U8(_) - | CairoType::U16(_) - | CairoType::U32(_) - | CairoType::U64(_) - | CairoType::USize(_) - | CairoType::Bool(_) => Ok(format!(", '{}'", value[0])), - CairoType::U128(_) - | CairoType::ContractAddress(_) - | CairoType::ClassHash(_) - | CairoType::Felt252(_) => Ok(format!(", '{:0>64x}'", value[0])), - CairoType::U256(_) => { - if value.len() < 2 { - Err(CairoTypeError::NotEnoughFieldElements) - } else { - let mut buffer = [0u8; 32]; - let value0_bytes = value[0].to_bytes_be(); - let value1_bytes = value[1].to_bytes_be(); - buffer[..16].copy_from_slice(&value0_bytes); - buffer[16..].copy_from_slice(&value1_bytes); - Ok(format!(", '{}'", hex::encode(buffer))) - } - } - } - } - - pub fn set_value_from_felts( - &mut self, - felts: &mut Vec, - ) -> Result<(), CairoTypeError> { - if felts.is_empty() { - return Err(CairoTypeError::MissingFieldElement); - } - - match self { - CairoType::U8(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U16(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U32(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U64(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::USize(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::Bool(ref mut value) => { - let raw = felts.remove(0); - *value = Some(raw == FieldElement::ONE); - Ok(()) - } - CairoType::U128(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U256(ref mut value) => { - if felts.len() < 2 { - return Err(CairoTypeError::NotEnoughFieldElements); - } - let value0 = felts.remove(0); - let value1 = felts.remove(0); - let value0_bytes = value0.to_bytes_be(); - let value1_bytes = value1.to_bytes_be(); - let mut bytes = [0u8; 32]; - bytes[..16].copy_from_slice(&value0_bytes); - bytes[16..].copy_from_slice(&value1_bytes); - *value = Some(U256::from(bytes)); - Ok(()) - } - CairoType::ContractAddress(ref mut value) => { - *value = Some(felts.remove(0)); - Ok(()) - } - CairoType::ClassHash(ref mut value) => { - *value = Some(felts.remove(0)); - Ok(()) - } - CairoType::Felt252(ref mut value) => { - *value = Some(felts.remove(0)); - Ok(()) - } - } - } -} diff --git a/crates/dojo-types/src/lib.rs b/crates/dojo-types/src/lib.rs index 15ac2373f5..3b595b028b 100644 --- a/crates/dojo-types/src/lib.rs +++ b/crates/dojo-types/src/lib.rs @@ -5,8 +5,8 @@ use serde::Serialize; use starknet::core::types::FieldElement; use system::SystemMetadata; -pub mod core; pub mod event; +pub mod primitive; pub mod schema; pub mod storage; pub mod system; diff --git a/crates/dojo-types/src/primitive.rs b/crates/dojo-types/src/primitive.rs new file mode 100644 index 0000000000..83d8f6d99c --- /dev/null +++ b/crates/dojo-types/src/primitive.rs @@ -0,0 +1,201 @@ +use ethabi::ethereum_types::U256; +use serde::{Deserialize, Serialize}; +use starknet::core::types::{FieldElement, ValueOutOfRangeError}; +use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; + +#[derive( + AsRefStr, Display, EnumIter, EnumString, Clone, Debug, Serialize, Deserialize, PartialEq, +)] +#[strum(serialize_all = "lowercase")] +pub enum Primitive { + U8(Option), + U16(Option), + U32(Option), + U64(Option), + U128(Option), + U256(Option), + USize(Option), + Bool(Option), + Felt252(Option), + #[strum(serialize = "ClassHash")] + ClassHash(Option), + #[strum(serialize = "ContractAddress")] + ContractAddress(Option), +} + +#[derive(Debug, thiserror::Error)] +pub enum PrimitiveError { + #[error("Value must have at least one FieldElement")] + MissingFieldElement, + #[error("Not enough FieldElements for U256")] + NotEnoughFieldElements, + #[error("Unsupported CairoType for SQL formatting")] + UnsupportedType, + #[error(transparent)] + ValueOutOfRange(#[from] ValueOutOfRangeError), +} + +impl Primitive { + pub fn to_sql_type(&self) -> String { + match self { + Primitive::U8(_) + | Primitive::U16(_) + | Primitive::U32(_) + | Primitive::U64(_) + | Primitive::USize(_) + | Primitive::Bool(_) => "INTEGER".to_string(), + Primitive::U128(_) + | Primitive::U256(_) + | Primitive::ContractAddress(_) + | Primitive::ClassHash(_) + | Primitive::Felt252(_) => "TEXT".to_string(), + } + } + + pub fn to_sql_value(&self) -> Result { + let value = self.serialize()?; + + if value.is_empty() { + return Err(PrimitiveError::MissingFieldElement); + } + + match self { + Primitive::U8(_) + | Primitive::U16(_) + | Primitive::U32(_) + | Primitive::U64(_) + | Primitive::USize(_) + | Primitive::Bool(_) => Ok(format!("'{}'", value[0])), + Primitive::U128(_) + | Primitive::ContractAddress(_) + | Primitive::ClassHash(_) + | Primitive::Felt252(_) => Ok(format!("'{:0>64x}'", value[0])), + Primitive::U256(_) => { + if value.len() < 2 { + Err(PrimitiveError::NotEnoughFieldElements) + } else { + let mut buffer = [0u8; 32]; + let value0_bytes = value[0].to_bytes_be(); + let value1_bytes = value[1].to_bytes_be(); + buffer[..16].copy_from_slice(&value0_bytes); + buffer[16..].copy_from_slice(&value1_bytes); + Ok(format!("'{}'", hex::encode(buffer))) + } + } + } + } + + pub fn deserialize(&mut self, felts: &mut Vec) -> Result<(), PrimitiveError> { + if felts.is_empty() { + return Err(PrimitiveError::MissingFieldElement); + } + + match self { + Primitive::U8(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U16(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U32(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U64(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::USize(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::Bool(ref mut value) => { + let raw = felts.remove(0); + *value = Some(raw == FieldElement::ONE); + Ok(()) + } + Primitive::U128(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U256(ref mut value) => { + if felts.len() < 2 { + return Err(PrimitiveError::NotEnoughFieldElements); + } + let value0 = felts.remove(0); + let value1 = felts.remove(0); + let value0_bytes = value0.to_bytes_be(); + let value1_bytes = value1.to_bytes_be(); + let mut bytes = [0u8; 32]; + bytes[..16].copy_from_slice(&value0_bytes); + bytes[16..].copy_from_slice(&value1_bytes); + *value = Some(U256::from(bytes)); + Ok(()) + } + Primitive::ContractAddress(ref mut value) => { + *value = Some(felts.remove(0)); + Ok(()) + } + Primitive::ClassHash(ref mut value) => { + *value = Some(felts.remove(0)); + Ok(()) + } + Primitive::Felt252(ref mut value) => { + *value = Some(felts.remove(0)); + Ok(()) + } + } + } + + pub fn serialize(&self) -> Result, PrimitiveError> { + match self { + Primitive::U8(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U16(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U32(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U64(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::USize(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::Bool(value) => value + .map(|v| Ok(vec![if v { FieldElement::ONE } else { FieldElement::ZERO }])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U128(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U256(value) => value + .map(|v| { + let mut bytes = [0u8; 32]; + v.to_big_endian(&mut bytes); + let value0_slice = &bytes[..16]; + let value1_slice = &bytes[16..]; + let mut value0_array = [0u8; 32]; + let mut value1_array = [0u8; 32]; + value0_array[16..].copy_from_slice(value0_slice); + value1_array[16..].copy_from_slice(value1_slice); + let value0 = FieldElement::from_bytes_be(&value0_array).unwrap(); + let value1 = FieldElement::from_bytes_be(&value1_array).unwrap(); + Ok(vec![value0, value1]) + }) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::ContractAddress(value) => { + value.map(|v| Ok(vec![v])).unwrap_or(Err(PrimitiveError::MissingFieldElement)) + } + Primitive::ClassHash(value) => { + value.map(|v| Ok(vec![v])).unwrap_or(Err(PrimitiveError::MissingFieldElement)) + } + Primitive::Felt252(value) => { + value.map(|v| Ok(vec![v])).unwrap_or(Err(PrimitiveError::MissingFieldElement)) + } + } + } +} diff --git a/crates/dojo-types/src/schema.rs b/crates/dojo-types/src/schema.rs index 269b656db6..8689e33b88 100644 --- a/crates/dojo-types/src/schema.rs +++ b/crates/dojo-types/src/schema.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use starknet::core::types::FieldElement; -use crate::core::{CairoType, CairoTypeError}; +use crate::primitive::{Primitive, PrimitiveError}; /// Represents a model member. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -12,6 +12,12 @@ pub struct Member { pub key: bool, } +impl Member { + pub fn serialize(&self) -> Result, PrimitiveError> { + self.ty.serialize() + } +} + /// Represents a model of an entity #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct EntityModel { @@ -28,7 +34,7 @@ pub struct ModelMetadata { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum Ty { - Primitive(CairoType), + Primitive(Primitive), Struct(Struct), Enum(Enum), Tuple(Vec), @@ -48,10 +54,42 @@ impl Ty { TyIter { stack: vec![self] } } - pub fn deserialize(&mut self, felts: &mut Vec) -> Result<(), CairoTypeError> { + pub fn serialize(&self) -> Result, PrimitiveError> { + let mut felts = vec![]; + + fn serialize_inner(ty: &Ty, felts: &mut Vec) -> Result<(), PrimitiveError> { + match ty { + Ty::Primitive(c) => { + felts.extend(c.serialize()?); + } + Ty::Struct(s) => { + for child in &s.children { + serialize_inner(&child.ty, felts)?; + } + } + Ty::Enum(e) => { + for (_, child) in &e.options { + serialize_inner(child, felts)?; + } + } + Ty::Tuple(tys) => { + for ty in tys { + serialize_inner(ty, felts)?; + } + } + } + Ok(()) + } + + serialize_inner(self, &mut felts)?; + + Ok(felts) + } + + pub fn deserialize(&mut self, felts: &mut Vec) -> Result<(), PrimitiveError> { match self { Ty::Primitive(c) => { - c.set_value_from_felts(felts)?; + c.deserialize(felts)?; } Ty::Struct(s) => { for child in &mut s.children { @@ -59,7 +97,7 @@ impl Ty { } } Ty::Enum(e) => { - for (_, child) in &mut e.children { + for (_, child) in &mut e.options { child.deserialize(felts)?; } } @@ -89,7 +127,7 @@ impl<'a> Iterator for TyIter<'a> { } } Ty::Enum(e) => { - for child in &e.children { + for child in &e.options { self.stack.push(&child.1); } } @@ -115,7 +153,7 @@ impl std::fmt::Display for Ty { } Ty::Enum(e) => { let mut enum_str = format!("enum {} {{\n", e.name); - for child in &e.children { + for child in &e.options { enum_str.push_str(&format!(" {}\n", child.0)); } enum_str.push('}'); @@ -142,10 +180,41 @@ pub struct Struct { pub children: Vec, } +impl Struct { + pub fn keys(&self) -> Vec { + self.children.iter().filter(|m| m.key).cloned().collect() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum EnumError { + #[error("Enum option not set")] + OptionNotSet, + #[error("Enum option invalid")] + OptionInvalid, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Enum { pub name: String, - pub children: Vec<(String, Ty)>, + pub option: Option, + pub options: Vec<(String, Ty)>, +} + +impl Enum { + pub fn to_sql_value(&self) -> Result { + let option: usize = if let Some(option) = self.option { + option as usize + } else { + return Err(EnumError::OptionNotSet); + }; + + if option >= self.options.len() { + return Err(EnumError::OptionInvalid); + } + + Ok(format!("'{}'", self.options[option].0)) + } } fn format_member(m: &Member) -> String { @@ -157,57 +226,57 @@ fn format_member(m: &Member) -> String { if let Ty::Primitive(ty) = &m.ty { match ty { - CairoType::U8(value) => { + Primitive::U8(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U16(value) => { + Primitive::U16(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U32(value) => { + Primitive::U32(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U64(value) => { + Primitive::U64(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U128(value) => { + Primitive::U128(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U256(value) => { + Primitive::U256(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::USize(value) => { + Primitive::USize(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::Bool(value) => { + Primitive::Bool(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::Felt252(value) => { + Primitive::Felt252(value) => { if let Some(value) = value { str.push_str(&format!(" = {:#x}", value)); } } - CairoType::ClassHash(value) => { + Primitive::ClassHash(value) => { if let Some(value) = value { str.push_str(&format!(" = {:#x}", value)); } } - CairoType::ContractAddress(value) => { + Primitive::ContractAddress(value) => { if let Some(value) = value { str.push_str(&format!(" = {:#x}", value)); } diff --git a/crates/torii/client/src/contract/model.rs b/crates/torii/client/src/contract/model.rs index 4968872364..024d0dbf2b 100644 --- a/crates/torii/client/src/contract/model.rs +++ b/crates/torii/client/src/contract/model.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use std::vec; use crypto_bigint::U256; -use dojo_types::core::{CairoType, CairoTypeError}; +use dojo_types::primitive::{Primitive, PrimitiveError}; use dojo_types::schema::{Enum, Member, Struct, Ty}; use starknet::core::types::{BlockId, FieldElement, FunctionCall}; use starknet::core::utils::{ @@ -36,7 +36,7 @@ pub enum ModelError

{ #[error(transparent)] ContractReaderError(ContractReaderError

), #[error(transparent)] - CairoTypeError(CairoTypeError), + CairoTypeError(PrimitiveError), } pub struct ModelReader<'a, P: Provider + Sync> { @@ -235,7 +235,7 @@ fn parse_ty(data: &[FieldElement]) -> Result(data: &[FieldElement]) -> Result> { let ty = parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; - Ok(Ty::Primitive(CairoType::from_str(&ty).unwrap())) + Ok(Ty::Primitive(Primitive::from_str(&ty).unwrap())) } fn parse_struct(data: &[FieldElement]) -> Result> { @@ -309,7 +309,7 @@ fn parse_enum(data: &[FieldElement]) -> Result(data: &[FieldElement]) -> Result> { diff --git a/crates/torii/client/src/contract/model_test.rs b/crates/torii/client/src/contract/model_test.rs index c67056ca3c..cc6dab73ef 100644 --- a/crates/torii/client/src/contract/model_test.rs +++ b/crates/torii/client/src/contract/model_test.rs @@ -2,7 +2,7 @@ use camino::Utf8PathBuf; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, TestSequencer, }; -use dojo_types::core::CairoType; +use dojo_types::primitive::Primitive; use dojo_types::schema::{Enum, Member, Struct, Ty}; use starknet::accounts::ConnectedAccount; use starknet::core::types::{BlockId, BlockTag, FieldElement}; @@ -34,7 +34,7 @@ async fn test_model() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Primitive(CairoType::ContractAddress(None)), + ty: Ty::Primitive(Primitive::ContractAddress(None)), key: true }, Member { @@ -44,12 +44,12 @@ async fn test_model() { children: vec![ Member { name: "x".to_string(), - ty: Ty::Primitive(CairoType::U32(None)), + ty: Ty::Primitive(Primitive::U32(None)), key: false }, Member { name: "y".to_string(), - ty: Ty::Primitive(CairoType::U32(None)), + ty: Ty::Primitive(Primitive::U32(None)), key: false } ] @@ -78,19 +78,20 @@ async fn test_model() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Primitive(CairoType::ContractAddress(None)), + ty: Ty::Primitive(Primitive::ContractAddress(None)), key: true }, Member { name: "remaining".to_string(), - ty: Ty::Primitive(CairoType::U8(None)), + ty: Ty::Primitive(Primitive::U8(None)), key: false }, Member { name: "last_direction".to_string(), ty: Ty::Enum(Enum { name: "Direction".to_string(), - children: vec![ + option: None, + options: vec![ ("None".to_string(), Ty::Tuple(vec![])), ("Left".to_string(), Ty::Tuple(vec![])), ("Right".to_string(), Ty::Tuple(vec![])), diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index 12591f12f5..1fef948d88 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -21,6 +21,7 @@ hex = "0.4.3" lazy_static = "1.4.0" log = "0.4.17" once_cell.workspace = true +scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true slab = "0.4.2" @@ -35,3 +36,5 @@ tracing.workspace = true [dev-dependencies] camino.workspace = true +dojo-test-utils = { path = "../../dojo-test-utils" } +sozo = { path = "../../sozo" } diff --git a/crates/torii/core/src/processors/store_set_record.rs b/crates/torii/core/src/processors/store_set_record.rs index ce62b968e4..1f7ccce3f5 100644 --- a/crates/torii/core/src/processors/store_set_record.rs +++ b/crates/torii/core/src/processors/store_set_record.rs @@ -1,6 +1,6 @@ use anyhow::{Error, Ok, Result}; use async_trait::async_trait; -use starknet::core::types::{BlockWithTxs, Event, InvokeTransactionReceipt}; +use starknet::core::types::{BlockId, BlockTag, BlockWithTxs, Event, InvokeTransactionReceipt}; use starknet::core::utils::parse_cairo_short_string; use starknet::providers::Provider; use starknet_crypto::FieldElement; @@ -17,14 +17,14 @@ const MODEL_INDEX: usize = 0; const NUM_KEYS_INDEX: usize = 1; #[async_trait] -impl EventProcessor

for StoreSetRecordProcessor { +impl EventProcessor

for StoreSetRecordProcessor { fn event_key(&self) -> String { "StoreSetRecord".to_string() } async fn process( &self, - _world: &WorldContractReader<'_, P>, + world: &WorldContractReader<'_, P>, db: &mut Sql, _provider: &P, _block: &BlockWithTxs, @@ -34,10 +34,10 @@ impl EventProcessor

for StoreSetRecordProcessor { let name = parse_cairo_short_string(&event.data[MODEL_INDEX])?; info!("store set record: {}", name); + let model = world.model(&name, BlockId::Tag(BlockTag::Pending)).await?; let keys = values_at(&event.data, NUM_KEYS_INDEX)?; - let values_index = keys.len() + NUM_KEYS_INDEX + 2; - let values = values_at(&event.data, values_index)?; - db.set_entity(name, keys, values).await?; + let entity = model.entity(keys, BlockId::Tag(BlockTag::Pending)).await?; + db.set_entity(entity).await?; Ok(()) } } diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 4991574bc8..eecd6b4ea0 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -1,12 +1,11 @@ use std::str::FromStr; -use anyhow::Result; +use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; -use dojo_types::core::CairoType; +use dojo_types::primitive::Primitive; use dojo_types::schema::Ty; use dojo_world::manifest::{Manifest, System}; use sqlx::pool::PoolConnection; -use sqlx::sqlite::SqliteRow; use sqlx::{Executor, Pool, Row, Sqlite}; use starknet::core::types::{Event, FieldElement}; use starknet_crypto::poseidon_hash_many; @@ -133,7 +132,7 @@ impl Sql { )); let mut model_idx = 0_usize; - self.build_queries_recursive(&model, vec![model.name()], &mut model_idx); + self.build_register_queries_recursive(&model, vec![model.name()], &mut model_idx); // Since previous query has not been executed, we have to make sure created_at exists let created_at: DateTime = @@ -166,20 +165,31 @@ impl Sql { Ok(()) } - pub async fn set_entity( - &mut self, - model: String, - keys: Vec, - values: Vec, - ) -> Result<()> { + pub async fn set_entity(&mut self, entity: Ty) -> Result<()> { + let keys = if let Ty::Struct(s) = &entity { + let mut keys = Vec::new(); + for m in s.keys() { + keys.extend(m.serialize()?); + } + keys + } else { + return Err(anyhow!("Entity is not a struct")); + }; + let entity_id = format!("{:#x}", poseidon_hash_many(&keys)); - let entity_result = sqlx::query("SELECT * FROM entities WHERE id = ?") - .bind(&entity_id) - .fetch_optional(&self.pool) - .await?; + let existing: Option<(String,)> = + sqlx::query_as("SELECT model_names FROM entities WHERE id = ?") + .bind(&entity_id) + .fetch_optional(&self.pool) + .await?; + + let model_names = if let Some((model_names,)) = existing { + format!("{},{}", model_names, entity.name()) + } else { + entity.name() + }; let keys_str = felts_sql_string(&keys); - let model_names = model_names_sql_string(entity_result, &model)?; self.query_queue.push(format!( "INSERT INTO entities (id, keys, model_names) VALUES ('{}', '{}', '{}') ON \ CONFLICT(id) DO UPDATE SET model_names=excluded.model_names, \ @@ -187,36 +197,9 @@ impl Sql { entity_id, keys_str, model_names )); - let members: Vec<(String, String, String)> = sqlx::query_as( - "SELECT id, name, type FROM model_members WHERE model_id = ? ORDER BY model_idx, \ - member_idx ASC", - ) - .bind(model.clone()) - .fetch_all(&self.pool) - .await?; + let path = vec![entity.name()]; + self.build_set_entity_queries_recursive(path, &entity_id, &entity); - let (primitive_members, _): (Vec<_>, Vec<_>) = - members.into_iter().partition(|member| CairoType::from_str(&member.2).is_ok()); - - // keys are part of model members, so combine keys and model values array - let mut member_values: Vec = Vec::new(); - member_values.extend(keys.clone()); - member_values.extend(values); - - let insert_models: Vec<_> = primitive_members - .into_iter() - .zip(member_values.into_iter()) - .map(|((id, name, ty), value)| { - format!( - "INSERT OR REPLACE INTO [{id}] (entity_id, external_{name}) VALUES \ - ('{entity_id}' {})", - CairoType::from_str(&ty).unwrap().format_for_sql(vec![&value]).unwrap() - ) - }) - .collect(); - - // tx commit required - self.query_queue.extend(insert_models); self.execute().await?; let query_result = sqlx::query("SELECT created_at FROM entities WHERE id = ?") @@ -293,81 +276,125 @@ impl Sql { Ok(()) } - fn build_queries_recursive( + fn build_register_queries_recursive( &mut self, model: &Ty, - table_path: Vec, + path: Vec, model_idx: &mut usize, ) { - self.build_model_query(table_path.clone(), model, *model_idx); + if let Ty::Enum(_) = model { + // Complex enum values not supported yet. + return; + } + + self.build_model_query(path.clone(), model, *model_idx); + + if let Ty::Struct(s) = model { + for member in s.children.iter() { + if let Ty::Primitive(_) = member.ty { + continue; + } + + let mut path_clone = path.clone(); + path_clone.push(member.ty.name()); + + self.build_register_queries_recursive( + &member.ty, + path_clone, + &mut (*model_idx + 1), + ); + } + } + } - match model { + fn build_set_entity_queries_recursive(&mut self, path: Vec, id: &str, entity: &Ty) { + match entity { Ty::Struct(s) => { + let table_id = path.join("$"); + let mut columns = vec!["entity_id".to_string()]; + let mut values = vec![format!("'{id}'")]; + for member in s.children.iter() { - if let Ty::Primitive(_) = member.ty { - continue; + match &member.ty { + Ty::Primitive(ty) => { + columns.push(format!("external_{}", &member.name)); + values.push(ty.to_sql_value().unwrap()); + } + Ty::Enum(e) => { + columns.push(format!("external_{}", &member.name)); + values.push(e.to_sql_value().unwrap()); + } + _ => {} } + } + + self.query_queue.push(format!( + "INSERT OR REPLACE INTO [{table_id}] ({}) VALUES ({})", + columns.join(", "), + values.join(", ") + )); - let mut table_path_clone = table_path.clone(); - table_path_clone.push(member.ty.name()); + for member in s.children.iter() { + if let Ty::Struct(_) = &member.ty { + let mut path_clone = path.clone(); + path_clone.push(member.ty.name()); - self.build_queries_recursive( - &member.ty, - table_path_clone, - &mut (*model_idx + 1), - ); + self.build_set_entity_queries_recursive(path_clone, id, &member.ty); + } } } Ty::Enum(e) => { - for child in e.children.iter() { - let mut table_path_clone = table_path.clone(); - table_path_clone.push(child.1.name()); - self.build_model_query(table_path_clone.clone(), &child.1, *model_idx); - self.build_queries_recursive(&child.1, table_path_clone, &mut (*model_idx + 1)); + for child in e.options.iter() { + let mut path_clone = path.clone(); + path_clone.push(child.1.name()); + // self.build_entity_query(path_clone.clone(), id, &child.1); + self.build_set_entity_queries_recursive(path_clone, id, &child.1); } } _ => {} } } - fn build_model_query(&mut self, table_path: Vec, model: &Ty, model_idx: usize) { - let table_id = table_path.join("$"); + fn build_model_query(&mut self, path: Vec, model: &Ty, model_idx: usize) { + let table_id = path.join("$"); let mut query = format!( "CREATE TABLE IF NOT EXISTS [{table_id}] (entity_id TEXT NOT NULL PRIMARY KEY, " ); - match model { - Ty::Struct(s) => { - for (member_idx, member) in s.children.iter().enumerate() { - if let Ok(cairo_type) = CairoType::from_str(&member.ty.name()) { - query.push_str(&format!( - "external_{} {}, ", - member.name, - cairo_type.to_sql_type() - )); - }; - - self.query_queue.push(format!( - "INSERT OR IGNORE INTO model_members (id, model_id, model_idx, \ - member_idx, name, type, key) VALUES ('{table_id}', '{}', '{model_idx}', \ - '{member_idx}', '{}', '{}', {})", - table_path[0], - member.name, - member.ty.name(), - member.key, + if let Ty::Struct(s) = model { + for (member_idx, member) in s.children.iter().enumerate() { + let name = member.name.clone(); + if let Ok(cairo_type) = Primitive::from_str(&member.ty.name()) { + query.push_str(&format!("external_{name} {}, ", cairo_type.to_sql_type())); + } else if let Ty::Enum(e) = &member.ty { + let options = e + .options + .iter() + .map(|c| format!("'{}'", c.0)) + .collect::>() + .join(", "); + query.push_str(&format!( + "external_{name} TEXT CHECK(external_{name} IN ({options})) NOT NULL, ", )); } + + self.query_queue.push(format!( + "INSERT OR IGNORE INTO model_members (id, model_id, model_idx, member_idx, \ + name, type, key) VALUES ('{table_id}', '{}', '{model_idx}', '{member_idx}', \ + '{name}', '{}', {})", + path[0], + member.ty.name(), + member.key, + )); } - Ty::Enum(_) => {} - _ => {} } query.push_str("created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); // If this is not the Model's root table, create a reference to the parent. - if table_path.len() > 1 { - let parent_table_id = table_path[..table_path.len() - 1].join("$"); + if path.len() > 1 { + let parent_table_id = path[..path.len() - 1].join("$"); query.push_str(&format!( "FOREIGN KEY (entity_id) REFERENCES {parent_table_id} (entity_id), " )); @@ -392,22 +419,6 @@ impl Sql { } } -fn model_names_sql_string(entity_result: Option, new_model: &str) -> Result { - let model_names = match entity_result { - Some(entity) => { - let existing = entity.try_get::("model_names")?; - if existing.contains(new_model) { - existing - } else { - format!("{},{}", existing, new_model) - } - } - None => new_model.to_string(), - }; - - Ok(model_names) -} - fn felts_sql_string(felts: &[FieldElement]) -> String { felts.iter().map(|k| format!("{:#x}", k)).collect::>().join("/") + "/" } diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index 1090cc09da..84916a4b42 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -1,12 +1,68 @@ use camino::Utf8PathBuf; -use dojo_types::core::CairoType; +use dojo_test_utils::migration::prepare_migration; +use dojo_test_utils::sequencer::{ + get_default_test_starknet_config, SequencerConfig, TestSequencer, +}; +use dojo_types::primitive::Primitive; use dojo_types::schema::{Member, Struct, Ty}; use dojo_world::manifest::System; -use sqlx::sqlite::SqlitePool; -use starknet::core::types::{Event, FieldElement}; - +use dojo_world::migration::strategy::MigrationStrategy; +use scarb_ui::{OutputFormat, Ui, Verbosity}; +use sozo::ops::migration::execute_strategy; +use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; +use starknet::core::types::{BlockId, BlockTag, Event, FieldElement}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; +use torii_client::contract::world::WorldContractReader; + +use crate::engine::{Engine, EngineConfig, Processors}; +use crate::processors::register_model::RegisterModelProcessor; +use crate::processors::register_system::RegisterSystemProcessor; +use crate::processors::store_set_record::StoreSetRecordProcessor; use crate::sql::Sql; +pub async fn bootstrap_engine<'a>( + world: &'a WorldContractReader<'a, JsonRpcClient>, + db: &'a mut Sql, + provider: &'a JsonRpcClient, + migration: &MigrationStrategy, + sequencer: &TestSequencer, +) -> Result>, Box> { + let mut account = sequencer.account(); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + let manifest = dojo_world::manifest::Manifest::load_from_path( + Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev/manifest.json".into()) + .unwrap(), + ) + .unwrap(); + + db.load_from_manifest(manifest.clone()).await.unwrap(); + + let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); + execute_strategy(migration, &account, &ui, None).await.unwrap(); + + let mut engine = Engine::new( + world, + db, + provider, + Processors { + event: vec![ + Box::new(RegisterModelProcessor), + Box::new(RegisterSystemProcessor), + Box::new(StoreSetRecordProcessor), + ], + ..Processors::default() + }, + EngineConfig::default(), + None, + ); + + let _ = engine.sync_to_head(0).await?; + + Ok(engine) +} + #[sqlx::test(migrations = "../migrations")] async fn test_load_from_manifest(pool: SqlitePool) { let manifest = dojo_world::manifest::Manifest::load_from_path( @@ -48,11 +104,23 @@ async fn test_load_from_manifest(pool: SqlitePool) { .register_model( Ty::Struct(Struct { name: "Position".into(), - children: vec![Member { - name: "test".into(), - ty: Ty::Primitive(CairoType::U32(None)), - key: false, - }], + children: vec![ + Member { + name: "player".into(), + ty: Ty::Primitive(Primitive::ContractAddress(None)), + key: false, + }, + Member { + name: "x".to_string(), + key: true, + ty: Ty::Primitive(Primitive::U32(None)), + }, + Member { + name: "y".to_string(), + key: true, + ty: Ty::Primitive(Primitive::U32(None)), + }, + ], }), vec![], FieldElement::TWO, @@ -98,15 +166,26 @@ async fn test_load_from_manifest(pool: SqlitePool) { assert_eq!(class_hash, format!("{:#x}", FieldElement::THREE)); state - .set_entity( - "Position".to_string(), - vec![FieldElement::ONE], - vec![ - FieldElement::ONE, - FieldElement::from_dec_str("42").unwrap(), - FieldElement::from_dec_str("69").unwrap(), + .set_entity(Ty::Struct(Struct { + name: "Position".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::ONE))), + }, + Member { + name: "x".to_string(), + key: true, + ty: Ty::Primitive(Primitive::U32(Some(42))), + }, + Member { + name: "y".to_string(), + key: true, + ty: Ty::Primitive(Primitive::U32(Some(69))), + }, ], - ) + })) .await .unwrap(); @@ -141,3 +220,18 @@ async fn test_load_from_manifest(pool: SqlitePool) { assert_eq!(data, format!("{:#x}/{:#x}/", FieldElement::TWO, FieldElement::THREE)); assert_eq!(tx_hash, format!("{:#x}", FieldElement::THREE)) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_load_from_remote() { + let pool = + SqlitePoolOptions::new().max_connections(5).connect("sqlite::memory:").await.unwrap(); + sqlx::migrate!("../migrations").run(&pool).await.unwrap(); + let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); + let migration = prepare_migration("../../../examples/ecs/target/dev".into()).unwrap(); + let sequencer = + TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; + let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); + let world = WorldContractReader::new(migration.world_address().unwrap(), &provider); + + let _ = bootstrap_engine(&world, &mut db, &provider, &migration, &sequencer).await; +} diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 05e33083c2..5842bf0917 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -19,7 +19,6 @@ indexmap = "1.9.3" scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true -sozo = { path = "../../sozo" } sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } tokio-stream = "0.1.11" tokio-util = "0.7.7" diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index 3fc6fbdc67..d4216c2cc6 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -1,37 +1,22 @@ #[cfg(test)] mod tests { - use dojo_test_utils::migration::prepare_migration; - use dojo_test_utils::sequencer::{ - get_default_test_starknet_config, SequencerConfig, TestSequencer, - }; + use sqlx::SqlitePool; - use starknet::providers::jsonrpc::HttpTransport; - use starknet::providers::JsonRpcClient; use starknet_crypto::{poseidon_hash_many, FieldElement}; - use torii_client::contract::world::WorldContractReader; use torii_core::sql::Sql; use crate::tests::{ - bootstrap_engine, create_pool, entity_fixtures, paginate, run_graphql_query, Entity, Moves, - Paginate, Position, + entity_fixtures, paginate, run_graphql_query, Entity, Moves, Paginate, Position, }; - #[tokio::test(flavor = "multi_thread")] - async fn test_entity() { - let pool = create_pool().await; + #[sqlx::test(migrations = "../migrations")] + async fn test_entity(pool: SqlitePool) { let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - let migration = prepare_migration("../../../examples/ecs/target/dev".into()).unwrap(); - let sequencer = - TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()) - .await; - let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); - let world = WorldContractReader::new(migration.world_address().unwrap(), &provider); - - let _ = bootstrap_engine(&world, &mut db, &provider, &migration, &sequencer).await; entity_fixtures(&mut db).await; let entity_id = poseidon_hash_many(&[FieldElement::ONE]); + println!("{:#x}", entity_id); let query = format!( r#" {{ diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 8742025a3b..7eb3c1fd7a 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -1,21 +1,10 @@ -use camino::Utf8PathBuf; -use dojo_test_utils::sequencer::TestSequencer; -use dojo_world::migration::strategy::MigrationStrategy; -use scarb_ui::{OutputFormat, Ui, Verbosity}; +use dojo_types::primitive::Primitive; +use dojo_types::schema::{Enum, Member, Struct, Ty}; use serde::Deserialize; use serde_json::Value; -use sozo::ops::migration::execute_strategy; -use sqlx::sqlite::SqlitePoolOptions; use sqlx::SqlitePool; -use starknet::core::types::{BlockId, BlockTag, FieldElement}; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; +use starknet::core::types::FieldElement; use tokio_stream::StreamExt; -use torii_client::contract::world::WorldContractReader; -use torii_core::engine::{Engine, EngineConfig, Processors}; -use torii_core::processors::register_model::RegisterModelProcessor; -use torii_core::processors::register_system::RegisterSystemProcessor; -use torii_core::processors::store_set_record::StoreSetRecordProcessor; use torii_core::sql::Sql; mod entities_test; @@ -67,7 +56,6 @@ pub enum Paginate { Backward, } -#[allow(dead_code)] pub async fn run_graphql_query(pool: &SqlitePool, query: &str) -> Value { let schema = build_schema(pool).await.unwrap(); let res = schema.execute(query).await; @@ -76,55 +64,6 @@ pub async fn run_graphql_query(pool: &SqlitePool, query: &str) -> Value { serde_json::to_value(res.data).expect("Failed to serialize GraphQL response") } -pub async fn create_pool() -> SqlitePool { - let pool = - SqlitePoolOptions::new().max_connections(5).connect("sqlite::memory:").await.unwrap(); - sqlx::migrate!("../migrations").run(&pool).await.unwrap(); - pool -} - -pub async fn bootstrap_engine<'a>( - world: &'a WorldContractReader<'a, JsonRpcClient>, - db: &'a mut Sql, - provider: &'a JsonRpcClient, - migration: &MigrationStrategy, - sequencer: &TestSequencer, -) -> Result>, Box> { - let mut account = sequencer.account(); - account.set_block_id(BlockId::Tag(BlockTag::Pending)); - - let manifest = dojo_world::manifest::Manifest::load_from_path( - Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev/manifest.json".into()) - .unwrap(), - ) - .unwrap(); - - db.load_from_manifest(manifest.clone()).await.unwrap(); - - let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); - execute_strategy(migration, &account, &ui, None).await.unwrap(); - - let mut engine = Engine::new( - world, - db, - provider, - Processors { - event: vec![ - Box::new(RegisterModelProcessor), - Box::new(RegisterSystemProcessor), - Box::new(StoreSetRecordProcessor), - ], - ..Processors::default() - }, - EngineConfig::default(), - None, - ); - - let _ = engine.sync_to_head(0).await?; - - Ok(engine) -} - #[allow(dead_code)] pub async fn run_graphql_subscription( pool: &SqlitePool, @@ -137,34 +76,209 @@ pub async fn run_graphql_subscription( } pub async fn entity_fixtures(db: &mut Sql) { - // Set entity with one moves model - // remaining: 10, last_direction: 0 - let key = vec![FieldElement::ONE]; - let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap(), FieldElement::ZERO]; - db.set_entity("Moves".to_string(), key, moves_values.clone()).await.unwrap(); - - // Set entity with one position model - // x: 42 - // y: 69 - let key = vec![FieldElement::TWO]; - let position_values = vec![ - FieldElement::from_hex_be("0x2a").unwrap(), - FieldElement::from_hex_be("0x45").unwrap(), - ]; - db.set_entity("Position".to_string(), key, position_values.clone()).await.unwrap(); + db.register_model( + Ty::Struct(Struct { + name: "Moves".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(None)), + }, + Member { + name: "remaining".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U8(None)), + }, + Member { + name: "last_direction".to_string(), + key: false, + ty: Ty::Enum(Enum { + name: "Direction".to_string(), + option: None, + options: vec![ + ("None".to_string(), Ty::Tuple(vec![])), + ("Left".to_string(), Ty::Tuple(vec![])), + ("Right".to_string(), Ty::Tuple(vec![])), + ("Up".to_string(), Ty::Tuple(vec![])), + ("Down".to_string(), Ty::Tuple(vec![])), + ], + }), + }, + ], + }), + vec![], + FieldElement::ONE, + ) + .await + .unwrap(); + + db.register_model( + Ty::Struct(Struct { + name: "Position".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(None)), + }, + Member { + name: "vec".to_string(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".to_string(), + children: vec![ + Member { + name: "x".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(None)), + }, + Member { + name: "y".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(None)), + }, + ], + }), + }, + ], + }), + vec![], + FieldElement::TWO, + ) + .await + .unwrap(); + + db.set_entity(Ty::Struct(Struct { + name: "Moves".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::ONE))), + }, + Member { + name: "remaining".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U8(Some(10))), + }, + Member { + name: "last_direction".to_string(), + key: false, + ty: Ty::Enum(Enum { + name: "Direction".to_string(), + option: Some(1), + options: vec![ + ("None".to_string(), Ty::Tuple(vec![])), + ("Left".to_string(), Ty::Tuple(vec![])), + ("Right".to_string(), Ty::Tuple(vec![])), + ("Up".to_string(), Ty::Tuple(vec![])), + ("Down".to_string(), Ty::Tuple(vec![])), + ], + }), + }, + ], + })) + .await + .unwrap(); + + db.set_entity(Ty::Struct(Struct { + name: "Position".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::TWO))), + }, + Member { + name: "vec".to_string(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".to_string(), + children: vec![ + Member { + name: "x".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(42))), + }, + Member { + name: "y".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(69))), + }, + ], + }), + }, + ], + })) + .await + .unwrap(); // Set an entity with both moves and position models - // remaining: 1, last_direction: 0 - // x: 69 - // y: 42 - let key = vec![FieldElement::THREE]; - let moves_values = vec![FieldElement::from_hex_be("0x1").unwrap(), FieldElement::ZERO]; - let position_values = vec![ - FieldElement::from_hex_be("0x45").unwrap(), - FieldElement::from_hex_be("0x2a").unwrap(), - ]; - db.set_entity("Moves".to_string(), key.clone(), moves_values).await.unwrap(); - db.set_entity("Position".to_string(), key, position_values).await.unwrap(); + db.set_entity(Ty::Struct(Struct { + name: "Moves".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::THREE))), + }, + Member { + name: "remaining".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U8(Some(10))), + }, + Member { + name: "last_direction".to_string(), + key: false, + ty: Ty::Enum(Enum { + name: "Direction".to_string(), + option: Some(2), + options: vec![ + ("None".to_string(), Ty::Tuple(vec![])), + ("Left".to_string(), Ty::Tuple(vec![])), + ("Right".to_string(), Ty::Tuple(vec![])), + ("Up".to_string(), Ty::Tuple(vec![])), + ("Down".to_string(), Ty::Tuple(vec![])), + ], + }), + }, + ], + })) + .await + .unwrap(); + + db.set_entity(Ty::Struct(Struct { + name: "Position".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::THREE))), + }, + Member { + name: "vec".to_string(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".to_string(), + children: vec![ + Member { + name: "x".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(42))), + }, + Member { + name: "y".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(69))), + }, + ], + }), + }, + ], + })) + .await + .unwrap(); db.execute().await.unwrap(); }