From a44f0d789de3a502a8c2d06097e050100493ab54 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Fri, 22 Sep 2023 11:16:19 -0400 Subject: [PATCH 01/14] Runtime model schema registration (#915) --- crates/dojo-lang/src/manifest.rs | 4 +- crates/dojo-world/src/manifest.rs | 6 +- crates/dojo-world/src/migration/world_test.rs | 6 +- crates/katana/core/src/fork/db.rs | 1 + crates/sozo/src/ops/migration/mod.rs | 2 +- crates/torii/core/src/processors/mod.rs | 2 +- ...egister_component.rs => register_model.rs} | 18 +-- crates/torii/core/src/sql.rs | 103 +++++++++--------- crates/torii/core/src/sql_test.rs | 20 ++-- crates/torii/core/src/types.rs | 4 +- crates/torii/graphql/src/object/entity.rs | 27 ++--- .../graphql/src/object/inputs/order_input.rs | 2 +- .../graphql/src/object/inputs/where_input.rs | 2 +- crates/torii/graphql/src/object/mod.rs | 4 +- .../src/object/{component.rs => model.rs} | 70 ++++++------ .../{component_state.rs => model_state.rs} | 44 ++++---- crates/torii/graphql/src/query/filter.rs | 2 +- crates/torii/graphql/src/schema.rs | 41 ++++--- crates/torii/graphql/src/tests/common/mod.rs | 10 +- .../torii/graphql/src/tests/entities_test.rs | 24 ++-- crates/torii/graphql/src/tests/mod.rs | 2 +- .../{components_test.rs => models_test.rs} | 39 ++++--- .../graphql/src/tests/subscription_test.rs | 42 +++---- .../torii/migrations/20230316154230_setup.sql | 16 +-- crates/torii/server/src/cli.rs | 4 +- 25 files changed, 239 insertions(+), 256 deletions(-) rename crates/torii/core/src/processors/{register_component.rs => register_model.rs} (54%) rename crates/torii/graphql/src/object/{component.rs => model.rs} (59%) rename crates/torii/graphql/src/object/{component_state.rs => model_state.rs} (90%) rename crates/torii/graphql/src/tests/{components_test.rs => models_test.rs} (81%) diff --git a/crates/dojo-lang/src/manifest.rs b/crates/dojo-lang/src/manifest.rs index 58a34e1b96..6efcf62b26 100644 --- a/crates/dojo-lang/src/manifest.rs +++ b/crates/dojo-lang/src/manifest.rs @@ -87,7 +87,7 @@ impl Manifest { } /// Finds the inline modules annotated as components in the given crate_ids and - /// returns the corresponding Components. + /// returns the corresponding Models. fn find_components( &mut self, db: &dyn SemanticGroup, @@ -108,7 +108,7 @@ impl Manifest { .with_context(|| format!("Component {name} not found in target.")) .unwrap(); - self.0.components.push(dojo_world::manifest::Component { + self.0.components.push(dojo_world::manifest::Model { name: component.name, members: component.members, class_hash: *class_hash, diff --git a/crates/dojo-world/src/manifest.rs b/crates/dojo-world/src/manifest.rs index d4134a0b69..892ca365c8 100644 --- a/crates/dojo-world/src/manifest.rs +++ b/crates/dojo-world/src/manifest.rs @@ -52,7 +52,7 @@ pub struct Member { /// Represents a declaration of a component. #[serde_as] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct Component { +pub struct Model { pub name: String, pub members: Vec, #[serde_as(as = "UfeHex")] @@ -106,7 +106,7 @@ pub struct Manifest { pub executor: Contract, pub systems: Vec, pub contracts: Vec, - pub components: Vec, + pub components: Vec, } impl Manifest { @@ -180,7 +180,7 @@ impl Manifest { .await .map_err(ManifestError::Provider)?; - components.push(Component { + components.push(Model { name: component.name.clone(), class_hash: result[0], ..Default::default() diff --git a/crates/dojo-world/src/migration/world_test.rs b/crates/dojo-world/src/migration/world_test.rs index cdd9f898c8..0d9e0d4e9d 100644 --- a/crates/dojo-world/src/migration/world_test.rs +++ b/crates/dojo-world/src/migration/world_test.rs @@ -1,5 +1,5 @@ use super::*; -use crate::manifest::{Component, Contract, Manifest, System}; +use crate::manifest::{Contract, Manifest, Model, System}; #[test] fn no_diff_when_local_and_remote_are_equal() { @@ -17,7 +17,7 @@ fn no_diff_when_local_and_remote_are_equal() { ..Default::default() }; - let components = vec![Component { + let components = vec![Model { members: vec![], name: "Component".into(), class_hash: 11_u32.into(), @@ -55,7 +55,7 @@ fn diff_when_local_and_remote_are_different() { ..Default::default() }; - let components = vec![Component { + let components = vec![Model { members: vec![], name: "Component".into(), class_hash: 11_u32.into(), diff --git a/crates/katana/core/src/fork/db.rs b/crates/katana/core/src/fork/db.rs index faa7131833..f7d55bb77a 100644 --- a/crates/katana/core/src/fork/db.rs +++ b/crates/katana/core/src/fork/db.rs @@ -253,6 +253,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] + #[ignore] async fn fetch_from_provider_if_not_in_cache() { let provider = JsonRpcClient::new(HttpTransport::new(Url::parse(FORKED_ENDPOINT).unwrap())); let mut db = ForkedDb::new(Arc::new(provider), BlockId::Tag(BlockTag::Latest)); diff --git a/crates/sozo/src/ops/migration/mod.rs b/crates/sozo/src/ops/migration/mod.rs index 2c790f6282..56a18c0437 100644 --- a/crates/sozo/src/ops/migration/mod.rs +++ b/crates/sozo/src/ops/migration/mod.rs @@ -366,7 +366,7 @@ where return Ok(None); } - ws_config.ui().print_header(format!("# Components ({})", components.len())); + ws_config.ui().print_header(format!("# Models ({})", components.len())); let mut declare_output = vec![]; diff --git a/crates/torii/core/src/processors/mod.rs b/crates/torii/core/src/processors/mod.rs index 825653837e..85c006005b 100644 --- a/crates/torii/core/src/processors/mod.rs +++ b/crates/torii/core/src/processors/mod.rs @@ -6,7 +6,7 @@ use torii_client::contract::world::WorldContractReader; use crate::sql::Sql; -pub mod register_component; +pub mod register_model; pub mod register_system; pub mod store_set_record; pub mod store_system_call; diff --git a/crates/torii/core/src/processors/register_component.rs b/crates/torii/core/src/processors/register_model.rs similarity index 54% rename from crates/torii/core/src/processors/register_component.rs rename to crates/torii/core/src/processors/register_model.rs index b70760c9ea..71275b8b0f 100644 --- a/crates/torii/core/src/processors/register_component.rs +++ b/crates/torii/core/src/processors/register_model.rs @@ -1,7 +1,7 @@ use anyhow::{Error, Ok, Result}; use async_trait::async_trait; -use dojo_world::manifest::Component; -use starknet::core::types::{BlockWithTxs, Event, InvokeTransactionReceipt}; +use dojo_world::manifest::Model; +use starknet::core::types::{BlockId, BlockTag, BlockWithTxs, Event, InvokeTransactionReceipt}; use starknet::core::utils::parse_cairo_short_string; use starknet::providers::Provider; use torii_client::contract::world::WorldContractReader; @@ -11,17 +11,17 @@ use super::EventProcessor; use crate::sql::Sql; #[derive(Default)] -pub struct RegisterComponentProcessor; +pub struct RegisterModelProcessor; #[async_trait] -impl EventProcessor

for RegisterComponentProcessor { +impl EventProcessor

for RegisterModelProcessor { fn event_key(&self) -> String { "ComponentRegistered".to_string() } async fn process( &self, - _world: &WorldContractReader<'_, P>, + world: &WorldContractReader<'_, P>, db: &Sql, _provider: &P, _block: &BlockWithTxs, @@ -29,11 +29,11 @@ impl EventProcessor

for RegisterComponentProcessor { event: &Event, ) -> Result<(), Error> { let name = parse_cairo_short_string(&event.data[0])?; + let model = world.component(&name, BlockId::Tag(BlockTag::Latest)).await?; + let _schema = model.schema(BlockId::Tag(BlockTag::Latest)).await?; + info!("registered model: {}", name); - info!("registered component: {}", name); - - db.register_component(Component { name, class_hash: event.data[1], ..Default::default() }) - .await?; + db.register_model(Model { name, class_hash: event.data[1], ..Default::default() }).await?; Ok(()) } } diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 9d5f2222d4..c778a60769 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::Result; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use dojo_world::manifest::{Component, Manifest, System}; +use dojo_world::manifest::{Manifest, Model, System}; use sqlx::pool::PoolConnection; use sqlx::sqlite::SqliteRow; use sqlx::{Executor, Pool, Row, Sqlite}; @@ -13,7 +13,7 @@ use tokio::sync::Mutex; use super::World; use crate::simple_broker::SimpleBroker; -use crate::types::{Component as ComponentType, Entity}; +use crate::types::{Entity, Model as ModelType}; #[cfg(test)] #[path = "sql_test.rs"] @@ -96,8 +96,8 @@ impl Sql { )]) .await; - for component in manifest.components { - self.register_component(component).await?; + for model in manifest.components { + self.register_model(model).await?; } for system in manifest.systems { @@ -151,24 +151,24 @@ impl Sql { Ok(()) } - pub async fn register_component(&self, component: Component) -> Result<()> { + pub async fn register_model(&self, model: Model) -> Result<()> { let mut sql_types = self.sql_types.lock().await; - let component_id = component.name.to_lowercase(); + let model_id = model.name.to_lowercase(); let mut queries = vec![format!( - "INSERT INTO components (id, name, class_hash) VALUES ('{}', '{}', '{:#x}') ON \ + "INSERT INTO models (id, name, class_hash) VALUES ('{}', '{}', '{:#x}') ON \ CONFLICT(id) DO UPDATE SET class_hash='{:#x}'", - component_id, component.name, component.class_hash, component.class_hash + model_id, model.name, model.class_hash, model.class_hash )]; - let mut component_table_query = format!( + let mut model_table_query = format!( "CREATE TABLE IF NOT EXISTS external_{} (entity_id TEXT NOT NULL PRIMARY KEY, ", - component.name.to_lowercase() + model.name.to_lowercase() ); - for member in &component.members { - // FIXME: defaults all unknown component types to Enum for now until we support nested - // components + for member in &model.members { + // FIXME: defaults all unknown model types to Enum for now until we support nested + // models let (sql_type, member_type) = match sql_types.get(&member.ty) { Some(sql_type) => (*sql_type, member.ty.as_str()), None => { @@ -178,26 +178,26 @@ impl Sql { }; queries.push(format!( - "INSERT OR IGNORE INTO component_members (component_id, name, type, key) VALUES \ - ('{}', '{}', '{}', {})", - component_id, member.name, member_type, member.key, + "INSERT OR IGNORE INTO model_members (model_id, name, type, key) VALUES ('{}', \ + '{}', '{}', {})", + model_id, member.name, member_type, member.key, )); - component_table_query.push_str(&format!("external_{} {}, ", member.name, sql_type)); + model_table_query.push_str(&format!("external_{} {}, ", member.name, sql_type)); } - component_table_query.push_str( + model_table_query.push_str( "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (entity_id) REFERENCES entities(id));", ); - queries.push(component_table_query); + queries.push(model_table_query); self.queue(queries).await; // Since previous query has not been executed, we have to make sure created_at exists let created_at: DateTime = - match sqlx::query("SELECT created_at FROM components WHERE id = ?") - .bind(component_id.clone()) + match sqlx::query("SELECT created_at FROM models WHERE id = ?") + .bind(model_id.clone()) .fetch_one(&self.pool) .await { @@ -205,10 +205,10 @@ impl Sql { Err(_) => Utc::now(), }; - SimpleBroker::publish(ComponentType { - id: component_id, - name: component.name, - class_hash: format!("{:#x}", component.class_hash), + SimpleBroker::publish(ModelType { + id: model_id, + name: model.name, + class_hash: format!("{:#x}", model.class_hash), transaction_hash: "0x0".to_string(), created_at, }); @@ -230,7 +230,7 @@ impl Sql { pub async fn set_entity( &self, - component: String, + model: String, keys: Vec, values: Vec, ) -> Result<()> { @@ -241,22 +241,22 @@ impl Sql { .await?; let keys_str = felts_sql_string(&keys); - let component_names = component_names_sql_string(entity_result, &component)?; + let model_names = model_names_sql_string(entity_result, &model)?; let insert_entities = format!( - "INSERT INTO entities (id, keys, component_names) VALUES ('{}', '{}', '{}') ON \ + "INSERT INTO entities (id, keys, model_names) VALUES ('{}', '{}', '{}') ON \ CONFLICT(id) DO UPDATE SET - component_names=excluded.component_names, + model_names=excluded.model_names, updated_at=CURRENT_TIMESTAMP", - entity_id, keys_str, component_names + entity_id, keys_str, model_names ); let member_names_result = - sqlx::query("SELECT * FROM component_members WHERE component_id = ? ORDER BY id ASC") - .bind(component.to_lowercase()) + sqlx::query("SELECT * FROM model_members WHERE model_id = ? ORDER BY id ASC") + .bind(model.to_lowercase()) .fetch_all(&self.pool) .await?; - // keys are part of component members, so combine keys and component values array + // keys are part of model members, so combine keys and model values array let mut member_values: Vec = Vec::new(); member_values.extend(keys); member_values.extend(values); @@ -265,16 +265,16 @@ impl Sql { let names_str = members_sql_string(&member_names_result)?; let values_str = values_sql_string(&member_names_result, &member_values, &sql_types)?; - let insert_components = format!( + let insert_models = format!( "INSERT OR REPLACE INTO external_{} (entity_id {}) VALUES ('{}' {})", - component.to_lowercase(), + model.to_lowercase(), names_str, entity_id, values_str ); // tx commit required - self.queue(vec![insert_entities, insert_components]).await; + self.queue(vec![insert_entities, insert_models]).await; self.execute().await?; let query_result = sqlx::query("SELECT created_at FROM entities WHERE id = ?") @@ -286,28 +286,28 @@ impl Sql { SimpleBroker::publish(Entity { id: entity_id.clone(), keys: keys_str, - component_names, + model_names, created_at, updated_at: Utc::now(), }); Ok(()) } - pub async fn delete_entity(&self, component: String, key: FieldElement) -> Result<()> { - let query = format!("DELETE FROM {component} WHERE id = {key}"); + pub async fn delete_entity(&self, model: String, key: FieldElement) -> Result<()> { + let query = format!("DELETE FROM {model} WHERE id = {key}"); self.queue(vec![query]).await; Ok(()) } - pub async fn entity(&self, component: String, key: FieldElement) -> Result> { - let query = format!("SELECT * FROM {component} WHERE id = {key}"); + pub async fn entity(&self, model: String, key: FieldElement) -> Result> { + let query = format!("SELECT * FROM {model} WHERE id = {key}"); let mut conn: PoolConnection = self.pool.acquire().await?; let row: (i32, String, String) = sqlx::query_as(&query).fetch_one(&mut conn).await?; Ok(serde_json::from_str(&row.2).unwrap()) } - pub async fn entities(&self, component: String) -> Result>> { - let query = format!("SELECT * FROM {component}"); + pub async fn entities(&self, model: String) -> Result>> { + let query = format!("SELECT * FROM {model}"); let mut conn: PoolConnection = self.pool.acquire().await?; let mut rows = sqlx::query_as::<_, (i32, String, String)>(&query).fetch_all(&mut conn).await?; @@ -379,23 +379,20 @@ impl Executable for Sql { } } -fn component_names_sql_string( - entity_result: Option, - new_component: &str, -) -> Result { - let component_names = match entity_result { +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::("component_names")?; - if existing.contains(new_component) { + let existing = entity.try_get::("model_names")?; + if existing.contains(new_model) { existing } else { - format!("{},{}", existing, new_component) + format!("{},{}", existing, new_model) } } - None => new_component.to_string(), + None => new_model.to_string(), }; - Ok(component_names) + Ok(model_names) } fn values_sql_string( diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index 731ae6c91b..bbe5d06bb1 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use camino::Utf8PathBuf; -use dojo_world::manifest::{Component, Member, System}; +use dojo_world::manifest::{Member, Model, System}; use sqlx::sqlite::SqlitePool; use starknet::core::types::{Event, FieldElement}; @@ -18,12 +18,11 @@ async fn test_load_from_manifest(pool: SqlitePool) { let state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); state.load_from_manifest(manifest.clone()).await.unwrap(); - let components = sqlx::query("SELECT * FROM components").fetch_all(&pool).await.unwrap(); - assert_eq!(components.len(), 2); + let models = sqlx::query("SELECT * FROM models").fetch_all(&pool).await.unwrap(); + assert_eq!(models.len(), 2); - let moves_components = - sqlx::query("SELECT * FROM external_moves").fetch_all(&pool).await.unwrap(); - assert_eq!(moves_components.len(), 0); + let moves_models = sqlx::query("SELECT * FROM external_moves").fetch_all(&pool).await.unwrap(); + assert_eq!(moves_models.len(), 0); let systems = sqlx::query("SELECT * FROM systems").fetch_all(&pool).await.unwrap(); assert_eq!(systems.len(), 3); @@ -52,7 +51,7 @@ async fn test_load_from_manifest(pool: SqlitePool) { assert_eq!(head, 1); state - .register_component(Component { + .register_model(Model { name: "Test".into(), members: vec![Member { name: "test".into(), ty: "u32".into(), key: false }], class_hash: FieldElement::TWO, @@ -63,7 +62,7 @@ async fn test_load_from_manifest(pool: SqlitePool) { state.execute().await.unwrap(); let (id, name, class_hash): (String, String, String) = - sqlx::query_as("SELECT id, name, class_hash FROM components WHERE id = 'test'") + sqlx::query_as("SELECT id, name, class_hash FROM models WHERE id = 'test'") .fetch_one(&pool) .await .unwrap(); @@ -72,9 +71,8 @@ async fn test_load_from_manifest(pool: SqlitePool) { assert_eq!(name, "Test"); assert_eq!(class_hash, format!("{:#x}", FieldElement::TWO)); - let test_components = - sqlx::query("SELECT * FROM external_test").fetch_all(&pool).await.unwrap(); - assert_eq!(test_components.len(), 0); + let test_models = sqlx::query("SELECT * FROM external_test").fetch_all(&pool).await.unwrap(); + assert_eq!(test_models.len(), 0); state .register_system(System { diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index ef21859ab9..62c7da91c4 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -33,14 +33,14 @@ impl fmt::LowerHex for SQLFieldElement { pub struct Entity { pub id: String, pub keys: String, - pub component_names: String, + pub model_names: String, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(FromRow, Deserialize, Clone)] #[serde(rename_all = "camelCase")] -pub struct Component { +pub struct Model { pub id: String, pub name: String, pub class_hash: String, diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 2890c5df34..c722195c62 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -9,11 +9,11 @@ use tokio_stream::StreamExt; use torii_core::simple_broker::SimpleBroker; use torii_core::types::Entity; -use super::component_state::{component_state_by_id_query, type_mapping_query}; use super::connection::{ connection_arguments, connection_output, decode_cursor, parse_connection_arguments, ConnectionArguments, }; +use super::model_state::{model_state_by_id_query, type_mapping_query}; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::constants::DEFAULT_LIMIT; use crate::query::{query_by_id, ID}; @@ -31,7 +31,7 @@ impl Default for EntityObject { type_mapping: IndexMap::from([ (Name::new("id"), TypeRef::named(TypeRef::ID)), (Name::new("keys"), TypeRef::named_list(TypeRef::STRING)), - (Name::new("componentNames"), TypeRef::named(TypeRef::STRING)), + (Name::new("modelNames"), TypeRef::named(TypeRef::STRING)), (Name::new("createdAt"), TypeRef::named(ScalarType::DateTime.to_string())), (Name::new("updatedAt"), TypeRef::named(ScalarType::DateTime.to_string())), ]), @@ -45,7 +45,7 @@ impl EntityObject { IndexMap::from([ (Name::new("id"), Value::from(entity.id)), (Name::new("keys"), Value::from(keys)), - (Name::new("componentNames"), Value::from(entity.component_names)), + (Name::new("modelNames"), Value::from(entity.model_names)), ( Name::new("createdAt"), Value::from(entity.created_at.format("%Y-%m-%d %H:%M:%S").to_string()), @@ -72,29 +72,24 @@ impl ObjectTrait for EntityObject { } fn nested_fields(&self) -> Option> { - Some(vec![Field::new("components", TypeRef::named_list("ComponentUnion"), move |ctx| { + Some(vec![Field::new("models", TypeRef::named_list("ModelUnion"), move |ctx| { FieldFuture::new(async move { match ctx.parent_value.try_to_value()? { Value::Object(indexmap) => { let mut conn = ctx.data::>()?.acquire().await?; - let components = - csv_to_vec(&extract::(indexmap, "componentNames")?); + let models = csv_to_vec(&extract::(indexmap, "modelNames")?); let id = extract::(indexmap, "id")?; let mut results: Vec> = Vec::new(); - for component_name in components { - let table_name = component_name.to_lowercase(); + for model_name in models { + let table_name = model_name.to_lowercase(); let type_mapping = type_mapping_query(&mut conn, &table_name).await?; - let state = component_state_by_id_query( - &mut conn, - &table_name, - &id, - &type_mapping, - ) - .await?; + let state = + model_state_by_id_query(&mut conn, &table_name, &id, &type_mapping) + .await?; results.push(FieldValue::with_type( FieldValue::owned_any(state), - component_name, + model_name, )); } diff --git a/crates/torii/graphql/src/object/inputs/order_input.rs b/crates/torii/graphql/src/object/inputs/order_input.rs index ace95726f4..c4c4cdee30 100644 --- a/crates/torii/graphql/src/object/inputs/order_input.rs +++ b/crates/torii/graphql/src/object/inputs/order_input.rs @@ -38,7 +38,7 @@ impl InputObjectTrait for OrderInputObject { // Direction enum has only two members ASC and DESC let direction = Enum::new("Direction").item("ASC").item("DESC"); - // Field Order enum consist of all members of a component + // Field Order enum consist of all members of a model let field_order = self .type_mapping .iter() diff --git a/crates/torii/graphql/src/object/inputs/where_input.rs b/crates/torii/graphql/src/object/inputs/where_input.rs index 6fa62379c4..a5b2c804b6 100644 --- a/crates/torii/graphql/src/object/inputs/where_input.rs +++ b/crates/torii/graphql/src/object/inputs/where_input.rs @@ -15,7 +15,7 @@ pub struct WhereInputObject { impl WhereInputObject { // Iterate through an object's type mapping and create a new mapping for whereInput. For each of - // the object type (component member), we add 6 additional types for comparators (great than, + // the object type (model member), we add 6 additional types for comparators (great than, // not equal, etc). Only filter on our custom scalar types and ignore async-graphql's types. // Due to sqlite column constraints, u8 thru u64 are treated as numerics and the rest of the // types are treated as strings. diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index 07484c0938..2a2d9215ac 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -1,9 +1,9 @@ -pub mod component; -pub mod component_state; pub mod connection; pub mod entity; pub mod event; pub mod inputs; +pub mod model; +pub mod model_state; pub mod system; pub mod system_call; diff --git a/crates/torii/graphql/src/object/component.rs b/crates/torii/graphql/src/object/model.rs similarity index 59% rename from crates/torii/graphql/src/object/component.rs rename to crates/torii/graphql/src/object/model.rs index 7cb038d9b6..6090b5d105 100644 --- a/crates/torii/graphql/src/object/component.rs +++ b/crates/torii/graphql/src/object/model.rs @@ -6,7 +6,7 @@ use indexmap::IndexMap; use sqlx::{Pool, Sqlite}; use tokio_stream::StreamExt; use torii_core::simple_broker::SimpleBroker; -use torii_core::types::Component; +use torii_core::types::Model; use super::connection::connection_output; use super::{ObjectTrait, TypeMapping, ValueMapping}; @@ -14,12 +14,12 @@ use crate::constants::DEFAULT_LIMIT; use crate::query::{query_all, query_by_id, query_total_count, ID}; use crate::types::ScalarType; -pub struct ComponentObject { +pub struct ModelObject { pub type_mapping: TypeMapping, } -impl Default for ComponentObject { - // Eventually used for component metadata +impl Default for ModelObject { + // Eventually used for model metadata fn default() -> Self { Self { type_mapping: IndexMap::from([ @@ -32,28 +32,28 @@ impl Default for ComponentObject { } } } -impl ComponentObject { - pub fn value_mapping(component: Component) -> ValueMapping { +impl ModelObject { + pub fn value_mapping(model: Model) -> ValueMapping { IndexMap::from([ - (Name::new("id"), Value::from(component.id)), - (Name::new("name"), Value::from(component.name)), - (Name::new("classHash"), Value::from(component.class_hash)), - (Name::new("transactionHash"), Value::from(component.transaction_hash)), + (Name::new("id"), Value::from(model.id)), + (Name::new("name"), Value::from(model.name)), + (Name::new("classHash"), Value::from(model.class_hash)), + (Name::new("transactionHash"), Value::from(model.transaction_hash)), ( Name::new("createdAt"), - Value::from(component.created_at.format("%Y-%m-%d %H:%M:%S").to_string()), + Value::from(model.created_at.format("%Y-%m-%d %H:%M:%S").to_string()), ), ]) } } -impl ObjectTrait for ComponentObject { +impl ObjectTrait for ModelObject { fn name(&self) -> &str { - "component" + "model" } fn type_name(&self) -> &str { - "Component" + "Model" } fn type_mapping(&self) -> &TypeMapping { @@ -66,8 +66,8 @@ impl ObjectTrait for ComponentObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let id = ctx.args.try_get("id")?.string()?.to_string(); - let component = query_by_id(&mut conn, "components", ID::Str(id)).await?; - let result = ComponentObject::value_mapping(component); + let model = query_by_id(&mut conn, "models", ID::Str(id)).await?; + let result = ModelObject::value_mapping(model); Ok(Some(Value::Object(result))) }) }) @@ -77,19 +77,17 @@ impl ObjectTrait for ComponentObject { fn resolve_many(&self) -> Option { Some(Field::new( - "components", + "models", TypeRef::named(format!("{}Connection", self.type_name())), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; - let total_count = - query_total_count(&mut conn, "components", &Vec::new()).await?; - let data: Vec = - query_all(&mut conn, "components", DEFAULT_LIMIT).await?; - let components: Vec = - data.into_iter().map(ComponentObject::value_mapping).collect(); + let total_count = query_total_count(&mut conn, "models", &Vec::new()).await?; + let data: Vec = query_all(&mut conn, "models", DEFAULT_LIMIT).await?; + let models: Vec = + data.into_iter().map(ModelObject::value_mapping).collect(); - Ok(Some(Value::Object(connection_output(components, total_count)))) + Ok(Some(Value::Object(connection_output(models, total_count)))) }) }, )) @@ -105,20 +103,16 @@ impl ObjectTrait for ComponentObject { Some(id) => Some(id.string()?.to_string()), None => None, }; - // if id is None, then subscribe to all components - // if id is Some, then subscribe to only the component with that id - Ok(SimpleBroker::::subscribe().filter_map( - move |component: Component| { - if id.is_none() || id == Some(component.id.clone()) { - Some(Ok(Value::Object(ComponentObject::value_mapping( - component, - )))) - } else { - // id != component.id, so don't send anything, still listening - None - } - }, - )) + // if id is None, then subscribe to all models + // if id is Some, then subscribe to only the model with that id + Ok(SimpleBroker::::subscribe().filter_map(move |model: Model| { + if id.is_none() || id == Some(model.id.clone()) { + Some(Ok(Value::Object(ModelObject::value_mapping(model)))) + } else { + // id != model.id, so don't send anything, still listening + None + } + })) }) } }) diff --git a/crates/torii/graphql/src/object/component_state.rs b/crates/torii/graphql/src/object/model_state.rs similarity index 90% rename from crates/torii/graphql/src/object/component_state.rs rename to crates/torii/graphql/src/object/model_state.rs index a8986447f6..81596d96f2 100644 --- a/crates/torii/graphql/src/object/component_state.rs +++ b/crates/torii/graphql/src/object/model_state.rs @@ -28,8 +28,8 @@ use crate::utils::extract_value::extract; const BOOLEAN_TRUE: i64 = 1; #[derive(FromRow, Deserialize)] -pub struct ComponentMembers { - pub component_id: String, +pub struct ModelMembers { + pub model_id: String, pub name: String, #[serde(rename = "type")] pub ty: String, @@ -37,7 +37,7 @@ pub struct ComponentMembers { pub created_at: DateTime, } -pub struct ComponentStateObject { +pub struct ModelStateObject { pub name: String, pub type_name: String, pub type_mapping: TypeMapping, @@ -45,7 +45,7 @@ pub struct ComponentStateObject { pub order_input: OrderInputObject, } -impl ComponentStateObject { +impl ModelStateObject { pub fn new(name: String, type_name: String, type_mapping: TypeMapping) -> Self { let where_input = WhereInputObject::new(type_name.as_str(), &type_mapping); let order_input = OrderInputObject::new(type_name.as_str(), &type_mapping); @@ -53,7 +53,7 @@ impl ComponentStateObject { } } -impl ObjectTrait for ComponentStateObject { +impl ObjectTrait for ModelStateObject { fn name(&self) -> &str { &self.name } @@ -62,12 +62,12 @@ impl ObjectTrait for ComponentStateObject { &self.type_name } - // Type mapping contains all component members and their corresponding type + // Type mapping contains all model members and their corresponding type fn type_mapping(&self) -> &TypeMapping { &self.type_mapping } - // Associate component to its parent entity + // Associate model to its parent entity fn nested_fields(&self) -> Option> { Some(vec![entity_field()]) } @@ -84,7 +84,7 @@ impl ObjectTrait for ComponentStateObject { let name = self.name.clone(); let type_mapping = self.type_mapping.clone(); let where_mapping = self.where_input.type_mapping.clone(); - let field_name = format!("{}Components", self.name()); + let field_name = format!("{}Models", self.name()); let field_type = format!("{}Connection", self.type_name()); let mut field = Field::new(field_name, TypeRef::named(field_type), move |ctx| { @@ -99,10 +99,10 @@ impl ObjectTrait for ComponentStateObject { let filters = parse_where_argument(&ctx, &where_mapping)?; let connection = parse_connection_arguments(&ctx)?; let data = - component_states_query(&mut conn, &table_name, &order, &filters, &connection) + model_states_query(&mut conn, &table_name, &order, &filters, &connection) .await?; let total_count = query_total_count(&mut conn, &table_name, &filters).await?; - let connection = component_connection(&data, &type_mapping, total_count)?; + let connection = model_connection(&data, &type_mapping, total_count)?; Ok(Some(Value::Object(connection))) }) @@ -135,7 +135,7 @@ fn entity_field() -> Field { }) } -pub async fn component_state_by_id_query( +pub async fn model_state_by_id_query( conn: &mut PoolConnection, name: &str, id: &str, @@ -148,7 +148,7 @@ pub async fn component_state_by_id_query( value_mapping_from_row(&row, fields) } -pub async fn component_states_query( +pub async fn model_states_query( conn: &mut PoolConnection, table_name: &str, order: &Option, @@ -216,14 +216,14 @@ pub async fn component_states_query( sqlx::query(&query).fetch_all(conn).await } -// TODO: make `connection_output()` more generic. Currently, `component_connection()` method +// TODO: make `connection_output()` more generic. Currently, `model_connection()` method // required as we need to explicity add `entity_id` to each edge. -pub fn component_connection( +pub fn model_connection( data: &[SqliteRow], types: &TypeMapping, total_count: i64, ) -> sqlx::Result { - let component_edges = data + let model_edges = data .iter() .map(|row| { // entity_id and created_at used to create cursor @@ -245,7 +245,7 @@ pub fn component_connection( Ok(ValueMapping::from([ (Name::new("totalCount"), Value::from(total_count)), - (Name::new("edges"), Value::List(component_edges?)), + (Name::new("edges"), Value::List(model_edges?)), // TODO: add pageInfo ])) } @@ -282,25 +282,25 @@ fn fetch_boolean(row: &SqliteRow, column_name: &str) -> sqlx::Result { pub async fn type_mapping_query( conn: &mut PoolConnection, - component_id: &str, + model_id: &str, ) -> sqlx::Result { - let component_members: Vec = sqlx::query_as( + let model_members: Vec = sqlx::query_as( r#" SELECT - component_id, + model_id, name, type AS ty, key, created_at - FROM component_members WHERE component_id = ? + FROM model_members WHERE model_id = ? "#, ) - .bind(component_id) + .bind(model_id) .fetch_all(conn) .await?; let mut type_mapping = TypeMapping::new(); - for member in component_members { + for member in model_members { type_mapping.insert(Name::new(member.name), TypeRef::named(member.ty)); } diff --git a/crates/torii/graphql/src/query/filter.rs b/crates/torii/graphql/src/query/filter.rs index 6ece9a9d89..5fd8cde198 100644 --- a/crates/torii/graphql/src/query/filter.rs +++ b/crates/torii/graphql/src/query/filter.rs @@ -49,7 +49,7 @@ pub fn parse_filter(input: &Name, value: FilterValue) -> Filter { for (suffix, comparator) in suffixes { if let Some(field) = input.strip_suffix(suffix) { - // Filtering only applies to component members which are stored in db with + // Filtering only applies to model members which are stored in db with // external_{name} return Filter { field: format!("external_{}", field), diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index ccb4291eaa..a5c4a2cda7 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -3,21 +3,21 @@ use async_graphql::dynamic::{ Field, Object, Scalar, Schema, Subscription, SubscriptionField, Union, }; use sqlx::SqlitePool; -use torii_core::types::Component; +use torii_core::types::Model; -use super::object::component_state::{type_mapping_query, ComponentStateObject}; use super::object::connection::page_info::PageInfoObject; use super::object::entity::EntityObject; use super::object::event::EventObject; +use super::object::model_state::{type_mapping_query, ModelStateObject}; use super::object::system::SystemObject; use super::object::system_call::SystemCallObject; use super::object::ObjectTrait; use super::types::ScalarType; use super::utils::format_name; -use crate::object::component::ComponentObject; +use crate::object::model::ModelObject; // The graphql schema is built dynamically at runtime, this is because we won't know the schema of -// the components until runtime. There are however, predefined objects such as entities and +// the models until runtime. There are however, predefined objects such as entities and // system_calls, their schema is known but we generate them dynamically as well since async-graphql // does not allow mixing of static and dynamic schemas. pub async fn build_schema(pool: &SqlitePool) -> Result { @@ -26,18 +26,18 @@ pub async fn build_schema(pool: &SqlitePool) -> Result { // predefined objects let mut objects: Vec> = vec![ Box::::default(), - Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), ]; - // register dynamic component objects - let (component_objects, component_union) = component_objects(pool).await?; - objects.extend(component_objects); + // register dynamic model objects + let (model_objects, model_union) = model_objects(pool).await?; + objects.extend(model_objects); - schema_builder = schema_builder.register(component_union); + schema_builder = schema_builder.register(model_union); // collect resolvers for single and plural queries let mut fields: Vec = Vec::new(); @@ -111,31 +111,30 @@ pub async fn build_schema(pool: &SqlitePool) -> Result { .map_err(|e| e.into()) } -async fn component_objects(pool: &SqlitePool) -> Result<(Vec>, Union)> { +async fn model_objects(pool: &SqlitePool) -> Result<(Vec>, Union)> { let mut conn = pool.acquire().await?; let mut objects: Vec> = Vec::new(); - let components: Vec = - sqlx::query_as("SELECT * FROM components").fetch_all(&mut conn).await?; + let models: Vec = sqlx::query_as("SELECT * FROM models").fetch_all(&mut conn).await?; - // component union object - let mut component_union = Union::new("ComponentUnion"); + // model union object + let mut model_union = Union::new("ModelUnion"); - // component state objects - for component_metadata in components { - let field_type_mapping = type_mapping_query(&mut conn, &component_metadata.id).await?; + // model state objects + for model_metadata in models { + let field_type_mapping = type_mapping_query(&mut conn, &model_metadata.id).await?; if !field_type_mapping.is_empty() { - let (name, type_name) = format_name(&component_metadata.name); - let state_object = Box::new(ComponentStateObject::new( + let (name, type_name) = format_name(&model_metadata.name); + let state_object = Box::new(ModelStateObject::new( name.clone(), type_name.clone(), field_type_mapping, )); - component_union = component_union.possible_type(&type_name); + model_union = model_union.possible_type(&type_name); objects.push(state_object); } } - Ok((objects, component_union)) + Ok((objects, model_union)) } diff --git a/crates/torii/graphql/src/tests/common/mod.rs b/crates/torii/graphql/src/tests/common/mod.rs index a19c321f9d..0ddb2d947a 100644 --- a/crates/torii/graphql/src/tests/common/mod.rs +++ b/crates/torii/graphql/src/tests/common/mod.rs @@ -25,7 +25,7 @@ pub struct Edge { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Entity { - pub component_names: String, + pub model_names: String, pub keys: Option>, pub created_at: Option, } @@ -73,13 +73,13 @@ pub async fn run_graphql_subscription( pub async fn entity_fixtures(pool: &SqlitePool) { let state = init(pool).await; - // Set entity with one moves component + // 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]; state.set_entity("Moves".to_string(), key, moves_values.clone()).await.unwrap(); - // Set entity with one position component + // Set entity with one position model // x: 42 // y: 69 let key = vec![FieldElement::TWO]; @@ -89,7 +89,7 @@ pub async fn entity_fixtures(pool: &SqlitePool) { ]; state.set_entity("Position".to_string(), key, position_values.clone()).await.unwrap(); - // Set an entity with both moves and position components + // Set an entity with both moves and position models // remaining: 1, last_direction: 0 // x: 69 // y: 42 @@ -138,7 +138,7 @@ pub async fn paginate( edges {{ cursor node {{ - componentNames + modelNames }} }} }} diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index 2f20cb86bb..3acf973c7f 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -15,7 +15,7 @@ mod tests { r#" {{ entity(id: "{:#x}") {{ - componentNames + modelNames }} }} "#, @@ -25,12 +25,12 @@ mod tests { let entity = value.get("entity").ok_or("no entity found").unwrap(); let entity: Entity = serde_json::from_value(entity.clone()).unwrap(); - assert_eq!(entity.component_names, "Moves".to_string()); + assert_eq!(entity.model_names, "Moves".to_string()); } #[ignore] #[sqlx::test(migrations = "../migrations")] - async fn test_entity_components(pool: SqlitePool) { + async fn test_entity_models(pool: SqlitePool) { entity_fixtures(&pool).await; let entity_id = poseidon_hash_many(&[FieldElement::THREE]); @@ -38,7 +38,7 @@ mod tests { r#" {{ entity (id: "{:#x}") {{ - components {{ + models {{ __typename ... on Moves {{ remaining @@ -57,15 +57,15 @@ mod tests { let value = run_graphql_query(&pool, &query).await; let entity = value.get("entity").ok_or("no entity found").unwrap(); - let components = entity.get("components").ok_or("no components found").unwrap(); - let component_moves: Moves = serde_json::from_value(components[0].clone()).unwrap(); - let component_position: Position = serde_json::from_value(components[1].clone()).unwrap(); + let models = entity.get("models").ok_or("no models found").unwrap(); + let model_moves: Moves = serde_json::from_value(models[0].clone()).unwrap(); + let model_position: Position = serde_json::from_value(models[1].clone()).unwrap(); - assert_eq!(component_moves.__typename, "Moves"); - assert_eq!(component_moves.remaining, 1); - assert_eq!(component_position.__typename, "Position"); - assert_eq!(component_position.x, 69); - assert_eq!(component_position.y, 42); + assert_eq!(model_moves.__typename, "Moves"); + assert_eq!(model_moves.remaining, 1); + assert_eq!(model_position.__typename, "Position"); + assert_eq!(model_position.x, 69); + assert_eq!(model_position.y, 42); } #[sqlx::test(migrations = "../migrations")] diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 93d63a4072..b578891237 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -1,4 +1,4 @@ mod common; -mod components_test; mod entities_test; +mod models_test; mod subscription_test; diff --git a/crates/torii/graphql/src/tests/components_test.rs b/crates/torii/graphql/src/tests/models_test.rs similarity index 81% rename from crates/torii/graphql/src/tests/components_test.rs rename to crates/torii/graphql/src/tests/models_test.rs index 62b653e13e..824e8487be 100644 --- a/crates/torii/graphql/src/tests/components_test.rs +++ b/crates/torii/graphql/src/tests/models_test.rs @@ -16,12 +16,12 @@ mod tests { #[ignore] #[sqlx::test(migrations = "../migrations")] - async fn test_component_no_filter(pool: SqlitePool) { + async fn test_model_no_filter(pool: SqlitePool) { entity_fixtures(&pool).await; let query = r#" { - movesComponents { + movesModels { totalCount edges { node { @@ -32,7 +32,7 @@ mod tests { cursor } } - positionComponents { + positionModels { totalCount edges { node { @@ -48,14 +48,13 @@ mod tests { let value = run_graphql_query(&pool, query).await; - let moves_components = value.get("movesComponents").ok_or("no moves found").unwrap(); + let moves_mdoels = value.get("movesModels").ok_or("no moves found").unwrap(); let moves_connection: Connection = - serde_json::from_value(moves_components.clone()).unwrap(); + serde_json::from_value(moves_mdoels.clone()).unwrap(); - let position_components = - value.get("positionComponents").ok_or("no position found").unwrap(); + let position_mdoels = value.get("positionModels").ok_or("no position found").unwrap(); let position_connection: Connection = - serde_json::from_value(position_components.clone()).unwrap(); + serde_json::from_value(position_mdoels.clone()).unwrap(); assert_eq!(moves_connection.edges[0].node.remaining, 10); assert_eq!(position_connection.edges[0].node.x, 42); @@ -64,10 +63,10 @@ mod tests { #[ignore] #[sqlx::test(migrations = "../migrations")] - async fn test_component_where_filter(pool: SqlitePool) { + async fn test_model_where_filter(pool: SqlitePool) { entity_fixtures(&pool).await; - // fixtures inserts two position components with members (x: 42, y: 69) and (x: 69, y: 42) + // fixtures inserts two position mdoels with members (x: 42, y: 69) and (x: 69, y: 42) // the following filters and expected total results can be simply calculated let where_filters = Vec::from([ ("where: { x: 42 }", 1), @@ -84,7 +83,7 @@ mod tests { let query = format!( r#" {{ - positionComponents ({}) {{ + positionModels ({}) {{ totalCount edges {{ node {{ @@ -101,7 +100,7 @@ mod tests { ); let value = run_graphql_query(&pool, &query).await; - let positions = value.get("positionComponents").ok_or("no positions found").unwrap(); + let positions = value.get("positionModels").ok_or("no positions found").unwrap(); let connection: Connection = serde_json::from_value(positions.clone()).unwrap(); assert_eq!(connection.total_count, expected_total); @@ -110,7 +109,7 @@ mod tests { #[ignore] #[sqlx::test(migrations = "../migrations")] - async fn test_component_ordering(pool: SqlitePool) { + async fn test_model_ordering(pool: SqlitePool) { entity_fixtures(&pool).await; let orders: Vec = vec![ @@ -148,7 +147,7 @@ mod tests { let query = format!( r#" {{ - positionComponents (order: {{ direction: {}, field: {} }}) {{ + positionModels (order: {{ direction: {}, field: {} }}) {{ totalCount edges {{ node {{ @@ -165,7 +164,7 @@ mod tests { ); let value = run_graphql_query(&pool, &query).await; - let positions = value.get("positionComponents").ok_or("no positions found").unwrap(); + let positions = value.get("positionModels").ok_or("no positions found").unwrap(); let connection: Connection = serde_json::from_value(positions.clone()).unwrap(); assert_eq!(connection.total_count, 2); @@ -175,12 +174,12 @@ mod tests { #[ignore] #[sqlx::test(migrations = "../migrations")] - async fn test_component_entity_relationship(pool: SqlitePool) { + async fn test_model_entity_relationship(pool: SqlitePool) { entity_fixtures(&pool).await; let query = r#" { - positionComponents { + positionModels { totalCount edges { node { @@ -189,7 +188,7 @@ mod tests { y entity { keys - componentNames + modelNames } } cursor @@ -199,9 +198,9 @@ mod tests { "#; let value = run_graphql_query(&pool, query).await; - let positions = value.get("positionComponents").ok_or("no positions found").unwrap(); + let positions = value.get("positionModels").ok_or("no positions found").unwrap(); let connection: Connection = serde_json::from_value(positions.clone()).unwrap(); let entity = connection.edges[0].node.entity.as_ref().unwrap(); - assert_eq!(entity.component_names, "Position".to_string()); + assert_eq!(entity.model_names, "Position".to_string()); } } diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index 0ae7fc60d7..f58d567350 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -3,7 +3,7 @@ mod tests { use std::time::Duration; use async_graphql::value; - use dojo_world::manifest::{Component, Member}; + use dojo_world::manifest::{Member, Model}; use sqlx::SqlitePool; use starknet_crypto::{poseidon_hash_many, FieldElement}; use tokio::sync::mpsc; @@ -21,7 +21,7 @@ mod tests { let entity_id = format!("{:#x}", poseidon_hash_many(&key)); let keys_str = key.iter().map(|k| format!("{:#x}", k)).collect::>().join(","); let expected_value: async_graphql::Value = value!({ - "entityUpdated": { "id": entity_id.clone(), "keys":vec![keys_str.clone()], "componentNames": "Moves" } + "entityUpdated": { "id": entity_id.clone(), "keys":vec![keys_str.clone()], "modelNames": "Moves" } }); let (tx, mut rx) = mpsc::channel(10); @@ -29,7 +29,7 @@ mod tests { // 1. Open process and sleep.Go to execute subscription tokio::time::sleep(Duration::from_secs(1)).await; - // Set entity with one moves component + // Set entity with one moves model // remaining: 10, last_direction: 0 let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap(), FieldElement::ZERO]; state.set_entity("Moves".to_string(), key, moves_values).await.unwrap(); @@ -44,7 +44,7 @@ mod tests { r#" subscription { entityUpdated { - id, keys, componentNames + id, keys, modelNames } }"#, ) @@ -65,7 +65,7 @@ mod tests { let entity_id = format!("{:#x}", poseidon_hash_many(&key)); let keys_str = key.iter().map(|k| format!("{:#x}", k)).collect::>().join(","); let expected_value: async_graphql::Value = value!({ - "entityUpdated": { "id": entity_id.clone(), "keys":vec![keys_str.clone()], "componentNames": "Moves" } + "entityUpdated": { "id": entity_id.clone(), "keys":vec![keys_str.clone()], "modelNames": "Moves" } }); let (tx, mut rx) = mpsc::channel(10); @@ -73,7 +73,7 @@ mod tests { // 1. Open process and sleep.Go to execute subscription tokio::time::sleep(Duration::from_secs(1)).await; - // Set entity with one moves component + // Set entity with one moves model // remaining: 10, last_direction: 0 let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap(), FieldElement::ZERO]; state.set_entity("Moves".to_string(), key, moves_values).await.unwrap(); @@ -88,7 +88,7 @@ mod tests { r#" subscription { entityUpdated(id: "0x579e8877c7755365d5ec1ec7d3a94a457eff5d1f40482bbe9729c064cdead2") { - id, keys, componentNames + id, keys, modelNames } }"#, ) @@ -100,18 +100,18 @@ mod tests { } #[sqlx::test(migrations = "../migrations")] - async fn test_component_subscription(pool: SqlitePool) { + async fn test_model_subscription(pool: SqlitePool) { // Sleep in order to run this test at the end in a single thread tokio::time::sleep(Duration::from_secs(2)).await; let state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - // 0. Preprocess component value + // 0. Preprocess model value let name = "Test".to_string(); - let component_id = name.to_lowercase(); + let model_id = name.to_lowercase(); let class_hash = FieldElement::TWO; let hex_class_hash = format!("{:#x}", class_hash); let expected_value: async_graphql::Value = value!({ - "componentRegistered": { "id": component_id.clone(), "name":name, "classHash": hex_class_hash } + "modelRegistered": { "id": model_id.clone(), "name":name, "classHash": hex_class_hash } }); let (tx, mut rx) = mpsc::channel(7); @@ -119,13 +119,13 @@ mod tests { // 1. Open process and sleep.Go to execute subscription tokio::time::sleep(Duration::from_secs(1)).await; - let component = Component { + let model = Model { name, members: vec![Member { name: "test".into(), ty: "u32".into(), key: false }], class_hash, ..Default::default() }; - state.register_component(component).await.unwrap(); + state.register_model(model).await.unwrap(); // 3. fn publish() is called from state.set_entity() tx.send(()).await.unwrap(); @@ -136,7 +136,7 @@ mod tests { &pool, r#" subscription { - componentRegistered { + modelRegistered { id, name, classHash } }"#, @@ -149,18 +149,18 @@ mod tests { } #[sqlx::test(migrations = "../migrations")] - async fn test_component_subscription_with_id(pool: SqlitePool) { + async fn test_model_subscription_with_id(pool: SqlitePool) { // Sleep in order to run this test at the end in a single thread tokio::time::sleep(Duration::from_secs(2)).await; let state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - // 0. Preprocess component value + // 0. Preprocess model value let name = "Test".to_string(); - let component_id = name.to_lowercase(); + let model_id = name.to_lowercase(); let class_hash = FieldElement::TWO; let hex_class_hash = format!("{:#x}", class_hash); let expected_value: async_graphql::Value = value!({ - "componentRegistered": { "id": component_id.clone(), "name":name, "classHash": hex_class_hash } + "modelRegistered": { "id": model_id.clone(), "name":name, "classHash": hex_class_hash } }); let (tx, mut rx) = mpsc::channel(7); @@ -168,13 +168,13 @@ mod tests { // 1. Open process and sleep.Go to execute subscription tokio::time::sleep(Duration::from_secs(1)).await; - let component = Component { + let model = Model { name, members: vec![Member { name: "test".into(), ty: "u32".into(), key: false }], class_hash, ..Default::default() }; - state.register_component(component).await.unwrap(); + state.register_model(model).await.unwrap(); // 3. fn publish() is called from state.set_entity() tx.send(()).await.unwrap(); @@ -185,7 +185,7 @@ mod tests { &pool, r#" subscription { - componentRegistered(id: "test") { + modelRegistered(id: "test") { id, name, classHash } }"#, diff --git a/crates/torii/migrations/20230316154230_setup.sql b/crates/torii/migrations/20230316154230_setup.sql index 0b49e4e2e0..280596cec8 100644 --- a/crates/torii/migrations/20230316154230_setup.sql +++ b/crates/torii/migrations/20230316154230_setup.sql @@ -13,7 +13,7 @@ CREATE TABLE worlds ( created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE components ( +CREATE TABLE models ( id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, class_hash TEXT NOT NULL, @@ -21,20 +21,20 @@ CREATE TABLE components ( created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX idx_components_created_at ON components (created_at); +CREATE INDEX idx_models_created_at ON models (created_at); -CREATE TABLE component_members( +CREATE TABLE model_members( id INTEGER PRIMARY KEY AUTOINCREMENT, - component_id TEXT NOT NULL, + model_id TEXT NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL, key BOOLEAN NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (component_id) REFERENCES components(id) - UNIQUE (component_id, name) + FOREIGN KEY (model_id) REFERENCES models(id) + UNIQUE (model_id, name) ); -CREATE INDEX idx_component_members_component_id ON component_members (component_id); +CREATE INDEX idx_model_members_model_id ON model_members (model_id); CREATE TABLE system_calls ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -61,7 +61,7 @@ CREATE INDEX idx_systems_created_at ON systems (created_at); CREATE TABLE entities ( id TEXT NOT NULL PRIMARY KEY, keys TEXT, - component_names TEXT, + model_names TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/crates/torii/server/src/cli.rs b/crates/torii/server/src/cli.rs index 91c55750e0..70461c4b07 100644 --- a/crates/torii/server/src/cli.rs +++ b/crates/torii/server/src/cli.rs @@ -13,7 +13,7 @@ use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use tokio_util::sync::CancellationToken; use torii_client::contract::world::WorldContractReader; -use torii_core::processors::register_component::RegisterComponentProcessor; +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::processors::store_system_call::StoreSystemCallProcessor; @@ -97,7 +97,7 @@ async fn main() -> anyhow::Result<()> { db.load_from_manifest(manifest.clone()).await?; let processors = Processors { event: vec![ - Box::new(RegisterComponentProcessor), + Box::new(RegisterModelProcessor), Box::new(RegisterSystemProcessor), Box::new(StoreSetRecordProcessor), ], From f0bdc392ce624c47596ef979ff4747949dc5012b Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Sat, 23 Sep 2023 18:04:43 -0400 Subject: [PATCH 02/14] Writer contracts (#912) --- crates/dojo-core/src/database_test.cairo | 25 +- crates/dojo-core/src/executor.cairo | 28 - crates/dojo-core/src/executor_test.cairo | 111 +-- crates/dojo-core/src/lib.cairo | 7 +- crates/dojo-core/src/test_utils.cairo | 44 +- crates/dojo-core/src/world.cairo | 254 ++---- crates/dojo-core/src/world_factory.cairo | 177 ---- crates/dojo-core/src/world_factory_test.cairo | 174 ---- crates/dojo-core/src/world_test.cairo | 367 +++----- crates/dojo-erc/src/erc1155.cairo | 4 - crates/dojo-erc/src/erc1155/components.cairo | 126 --- crates/dojo-erc/src/erc1155/erc1155.cairo | 305 ------- crates/dojo-erc/src/erc1155/interface.cairo | 105 --- crates/dojo-erc/src/erc1155/systems.cairo | 344 ------- crates/dojo-erc/src/erc165.cairo | 3 - crates/dojo-erc/src/erc165/interface.cairo | 8 - crates/dojo-erc/src/erc20.cairo | 4 - crates/dojo-erc/src/erc20/erc20.cairo | 200 ---- crates/dojo-erc/src/erc20/interface.cairo | 17 - crates/dojo-erc/src/erc20/systems.cairo | 103 --- crates/dojo-erc/src/erc721.cairo | 4 - crates/dojo-erc/src/erc721/components.cairo | 171 ---- crates/dojo-erc/src/erc721/erc721.cairo | 284 ------ crates/dojo-erc/src/erc721/interface.cairo | 79 -- crates/dojo-erc/src/erc721/systems.cairo | 276 ------ crates/dojo-erc/src/erc_common.cairo | 3 - .../dojo-erc/src/erc_common/components.cairo | 5 - .../components/base_uri_component.cairo | 28 - .../operator_approval_component.cairo | 55 -- crates/dojo-erc/src/erc_common/systems.cairo | 1 - crates/dojo-erc/src/erc_common/utils.cairo | 30 - crates/dojo-erc/src/lib.cairo | 16 +- crates/dojo-erc/src/tests.cairo | 11 - crates/dojo-erc/src/tests/constants.cairo | 57 ++ crates/dojo-erc/src/tests/erc20_tests.cairo | 548 +++++++++++ crates/dojo-erc/src/tests/test_erc1155.cairo | 629 ------------- .../src/tests/test_erc1155_utils.cairo | 131 --- crates/dojo-erc/src/tests/test_erc20.cairo | 162 ---- .../dojo-erc/src/tests/test_erc20_utils.cairo | 61 -- crates/dojo-erc/src/tests/test_erc721.cairo | 862 ------------------ .../src/tests/test_erc721_utils.cairo | 153 ---- crates/dojo-erc/src/tests/test_utils.cairo | 11 - crates/dojo-erc/src/tests/utils.cairo | 38 + crates/dojo-erc/src/token/erc20.cairo | 331 +++++++ .../erc20_components.cairo} | 22 +- crates/dojo-lang/src/compiler.rs | 2 - crates/dojo-lang/src/manifest.rs | 105 +-- .../dojo-lang/src/manifest_test_data/manifest | 427 +++------ crates/dojo-lang/src/plugin_test_data/system | 6 +- crates/dojo-lang/src/system.rs | 48 +- crates/dojo-world/src/migration/mod.rs | 24 +- crates/dojo-world/src/migration/strategy.rs | 47 +- crates/dojo-world/src/migration/world.rs | 4 +- crates/sozo/src/commands/register.rs | 18 - crates/sozo/src/ops/migration/mod.rs | 211 ++--- crates/sozo/src/ops/register.rs | 15 - crates/torii/client/src/contract/component.rs | 21 +- .../torii/client/src/contract/system_test.rs | 84 +- crates/torii/client/src/contract/world.rs | 44 +- .../torii/client/src/contract/world_test.rs | 19 +- crates/torii/core/src/sql_test.rs | 3 - examples/ecs/Scarb.toml | 22 +- examples/ecs/src/components.cairo | 65 +- examples/ecs/src/lib.cairo | 11 +- examples/ecs/src/systems.cairo | 140 --- examples/ecs/src/systems/raw_contract.cairo | 108 +++ examples/ecs/src/systems/with_decorator.cairo | 101 ++ examples/ecs/src/utils.cairo | 23 + scripts/cairo_test.sh | 6 +- 69 files changed, 1946 insertions(+), 5982 deletions(-) delete mode 100644 crates/dojo-core/src/world_factory.cairo delete mode 100644 crates/dojo-core/src/world_factory_test.cairo delete mode 100644 crates/dojo-erc/src/erc1155.cairo delete mode 100644 crates/dojo-erc/src/erc1155/components.cairo delete mode 100644 crates/dojo-erc/src/erc1155/erc1155.cairo delete mode 100644 crates/dojo-erc/src/erc1155/interface.cairo delete mode 100644 crates/dojo-erc/src/erc1155/systems.cairo delete mode 100644 crates/dojo-erc/src/erc165.cairo delete mode 100644 crates/dojo-erc/src/erc165/interface.cairo delete mode 100644 crates/dojo-erc/src/erc20.cairo delete mode 100644 crates/dojo-erc/src/erc20/erc20.cairo delete mode 100644 crates/dojo-erc/src/erc20/interface.cairo delete mode 100644 crates/dojo-erc/src/erc20/systems.cairo delete mode 100644 crates/dojo-erc/src/erc721.cairo delete mode 100644 crates/dojo-erc/src/erc721/components.cairo delete mode 100644 crates/dojo-erc/src/erc721/erc721.cairo delete mode 100644 crates/dojo-erc/src/erc721/interface.cairo delete mode 100644 crates/dojo-erc/src/erc721/systems.cairo delete mode 100644 crates/dojo-erc/src/erc_common.cairo delete mode 100644 crates/dojo-erc/src/erc_common/components.cairo delete mode 100644 crates/dojo-erc/src/erc_common/components/base_uri_component.cairo delete mode 100644 crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo delete mode 100644 crates/dojo-erc/src/erc_common/systems.cairo delete mode 100644 crates/dojo-erc/src/erc_common/utils.cairo delete mode 100644 crates/dojo-erc/src/tests.cairo create mode 100644 crates/dojo-erc/src/tests/constants.cairo create mode 100644 crates/dojo-erc/src/tests/erc20_tests.cairo delete mode 100644 crates/dojo-erc/src/tests/test_erc1155.cairo delete mode 100644 crates/dojo-erc/src/tests/test_erc1155_utils.cairo delete mode 100644 crates/dojo-erc/src/tests/test_erc20.cairo delete mode 100644 crates/dojo-erc/src/tests/test_erc20_utils.cairo delete mode 100644 crates/dojo-erc/src/tests/test_erc721.cairo delete mode 100644 crates/dojo-erc/src/tests/test_erc721_utils.cairo delete mode 100644 crates/dojo-erc/src/tests/test_utils.cairo create mode 100644 crates/dojo-erc/src/tests/utils.cairo create mode 100644 crates/dojo-erc/src/token/erc20.cairo rename crates/dojo-erc/src/{erc20/components.cairo => token/erc20_components.cairo} (65%) delete mode 100644 examples/ecs/src/systems.cairo create mode 100644 examples/ecs/src/systems/raw_contract.cairo create mode 100644 examples/ecs/src/systems/with_decorator.cairo create mode 100644 examples/ecs/src/utils.cairo diff --git a/crates/dojo-core/src/database_test.cairo b/crates/dojo-core/src/database_test.cairo index d419a87876..e329ef0f45 100644 --- a/crates/dojo-core/src/database_test.cairo +++ b/crates/dojo-core/src/database_test.cairo @@ -6,14 +6,11 @@ use array::SpanTrait; use traits::{Into, TryInto}; use starknet::syscalls::deploy_syscall; -use starknet::class_hash::Felt252TryIntoClassHash; -use dojo::executor::{executor, IExecutorDispatcher, IExecutorDispatcherTrait}; -use dojo::world::{Context, IWorldDispatcher}; - +use starknet::class_hash::{Felt252TryIntoClassHash, ClassHash}; +use dojo::world::{IWorldDispatcher}; +use dojo::executor::executor; use dojo::database::{get, set, del, all}; - - #[test] #[available_gas(1000000)] fn test_database_basic() { @@ -40,7 +37,7 @@ fn test_database_different_tables() { let mut other = ArrayTrait::new(); other.append(0x3); other.append(0x4); - + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); set(class_hash, 'first', 'key', 0, values.span(), array![251, 251].span()); set(class_hash, 'second', 'key', 0, other.span(), array![251, 251].span()); @@ -63,7 +60,7 @@ fn test_database_different_keys() { let mut other = ArrayTrait::new(); other.append(0x3); other.append(0x4); - + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); set(class_hash, 'table', 'key', 0, values.span(), array![251, 251].span()); set(class_hash, 'table', 'other', 0, other.span(), array![251, 251].span()); @@ -85,12 +82,14 @@ fn test_database_pagination() { values.append(0x3); values.append(0x4); values.append(0x5); - + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); set(class_hash, 'table', 'key', 1, values.span(), array![251, 251, 251, 251, 251].span()); let first_res = get(class_hash, 'table', 'key', 1, 3, array![251, 251, 251].span()); let second_res = get(class_hash, 'table', 'key', 3, 5, array![251, 251, 251, 251, 251].span()); - let third_res = get(class_hash, 'table', 'key', 5, 7, array![251, 251, 251, 251, 251, 251, 251].span()); + let third_res = get( + class_hash, 'table', 'key', 5, 7, array![251, 251, 251, 251, 251, 251, 251].span() + ); assert(*first_res.at(0) == *values.at(0), 'Values different at index 0!'); assert(*first_res.at(1) == *values.at(1), 'Values different at index 1!'); @@ -105,10 +104,10 @@ fn test_database_pagination() { fn test_database_del() { let mut values = ArrayTrait::new(); values.append(0x42); - + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); set(class_hash, 'table', 'key', 0, values.span(), array![251].span()); - + let before = get(class_hash, 'table', 'key', 0, values.len(), array![251].span()); assert(*before.at(0) == *values.at(0), 'Values different at index 0!'); @@ -123,7 +122,7 @@ fn test_database_all() { let mut even = ArrayTrait::new(); even.append(0x2); even.append(0x4); - + let mut odd = ArrayTrait::new(); odd.append(0x1); odd.append(0x3); diff --git a/crates/dojo-core/src/executor.cairo b/crates/dojo-core/src/executor.cairo index 85877efdc6..897aa228e4 100644 --- a/crates/dojo-core/src/executor.cairo +++ b/crates/dojo-core/src/executor.cairo @@ -1,10 +1,7 @@ use starknet::ClassHash; -use dojo::world::Context; - #[starknet::interface] trait IExecutor { - fn execute(self: @T, class_hash: ClassHash, calldata: Span) -> Span; fn call( self: @T, class_hash: ClassHash, entrypoint: felt252, calldata: Span ) -> Span; @@ -21,36 +18,11 @@ mod executor { const EXECUTE_ENTRYPOINT: felt252 = 0x0240060cdb34fcc260f41eac7474ee1d7c80b7e3607daff9ac67c7ea2ebb1c44; - const WORLD_ADDRESS_OFFSET: u32 = 4; - #[storage] struct Storage {} #[external(v0)] impl Executor of IExecutor { - /// Executes a System by calling its execute entrypoint. - /// - /// # Arguments - /// - /// * `class_hash` - Class Hash of the System. - /// * `calldata` - Calldata to pass to the System. - /// - /// # Returns - /// - /// The return value of the System's execute entrypoint. - fn execute( - self: @ContractState, class_hash: ClassHash, calldata: Span - ) -> Span { - assert( - traits::Into::::into(starknet::get_caller_address()) == *calldata - .at(calldata.len() - WORLD_ADDRESS_OFFSET), - 'Only world caller' - ); - starknet::syscalls::library_call_syscall(class_hash, EXECUTE_ENTRYPOINT, calldata) - .unwrap_syscall() - } - /// Call the provided `entrypoint` method on the given `class_hash`. /// /// # Arguments diff --git a/crates/dojo-core/src/executor_test.cairo b/crates/dojo-core/src/executor_test.cairo index 395ca322fb..0efbdf852b 100644 --- a/crates/dojo-core/src/executor_test.cairo +++ b/crates/dojo-core/src/executor_test.cairo @@ -8,7 +8,7 @@ use traits::TryInto; use starknet::syscalls::deploy_syscall; use starknet::class_hash::Felt252TryIntoClassHash; use dojo::executor::{executor, IExecutorDispatcher, IExecutorDispatcherTrait}; -use dojo::world::{Context, IWorldDispatcher}; +use dojo::world::{IWorldDispatcher}; #[derive(Component, Copy, Drop, Serde)] struct Foo { @@ -18,52 +18,29 @@ struct Foo { b: u128, } -#[system] -mod Bar { - use super::Foo; +#[starknet::contract] +mod bar { + use super::{Foo}; - fn execute(foo: Foo) -> Foo { - foo - } -} - -#[test] -#[available_gas(40000000)] -fn test_executor() { - let constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ) - .unwrap(); - - let executor = IExecutorDispatcher { contract_address: executor_address }; - - let mut system_calldata = ArrayTrait::new(); - system_calldata.append(1); - system_calldata.append(42); - system_calldata.append(53); - - let ctx = Context { - world: IWorldDispatcher { - contract_address: starknet::contract_address_const::<0x1337>() - }, - origin: starknet::contract_address_const::<0x1337>(), - system: 'Bar', - system_class_hash: Bar::TEST_CLASS_HASH.try_into().unwrap(), - }; - - ctx.serialize(ref system_calldata); + #[storage] + struct Storage {} - starknet::testing::set_contract_address(ctx.world.contract_address); + #[external(v0)] + fn name(self: @ContractState) -> felt252 { + 'bar' + } - let res = executor.execute(ctx.system_class_hash, system_calldata.span()); + #[external(v0)] + fn execute(self: @ContractState, foo: Foo) -> Foo { + foo + } } +const NAME_ENTRYPOINT: felt252 = 0x0361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60; #[test] #[available_gas(40000000)] -#[should_panic] -fn test_executor_bad_caller() { +fn test_executor() { let constructor_calldata = array::ArrayTrait::new(); let (executor_address, _) = deploy_syscall( executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false @@ -72,58 +49,10 @@ fn test_executor_bad_caller() { let executor = IExecutorDispatcher { contract_address: executor_address }; - let mut system_calldata = ArrayTrait::new(); - system_calldata.append(1); - system_calldata.append(42); - system_calldata.append(53); - - let ctx = Context { - world: IWorldDispatcher { - contract_address: starknet::contract_address_const::<0x1337>() - }, - origin: starknet::contract_address_const::<0x1337>(), - system: 'Bar', - system_class_hash: Bar::TEST_CLASS_HASH.try_into().unwrap(), - }; + starknet::testing::set_contract_address(starknet::contract_address_const::<0x1337>()); - ctx.serialize(ref system_calldata); - - let res = executor.execute(ctx.system_class_hash, system_calldata.span()); -} - -#[test] -#[available_gas(40000000)] -fn test_executor_with_struct() { - let constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ).unwrap(); - - let foo = Foo { - id: 1, - a: 42, - b: 53, - }; - let ctx = Context { - world: IWorldDispatcher { - contract_address: starknet::contract_address_const::<0x1337>() - }, - origin: starknet::contract_address_const::<0x1337>(), - system: 'Bar', - system_class_hash: Bar::TEST_CLASS_HASH.try_into().unwrap(), - }; - - let mut system_calldata = ArrayTrait::new(); - foo.serialize(ref system_calldata); - ctx.serialize(ref system_calldata); - - starknet::testing::set_contract_address(ctx.world.contract_address); - let executor = IExecutorDispatcher { contract_address: executor_address }; - let mut res = executor.execute(ctx.system_class_hash, system_calldata.span()); + let res = *executor + .call(bar::TEST_CLASS_HASH.try_into().unwrap(), NAME_ENTRYPOINT, array![].span())[0]; - let foo = Serde::::deserialize(ref res).unwrap(); - assert(foo.id == 1, 'Invalid deserialized data'); - assert(foo.a == 42, 'Invalid deserialized data'); - assert(foo.b == 53, 'Invalid deserialized data'); - + assert(res == 'bar', 'executor call incorrect') } diff --git a/crates/dojo-core/src/lib.cairo b/crates/dojo-core/src/lib.cairo index 20fae948c9..9370cb4f6a 100644 --- a/crates/dojo-core/src/lib.cairo +++ b/crates/dojo-core/src/lib.cairo @@ -9,11 +9,8 @@ mod packing; #[cfg(test)] mod packing_test; mod world; -// #[cfg(test)] -// mod world_test; -// mod world_factory; -// #[cfg(test)] -// mod world_factory_test; +#[cfg(test)] +mod world_test; #[cfg(test)] mod test_utils; diff --git a/crates/dojo-core/src/test_utils.cairo b/crates/dojo-core/src/test_utils.cairo index e70afaa290..faad96adde 100644 --- a/crates/dojo-core/src/test_utils.cairo +++ b/crates/dojo-core/src/test_utils.cairo @@ -7,17 +7,47 @@ use traits::TryInto; use option::OptionTrait; use core::{result::ResultTrait, traits::Into}; + use dojo::executor::executor; use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; -fn spawn_test_world(components: Array, systems: Array) -> IWorldDispatcher { +/// Deploy classhash with calldata for constructor +/// +/// # Arguments +/// +/// * `class_hash` - Class to deploy +/// * `calldata` - calldata for constructor +/// +/// # Returns +/// * address of contract deployed +fn deploy_contract(class_hash: felt252, calldata: Span) -> ContractAddress { + let (system_contract, _) = starknet::deploy_syscall( + class_hash.try_into().unwrap(), 0, calldata, false + ) + .unwrap(); + system_contract +} + +/// Deploy classhash and passes in world address to constructor +/// +/// # Arguments +/// +/// * `class_hash` - Class to deploy +/// * `world` - World dispatcher to pass as world address +/// +/// # Returns +/// * address of contract deployed +fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) -> ContractAddress { + deploy_contract(class_hash, array![world.contract_address.into()].span()) +} + +fn spawn_test_world(components: Array) -> IWorldDispatcher { // deploy executor let constructor_calldata = array::ArrayTrait::new(); let (executor_address, _) = deploy_syscall( executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false ) .unwrap(); - // deploy world let mut world_constructor_calldata = array::ArrayTrait::new(); world_constructor_calldata.append(executor_address.into()); @@ -37,15 +67,5 @@ fn spawn_test_world(components: Array, systems: Array) -> IWor index += 1; }; - // register systems - let mut index = 0; - loop { - if index == systems.len() { - break (); - } - world.register_system((*systems[index]).try_into().unwrap()); - index += 1; - }; - world } diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index d08f711e5a..0ee99ffd0e 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -2,28 +2,27 @@ use starknet::{ContractAddress, ClassHash, StorageBaseAddress, SyscallResult}; use traits::{Into, TryInto}; use option::OptionTrait; -#[derive(Copy, Drop, Serde)] -struct Context { - world: IWorldDispatcher, // Dispatcher to the world contract - origin: ContractAddress, // Address of the origin - system: felt252, // Name of the calling system - system_class_hash: ClassHash, // Class hash of the calling system -} - #[starknet::interface] trait IWorld { fn component(self: @T, name: felt252) -> ClassHash; fn register_component(ref self: T, class_hash: ClassHash); - fn system(self: @T, name: felt252) -> ClassHash; - fn register_system(ref self: T, class_hash: ClassHash); fn uuid(ref self: T) -> usize; fn emit(self: @T, keys: Array, values: Span); - fn execute(ref self: T, system: felt252, calldata: Array) -> Span; fn entity( - self: @T, component: felt252, keys: Span, offset: u8, length: usize, layout: Span + self: @T, + component: felt252, + keys: Span, + offset: u8, + length: usize, + layout: Span ) -> Span; fn set_entity( - ref self: T, component: felt252, keys: Span, offset: u8, values: Span, layout: Span + ref self: T, + component: felt252, + keys: Span, + offset: u8, + values: Span, + layout: Span ); fn entities( self: @T, component: felt252, index: felt252, length: usize, layout: Span @@ -31,16 +30,13 @@ trait IWorld { fn set_executor(ref self: T, contract_address: ContractAddress); fn executor(self: @T) -> ContractAddress; fn delete_entity(ref self: T, component: felt252, keys: Span); - fn origin(self: @T) -> ContractAddress; - fn caller_system(self: @T) -> felt252; - fn is_owner(self: @T, address: ContractAddress, target: felt252) -> bool; fn grant_owner(ref self: T, address: ContractAddress, target: felt252); fn revoke_owner(ref self: T, address: ContractAddress, target: felt252); - fn is_writer(self: @T, component: felt252, system: felt252) -> bool; - fn grant_writer(ref self: T, component: felt252, system: felt252); - fn revoke_writer(ref self: T, component: felt252, system: felt252); + fn is_writer(self: @T, component: felt252, system: ContractAddress) -> bool; + fn grant_writer(ref self: T, component: felt252, system: ContractAddress); + fn revoke_writer(ref self: T, component: felt252, system: ContractAddress); } #[starknet::contract] @@ -60,11 +56,9 @@ mod world { use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; use dojo::world::{IWorldDispatcher, IWorld}; - use super::Context; - const NAME_ENTRYPOINT: felt252 = 0x0361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60; - + const WORLD: felt252 = 0; #[event] @@ -92,7 +86,7 @@ mod world { #[derive(Drop, starknet::Event)] struct SystemRegistered { name: felt252, - class_hash: ClassHash + contract_address: ContractAddress } #[derive(Drop, starknet::Event)] @@ -113,12 +107,9 @@ mod world { struct Storage { executor_dispatcher: IExecutorDispatcher, components: LegacyMap::, - systems: LegacyMap::, nonce: usize, owners: LegacyMap::<(felt252, ContractAddress), bool>, - writers: LegacyMap::<(felt252, felt252), bool>, - // Tracks the origin executor. - call_origin: ContractAddress, + writers: LegacyMap::<(felt252, ContractAddress), bool>, // Tracks the calling systems name for auth purposes. call_stack_len: felt252, call_stack: LegacyMap::, @@ -129,7 +120,9 @@ mod world { self.executor_dispatcher.write(IExecutorDispatcher { contract_address: executor }); self .owners - .write((WORLD, starknet::get_tx_info().unbox().account_contract_address), bool::True(())); + .write( + (WORLD, starknet::get_tx_info().unbox().account_contract_address), bool::True(()) + ); EventEmitter::emit( ref self, @@ -140,6 +133,24 @@ mod world { ); } + /// Call Helper, + /// Call the provided `entrypoint` method on the given `class_hash`. + /// + /// # Arguments + /// + /// * `class_hash` - Class Hash to call. + /// * `entrypoint` - Entrypoint to call. + /// * `calldata` - The calldata to pass. + /// + /// # Returns + /// + /// The return value of the call. + fn class_call( + self: @ContractState, class_hash: ClassHash, entrypoint: felt252, calldata: Span + ) -> Span { + self.executor_dispatcher.read().call(class_hash, entrypoint, calldata) + } + #[external(v0)] impl World of IWorld { /// Checks if the provided account is an owner of the target. @@ -165,10 +176,7 @@ mod world { /// * `target` - The target. fn grant_owner(ref self: ContractState, address: ContractAddress, target: felt252) { let caller = get_caller_address(); - assert( - self.is_owner(caller, target) || self.is_owner(caller, WORLD), - 'not owner' - ); + assert(self.is_owner(caller, target) || self.is_owner(caller, WORLD), 'not owner'); self.owners.write((target, address), bool::True(())); } @@ -181,11 +189,7 @@ mod world { /// * `target` - The target. fn revoke_owner(ref self: ContractState, address: ContractAddress, target: felt252) { let caller = get_caller_address(); - assert( - self.is_owner(caller, target) - || self.is_owner(caller, WORLD), - 'not owner' - ); + assert(self.is_owner(caller, target) || self.is_owner(caller, WORLD), 'not owner'); self.owners.write((target, address), bool::False(())); } @@ -199,7 +203,7 @@ mod world { /// # Returns /// /// * `bool` - True if the system is a writer of the component, false otherwise - fn is_writer(self: @ContractState, component: felt252, system: felt252) -> bool { + fn is_writer(self: @ContractState, component: felt252, system: ContractAddress) -> bool { self.writers.read((component, system)) } @@ -210,12 +214,11 @@ mod world { /// /// * `component` - The name of the component. /// * `system` - The name of the system. - fn grant_writer(ref self: ContractState, component: felt252, system: felt252) { + fn grant_writer(ref self: ContractState, component: felt252, system: ContractAddress) { let caller = get_caller_address(); assert( - self.is_owner(caller, component) - || self.is_owner(caller, WORLD), + self.is_owner(caller, component) || self.is_owner(caller, WORLD), 'not owner or writer' ); self.writers.write((component, system), bool::True(())); @@ -228,11 +231,11 @@ mod world { /// /// * `component` - The name of the component. /// * `system` - The name of the system. - fn revoke_writer(ref self: ContractState, component: felt252, system: felt252) { + fn revoke_writer(ref self: ContractState, component: felt252, system: ContractAddress) { let caller = get_caller_address(); assert( - self.is_writer(component, self.caller_system()) + self.is_writer(component, caller) || self.is_owner(caller, component) || self.is_owner(caller, WORLD), 'not owner or writer' @@ -249,16 +252,11 @@ mod world { fn register_component(ref self: ContractState, class_hash: ClassHash) { let caller = get_caller_address(); let calldata = ArrayTrait::new(); - let name = *self - .executor_dispatcher - .read() - .call(class_hash, NAME_ENTRYPOINT, calldata.span())[0]; + let name = *class_call(@self, class_hash, NAME_ENTRYPOINT, calldata.span())[0]; // If component is already registered, validate permission to update. if self.components.read(name).is_non_zero() { - assert( - self.is_owner(caller, name), 'only owner can update' - ); + assert(self.is_owner(caller, name), 'only owner can update'); } else { self.owners.write((name, caller), bool::True(())); }; @@ -280,101 +278,6 @@ mod world { self.components.read(name) } - /// Registers a system in the world. If the system is already registered, - /// the implementation will be updated. - /// - /// # Arguments - /// - /// * `class_hash` - The class hash of the system to be registered. - fn register_system(ref self: ContractState, class_hash: ClassHash) { - let caller = get_caller_address(); - let calldata = ArrayTrait::new(); - let name = *self - .executor_dispatcher - .read() - .call(class_hash, NAME_ENTRYPOINT, calldata.span())[0]; - - // If system is already registered, validate permission to update. - if self.systems.read(name).is_non_zero() { - assert( - self.is_owner(caller, name), 'only owner can update' - ); - } else { - self.owners.write((name, caller), bool::True(())); - }; - - self.systems.write(name, class_hash); - EventEmitter::emit(ref self, SystemRegistered { name, class_hash }); - } - - /// Gets the class hash of a registered system. - /// - /// # Arguments - /// - /// * `name` - The name of the system. - /// - /// # Returns - /// - /// * `ClassHash` - The class hash of the system. - fn system(self: @ContractState, name: felt252) -> ClassHash { - self.systems.read(name) - } - - /// Executes a system with the given calldata. - /// - /// # Arguments - /// - /// * `system` - The name of the system to be executed. - /// * `calldata` - The calldata to be passed to the system. - /// - /// # Returns - /// - /// * `Span` - The result of the system execution. - fn execute( - ref self: ContractState, system: felt252, mut calldata: Array - ) -> Span { - let stack_len = self.call_stack_len.read(); - self.call_stack.write(stack_len, system); - self.call_stack_len.write(stack_len + 1); - - // Get the class hash of the system to be executed - let system_class_hash = self.systems.read(system); - - // If this is the initial call, set the origin to the caller - let mut call_origin = self.call_origin.read(); - if stack_len.is_zero() { - call_origin = get_caller_address(); - self.call_origin.write(call_origin); - } - - let ctx = Context { - world: IWorldDispatcher { - contract_address: get_contract_address() - }, origin: self.call_origin.read(), system, system_class_hash, - }; - - // Add context to calldata - ctx.serialize(ref calldata); - - // Call the system via executor - let res = self - .executor_dispatcher - .read() - .execute(ctx.system_class_hash, calldata.span()); - - // Reset the current call stack frame - self.call_stack.write(stack_len, 0); - // Decrement the call stack pointer - self.call_stack_len.write(stack_len); - - // If this is the initial call, reset the origin on exit - if stack_len.is_zero() { - self.call_origin.write(starknet::contract_address_const::<0x0>()); - } - - res - } - /// Issues an autoincremented id to the caller. /// /// # Returns @@ -393,7 +296,8 @@ mod world { /// * `keys` - The keys of the event. /// * `values` - The data to be logged by the event. fn emit(self: @ContractState, mut keys: Array, values: Span) { - self.caller_system().serialize(ref keys); + let system = get_caller_address(); + system.serialize(ref keys); emit_event_syscall(keys.span(), values).unwrap_syscall(); } @@ -413,7 +317,7 @@ mod world { values: Span, layout: Span ) { - assert_can_write(@self, component); + assert_can_write(@self, component, get_caller_address()); let key = poseidon::poseidon_hash_span(keys); let component_class_hash = self.components.read(component); @@ -429,7 +333,9 @@ mod world { /// * `component` - The name of the component to be deleted. /// * `query` - The query to be used to find the entity. fn delete_entity(ref self: ContractState, component: felt252, keys: Span) { - assert_can_write(@self, component); + let system = get_caller_address(); + assert(system.is_non_zero(), 'must be called thru system'); + assert_can_write(@self, component, system); let key = poseidon::poseidon_hash_span(keys); let component_class_hash = self.components.read(component); @@ -452,7 +358,12 @@ mod world { /// /// * `Span` - The value of the component, zero initialized if not set. fn entity( - self: @ContractState, component: felt252, keys: Span, offset: u8, length: usize, layout: Span + self: @ContractState, + component: felt252, + keys: Span, + offset: u8, + length: usize, + layout: Span ) -> Span { let class_hash = self.components.read(component); let key = poseidon::poseidon_hash_span(keys); @@ -471,7 +382,11 @@ mod world { /// * `Span` - The entity IDs. /// * `Span>` - The entities. fn entities( - self: @ContractState, component: felt252, index: felt252, length: usize, layout: Span + self: @ContractState, + component: felt252, + index: felt252, + length: usize, + layout: Span ) -> (Span, Span>) { let class_hash = self.components.read(component); database::all(class_hash, component.into(), index, length, layout) @@ -498,24 +413,6 @@ mod world { fn executor(self: @ContractState) -> ContractAddress { self.executor_dispatcher.read().contract_address } - - /// Gets the origin caller. - /// - /// # Returns - /// - /// * `felt252` - The origin caller. - fn origin(self: @ContractState) -> ContractAddress { - self.call_origin.read() - } - - /// Gets the caller system's name. - /// - /// # Returns - /// - /// * `felt252` - The caller system's name. - fn caller_system(self: @ContractState) -> felt252 { - self.call_stack.read(self.call_stack_len.read() - 1) - } } /// Asserts that the current caller can write to the component. @@ -523,28 +420,13 @@ mod world { /// # Arguments /// /// * `component` - The name of the component being written to. - fn assert_can_write(self: @ContractState, component: felt252) { - assert( - get_caller_address() == self.executor_dispatcher.read().contract_address, - 'must be called thru executor' - ); - + /// * `caller` - The name of the caller writing. + fn assert_can_write(self: @ContractState, component: felt252, caller: ContractAddress) { assert( - IWorld::is_writer(self, component, self.caller_system()) + IWorld::is_writer(self, component, caller) || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, component) || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, WORLD), 'not writer' ); } } - -#[system] -mod library_call { - use starknet::{SyscallResultTrait, SyscallResultTraitImpl}; - - fn execute( - class_hash: starknet::ClassHash, entrypoint: felt252, calladata: Span - ) -> Span { - starknet::syscalls::library_call_syscall(class_hash, entrypoint, calladata).unwrap_syscall() - } -} diff --git a/crates/dojo-core/src/world_factory.cairo b/crates/dojo-core/src/world_factory.cairo deleted file mode 100644 index 06701ce0b3..0000000000 --- a/crates/dojo-core/src/world_factory.cairo +++ /dev/null @@ -1,177 +0,0 @@ -use array::ArrayTrait; - -use starknet::{ClassHash, ContractAddress}; - -#[starknet::interface] -trait IWorldFactory { - fn set_world(ref self: T, class_hash: ClassHash); - fn set_executor(ref self: T, executor_address: ContractAddress); - fn spawn( - ref self: T, components: Array, systems: Array, - ) -> ContractAddress; - fn world(self: @T) -> ClassHash; - fn executor(self: @T) -> ContractAddress; -} - -#[starknet::contract] -mod world_factory { - use array::ArrayTrait; - use option::OptionTrait; - use traits::Into; - - use starknet::{ - ClassHash, ContractAddress, contract_address::ContractAddressIntoFelt252, - syscalls::deploy_syscall, get_caller_address, SyscallResultTrait, SyscallResultTraitImpl - }; - - use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - - use super::IWorldFactory; - - #[storage] - struct Storage { - world_class_hash: ClassHash, - executor_address: ContractAddress, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - WorldSpawned: WorldSpawned - } - - #[derive(Drop, starknet::Event)] - struct WorldSpawned { - address: ContractAddress - } - - #[constructor] - fn constructor( - ref self: ContractState, world_class_hash_: ClassHash, executor_address_: ContractAddress, - ) { - self.world_class_hash.write(world_class_hash_); - self.executor_address.write(executor_address_); - } - - #[external(v0)] - impl WorldFactory of IWorldFactory { - /// Spawns a new world with the given components and systems. - /// - /// # Arguments - /// - /// * `components` - The components to be registered. - /// * `systems` - The systems to be registered. - /// - /// # Returns - /// - /// The address of the deployed world. - fn spawn( - ref self: ContractState, components: Array, systems: Array, - ) -> ContractAddress { - // deploy world - let mut world_constructor_calldata: Array = ArrayTrait::new(); - world_constructor_calldata.append(self.executor_address.read().into()); - let world_class_hash = self.world_class_hash.read(); - let result = deploy_syscall( - world_class_hash, 0, world_constructor_calldata.span(), true - ); - let (world_address, _) = result.unwrap_syscall(); - let world = IWorldDispatcher { contract_address: world_address }; - - // events - self.emit(WorldSpawned { address: world_address }); - - // register components - let components_len = components.len(); - register_components(@self, components, components_len, 0, world_address); - - // register systems - let systems_len = systems.len(); - register_systems(@self, systems, systems_len, 0, world_address); - - return world_address; - } - - /// Sets the executor address. - /// - /// # Arguments - /// - /// * `executor_address` - The address of the executor. - fn set_executor(ref self: ContractState, executor_address: ContractAddress) { - self.executor_address.write(executor_address); - } - - /// Sets the class hash for the world. - /// - /// # Arguments - /// - /// * `class_hash` - The class hash of world. - fn set_world(ref self: ContractState, class_hash: ClassHash) { - self.world_class_hash.write(class_hash); - } - - /// Gets the executor contract address. - /// - /// # Returns - /// - /// * `ContractAddress` - The address of the executor contract. - fn executor(self: @ContractState) -> ContractAddress { - return self.executor_address.read(); - } - - /// Gets the world class hash. - /// - /// # Returns - /// - /// * `ClassHash` - The class hash of the world. - fn world(self: @ContractState) -> ClassHash { - return self.world_class_hash.read(); - } - } - - /// Registers all the given components in the world at the given address. - /// - /// # Arguments - /// - /// * `components` - The components to be registered. - /// * `components_len` - The number of components to register. - /// * `index` - The index where to start the registration in the components list. - /// * `world_address` - The address of the world where components are registered. - fn register_components( - self: @ContractState, - components: Array, - components_len: usize, - index: usize, - world_address: ContractAddress - ) { - if (index == components_len) { - return (); - } - IWorldDispatcher { - contract_address: world_address - }.register_component(*components.at(index)); - return register_components(self, components, components_len, index + 1, world_address); - } - - /// Registers all the given systems in the world at the given address. - /// - /// # Arguments - /// - /// * `systems` - The systems to be registered. - /// * `systems_len` - The number of systems to register. - /// * `index` - The index where to start the registration in the system list. - /// * `world_address` - The address of the world where systems are registered. - fn register_systems( - self: @ContractState, - systems: Array, - systems_len: usize, - index: usize, - world_address: ContractAddress - ) { - if (index == systems_len) { - return (); - } - IWorldDispatcher { contract_address: world_address }.register_system(*systems.at(index)); - return register_systems(self, systems, systems_len, index + 1, world_address); - } -} diff --git a/crates/dojo-core/src/world_factory_test.cairo b/crates/dojo-core/src/world_factory_test.cairo deleted file mode 100644 index 3c7bdf92f5..0000000000 --- a/crates/dojo-core/src/world_factory_test.cairo +++ /dev/null @@ -1,174 +0,0 @@ -use core::traits::Into; -use core::result::ResultTrait; -use array::{ArrayTrait, SpanTrait}; -use clone::Clone; -use option::OptionTrait; -use traits::TryInto; -use serde::Serde; -use debug::PrintTrait; -use starknet::syscalls::deploy_syscall; -use starknet::get_caller_address; -use starknet::class_hash::ClassHash; -use starknet::class_hash::Felt252TryIntoClassHash; - -use dojo::executor::executor; -use dojo::world_factory::{IWorldFactoryDispatcher, IWorldFactoryDispatcherTrait, world_factory}; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; - - -#[derive(Component, Copy, Drop, Serde)] -struct Foo { - #[key] - id: felt252, - a: felt252, - b: u128, -} - -#[system] -mod bar { - use super::Foo; - - fn execute(foo: Foo) -> Foo { - foo - } -} - -#[test] -#[available_gas(40000000)] -fn test_constructor() { - let mut calldata: Array = array::ArrayTrait::new(); - calldata.append(starknet::class_hash_const::<0x420>().into()); - calldata.append(starknet::contract_address_const::<0x69>().into()); - - let (factory_address, _) = deploy_syscall( - world_factory::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false - ) - .unwrap(); - - let factory = IWorldFactoryDispatcher { contract_address: factory_address }; - - assert(factory.world() == starknet::class_hash_const::<0x420>(), 'wrong world class hash'); - assert( - factory.executor() == starknet::contract_address_const::<0x69>(), 'wrong executor contract' - ); -} - -#[test] -#[available_gas(90000000)] -fn test_spawn_world() { - // Deploy Executor - let constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ) - .unwrap(); - - // WorldFactory constructor - let mut calldata: Array = array::ArrayTrait::new(); - calldata.append(world::TEST_CLASS_HASH); - calldata.append(executor_address.into()); - - let (factory_address, _) = deploy_syscall( - world_factory::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false - ) - .unwrap(); - - let factory = IWorldFactoryDispatcher { contract_address: factory_address }; - - assert(factory.executor() == executor_address, 'wrong executor address'); - assert(factory.world() == world::TEST_CLASS_HASH.try_into().unwrap(), 'wrong world class hash'); - - // Prepare components and systems - let mut systems: Array = array::ArrayTrait::new(); - systems.append(bar::TEST_CLASS_HASH.try_into().unwrap()); - - let mut components: Array = array::ArrayTrait::new(); - components.append(foo::TEST_CLASS_HASH.try_into().unwrap()); - - // Spawn World from WorldFactory - let world_address = factory.spawn(components, systems); - let world = IWorldDispatcher { contract_address: world_address }; - - // Check Foo component and Bar system are registered - let foo_hash = world.component('Foo'.into()); - assert(foo_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'component not registered'); - - let bar_hash = world.system('bar'.into()); - assert(bar_hash == bar::TEST_CLASS_HASH.try_into().unwrap(), 'system not registered'); -} - -#[test] -#[available_gas(40000000)] -fn test_setters() { - let mut calldata: Array = array::ArrayTrait::new(); - calldata.append(starknet::class_hash_const::<0x420>().into()); - calldata.append(starknet::contract_address_const::<0x69>().into()); - - let (factory_address, _) = deploy_syscall( - world_factory::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false - ) - .unwrap(); - - let factory = IWorldFactoryDispatcher { contract_address: factory_address }; - - assert(factory.world() == starknet::class_hash_const::<0x420>(), 'wrong world class hash'); - assert( - factory.executor() == starknet::contract_address_const::<0x69>(), 'wrong executor contract' - ); - - factory.set_executor(starknet::contract_address_const::<0x96>().into()); - assert(factory.world() == starknet::class_hash_const::<0x420>(), 'wrong world class hash'); - assert( - factory.executor() == starknet::contract_address_const::<0x96>(), 'wrong executor contract' - ); - - factory.set_world(starknet::class_hash_const::<0x421>().into()); - assert(factory.world() == starknet::class_hash_const::<0x421>(), 'wrong world class hash'); - assert( - factory.executor() == starknet::contract_address_const::<0x96>(), 'wrong executor contract' - ); -} - -#[test] -#[available_gas(90000000)] -fn test_spawn_multiple_worlds() { - // Deploy Executor - let constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ) - .unwrap(); - - // WorldFactory constructor - let mut calldata: Array = array::ArrayTrait::new(); - calldata.append(world::TEST_CLASS_HASH); - calldata.append(executor_address.into()); - - let (factory_address, _) = deploy_syscall( - world_factory::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false - ) - .unwrap(); - - let factory = IWorldFactoryDispatcher { contract_address: factory_address }; - - assert(factory.executor() == executor_address, 'wrong executor address'); - assert(factory.world() == world::TEST_CLASS_HASH.try_into().unwrap(), 'wrong world class hash'); - - // Prepare components and systems - let mut systems: Array = array::ArrayTrait::new(); - systems.append(bar::TEST_CLASS_HASH.try_into().unwrap()); - let mut components: Array = array::ArrayTrait::new(); - components.append(foo::TEST_CLASS_HASH.try_into().unwrap()); - - // Spawn World from WorldFactory - let world_address = factory.spawn(components, systems); - let another_world_address = factory.spawn(array::ArrayTrait::new(), array::ArrayTrait::new()); - let world = IWorldDispatcher { contract_address: world_address }; - let another_world = IWorldDispatcher { contract_address: another_world_address }; - - // Check Foo component and Bar system are registered - let foo_hash = world.component('Foo'.into()); - let foo_hash = world.component('Foo'.into()); - assert(foo_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'component not registered'); - assert(foo_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'component not registered'); -} diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index dad8dbd420..4259a98ce8 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -1,19 +1,16 @@ -use array::ArrayTrait; -use array::SpanTrait; +use array::{ArrayTrait, SpanTrait}; use clone::Clone; use core::result::ResultTrait; -use traits::Into; -use traits::TryInto; +use traits::{Into, TryInto}; use option::OptionTrait; use starknet::class_hash::Felt252TryIntoClassHash; -use starknet::contract_address_const; -use starknet::ContractAddress; -use starknet::get_caller_address; +use starknet::{contract_address_const, ContractAddress, ClassHash, get_caller_address}; use starknet::syscalls::deploy_syscall; use dojo::executor::executor; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, library_call, world}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; use dojo::database::schema::SchemaIntrospection; +use dojo::test_utils::{spawn_test_world, deploy_with_world_address}; // Components and Systems @@ -32,15 +29,31 @@ struct Fizz { a: felt252 } -#[system] +#[starknet::interface] +trait Ibar { + fn set_foo(self: @TContractState, a: felt252, b: u128); +} + +#[starknet::contract] mod bar { - use super::Foo; + use super::{Foo, IWorldDispatcher, IWorldDispatcherTrait}; use traits::Into; - use starknet::get_caller_address; - use dojo::world::Context; + use starknet::{get_caller_address, ContractAddress}; - fn execute(ctx: Context, a: felt252, b: u128) { - set !(ctx.world, Foo { caller: ctx.origin, a, b }); + #[storage] + struct Storage { + world: IWorldDispatcher, + } + #[constructor] + fn constructor(ref self: ContractState, world: ContractAddress) { + self.world.write(IWorldDispatcher { contract_address: world }) + } + + #[external(v0)] + impl IbarImpl of super::Ibar { + fn set_foo(self: @ContractState, a: felt252, b: u128) { + set!(self.world.read(), Foo { caller: get_caller_address(), a, b }); + } } } @@ -50,7 +63,6 @@ mod bar { #[available_gas(2000000)] fn test_component() { let world = deploy_world(); - world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); } @@ -59,34 +71,28 @@ fn test_component() { fn test_system() { // Spawn empty world let world = deploy_world(); - - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); - let mut data = ArrayTrait::new(); - data.append(1337); - data.append(1337); - let id = world.uuid(); - world.execute('bar', data); - let mut keys = ArrayTrait::new(); - keys.append(0); + // System contract + let bar_contract = IbarDispatcher { + contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) + }; + + bar_contract.set_foo(1337, 1337); - let stored = world.entity('Foo', keys.span(), 0, SchemaIntrospection::::size()); - assert(*stored.snapshot.at(0) == 1337, 'data not stored'); + let stored: Foo = get!(world, get_caller_address(), Foo); + assert(stored.a == 1337, 'data not stored'); + assert(stored.b == 1337, 'data not stored'); } #[test] #[available_gas(6000000)] -fn test_class_hash_getters() { +fn test_component_class_hash_getter() { let world = deploy_world(); - - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); let foo = world.component('Foo'); assert(foo == foo::TEST_CLASS_HASH.try_into().unwrap(), 'foo does not exists'); - let bar = world.system('bar'); - assert(bar == bar::TEST_CLASS_HASH.try_into().unwrap(), 'bar does not exists'); } #[test] @@ -107,23 +113,20 @@ fn test_emit() { fn test_set_entity_admin() { // Spawn empty world let world = deploy_world(); - - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + let bar_contract = IbarDispatcher { + contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) + }; + let alice = starknet::contract_address_const::<0x1337>(); starknet::testing::set_contract_address(alice); - let mut keys = array::ArrayTrait::new(); - keys.append(alice.into()); + bar_contract.set_foo(420, 1337); - let mut data = ArrayTrait::new(); - data.append(420); - data.append(1337); - world.execute('bar', data); - let foo = world.entity('Foo', keys.span(), 0, SchemaIntrospection::::size()); - assert(*foo[0] == 420, 'data not stored'); - assert(*foo[1] == 1337, 'data not stored'); + let foo: Foo = get!(world, alice, Foo); + assert(foo.a == 420, 'data not stored'); + assert(foo.b == 1337, 'data not stored'); } #[test] @@ -133,109 +136,40 @@ fn test_set_entity_unauthorized() { // Spawn empty world let world = deploy_world(); - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); - world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let caller = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_account_contract_address(caller); - - // Call bar system, should panic as it's not authorized - let mut data = ArrayTrait::new(); - data.append(420); - data.append(1337); - world.execute('bar', data); -} - -#[test] -#[available_gas(8000000)] -#[should_panic] -fn test_set_entity_invalid_data() { - // Spawn empty world - let world = deploy_world(); - - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); - world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let caller = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_account_contract_address(caller); - - // Call bar system, should panic as data is invalid - let mut data = ArrayTrait::new(); - data.append(420); - world.execute('bar', data); -} - -#[test] -#[available_gas(8000000)] -#[should_panic] -fn test_set_entity_excess_data() { - // Spawn empty world - let world = deploy_world(); + let bar_contract = IbarDispatcher { + contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) + }; - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); let caller = starknet::contract_address_const::<0x1337>(); starknet::testing::set_account_contract_address(caller); // Call bar system, should panic as it's not authorized - let mut data = ArrayTrait::new(); - data.append(420); - data.append(420); - data.append(420); - world.execute('bar', data); + bar_contract.set_foo(420, 1337); } -#[test] -#[available_gas(8000000)] -#[should_panic] -fn test_set_entity_directly() { - // Spawn empty world - let world = deploy_world(); +// This test is probably irrelevant now because we have no systems, +// so all `set_entity` call are from arbitrary contracts. +// Owners can still update via unregistered contracts/call from account +// #[test] +// #[available_gas(8000000)] +// #[should_panic] +// fn test_set_entity_directly() { +// // Spawn world +// let world = deploy_world(); +// world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); - world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); +// let bar_contract = IbarDispatcher { +// contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) +// }; - set !(world, Foo { caller: starknet::contract_address_const::<0x1337>(), a: 420, b: 1337 }); -} +// set!(world, Foo { caller: starknet::contract_address_const::<0x1337>(), a: 420, b: 1337 }); +// } // Utils fn deploy_world() -> IWorldDispatcher { - // Deploy executor contract - let executor_constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), - 0, - executor_constructor_calldata.span(), - false - ) - .unwrap(); - - // Deploy world contract - let mut constructor_calldata = array::ArrayTrait::new(); - constructor_calldata.append(executor_address.into()); - let (world_address, _) = deploy_syscall( - world::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ) - .unwrap(); - let world = IWorldDispatcher { contract_address: world_address }; - - world -} - -#[test] -#[available_gas(6000000)] -fn test_library_call_system() { - // Spawn empty world - let world = deploy_world(); - - world.register_system(library_call::TEST_CLASS_HASH.try_into().unwrap()); - let mut calldata = ArrayTrait::new(); - calldata.append(foo::TEST_CLASS_HASH); - // 'name' entrypoint - calldata.append(0x0361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60); - calldata.append(0); - world.execute('library_call', calldata); + spawn_test_world(array![]) } #[test] @@ -269,7 +203,6 @@ fn test_set_owner_fails_for_non_owner() { let world = deploy_world(); let alice = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_contract_address(alice); world.revoke_owner(alice, 0); @@ -283,132 +216,124 @@ fn test_set_owner_fails_for_non_owner() { fn test_writer() { let world = deploy_world(); - assert(!world.is_writer(42, 69), 'should not be writer'); + assert(!world.is_writer(42, 69.try_into().unwrap()), 'should not be writer'); - world.grant_writer(42, 69); - assert(world.is_writer(42, 69), 'should be writer'); + world.grant_writer(42, 69.try_into().unwrap()); + assert(world.is_writer(42, 69.try_into().unwrap()), 'should be writer'); - world.revoke_writer(42, 69); - assert(!world.is_writer(42, 69), 'should not be writer'); + world.revoke_writer(42, 69.try_into().unwrap()); + assert(!world.is_writer(42, 69.try_into().unwrap()), 'should not be writer'); } #[test] #[available_gas(6000000)] #[should_panic] -fn test_set_writer_fails_for_non_owner() { - let world = deploy_world(); +fn test_system_not_writer_fail() { + let world = spawn_test_world(array![foo::TEST_CLASS_HASH],); - let alice = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_contract_address(alice); - assert(!world.is_owner(alice, 0), 'should not be owner'); + let bar_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world); + let bar_contract = IbarDispatcher { contract_address: bar_address }; + + // Caller is not owner now + let caller = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_account_contract_address(caller); - world.grant_writer(42, 69); + // Should panic, system not writer + bar_contract.set_foo(25, 16); } -#[system] -mod origin { - use dojo::world::Context; +#[test] +#[available_gas(6000000)] +fn test_system_writer_access() { + let world = spawn_test_world(array![foo::TEST_CLASS_HASH],); - fn execute(ctx: Context) { - assert(ctx.origin == starknet::contract_address_const::<0x1337>(), 'should be equal'); - } -} + let bar_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world); + let bar_contract = IbarDispatcher { contract_address: bar_address }; -#[system] -mod origin_wrapper { - use traits::Into; - use array::ArrayTrait; - use dojo::world::Context; - - fn execute(ctx: Context) { - let data: Array = ArrayTrait::new(); - assert(ctx.origin == starknet::contract_address_const::<0x1337>(), 'should be equal'); - ctx.world.execute('origin', data); - assert(ctx.origin == starknet::contract_address_const::<0x1337>(), 'should be equal'); - } + world.grant_writer('Foo', bar_address); + assert(world.is_writer('Foo', bar_address), 'should be writer'); + + // Caller is not owner now + let caller = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_account_contract_address(caller); + + // Should not panic, system is writer + bar_contract.set_foo(25, 16); } #[test] #[available_gas(6000000)] -fn test_execute_origin() { - // Spawn empty world +#[should_panic] +fn test_set_writer_fails_for_non_owner() { let world = deploy_world(); - world.register_system(origin::TEST_CLASS_HASH.try_into().unwrap()); - world.register_system(origin_wrapper::TEST_CLASS_HASH.try_into().unwrap()); - world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); - let data = ArrayTrait::new(); - let alice = starknet::contract_address_const::<0x1337>(); starknet::testing::set_contract_address(alice); - assert(world.origin() == starknet::contract_address_const::<0x0>(), 'should be equal'); - world.execute('origin_wrapper', data); - assert(world.origin() == starknet::contract_address_const::<0x0>(), 'should be equal'); + + assert(!world.is_owner(alice, 0), 'should not be owner'); + + world.grant_writer(42, 69.try_into().unwrap()); } -#[test] -#[available_gas(6000000)] -#[should_panic] -fn test_execute_origin_failing() { - // Spawn empty world - let world = deploy_world(); - world.register_system(origin::TEST_CLASS_HASH.try_into().unwrap()); - world.register_system(origin_wrapper::TEST_CLASS_HASH.try_into().unwrap()); - world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); - let data = ArrayTrait::new(); +#[starknet::interface] +trait IOrigin { + fn assert_origin(self: @TContractState); +} + +#[starknet::contract] +mod origin { + use super::{IWorldDispatcher, ContractAddress}; + + #[storage] + struct Storage { + world: IWorldDispatcher, + } - let eve = starknet::contract_address_const::<0x1338>(); - world.execute('origin_wrapper', data); + #[constructor] + fn constructor(ref self: ContractState, world: ContractAddress) { + self.world.write(IWorldDispatcher { contract_address: world }) + } + + #[external(v0)] + impl IOriginImpl of super::IOrigin { + fn assert_origin(self: @ContractState) { + assert( + starknet::get_caller_address() == starknet::contract_address_const::<0x1337>(), + 'should be equal' + ); + } + } } #[test] #[available_gas(60000000)] fn test_execute_multiple_worlds() { - // Deploy executor contract - let executor_constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), - 0, - executor_constructor_calldata.span(), - false - ) - .unwrap(); - // Deploy world contract - let mut constructor_calldata = array::ArrayTrait::new(); - constructor_calldata.append(executor_address.into()); - let (world_address, _) = deploy_syscall( - world::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ).unwrap(); - let world = IWorldDispatcher { contract_address: world_address }; + let world1 = spawn_test_world(array![foo::TEST_CLASS_HASH],); + + let bar1_contract = IbarDispatcher { + contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world1) + }; // Deploy another world contract - let (world_address, _) = deploy_syscall( - world::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ).unwrap(); - let another_world = IWorldDispatcher { contract_address: world_address }; + let world2 = spawn_test_world(array![foo::TEST_CLASS_HASH],); - world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); - world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); - another_world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); - another_world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + let bar2_contract = IbarDispatcher { + contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world2) + }; + + let alice = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_contract_address(alice); + bar1_contract.set_foo(1337, 1337); + bar2_contract.set_foo(7331, 7331); - let mut data = ArrayTrait::new(); - data.append(1337); - data.append(1337); - let mut another_data = ArrayTrait::new(); - another_data.append(7331); - another_data.append(7331); let mut keys = ArrayTrait::new(); keys.append(0); - world.execute('bar', data); - another_world.execute('bar', another_data); - - let stored = world.entity('Foo', keys.span(), 0, SchemaIntrospection::::size()); - let another_stored = another_world.entity('Foo', keys.span(), 0, SchemaIntrospection::::size()); - assert(*stored.snapshot.at(0) == 1337, 'data not stored'); - assert(*another_stored.snapshot.at(0) == 7331, 'data not stored'); + let data1 = get!(world1, alice, Foo); + let data2 = get!(world2, alice, Foo); + assert(data1.a == 1337, 'data1 not stored'); + assert(data2.a == 7331, 'data2 not stored'); } diff --git a/crates/dojo-erc/src/erc1155.cairo b/crates/dojo-erc/src/erc1155.cairo deleted file mode 100644 index 7e4072ff84..0000000000 --- a/crates/dojo-erc/src/erc1155.cairo +++ /dev/null @@ -1,4 +0,0 @@ -mod components; -mod erc1155; -mod systems; -mod interface; diff --git a/crates/dojo-erc/src/erc1155/components.cairo b/crates/dojo-erc/src/erc1155/components.cairo deleted file mode 100644 index 0d5d73222a..0000000000 --- a/crates/dojo-erc/src/erc1155/components.cairo +++ /dev/null @@ -1,126 +0,0 @@ -use starknet::ContractAddress; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; -use zeroable::Zeroable; -use array::{ArrayTrait, SpanTrait}; -use option::OptionTrait; - -// re-export components from erc_common -use dojo_erc::erc_common::components::{operator_approval, OperatorApproval, OperatorApprovalTrait}; - - -// -// Uri TODO: use BaseURI from erc_common -// - -#[derive(Component, Copy, Drop, Serde)] -struct Uri { - #[key] - token: ContractAddress, - uri: felt252 -} - -// -// ERC1155Balance -// - -#[derive(Component, Copy, Drop, Serde)] -struct ERC1155Balance { - #[key] - token: ContractAddress, - #[key] - account: ContractAddress, - #[key] - token_id: felt252, - amount: u128 -} - -trait ERC1155BalanceTrait { - fn balance_of( - world: IWorldDispatcher, token: ContractAddress, account: ContractAddress, id: felt252 - ) -> u128; - fn unchecked_transfer_tokens( - world: IWorldDispatcher, - token: ContractAddress, - from: ContractAddress, - to: ContractAddress, - ids: Span, - amounts: Span, - ); - fn unchecked_increase_balance( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - id: felt252, - amount: u128, - ); - fn unchecked_decrease_balance( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - id: felt252, - amount: u128, - ); -} - -impl ERC1155BalanceImpl of ERC1155BalanceTrait { - fn balance_of( - world: IWorldDispatcher, token: ContractAddress, account: ContractAddress, id: felt252 - ) -> u128 { - // ERC1155: address zero is not a valid owner - assert(account.is_non_zero(), 'ERC1155: invalid owner address'); - get!(world, (token, account, id), ERC1155Balance).amount - } - - fn unchecked_transfer_tokens( - world: IWorldDispatcher, - token: ContractAddress, - from: ContractAddress, - to: ContractAddress, - mut ids: Span, - mut amounts: Span, - ) { - loop { - if ids.len() == 0 { - break (); - } - let id = *ids.pop_front().unwrap(); - let amount: u128 = *amounts.pop_front().unwrap(); - - if (from.is_non_zero()) { - let mut from_balance = get!(world, (token, from, id), ERC1155Balance); - from_balance.amount -= amount; - set!(world, (from_balance)); - } - - if (to.is_non_zero()) { - let mut to_balance = get!(world, (token, to, id), ERC1155Balance); - to_balance.amount += amount; - set!(world, (to_balance)); - }; - }; - } - - fn unchecked_increase_balance( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - id: felt252, - amount: u128, - ) { - let mut balance = get!(world, (token, owner, id), ERC1155Balance); - balance.amount += amount; - set!(world, (balance)); - } - - fn unchecked_decrease_balance( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - id: felt252, - amount: u128, - ) { - let mut balance = get!(world, (token, owner, id), ERC1155Balance); - balance.amount -= amount; - set!(world, (balance)); - } -} diff --git a/crates/dojo-erc/src/erc1155/erc1155.cairo b/crates/dojo-erc/src/erc1155/erc1155.cairo deleted file mode 100644 index 0b7cd147e9..0000000000 --- a/crates/dojo-erc/src/erc1155/erc1155.cairo +++ /dev/null @@ -1,305 +0,0 @@ -#[starknet::contract] -mod ERC1155 { - use array::ArrayTrait; - use option::OptionTrait; - use clone::Clone; - use array::ArrayTCloneImpl; - use starknet::{ContractAddress, get_caller_address, get_contract_address}; - use traits::{Into, TryInto}; - use zeroable::Zeroable; - use serde::Serde; - use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - use dojo_erc::erc1155::components::{ - Uri, ERC1155BalanceTrait, OperatorApproval, OperatorApprovalTrait - }; - use dojo_erc::erc1155::interface::{ - IERC1155, IERC1155Metadata, IERC1155TokenReceiver, IERC1155TokenReceiverDispatcher, - IERC1155TokenReceiverDispatcherTrait, IERC1155_ID, IERC1155_METADATA_ID, - IERC1155_RECEIVER_ID - }; - use dojo_erc::erc165::interface::{IERC165, IERC165_ID}; - - use dojo_erc::erc_common::utils::{ - to_calldata, ToCallDataTrait, system_calldata - }; - - use dojo_erc::erc1155::systems::{ - ERC1155SetApprovalForAllParams, ERC1155SafeTransferFromParams, - ERC1155SafeBatchTransferFromParams, ERC1155MintParams, ERC1155BurnParams - }; - - const UNLIMITED_ALLOWANCE: felt252 = - 3618502788666131213697322783095070105623107215331596699973092056135872020480; - - #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] - struct TransferSingle { - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - id: u256, - value: u256 - } - - #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] - struct TransferBatch { - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - ids: Array, - values: Array - } - - #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] - struct ApprovalForAll { - owner: ContractAddress, - operator: ContractAddress, - approved: bool - } - - #[starknet::interface] - trait IERC1155Events { - fn on_transfer_single(ref self: ContractState, event: TransferSingle); - fn on_transfer_batch(ref self: ContractState, event: TransferBatch); - fn on_approval_for_all(ref self: ContractState, event: ApprovalForAll); - } - - #[event] - #[derive(Drop, PartialEq, starknet::Event)] - enum Event { - TransferSingle: TransferSingle, - TransferBatch: TransferBatch, - ApprovalForAll: ApprovalForAll - } - - #[storage] - struct Storage { - world: IWorldDispatcher, - owner_: ContractAddress - } - - // - // Constructor - // - - #[constructor] - fn constructor( - ref self: ContractState, world: ContractAddress, deployer: ContractAddress, uri: felt252 - ) { - self.world.write(IWorldDispatcher { contract_address: world }); - self.owner_.write(deployer); - self - .world - .read() - .execute('ERC1155SetUri', to_calldata(get_contract_address()).plus(uri).data); - } - - #[external(v0)] - impl ERC165 of IERC165 { - fn supports_interface(self: @ContractState, interface_id: u32) -> bool { - interface_id == IERC165_ID - || interface_id == IERC1155_ID - || interface_id == IERC1155_METADATA_ID - } - } - - #[external(v0)] - impl ERC1155Metadata of IERC1155Metadata { - fn uri(self: @ContractState, token_id: u256) -> felt252 { - let token = get_contract_address(); - let token_id_felt: felt252 = token_id.try_into().unwrap(); - get!(self.world.read(), (token), Uri).uri - } - } - - - #[external(v0)] - impl ERC1155 of IERC1155 { - fn balance_of(self: @ContractState, account: ContractAddress, id: u256) -> u256 { - ERC1155BalanceTrait::balance_of( - self.world.read(), get_contract_address(), account, id.try_into().unwrap() - ) - .into() - } - - fn balance_of_batch( - self: @ContractState, accounts: Array, ids: Array - ) -> Array { - assert(ids.len() == accounts.len(), 'ERC1155: invalid length'); - - let mut batch_balances = ArrayTrait::new(); - let mut index = 0; - loop { - if index == ids.len() { - break batch_balances.clone(); - } - batch_balances - .append( - ERC1155BalanceTrait::balance_of( - self.world.read(), - get_contract_address(), - *accounts.at(index), - (*ids.at(index)).try_into().unwrap() - ) - .into() - ); - index += 1; - } - } - - fn is_approved_for_all( - self: @ContractState, account: ContractAddress, operator: ContractAddress - ) -> bool { - OperatorApprovalTrait::is_approved_for_all( - self.world.read(), get_contract_address(), account, operator - ) - } - - fn set_approval_for_all( - ref self: ContractState, operator: ContractAddress, approved: bool - ) { - self - .world - .read() - .execute( - 'ERC1155SetApprovalForAll', - system_calldata( - ERC1155SetApprovalForAllParams { - token: get_contract_address(), - owner: get_caller_address(), - operator, - approved - } - ) - ); - } - - fn safe_transfer_from( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - id: u256, - amount: u256, - data: Array - ) { - self - .world - .read() - .execute( - 'ERC1155SafeTransferFrom', - system_calldata( - ERC1155SafeTransferFromParams { - token: get_contract_address(), - operator: get_caller_address(), - from, - to, - id: id.try_into().unwrap(), - amount: amount.try_into().unwrap(), - data: data - } - ) - ); - } - - fn safe_batch_transfer_from( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - mut ids: Array, - mut amounts: Array, - data: Array - ) { - let mut idsf: Array = ArrayTrait::new(); - let mut amounts128: Array = ArrayTrait::new(); - loop { - if ids.len() == 0 { - break; - } - idsf.append(ids.pop_front().unwrap().try_into().unwrap()); - amounts128.append(amounts.pop_front().unwrap().try_into().unwrap()); - }; - - self - .world - .read() - .execute( - 'ERC1155SafeBatchTransferFrom', - system_calldata( - ERC1155SafeBatchTransferFromParams { - token: get_contract_address(), - operator: get_caller_address(), - from, - to, - ids: idsf, - amounts: amounts128, - data: data - } - ) - ); - } - } - - #[external(v0)] - impl ERC1155Events of IERC1155Events { - fn on_transfer_single(ref self: ContractState, event: TransferSingle) { - assert(get_caller_address() == self.world.read().executor(), 'ERC1155: not authorized'); - self.emit(event); - } - fn on_transfer_batch(ref self: ContractState, event: TransferBatch) { - assert(get_caller_address() == self.world.read().executor(), 'ERC1155: not authorized'); - self.emit(event); - } - fn on_approval_for_all(ref self: ContractState, event: ApprovalForAll) { - assert(get_caller_address() == self.world.read().executor(), 'ERC1155: not authorized'); - self.emit(event); - } - } - - - #[external(v0)] - #[generate_trait] - impl ERC721Custom of ERC721CustomTrait { - // TODO: use systems directly for these instead. - fn owner(self: @ContractState) -> ContractAddress { - self.owner_.read() - } - - fn mint( - ref self: ContractState, to: ContractAddress, id: felt252, amount: u128, data: Array - ) { - self - .world - .read() - .execute( - 'ERC1155Mint', - system_calldata( - ERC1155MintParams { - token: get_contract_address(), - operator: get_caller_address(), - to, - ids: array![id], - amounts: array![amount], - data: data - } - ) - ); - } - - fn burn(ref self: ContractState, from: ContractAddress, id: felt252, amount: u128) { - self - .world - .read() - .execute( - 'ERC1155Burn', - system_calldata( - ERC1155BurnParams { - token: get_contract_address(), - operator: get_caller_address(), - from, - ids: array![id], - amounts: array![amount] - } - ) - ); - } - } -} diff --git a/crates/dojo-erc/src/erc1155/interface.cairo b/crates/dojo-erc/src/erc1155/interface.cairo deleted file mode 100644 index 1604c812f8..0000000000 --- a/crates/dojo-erc/src/erc1155/interface.cairo +++ /dev/null @@ -1,105 +0,0 @@ -use starknet::ContractAddress; -use dojo_erc::erc165::interface::{IERC165}; - -// ERC165 -const IERC1155_ID: u32 = 0xd9b67a26_u32; -const IERC1155_METADATA_ID: u32 = 0x0e89341c_u32; -const IERC1155_RECEIVER_ID: u32 = 0x4e2312e0_u32; -const ON_ERC1155_RECEIVED_SELECTOR: u32 = 0xf23a6e61_u32; -const ON_ERC1155_BATCH_RECEIVED_SELECTOR: u32 = 0xbc197c81_u32; - - -// full interface IERC165 + IERC1155 + IERC1155Metadata + custom -#[starknet::interface] -trait IERC1155A { - // IERC165 - fn supports_interface(self: @TState, interface_id: u32) -> bool; - - // IERC1155 - fn safe_batch_transfer_from( - ref self: TState, - from: ContractAddress, - to: ContractAddress, - ids: Array, - amounts: Array, - data: Array - ); - fn safe_transfer_from( - ref self: TState, - from: ContractAddress, - to: ContractAddress, - id: u256, - amount: u256, - data: Array - ); - fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); - fn is_approved_for_all( - self: @TState, account: ContractAddress, operator: ContractAddress - ) -> bool; - fn balance_of(self: @TState, account: ContractAddress, id: u256) -> u256; - fn balance_of_batch( - self: @TState, accounts: Array, ids: Array - ) -> Array; - - // IERC1155Metadata - fn uri(self: @TState, token_id: u256) -> felt252; - - // custom - fn owner(self: @TState) -> ContractAddress; - fn mint(ref self: TState, to: ContractAddress, id: felt252, amount: u128, data: Array); - fn burn(ref self: TState, from: ContractAddress, id: felt252, amount: u128); -} - -#[starknet::interface] -trait IERC1155 { - fn safe_batch_transfer_from( - ref self: TState, - from: ContractAddress, - to: ContractAddress, - ids: Array, - amounts: Array, - data: Array - ); - fn safe_transfer_from( - ref self: TState, - from: ContractAddress, - to: ContractAddress, - id: u256, - amount: u256, - data: Array - ); - fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); - fn is_approved_for_all( - self: @TState, account: ContractAddress, operator: ContractAddress - ) -> bool; - fn balance_of(self: @TState, account: ContractAddress, id: u256) -> u256; - fn balance_of_batch( - self: @TState, accounts: Array, ids: Array - ) -> Array; -} - -#[starknet::interface] -trait IERC1155Metadata { - fn uri(self: @TState, token_id: u256) -> felt252; -} - -#[starknet::interface] -trait IERC1155TokenReceiver { - fn on_erc1155_received( - self: TState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - amount: u256, - data: Array - ) -> u32; - fn on_erc1155_batch_received( - self: TState, - operator: ContractAddress, - from: ContractAddress, - token_ids: Array, - amounts: Array, - data: Array - ) -> u32; -} - diff --git a/crates/dojo-erc/src/erc1155/systems.cairo b/crates/dojo-erc/src/erc1155/systems.cairo deleted file mode 100644 index 8ab5be0ffe..0000000000 --- a/crates/dojo-erc/src/erc1155/systems.cairo +++ /dev/null @@ -1,344 +0,0 @@ -use core::array::SpanTrait; -use starknet::{ContractAddress, get_contract_address}; -use zeroable::Zeroable; -use array::ArrayTrait; -use option::OptionTrait; -use serde::Serde; -use clone::Clone; -use traits::{Into, TryInto}; - -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -use dojo_erc::erc1155::erc1155::ERC1155::{ - ApprovalForAll, TransferSingle, TransferBatch, IERC1155EventsDispatcher, - IERC1155EventsDispatcherTrait -}; -#[event] -use dojo_erc::erc1155::erc1155::ERC1155::Event; -use dojo_erc::erc1155::components::{ERC1155BalanceTrait, OperatorApprovalTrait}; -use dojo_erc::erc165::interface::{IERC165Dispatcher, IERC165DispatcherTrait, IACCOUNT_ID}; -use dojo_erc::erc1155::interface::{ - IERC1155TokenReceiverDispatcher, IERC1155TokenReceiverDispatcherTrait, IERC1155TokenReceiver, - IERC1155_RECEIVER_ID, ON_ERC1155_RECEIVED_SELECTOR, ON_ERC1155_BATCH_RECEIVED_SELECTOR -}; - -fn emit_transfer_single( - world: IWorldDispatcher, - token: ContractAddress, - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - id: felt252, - amount: u128 -) { - let event = TransferSingle { operator, from, to, id: id.into(), value: amount.into() }; - IERC1155EventsDispatcher { contract_address: token }.on_transfer_single(event.clone()); - emit!(world, event); -} - -fn emit_transfer_batch( - world: IWorldDispatcher, - token: ContractAddress, - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - mut ids: Span, - mut amounts: Span -) { - let mut ids_u256: Array = ArrayTrait::new(); - let mut amounts_u256: Array = ArrayTrait::new(); - loop { - if ids.len() == 0 { - break; - } - ids_u256.append((*ids.pop_front().unwrap()).into()); - amounts_u256.append((*amounts.pop_front().unwrap()).into()); - }; - let event = TransferBatch { - operator: operator, from: from, to: to, ids: ids_u256, values: amounts_u256, - }; - IERC1155EventsDispatcher { contract_address: token }.on_transfer_batch(event.clone()); - emit!(world, event); -} - -fn unchecked_update( - world: IWorldDispatcher, - operator: ContractAddress, - token: ContractAddress, - from: ContractAddress, - to: ContractAddress, - ids: Array, - amounts: Array, - data: Array -) { - assert(ids.len() == amounts.len(), 'ERC1155: invalid length'); - - ERC1155BalanceTrait::unchecked_transfer_tokens(world, token, from, to, ids.span(), amounts.span()); - - if (ids.len() == 1) { - let id = *ids.at(0); - let amount = *amounts.at(0); - - emit_transfer_single(world, token, operator, from, to, id, amount); - // TODO: call do_safe_transfer_acceptance_check - // (not done as it would break tests). - } else { - emit_transfer_batch(world, token, operator, from, to, ids.span(), amounts.span()); - // TODO: call do_safe_batch_transfer_acceptance_check - // (not done as it would break tests). - } -} - -fn do_safe_transfer_acceptance_check( - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - id: u256, - amount: u256, - data: Array -) { - if (IERC165Dispatcher { contract_address: to }.supports_interface(IERC1155_RECEIVER_ID)) { - assert( - IERC1155TokenReceiverDispatcher { contract_address: to } - .on_erc1155_received( - operator, from, id, amount, data - ) == ON_ERC1155_RECEIVED_SELECTOR, - 'ERC1155: ERC1155Receiver reject' - ); - return (); - } - assert( - IERC165Dispatcher { contract_address: to }.supports_interface(IACCOUNT_ID), - 'Transfer to non-ERC1155Receiver' - ); -} - -fn do_safe_batch_transfer_acceptance_check( - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - ids: Array, - amounts: Array, - data: Array -) { - if (IERC165Dispatcher { contract_address: to }.supports_interface(IERC1155_RECEIVER_ID)) { - assert( - IERC1155TokenReceiverDispatcher { contract_address: to } - .on_erc1155_batch_received( - operator, from, ids, amounts, data - ) == ON_ERC1155_BATCH_RECEIVED_SELECTOR, - 'ERC1155: ERC1155Receiver reject' - ); - return (); - } - assert( - IERC165Dispatcher { contract_address: to }.supports_interface(IACCOUNT_ID), - 'Transfer to non-ERC1155Receiver' - ); -} - -use ERC1155SetApprovalForAll::ERC1155SetApprovalForAllParams; -use ERC1155SafeTransferFrom::ERC1155SafeTransferFromParams; -use ERC1155SafeBatchTransferFrom::ERC1155SafeBatchTransferFromParams; -use ERC1155Mint::ERC1155MintParams; -use ERC1155Burn::ERC1155BurnParams; - -#[system] -mod ERC1155SetApprovalForAll { - use traits::Into; - use dojo::world::Context; - use starknet::ContractAddress; - use array::ArrayTrait; - use clone::Clone; - - use dojo_erc::erc1155::components::OperatorApprovalTrait; - use super::{IERC1155EventsDispatcher, IERC1155EventsDispatcherTrait, ApprovalForAll}; - #[event] - use super::Event; - - #[derive(Drop, Serde)] - struct ERC1155SetApprovalForAllParams { - token: ContractAddress, - owner: ContractAddress, - operator: ContractAddress, - approved: bool, - } - - fn execute(ctx: Context, params: ERC1155SetApprovalForAllParams) { - let ERC1155SetApprovalForAllParams{token, owner, operator, approved } = params; - assert(owner != operator, 'ERC1155: wrong approval'); - - OperatorApprovalTrait::unchecked_set_approval_for_all(ctx.world, token, owner, operator, approved); - - let event = ApprovalForAll { owner, operator, approved }; - IERC1155EventsDispatcher { contract_address: token }.on_approval_for_all(event.clone()); - emit!(ctx.world, event); - } -} - -// TODO uri storage may not fit in a single felt -#[system] -mod ERC1155SetUri { - use traits::Into; - use dojo::world::Context; - use dojo_erc::erc1155::components::Uri; - use starknet::ContractAddress; - - fn execute(ctx: Context, token: ContractAddress, uri: felt252) { - assert(ctx.origin == token, 'ERC1155: not authorized'); - let mut _uri = get!(ctx.world, (token), Uri); - _uri.uri = uri; - set!(ctx.world, (_uri)) - } -} - -#[system] -mod ERC1155SafeTransferFrom { - use traits::{Into, TryInto}; - use option::OptionTrait; - use array::ArrayTrait; - use dojo::world::Context; - use zeroable::Zeroable; - use starknet::ContractAddress; - - - #[derive(Drop, Serde)] - struct ERC1155SafeTransferFromParams { - token: ContractAddress, - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - id: felt252, - amount: u128, - data: Array - } - - fn execute(ctx: Context, params: ERC1155SafeTransferFromParams) { - let ERC1155SafeTransferFromParams{token, operator, from, to, id, amount, data } = params; - assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); - assert(to.is_non_zero(), 'ERC1155: to cannot be 0'); - assert(from.is_non_zero(), 'ERC1155: from cannot be 0'); - - assert( - operator == from - || super::OperatorApprovalTrait::is_approved_for_all( - ctx.world, token, from, operator - ), - 'ERC1155: insufficient approval' - ); - - super::unchecked_update( - ctx.world, operator, token, from, to, array![id], array![amount], data - ); - } -} - -#[system] -mod ERC1155SafeBatchTransferFrom { - use traits::{Into, TryInto}; - use option::OptionTrait; - use array::ArrayTrait; - use dojo::world::Context; - use zeroable::Zeroable; - use starknet::ContractAddress; - - #[derive(Drop, Serde)] - struct ERC1155SafeBatchTransferFromParams { - token: ContractAddress, - operator: ContractAddress, - from: ContractAddress, - to: ContractAddress, - ids: Array, - amounts: Array, - data: Array - } - - fn execute(ctx: Context, params: ERC1155SafeBatchTransferFromParams) { - let ERC1155SafeBatchTransferFromParams{token, operator, from, to, ids, amounts, data } = - params; - - assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); - assert(to.is_non_zero(), 'ERC1155: to cannot be 0'); - assert(from.is_non_zero(), 'ERC1155: from cannot be 0'); - - assert( - operator == from - || super::OperatorApprovalTrait::is_approved_for_all( - ctx.world, token, from, operator - ), - 'ERC1155: insufficient approval' - ); - - super::unchecked_update(ctx.world, operator, token, from, to, ids, amounts, data); - } -} - - -#[system] -mod ERC1155Mint { - use traits::{Into, TryInto}; - use option::OptionTrait; - use array::ArrayTrait; - use dojo::world::Context; - use zeroable::Zeroable; - use starknet::ContractAddress; - - #[derive(Drop, Serde)] - struct ERC1155MintParams { - token: ContractAddress, - operator: ContractAddress, - to: ContractAddress, - ids: Array, - amounts: Array, - data: Array - } - - fn execute(ctx: Context, params: ERC1155MintParams) { - let ERC1155MintParams{token, operator, to, ids, amounts, data } = params; - - assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); - assert(to.is_non_zero(), 'ERC1155: invalid receiver'); - - super::unchecked_update( - ctx.world, operator, token, Zeroable::zero(), to, ids, amounts, data - ); - } -} - - -#[system] -mod ERC1155Burn { - use traits::{Into, TryInto}; - use option::OptionTrait; - use array::ArrayTrait; - use dojo::world::Context; - use zeroable::Zeroable; - use starknet::ContractAddress; - - #[derive(Drop, Serde)] - struct ERC1155BurnParams { - token: ContractAddress, - operator: ContractAddress, - from: ContractAddress, - ids: Array, - amounts: Array - } - - fn execute(ctx: Context, params: ERC1155BurnParams) { - let ERC1155BurnParams{token, operator, from, ids, amounts } = params; - assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); - assert(from.is_non_zero(), 'ERC1155: invalid sender'); - - assert( - operator == from - || super::OperatorApprovalTrait::is_approved_for_all( - ctx.world, token, from, operator - ), - 'ERC1155: insufficient approval' - ); - - super::unchecked_update( - ctx.world, operator, token, from, Zeroable::zero(), ids, amounts, array![] - ); - } -} diff --git a/crates/dojo-erc/src/erc165.cairo b/crates/dojo-erc/src/erc165.cairo deleted file mode 100644 index c7f08b4631..0000000000 --- a/crates/dojo-erc/src/erc165.cairo +++ /dev/null @@ -1,3 +0,0 @@ -mod interface; - -use interface::{IERC165, IERC165Dispatcher, IERC165DispatcherTrait}; diff --git a/crates/dojo-erc/src/erc165/interface.cairo b/crates/dojo-erc/src/erc165/interface.cairo deleted file mode 100644 index b8a067796b..0000000000 --- a/crates/dojo-erc/src/erc165/interface.cairo +++ /dev/null @@ -1,8 +0,0 @@ -// ERC165 -const IERC165_ID: u32 = 0x01ffc9a7_u32; -const IACCOUNT_ID: u32 = 0xa66bd575_u32; - -#[starknet::interface] -trait IERC165 { - fn supports_interface(self: @TState, interface_id: u32) -> bool; -} diff --git a/crates/dojo-erc/src/erc20.cairo b/crates/dojo-erc/src/erc20.cairo deleted file mode 100644 index 7647729efb..0000000000 --- a/crates/dojo-erc/src/erc20.cairo +++ /dev/null @@ -1,4 +0,0 @@ -mod erc20; -mod components; -mod systems; -mod interface; diff --git a/crates/dojo-erc/src/erc20/erc20.cairo b/crates/dojo-erc/src/erc20/erc20.cairo deleted file mode 100644 index 8d8c32f7f2..0000000000 --- a/crates/dojo-erc/src/erc20/erc20.cairo +++ /dev/null @@ -1,200 +0,0 @@ -// * use ufelt when available - -#[starknet::contract] -mod ERC20 { - use array::ArrayTrait; - use integer::BoundedInt; - use option::OptionTrait; - use starknet::{ContractAddress, get_caller_address, get_contract_address}; - use traits::{Into, TryInto}; - use zeroable::Zeroable; - - use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - use dojo_erc::erc20::components::{Allowance, Balance, Supply}; - use dojo_erc::erc20::interface::IERC20; - - const UNLIMITED_ALLOWANCE: felt252 = - 3618502788666131213697322783095070105623107215331596699973092056135872020480; - - #[storage] - struct Storage { - world: IWorldDispatcher, - token_name: felt252, - token_symbol: felt252, - token_decimals: u8, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - Transfer: Transfer, - Approval: Approval - } - - - #[derive(Drop, starknet::Event)] - struct Transfer { - from: ContractAddress, - to: ContractAddress, - value: u256 - } - - #[derive(Drop, starknet::Event)] - struct Approval { - owner: ContractAddress, - spender: ContractAddress, - value: u256 - } - - #[constructor] - fn constructor( - ref self: ContractState, - world: ContractAddress, - name: felt252, - symbol: felt252, - decimals: u8, - initial_supply: felt252, - recipient: ContractAddress - ) { - self.world.write(IWorldDispatcher { contract_address: world }); - self.token_name.write(name); - self.token_symbol.write(symbol); - self.token_decimals.write(decimals); - let mut calldata: Array = array![]; - if initial_supply != 0 { - assert(!recipient.is_zero(), 'ERC20: mint to 0'); - let mut calldata: Array = array![]; - let token = get_contract_address(); - calldata.append(token.into()); - calldata.append(recipient.into()); - calldata.append(initial_supply); - self.world.read().execute('erc20_mint', calldata); - - self - .emit( - Transfer { from: Zeroable::zero(), to: recipient, value: initial_supply.into() } - ); - } - } - - #[external(v0)] - impl ERC20 of IERC20 { - fn name(self: @ContractState) -> felt252 { - self.token_name.read() - } - - fn symbol(self: @ContractState) -> felt252 { - self.token_symbol.read() - } - - fn decimals(self: @ContractState) -> u8 { - self.token_decimals.read() - } - - fn total_supply(self: @ContractState) -> u256 { - let contract_address = get_contract_address(); - let supply = get!(self.world.read(), contract_address, Supply); - supply.amount.into() - } - - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - let token = get_contract_address(); - let balance = get!(self.world.read(), (token, account), Balance); - balance.amount.into() - } - - fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress - ) -> u256 { - let token = get_contract_address(); - let allowance = get!(self.world.read(), (token, owner, spender), Allowance); - - if (allowance.amount == UNLIMITED_ALLOWANCE) { - return BoundedInt::max(); - } - - allowance.amount.into() - } - - fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { - let owner = get_caller_address(); - self._approve(owner, spender, amount); - true - } - - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { - let sender = get_caller_address(); - self._transfer(sender, recipient, amount); - true - } - - fn transfer_from( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) -> bool { - let caller = get_caller_address(); - self._spend_allowance(sender, caller, amount); - self._transfer(sender, recipient, amount); - true - } - } - - // - // Internal - // - #[generate_trait] - impl InternalImpl of InternalTrait { - fn _approve( - ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 - ) { - assert(!owner.is_zero(), 'ERC20: approve from 0'); - assert(!spender.is_zero(), 'ERC20: approve to 0'); - let token = get_contract_address(); - let mut calldata: Array = array![ - token.into(), owner.into(), spender.into(), self.u256_as_allowance(amount) - ]; - self.world.read().execute('erc20_approve', calldata); - - self.emit(Approval { owner, spender, value: amount }); - } - - fn _transfer( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) { - assert(!sender.is_zero(), 'ERC20: transfer from 0'); - assert(!recipient.is_zero(), 'ERC20: transfer to 0'); - assert(ERC20::balance_of(@self, sender) >= amount, 'ERC20: not enough balance'); - - let token = get_contract_address(); - let mut calldata: Array = array![ - token.into(), sender.into(), recipient.into(), amount.try_into().unwrap() - ]; - self.world.read().execute('erc20_transfer_from', calldata); - - self.emit(Transfer { from: Zeroable::zero(), to: recipient, value: amount }); - } - - fn _spend_allowance( - ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 - ) { - let current_allowance = ERC20::allowance(@self, owner, spender); - if current_allowance != BoundedInt::max() { - self._approve(owner, spender, current_allowance - amount); - } - } - - fn u256_as_allowance(ref self: ContractState, val: u256) -> felt252 { - // by convention, max(u256) means unlimited amount, - // but since we're using felts, use max(felt252) to do the same - if val == BoundedInt::max() { - return UNLIMITED_ALLOWANCE; - } - val.try_into().unwrap() - } - } -} diff --git a/crates/dojo-erc/src/erc20/interface.cairo b/crates/dojo-erc/src/erc20/interface.cairo deleted file mode 100644 index de98d3fdba..0000000000 --- a/crates/dojo-erc/src/erc20/interface.cairo +++ /dev/null @@ -1,17 +0,0 @@ -use core::traits::TryInto; -use starknet::ContractAddress; - -#[starknet::interface] -trait IERC20 { - fn name(self: @TState) -> felt252; - fn symbol(self: @TState) -> felt252; - fn decimals(self: @TState) -> u8; - fn total_supply(self: @TState) -> u256; - fn balance_of(self: @TState, account: ContractAddress) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; -} diff --git a/crates/dojo-erc/src/erc20/systems.cairo b/crates/dojo-erc/src/erc20/systems.cairo deleted file mode 100644 index 52466865f2..0000000000 --- a/crates/dojo-erc/src/erc20/systems.cairo +++ /dev/null @@ -1,103 +0,0 @@ -#[system] -mod erc20_approve { - use traits::Into; - use starknet::ContractAddress; - - use dojo_erc::erc20::components::Allowance; - use dojo::world::Context; - - fn execute( - ctx: Context, - token: ContractAddress, - owner: ContractAddress, - spender: ContractAddress, - amount: felt252 - ) { - set!(ctx.world, Allowance { token, owner, spender, amount }) - } -} - -#[system] -mod erc20_transfer_from { - use starknet::ContractAddress; - use traits::Into; - use zeroable::Zeroable; - - use dojo_erc::erc20::components::{Allowance, Balance}; - use dojo::world::Context; - - const UNLIMITED_ALLOWANCE: felt252 = - 3618502788666131213697322783095070105623107215331596699973092056135872020480; - - fn execute( - ctx: Context, - token: ContractAddress, - sender: ContractAddress, - recipient: ContractAddress, - amount: felt252 - ) { - assert(token == ctx.origin, 'ERC20: not authorized'); - let mut balance = get!(ctx.world, (token, sender), Balance); - balance.amount -= amount; - set!(ctx.world, (balance)); - - // increase recipient's balance - let mut balance = get!(ctx.world, (token, recipient), Balance); - balance.amount += amount; - set!(ctx.world, (balance)); - } - - fn is_unlimited_allowance(allowance: Allowance) -> bool { - allowance.amount == UNLIMITED_ALLOWANCE - } -} - -#[system] -mod erc20_mint { - use starknet::ContractAddress; - use traits::Into; - use zeroable::Zeroable; - - use dojo::world::Context; - use dojo_erc::erc20::components::{Balance, Supply}; - - fn execute(ctx: Context, token: ContractAddress, recipient: ContractAddress, amount: felt252) { - assert(token == ctx.origin, 'ERC20: not authorized'); - assert(recipient.is_non_zero(), 'ERC20: mint to 0'); - // increase token supply - let mut supply = get!(ctx.world, token, Supply); - supply.amount += amount; - set!(ctx.world, (supply)); - - // increase balance of recipient - let mut balance = get!(ctx.world, (token, recipient), Balance); - - balance.amount += amount; - set!(ctx.world, (balance)); - } -} - -#[system] -mod erc20_burn { - use starknet::ContractAddress; - use traits::Into; - use zeroable::Zeroable; - - use dojo::world::Context; - use dojo_erc::erc20::components::{Balance, Supply}; - - fn execute(ctx: Context, token: ContractAddress, owner: ContractAddress, amount: felt252) { - assert(token == ctx.origin, 'ERC20: not authorized'); - assert(owner.is_non_zero(), 'ERC20: burn from 0'); - - // decrease token supply - let mut supply = get!(ctx.world, token, Supply); - supply.amount -= amount; - set!(ctx.world, (supply)); - - // decrease balance of owner - let mut balance = get!(ctx.world, (token, owner), Balance); - balance.amount -= amount; - set!(ctx.world, (balance)); - } -} diff --git a/crates/dojo-erc/src/erc721.cairo b/crates/dojo-erc/src/erc721.cairo deleted file mode 100644 index 0dd5fcd51f..0000000000 --- a/crates/dojo-erc/src/erc721.cairo +++ /dev/null @@ -1,4 +0,0 @@ -mod components; -mod erc721; -mod systems; -mod interface; diff --git a/crates/dojo-erc/src/erc721/components.cairo b/crates/dojo-erc/src/erc721/components.cairo deleted file mode 100644 index 25d011fbd5..0000000000 --- a/crates/dojo-erc/src/erc721/components.cairo +++ /dev/null @@ -1,171 +0,0 @@ -use starknet::ContractAddress; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -use zeroable::Zeroable; -use array::{ArrayTrait, SpanTrait}; -use option::OptionTrait; - -// re-export components from erc_common -use dojo_erc::erc_common::components::{ - operator_approval, OperatorApproval, OperatorApprovalTrait, base_uri, BaseUri, BaseUriTrait -}; - -// -// ERC721Owner -// - -#[derive(Component, Copy, Drop, Serde)] -struct ERC721Owner { - #[key] - token: ContractAddress, - #[key] - token_id: felt252, - address: ContractAddress -} - - -trait ERC721OwnerTrait { - fn owner_of( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252 - ) -> ContractAddress; - fn unchecked_set_owner( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252, account: ContractAddress - ); -} - -impl ERC721OwnerImpl of ERC721OwnerTrait { - // ERC721: address zero is not a valid owner - - fn owner_of( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252 - ) -> ContractAddress { - get!(world, (token, token_id), ERC721Owner).address - } - - fn unchecked_set_owner( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252, account: ContractAddress - ) { - let mut owner = get!(world, (token, token_id), ERC721Owner); - owner.address = account; - set!(world, (owner)); - } -} - - -// -// ERC721Balance -// - -#[derive(Component, Copy, Drop, Serde)] -struct ERC721Balance { - #[key] - token: ContractAddress, - #[key] - account: ContractAddress, - amount: u128, -} - -trait ERC721BalanceTrait { - fn balance_of( - world: IWorldDispatcher, token: ContractAddress, account: ContractAddress - ) -> u128; - fn unchecked_transfer_token( - world: IWorldDispatcher, - token: ContractAddress, - from: ContractAddress, - to: ContractAddress, - amount: u128, - ); - - fn unchecked_increase_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, - ); - - fn unchecked_decrease_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, - ); -} - -impl ERC721BalanceImpl of ERC721BalanceTrait { - fn balance_of( - world: IWorldDispatcher, token: ContractAddress, account: ContractAddress - ) -> u128 { - // ERC721: address zero is not a valid owner - assert(account.is_non_zero(), 'ERC721: invalid owner address'); - get!(world, (token, account), ERC721Balance).amount - } - - fn unchecked_transfer_token( - world: IWorldDispatcher, - token: ContractAddress, - from: ContractAddress, - to: ContractAddress, - amount: u128, - ) { - let mut from_balance = get!(world, (token, from), ERC721Balance); - from_balance.amount -= amount; - set!(world, (from_balance)); - - let mut to_balance = get!(world, (token, to), ERC721Balance); - to_balance.amount += amount; - set!(world, (to_balance)); - } - - fn unchecked_increase_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, - ) { - let mut balance = get!(world, (token, owner), ERC721Balance); - balance.amount += amount; - set!(world, (balance)); - } - - fn unchecked_decrease_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, - ) { - let mut balance = get!(world, (token, owner), ERC721Balance); - balance.amount -= amount; - set!(world, (balance)); - } -} - - -// -// ERC721TokenApproval -// - -#[derive(Component, Copy, Drop, Serde)] -struct ERC721TokenApproval { - #[key] - token: ContractAddress, - #[key] - token_id: felt252, - address: ContractAddress, -} - - -trait ERC721TokenApprovalTrait { - fn get_approved( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252 - ) -> ContractAddress; - - fn unchecked_approve( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252, to: ContractAddress - ); -} - -impl ERC721TokenApprovalImpl of ERC721TokenApprovalTrait { - fn get_approved( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252 - ) -> ContractAddress { - let approval = get!(world, (token, token_id), ERC721TokenApproval); - approval.address - } - - fn unchecked_approve( - world: IWorldDispatcher, token: ContractAddress, token_id: felt252, to: ContractAddress - ) { - let mut approval = get!(world, (token, token_id), ERC721TokenApproval); - approval.address = to; - set!(world, (approval)) - } -} diff --git a/crates/dojo-erc/src/erc721/erc721.cairo b/crates/dojo-erc/src/erc721/erc721.cairo deleted file mode 100644 index 9614390daf..0000000000 --- a/crates/dojo-erc/src/erc721/erc721.cairo +++ /dev/null @@ -1,284 +0,0 @@ -#[starknet::contract] -mod ERC721 { - use array::ArrayTrait; - use option::OptionTrait; - use starknet::{ContractAddress, get_caller_address, get_contract_address}; - use traits::{Into, TryInto}; - use zeroable::Zeroable; - use serde::Serde; - use clone::Clone; - - use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - use dojo_erc::erc721::components::{ - ERC721Owner, ERC721OwnerTrait, BaseUri, BaseUriTrait, ERC721Balance, ERC721BalanceTrait, - ERC721TokenApproval, ERC721TokenApprovalTrait, OperatorApproval, OperatorApprovalTrait - }; - use dojo_erc::erc721::systems::{ - ERC721ApproveParams, ERC721SetApprovalForAllParams, ERC721TransferFromParams, - ERC721MintParams, ERC721BurnParams - }; - - use dojo_erc::erc165::interface::{IERC165, IERC165_ID}; - use dojo_erc::erc721::interface::{IERC721, IERC721Metadata, IERC721_ID, IERC721_METADATA_ID}; - - use dojo_erc::erc_common::utils::{to_calldata, ToCallDataTrait, system_calldata}; - - - #[storage] - struct Storage { - world: IWorldDispatcher, - owner_: ContractAddress, // TODO: move in components - name_: felt252, - symbol_: felt252, - } - - #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] - struct Transfer { - from: ContractAddress, - to: ContractAddress, - token_id: u256 - } - - #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] - struct Approval { - owner: ContractAddress, - to: ContractAddress, - token_id: u256 - } - - #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] - struct ApprovalForAll { - owner: ContractAddress, - operator: ContractAddress, - approved: bool - } - - #[event] - #[derive(Drop, PartialEq, starknet::Event)] - enum Event { - Transfer: Transfer, - Approval: Approval, - ApprovalForAll: ApprovalForAll - } - - #[starknet::interface] - trait IERC721Events { - fn on_transfer(ref self: ContractState, event: Transfer); - fn on_approval(ref self: ContractState, event: Approval); - fn on_approval_for_all(ref self: ContractState, event: ApprovalForAll); - } - - // - // Constructor - // - - #[constructor] - fn constructor( - ref self: ContractState, - world: IWorldDispatcher, - owner: ContractAddress, - name: felt252, - symbol: felt252, - uri: felt252, - ) { - self.world.write(world); - self.owner_.write(owner); - self.name_.write(name); - self.symbol_.write(symbol); - - world.execute('ERC721SetBaseUri', to_calldata(get_contract_address()).plus(uri).data); - } - - - #[external(v0)] - impl ERC165 of IERC165 { - fn supports_interface(self: @ContractState, interface_id: u32) -> bool { - interface_id == IERC165_ID - || interface_id == IERC721_ID - || interface_id == IERC721_METADATA_ID - } - } - - #[external(v0)] - impl ERC721 of IERC721 { - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - ERC721BalanceTrait::balance_of(self.world.read(), get_contract_address(), account) - .into() - } - - fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { - let owner = ERC721OwnerTrait::owner_of( - self.world.read(), get_contract_address(), token_id.try_into().unwrap() - ); - assert(owner.is_non_zero(), 'ERC721: invalid token_id'); - owner - } - - fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { - assert(self.exists(token_id), 'ERC721: invalid token_id'); - - let token_id_felt: felt252 = token_id.try_into().unwrap(); - ERC721TokenApprovalTrait::get_approved( - self.world.read(), get_contract_address(), token_id_felt - ) - } - - fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { - self - .world - .read() - .execute( - 'ERC721Approve', - system_calldata( - ERC721ApproveParams { - token: get_contract_address(), - caller: get_caller_address(), - token_id: token_id.try_into().unwrap(), - to - } - ) - ); - } - - fn is_approved_for_all( - self: @ContractState, owner: ContractAddress, operator: ContractAddress - ) -> bool { - OperatorApprovalTrait::is_approved_for_all( - self.world.read(), get_contract_address(), owner, operator - ) - } - - fn set_approval_for_all( - ref self: ContractState, operator: ContractAddress, approved: bool - ) { - self - .world - .read() - .execute( - 'ERC721SetApprovalForAll', - system_calldata( - ERC721SetApprovalForAllParams { - token: get_contract_address(), - owner: get_caller_address(), - operator, - approved - } - ) - ); - } - - fn transfer_from( - ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 - ) { - self - .world - .read() - .execute( - 'ERC721TransferFrom', - system_calldata( - ERC721TransferFromParams { - token: get_contract_address(), - caller: get_caller_address(), - from, - to, - token_id: token_id.try_into().unwrap() - } - ) - ); - } - - fn safe_transfer_from( - ref self: ContractState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ) { - // TODO: check if we should do it - panic(array!['not implemented !']); - } - } - - #[external(v0)] - impl ERC721Metadata of IERC721Metadata { - fn name(self: @ContractState) -> felt252 { - self.name_.read() - } - - fn symbol(self: @ContractState) -> felt252 { - self.symbol_.read() - } - - fn token_uri(self: @ContractState, token_id: u256) -> felt252 { - // TODO : add token_id to base_uri - assert(self.exists(token_id), 'ERC721: invalid token_id'); - BaseUriTrait::get_base_uri(self.world.read(), get_contract_address()) - } - } - - - #[external(v0)] - #[generate_trait] - impl ERC721Custom of ERC721CustomTrait { - fn exists(self: @ContractState, token_id: u256) -> bool { - self.owner_of(token_id).is_non_zero() - } - - fn owner(self: @ContractState) -> ContractAddress { - self.owner_.read() - } - - fn transfer(ref self: ContractState, to: ContractAddress, token_id: u256) { - self.transfer_from(get_caller_address(), to, token_id); - } - - fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { - self - .world - .read() - .execute( - 'ERC721Mint', - system_calldata( - ERC721MintParams { - token: get_contract_address(), - recipient: to, - token_id: token_id.try_into().unwrap() - } - ) - ); - } - - fn burn(ref self: ContractState, token_id: u256) { - self - .world - .read() - .execute( - 'ERC721Burn', - system_calldata( - ERC721BurnParams { - token: get_contract_address(), - caller: get_caller_address(), - token_id: token_id.try_into().unwrap() - } - ) - ); - } - } - - - #[external(v0)] - impl ERC721EventEmitter of IERC721Events { - fn on_transfer(ref self: ContractState, event: Transfer) { - assert(get_caller_address() == self.world.read().executor(), 'ERC721: not authorized'); - self.emit(event); - } - fn on_approval(ref self: ContractState, event: Approval) { - assert(get_caller_address() == self.world.read().executor(), 'ERC721: not authorized'); - self.emit(event); - } - fn on_approval_for_all(ref self: ContractState, event: ApprovalForAll) { - assert(get_caller_address() == self.world.read().executor(), 'ERC721: not authorized'); - self.emit(event); - } - } -} diff --git a/crates/dojo-erc/src/erc721/interface.cairo b/crates/dojo-erc/src/erc721/interface.cairo deleted file mode 100644 index 43413dff35..0000000000 --- a/crates/dojo-erc/src/erc721/interface.cairo +++ /dev/null @@ -1,79 +0,0 @@ -use starknet::ContractAddress; - -// ERC165 -const IERC721_ID: u32 = 0x80ac58cd_u32; -const IERC721_METADATA_ID: u32 = 0x5b5e139f_u32; -const IERC721_RECEIVER_ID: u32 = 0x150b7a02_u32; -const IERC721_ENUMERABLE_ID: u32 = 0x780e9d63_u32; - - -// full interface IERC165 + IERC721 + IERC721Metadata + custom -#[starknet::interface] -trait IERC721A { - // IERC165 - fn supports_interface(self: @TState, interface_id: u32) -> bool; - - // IERC721 - fn balance_of(self: @TState, account: ContractAddress) -> u256; - fn owner_of(self: @TState, token_id: u256) -> ContractAddress; - - fn approve(ref self: TState, to: ContractAddress, token_id: u256); - fn get_approved(self: @TState, token_id: u256) -> ContractAddress; - fn is_approved_for_all( - self: @TState, owner: ContractAddress, operator: ContractAddress - ) -> bool; - fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); - - fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256); - fn safe_transfer_from( - ref self: TState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ); - - // IERC721Metadata - fn name(self: @TState) -> felt252; - fn symbol(self: @TState) -> felt252; - fn token_uri(self: @TState, token_id: u256) -> felt252; - - // custom - fn owner(self: @TState) -> ContractAddress; - fn mint(ref self: TState, to: ContractAddress, token_id: u256); - fn burn(ref self: TState, token_id: u256); - fn exists(self: @TState, token_id: u256) -> bool; - fn transfer(ref self: TState, to: ContractAddress, token_id: u256); -} - - -#[starknet::interface] -trait IERC721 { - fn balance_of(self: @TState, account: ContractAddress) -> u256; - fn owner_of(self: @TState, token_id: u256) -> ContractAddress; - - fn approve(ref self: TState, to: ContractAddress, token_id: u256); - fn get_approved(self: @TState, token_id: u256) -> ContractAddress; - - fn is_approved_for_all( - self: @TState, owner: ContractAddress, operator: ContractAddress - ) -> bool; - fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); - - fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256); - fn safe_transfer_from( - ref self: TState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ); -} - - -#[starknet::interface] -trait IERC721Metadata { - fn name(self: @TState) -> felt252; - fn symbol(self: @TState) -> felt252; - fn token_uri(self: @TState, token_id: u256) -> felt252; -} diff --git a/crates/dojo-erc/src/erc721/systems.cairo b/crates/dojo-erc/src/erc721/systems.cairo deleted file mode 100644 index e511db81c5..0000000000 --- a/crates/dojo-erc/src/erc721/systems.cairo +++ /dev/null @@ -1,276 +0,0 @@ -use array::{ArrayTrait, SpanTrait}; -use option::OptionTrait; -use serde::Serde; -use clone::Clone; -use traits::{Into, TryInto}; -use starknet::{ContractAddress, get_contract_address}; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; -use dojo_erc::erc721::erc721::ERC721; - -use dojo_erc::erc721::erc721::ERC721::{ - IERC721EventsDispatcher, IERC721EventsDispatcherTrait, Approval, Transfer, ApprovalForAll, Event -}; - -use ERC721Approve::ERC721ApproveParams; -use ERC721SetApprovalForAll::ERC721SetApprovalForAllParams; -use ERC721TransferFrom::ERC721TransferFromParams; -use ERC721Mint::ERC721MintParams; -use ERC721Burn::ERC721BurnParams; - -fn emit_transfer( - world: IWorldDispatcher, - token: ContractAddress, - from: ContractAddress, - to: ContractAddress, - token_id: felt252, -) { - let event = Transfer { from, to, token_id: token_id.into() }; - IERC721EventsDispatcher { contract_address: token }.on_transfer(event.clone()); - emit!(world, event); -} - -fn emit_approval( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - to: ContractAddress, - token_id: felt252, -) { - let event = Approval { owner, to, token_id: token_id.into() }; - IERC721EventsDispatcher { contract_address: token }.on_approval(event.clone()); - emit!(world, event); -} - - -fn emit_approval_for_all( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - operator: ContractAddress, - approved: bool, -) { - let event = ApprovalForAll { owner, operator, approved }; - IERC721EventsDispatcher { contract_address: token }.on_approval_for_all(event.clone()); - emit!(world, event); -} - - -#[system] -mod ERC721Approve { - use starknet::ContractAddress; - use traits::{Into, TryInto}; - use option::{OptionTrait}; - use clone::Clone; - use array::{ArrayTrait, SpanTrait}; - - use dojo::world::Context; - use dojo_erc::erc721::components::{ - ERC721OwnerTrait, ERC721TokenApprovalTrait, OperatorApprovalTrait - }; - use super::{IERC721EventsDispatcher, IERC721EventsDispatcherTrait, Approval}; - use zeroable::Zeroable; - - #[derive(Drop, Serde)] - struct ERC721ApproveParams { - token: ContractAddress, - caller: ContractAddress, - token_id: felt252, - to: ContractAddress - } - - fn execute(ctx: Context, params: ERC721ApproveParams) { - let ERC721ApproveParams{token, caller, token_id, to } = params; - - assert(token == ctx.origin, 'ERC721: not authorized'); - assert(caller != to, 'ERC721: invalid self approval'); - - let owner = ERC721OwnerTrait::owner_of(ctx.world, token, token_id); - assert(owner.is_non_zero(), 'ERC721: invalid token_id'); - - let is_approved_for_all = OperatorApprovalTrait::is_approved_for_all( - ctx.world, token, owner, caller - ); - // // ERC721: approve caller is not token owner or approved for all - assert(caller == owner || is_approved_for_all, 'ERC721: unauthorized caller'); - ERC721TokenApprovalTrait::unchecked_approve(ctx.world, token, token_id, to,); - - // emit events - super::emit_approval(ctx.world, token, owner, to, token_id); - } -} - -#[system] -mod ERC721SetApprovalForAll { - use starknet::ContractAddress; - use traits::Into; - use dojo::world::Context; - use clone::Clone; - use array::{ArrayTrait, SpanTrait}; - - use dojo_erc::erc721::components::{OperatorApprovalTrait}; - - - #[derive(Drop, Serde)] - struct ERC721SetApprovalForAllParams { - token: ContractAddress, - owner: ContractAddress, - operator: ContractAddress, - approved: bool - } - - fn execute(ctx: Context, params: ERC721SetApprovalForAllParams) { - let ERC721SetApprovalForAllParams{token, owner, operator, approved } = params; - - assert(token == ctx.origin, 'ERC721: not authorized'); - assert(owner != operator, 'ERC721: self approval'); - - OperatorApprovalTrait::unchecked_set_approval_for_all(ctx.world, token, owner, operator, approved); - - // emit event - super::emit_approval_for_all(ctx.world, token, owner, operator, approved); - } -} - -#[system] -mod ERC721TransferFrom { - use starknet::ContractAddress; - use traits::Into; - use zeroable::Zeroable; - use array::SpanTrait; - - use dojo::world::Context; - use dojo_erc::erc721::components::{ - OperatorApprovalTrait, ERC721BalanceTrait, ERC721TokenApprovalTrait, ERC721OwnerTrait, - }; - - #[derive(Drop, Serde)] - struct ERC721TransferFromParams { - caller: ContractAddress, - token: ContractAddress, - from: ContractAddress, - to: ContractAddress, - token_id: felt252 - } - - fn execute(ctx: Context, params: ERC721TransferFromParams) { - let ERC721TransferFromParams{caller, token, from, to, token_id } = params; - - assert(token == ctx.origin, 'ERC721: not authorized'); - assert(!to.is_zero(), 'ERC721: invalid receiver'); - - let owner = ERC721OwnerTrait::owner_of(ctx.world, token, token_id); - assert(owner.is_non_zero(), 'ERC721: invalid token_id'); - - let is_approved_for_all = OperatorApprovalTrait::is_approved_for_all( - ctx.world, token, owner, caller - ); - let approved = ERC721TokenApprovalTrait::get_approved(ctx.world, token, token_id); - - assert( - owner == caller || is_approved_for_all || approved == caller, - 'ERC721: unauthorized caller' - ); - - ERC721OwnerTrait::unchecked_set_owner(ctx.world, token, token_id, to); - ERC721BalanceTrait::unchecked_transfer_token(ctx.world, token, from, to, 1); - ERC721TokenApprovalTrait::unchecked_approve(ctx.world, token, token_id, Zeroable::zero()); - - // emit events - super::emit_transfer(ctx.world, token, from, to, token_id); - } -} - -#[system] -mod ERC721Mint { - use starknet::ContractAddress; - use traits::Into; - use zeroable::Zeroable; - use array::SpanTrait; - - use dojo::world::Context; - use dojo_erc::erc721::components::{ERC721BalanceTrait, ERC721OwnerTrait}; - - #[derive(Drop, Serde)] - struct ERC721MintParams { - token: ContractAddress, - recipient: ContractAddress, - token_id: felt252 - } - - fn execute(ctx: Context, params: ERC721MintParams) { - let ERC721MintParams{token, recipient, token_id } = params; - - assert(token == ctx.origin, 'ERC721: not authorized'); - assert(recipient.is_non_zero(), 'ERC721: mint to 0'); - - let owner = ERC721OwnerTrait::owner_of(ctx.world, token, token_id); - assert(owner.is_zero(), 'ERC721: already minted'); - - ERC721BalanceTrait::unchecked_increase_balance(ctx.world, token, recipient, 1); - ERC721OwnerTrait::unchecked_set_owner(ctx.world, token, token_id, recipient); - // emit events - super::emit_transfer(ctx.world, token, Zeroable::zero(), recipient, token_id); - } -} - -#[system] -mod ERC721Burn { - use starknet::ContractAddress; - use traits::Into; - use zeroable::Zeroable; - use array::SpanTrait; - - use dojo::world::Context; - use dojo_erc::erc721::components::{ - ERC721BalanceTrait, ERC721OwnerTrait, ERC721TokenApprovalTrait, OperatorApprovalTrait, - }; - - #[derive(Drop, Serde)] - struct ERC721BurnParams { - token: ContractAddress, - caller: ContractAddress, - token_id: felt252 - } - - fn execute(ctx: Context, params: ERC721BurnParams) { - let ERC721BurnParams{token, caller, token_id } = params; - - assert(token == ctx.origin, 'ERC721: not authorized'); - - let owner = ERC721OwnerTrait::owner_of(ctx.world, token, token_id); - assert(owner.is_non_zero(), 'ERC721: invalid token_id'); - - let is_approved_for_all = OperatorApprovalTrait::is_approved_for_all( - ctx.world, token, owner, caller - ); - let approved = ERC721TokenApprovalTrait::get_approved(ctx.world, token, token_id); - - assert( - owner == caller || is_approved_for_all || approved == caller, - 'ERC721: unauthorized caller' - ); - - ERC721BalanceTrait::unchecked_decrease_balance(ctx.world, token, owner, 1); - ERC721OwnerTrait::unchecked_set_owner(ctx.world, token, token_id, Zeroable::zero()); - - // emit events - super::emit_transfer(ctx.world, token, owner, Zeroable::zero(), token_id); - } -} - - -// TODO: move in erc_common - -#[system] -mod ERC721SetBaseUri { - use traits::Into; - use dojo::world::Context; - use dojo_erc::erc_common::components::{BaseUri, BaseUriTrait}; - use starknet::ContractAddress; - - fn execute(ctx: Context, token: ContractAddress, uri: felt252) { - assert(ctx.origin == token, 'ERC721: not authorized'); - BaseUriTrait::unchecked_set_base_uri(ctx.world, token, uri); - // TODO: emit event - } -} diff --git a/crates/dojo-erc/src/erc_common.cairo b/crates/dojo-erc/src/erc_common.cairo deleted file mode 100644 index a6159592a7..0000000000 --- a/crates/dojo-erc/src/erc_common.cairo +++ /dev/null @@ -1,3 +0,0 @@ -mod components; -mod systems; -mod utils; diff --git a/crates/dojo-erc/src/erc_common/components.cairo b/crates/dojo-erc/src/erc_common/components.cairo deleted file mode 100644 index 2c764b6570..0000000000 --- a/crates/dojo-erc/src/erc_common/components.cairo +++ /dev/null @@ -1,5 +0,0 @@ -mod base_uri_component; -mod operator_approval_component; - -use base_uri_component::{base_uri, BaseUri, BaseUriTrait}; -use operator_approval_component::{operator_approval, OperatorApproval, OperatorApprovalTrait}; diff --git a/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo b/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo deleted file mode 100644 index 7c6a5fa876..0000000000 --- a/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo +++ /dev/null @@ -1,28 +0,0 @@ -use starknet::ContractAddress; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -#[derive(Component, Copy, Drop, Serde)] -struct BaseUri { - #[key] - token: ContractAddress, - uri: felt252 -} - -trait BaseUriTrait { - fn get_base_uri(world: IWorldDispatcher, token: ContractAddress) -> felt252; - fn unchecked_set_base_uri(world: IWorldDispatcher, token: ContractAddress, new_base_uri: felt252); -} - -impl BaseUriImpl of BaseUriTrait { - fn get_base_uri(world: IWorldDispatcher, token: ContractAddress,) -> felt252 { - let base_uri = get!(world, (token), BaseUri); - base_uri.uri - } - - fn unchecked_set_base_uri(world: IWorldDispatcher, token: ContractAddress, new_base_uri: felt252) { - let mut base_uri = get!(world, (token), BaseUri); - base_uri.uri = new_base_uri; - set!(world, (base_uri)) - } -} - diff --git a/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo b/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo deleted file mode 100644 index b8dd10f302..0000000000 --- a/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo +++ /dev/null @@ -1,55 +0,0 @@ -use starknet::ContractAddress; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -#[derive(Component, Copy, Drop, Serde)] -struct OperatorApproval { - #[key] - token: ContractAddress, - #[key] - owner: ContractAddress, - #[key] - operator: ContractAddress, - approved: bool -} - -trait OperatorApprovalTrait { - fn is_approved_for_all( - world: IWorldDispatcher, - token: ContractAddress, - account: ContractAddress, - operator: ContractAddress - ) -> bool; - - fn unchecked_set_approval_for_all( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - operator: ContractAddress, - approved: bool - ); -} - -impl OperatorApprovalImpl of OperatorApprovalTrait { - fn is_approved_for_all( - world: IWorldDispatcher, - token: ContractAddress, - account: ContractAddress, - operator: ContractAddress - ) -> bool { - let approval = get!(world, (token, account, operator), OperatorApproval); - approval.approved - } - - // perform safety checks before calling this fn - fn unchecked_set_approval_for_all( - world: IWorldDispatcher, - token: ContractAddress, - owner: ContractAddress, - operator: ContractAddress, - approved: bool - ) { - let mut operator_approval = get!(world, (token, owner, operator), OperatorApproval); - operator_approval.approved = approved; - set!(world, (operator_approval)) - } -} diff --git a/crates/dojo-erc/src/erc_common/systems.cairo b/crates/dojo-erc/src/erc_common/systems.cairo deleted file mode 100644 index 8b13789179..0000000000 --- a/crates/dojo-erc/src/erc_common/systems.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/dojo-erc/src/erc_common/utils.cairo b/crates/dojo-erc/src/erc_common/utils.cairo deleted file mode 100644 index 005d55eac7..0000000000 --- a/crates/dojo-erc/src/erc_common/utils.cairo +++ /dev/null @@ -1,30 +0,0 @@ -use serde::Serde; -use array::ArrayTrait; - -#[derive(Drop)] -struct ToCallData { - data: Array, -} - -#[generate_trait] -impl ToCallDataImpl of ToCallDataTrait { - fn plus, impl TD: Drop>( - mut self: ToCallData, data: T - ) -> ToCallData { - data.serialize(ref self.data); - self - } -} - -fn to_calldata, impl TD: Drop>(data: T) -> ToCallData { - let mut calldata: Array = ArrayTrait::new(); - data.serialize(ref calldata); - ToCallData { data: calldata } -} - - -fn system_calldata, impl TD: Drop>(data: T) -> Array { - let mut calldata: Array = ArrayTrait::new(); - data.serialize(ref calldata); - calldata -} diff --git a/crates/dojo-erc/src/lib.cairo b/crates/dojo-erc/src/lib.cairo index 9633ddbd5c..810dc43dd8 100644 --- a/crates/dojo-erc/src/lib.cairo +++ b/crates/dojo-erc/src/lib.cairo @@ -1,8 +1,12 @@ -mod erc20; -mod erc165; -mod erc721; -mod erc1155; -mod erc_common; +mod token { + mod erc20; + mod erc20_components; +} #[cfg(test)] -mod tests; +mod tests { + mod constants; + mod utils; + + mod erc20_tests; +} diff --git a/crates/dojo-erc/src/tests.cairo b/crates/dojo-erc/src/tests.cairo deleted file mode 100644 index 4024c5a779..0000000000 --- a/crates/dojo-erc/src/tests.cairo +++ /dev/null @@ -1,11 +0,0 @@ -mod test_utils; - -mod test_erc20; -mod test_erc20_utils; - -mod test_erc721; -mod test_erc721_utils; - -mod test_erc1155; -mod test_erc1155_utils; - diff --git a/crates/dojo-erc/src/tests/constants.cairo b/crates/dojo-erc/src/tests/constants.cairo new file mode 100644 index 0000000000..a66b6e7218 --- /dev/null +++ b/crates/dojo-erc/src/tests/constants.cairo @@ -0,0 +1,57 @@ +use starknet::ContractAddress; +use starknet::contract_address_const; + +const NAME: felt252 = 'NAME'; +const SYMBOL: felt252 = 'SYMBOL'; +const DECIMALS: u8 = 18_u8; +const SUPPLY: u256 = 2000; +const VALUE: u256 = 300; +const ROLE: felt252 = 'ROLE'; +const OTHER_ROLE: felt252 = 'OTHER_ROLE'; +const URI: felt252 = 'URI'; +const TOKEN_ID: u256 = 21; +const PUBKEY: felt252 = 'PUBKEY'; + +fn ADMIN() -> ContractAddress { + contract_address_const::<'ADMIN'>() +} + +fn AUTHORIZED() -> ContractAddress { + contract_address_const::<'AUTHORIZED'>() +} + +fn ZERO() -> ContractAddress { + contract_address_const::<0>() +} + +fn CALLER() -> ContractAddress { + contract_address_const::<'CALLER'>() +} + +fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +fn NEW_OWNER() -> ContractAddress { + contract_address_const::<'NEW_OWNER'>() +} + +fn OTHER() -> ContractAddress { + contract_address_const::<'OTHER'>() +} + +fn OTHER_ADMIN() -> ContractAddress { + contract_address_const::<'OTHER_ADMIN'>() +} + +fn SPENDER() -> ContractAddress { + contract_address_const::<'SPENDER'>() +} + +fn RECIPIENT() -> ContractAddress { + contract_address_const::<'RECIPIENT'>() +} + +fn OPERATOR() -> ContractAddress { + contract_address_const::<'OPERATOR'>() +} diff --git a/crates/dojo-erc/src/tests/erc20_tests.cairo b/crates/dojo-erc/src/tests/erc20_tests.cairo new file mode 100644 index 0000000000..666021c0b9 --- /dev/null +++ b/crates/dojo-erc/src/tests/erc20_tests.cairo @@ -0,0 +1,548 @@ +use integer::BoundedInt; +use integer::u256; +use integer::u256_from_felt252; +use dojo_erc::tests::utils; +use dojo_erc::tests::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE +}; +use dojo_erc::token::erc20::ERC20::Approval; +use dojo_erc::token::erc20::ERC20::ERC20Impl; +use dojo_erc::token::erc20::ERC20::InternalImpl; +use dojo_erc::token::erc20::ERC20::Transfer; +use dojo_erc::token::erc20::ERC20; +use option::OptionTrait; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::testing; +use traits::Into; +use zeroable::Zeroable; +use dojo::test_utils::spawn_test_world; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::token::erc20_components::{ + ERC20Allowance, erc_20_allowance, ERC20Balance, erc_20_balance, ERC20Meta, erc_20_meta +}; +use dojo_erc::token::erc20::ERC20::_worldContractMemberStateTrait; +use debug::PrintTrait; + +// +// Setup +// + +fn STATE() -> (IWorldDispatcher, ERC20::ContractState) { + let world = spawn_test_world( + array![ + erc_20_allowance::TEST_CLASS_HASH, + erc_20_balance::TEST_CLASS_HASH, + erc_20_meta::TEST_CLASS_HASH, + ] + ); + let mut state = ERC20::contract_state_for_testing(); + state._world.write(world.contract_address); + (world, state) +} + +fn setup() -> ERC20::ContractState { + let (world, mut state) = STATE(); + ERC20::constructor(ref state, world.contract_address, NAME, SYMBOL, SUPPLY, OWNER()); + utils::drop_event(ZERO()); + state +} + +// +// initializer & constructor +// + +#[test] +#[available_gas(25000000)] +fn test_initializer() { + let (world, mut state) = STATE(); + InternalImpl::initializer(ref state, NAME, SYMBOL); + + assert(ERC20Impl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC20Impl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC20Impl::decimals(@state) == DECIMALS, 'Decimals should be 18'); + assert(ERC20Impl::total_supply(@state) == 0, 'Supply should eq 0'); +} + + +#[test] +#[available_gas(25000000)] +fn test_constructor() { + let (world, mut state) = STATE(); + ERC20::constructor(ref state, world.contract_address, NAME, SYMBOL, SUPPLY, OWNER()); + + assert_only_event_transfer(ZERO(), OWNER(), SUPPLY); + + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY, 'Should eq inital_supply'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Should eq inital_supply'); + assert(ERC20Impl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC20Impl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC20Impl::decimals(@state) == DECIMALS, 'Decimals should be 18'); +} + +// +// Getters +// + +#[test] +#[available_gas(25000000)] +fn test_total_supply() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, OWNER(), SUPPLY); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Should eq SUPPLY'); +} + +#[test] +#[available_gas(25000000)] +fn test_balance_of() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, OWNER(), SUPPLY); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY, 'Should eq SUPPLY'); +} + + +#[test] +#[available_gas(25000000)] +fn test_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE, 'Should eq VALUE'); +} + +// +// approve & _approve +// + +#[test] +#[available_gas(25000000)] +fn test_approve() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert(ERC20Impl::approve(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), VALUE); + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE, 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test_approve_from_zero() { + let mut state = setup(); + ERC20Impl::approve(ref state, SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test_approve_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +fn test__approve() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + InternalImpl::_approve(ref state, OWNER(), SPENDER(), VALUE); + + assert_only_event_approval(OWNER(), SPENDER(), VALUE); + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE, 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test__approve_from_zero() { + let mut state = setup(); + InternalImpl::_approve(ref state, Zeroable::zero(), SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test__approve_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + InternalImpl::_approve(ref state, OWNER(), Zeroable::zero(), VALUE); +} + +// +// transfer & _transfer +// + +#[test] +#[available_gas(25000000)] +fn test_transfer() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert(ERC20Impl::transfer(ref state, RECIPIENT(), VALUE), 'Should return true'); + + assert_only_event_transfer(OWNER(), RECIPIENT(), VALUE); + assert(ERC20Impl::balance_of(@state, RECIPIENT()) == VALUE, 'Balance should eq VALUE'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq supply - VALUE'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Total supply should not change'); +} + +#[test] +#[available_gas(25000000)] +fn test__transfer() { + let mut state = setup(); + + InternalImpl::_transfer(ref state, OWNER(), RECIPIENT(), VALUE); + + assert_only_event_transfer(OWNER(), RECIPIENT(), VALUE); + assert(ERC20Impl::balance_of(@state, RECIPIENT()) == VALUE, 'Balance should eq amount'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq supply - amount'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Total supply should not change'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test__transfer_not_enough_balance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + + let balance_plus_one = SUPPLY + 1; + InternalImpl::_transfer(ref state, OWNER(), RECIPIENT(), balance_plus_one); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: transfer from 0',))] +fn test__transfer_from_zero() { + let mut state = setup(); + InternalImpl::_transfer(ref state, Zeroable::zero(), RECIPIENT(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: transfer to 0',))] +fn test__transfer_to_zero() { + let mut state = setup(); + InternalImpl::_transfer(ref state, OWNER(), Zeroable::zero(), VALUE); +} + +// +// transfer_from +// + +#[test] +#[available_gas(25000000)] +fn test_transfer_from() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + testing::set_caller_address(SPENDER()); + assert(ERC20Impl::transfer_from(ref state, OWNER(), RECIPIENT(), VALUE), 'Should return true'); + + assert_event_approval(OWNER(), SPENDER(), 0); + assert_only_event_transfer(OWNER(), RECIPIENT(), VALUE); + + assert(ERC20Impl::balance_of(@state, RECIPIENT()) == VALUE, 'Should eq amount'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq suppy - amount'); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == 0, 'Should eq 0'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Total supply should not change'); +} + +#[test] +#[available_gas(25000000)] +fn test_transfer_from_doesnt_consume_infinite_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), BoundedInt::max()); + + testing::set_caller_address(SPENDER()); + ERC20Impl::transfer_from(ref state, OWNER(), RECIPIENT(), VALUE); + + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == BoundedInt::max(), + 'Allowance should not change' + ); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_transfer_from_greater_than_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + + testing::set_caller_address(SPENDER()); + let allowance_plus_one = VALUE + 1; + ERC20Impl::transfer_from(ref state, OWNER(), RECIPIENT(), allowance_plus_one); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: transfer to 0',))] +fn test_transfer_from_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + + testing::set_caller_address(SPENDER()); + ERC20Impl::transfer_from(ref state, OWNER(), Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_transfer_from_from_zero_address() { + let mut state = setup(); + ERC20Impl::transfer_from(ref state, Zeroable::zero(), RECIPIENT(), VALUE); +} + +// +// increase_allowance & increaseAllowance +// + +#[test] +#[available_gas(25000000)] +fn test_increase_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::increase_allowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), VALUE * 2); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE * 2, 'Should be amount * 2'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test_increase_allowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::increase_allowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test_increase_allowance_from_zero_address() { + let mut state = setup(); + ERC20::increase_allowance(ref state, SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +fn test_increaseAllowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::increaseAllowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), 2 * VALUE); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE * 2, 'Should be amount * 2'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test_increaseAllowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::increaseAllowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test_increaseAllowance_from_zero_address() { + let mut state = setup(); + ERC20::increaseAllowance(ref state, SPENDER(), VALUE); +} + +// +// decrease_allowance & decreaseAllowance +// + +#[test] +#[available_gas(25000000)] +fn test_decrease_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::decrease_allowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), 0); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE - VALUE, 'Should be 0'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decrease_allowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::decrease_allowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decrease_allowance_from_zero_address() { + let mut state = setup(); + ERC20::decrease_allowance(ref state, SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +fn test_decreaseAllowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::decreaseAllowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), 0); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE - VALUE, 'Should be 0'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decreaseAllowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::decreaseAllowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decreaseAllowance_from_zero_address() { + let mut state = setup(); + ERC20::decreaseAllowance(ref state, SPENDER(), VALUE); +} + +// +// _spend_allowance +// + +#[test] +#[available_gas(25000000)] +fn test__spend_allowance_not_unlimited() { + let mut state = setup(); + + InternalImpl::_approve(ref state, OWNER(), SPENDER(), SUPPLY); + utils::drop_event(ZERO()); + + InternalImpl::_spend_allowance(ref state, OWNER(), SPENDER(), VALUE); + + assert_only_event_approval(OWNER(), SPENDER(), SUPPLY - VALUE); + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == SUPPLY - VALUE, + 'Should eq supply - amount' + ); +} + +#[test] +#[available_gas(25000000)] +fn test__spend_allowance_unlimited() { + let mut state = setup(); + InternalImpl::_approve(ref state, OWNER(), SPENDER(), BoundedInt::max()); + + let max_minus_one: u256 = BoundedInt::max() - 1; + InternalImpl::_spend_allowance(ref state, OWNER(), SPENDER(), max_minus_one); + + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == BoundedInt::max(), + 'Allowance should not change' + ); +} + +// +// _mint +// + +#[test] +#[available_gas(25000000)] +fn test__mint() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, OWNER(), VALUE); + assert_only_event_transfer(ZERO(), OWNER(), VALUE); + assert(ERC20Impl::balance_of(@state, OWNER()) == VALUE, 'Should eq amount'); + assert(ERC20Impl::total_supply(@state) == VALUE, 'Should eq total supply'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: mint to 0',))] +fn test__mint_to_zero() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, Zeroable::zero(), VALUE); +} + +// +// _burn +// + +#[test] +#[available_gas(25000000)] +fn test__burn() { + let mut state = setup(); + InternalImpl::_burn(ref state, OWNER(), VALUE); + + assert_only_event_transfer(OWNER(), ZERO(), VALUE); + assert(ERC20Impl::total_supply(@state) == SUPPLY - VALUE, 'Should eq supply - amount'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq supply - amount'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: burn from 0',))] +fn test__burn_from_zero() { + let mut state = setup(); + InternalImpl::_burn(ref state, Zeroable::zero(), VALUE); +} + +// +// Helpers +// + +fn assert_event_approval(owner: ContractAddress, spender: ContractAddress, value: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.spender == spender, 'Invalid `spender`'); + assert(event.value == value, 'Invalid `value`'); +} + +fn assert_only_event_approval(owner: ContractAddress, spender: ContractAddress, value: u256) { + assert_event_approval(owner, spender, value); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_transfer(from: ContractAddress, to: ContractAddress, value: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.value == value, 'Invalid `value`'); +} + +fn assert_only_event_transfer(from: ContractAddress, to: ContractAddress, value: u256) { + assert_event_transfer(from, to, value); + utils::assert_no_events_left(ZERO()); +} diff --git a/crates/dojo-erc/src/tests/test_erc1155.cairo b/crates/dojo-erc/src/tests/test_erc1155.cairo deleted file mode 100644 index b040444925..0000000000 --- a/crates/dojo-erc/src/tests/test_erc1155.cairo +++ /dev/null @@ -1,629 +0,0 @@ -use zeroable::Zeroable; -use traits::{Into, Default, IndexView}; -use option::OptionTrait; -use array::ArrayTrait; -use serde::Serde; -use starknet::ContractAddress; -use starknet::testing::set_contract_address; - -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -use dojo_erc::tests::test_utils::impersonate; -use dojo_erc::tests::test_erc1155_utils::{ - spawn_world, deploy_erc1155, deploy_default, deploy_testcase1, ZERO, USER1, USER2, DEPLOYER, - PROXY -}; - -use dojo_erc::erc165::interface::IERC165_ID; -use dojo_erc::erc1155::interface::{ - IERC1155A, IERC1155ADispatcher, IERC1155ADispatcherTrait, IERC1155_ID, IERC1155_METADATA_ID, - IERC1155_RECEIVER_ID -}; - -use dojo_erc::erc1155::erc1155::ERC1155::{Event, TransferSingle, TransferBatch, ApprovalForAll}; - - -#[test] -#[available_gas(30000000)] -fn test_deploy() { - let world = spawn_world(DEPLOYER()); - let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); - let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; - assert(erc1155.owner() == DEPLOYER(), 'invalid owner'); -} - -#[test] -#[available_gas(30000000)] -fn test_deploy_default() { - let (world, erc1155) = deploy_default(); - assert(erc1155.owner() == DEPLOYER(), 'invalid owner'); -} - - -// -// supports_interface -// - -#[test] -#[available_gas(30000000)] -fn test_should_support_interfaces() { - let (world, erc1155) = deploy_default(); - - assert(erc1155.supports_interface(IERC165_ID) == true, 'should support erc165'); - assert(erc1155.supports_interface(IERC1155_ID) == true, 'should support erc1155'); - assert( - erc1155.supports_interface(IERC1155_METADATA_ID) == true, 'should support erc1155_metadata' - ); -} - -// -// uri -// - -#[test] -#[available_gas(30000000)] -fn test_uri() { - let (world, erc1155) = deploy_default(); - assert(erc1155.uri(64) == 'uri', 'invalid uri'); -} - - -// -// behaves like an ERC1155 -// - -// -// balance_of -// -#[test] -#[available_gas(30000000)] -#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] -fn test_balance_of_zero_address() { - //reverts when queried about the zero address - - let (world, erc1155) = deploy_default(); - erc1155.balance_of(ZERO(), 0); // should panic -} - -#[test] -#[available_gas(30000000)] -fn test_balance_of_empty_balance() { - // when accounts don't own tokens - // returns zero for given addresses - let (world, erc1155) = deploy_default(); - assert(erc1155.balance_of(USER1(), 0) == 0, 'should be 0'); - assert(erc1155.balance_of(USER1(), 69) == 0, 'should be 0'); - assert(erc1155.balance_of(USER2(), 0) == 0, 'should be 0'); -} - -#[test] -#[available_gas(30000000)] -fn test_balance_with_tokens() { - // when accounts own some tokens - // returns the amount of tokens owned by the given addresses - let (world, erc1155) = deploy_default(); - - erc1155.mint(USER1(), 0, 1, array![]); - erc1155.mint(USER1(), 69, 42, array![]); - erc1155.mint(USER2(), 69, 5, array![]); - - assert(erc1155.balance_of(USER1(), 0) == 1, 'should be 1'); - assert(erc1155.balance_of(USER1(), 69) == 42, 'should be 42'); - assert(erc1155.balance_of(USER2(), 69) == 5, 'should be 5'); -} - -// -// balance_of_batch -// - -#[test] -#[available_gas(30000000)] -#[should_panic(expected: ('ERC1155: invalid length', 'ENTRYPOINT_FAILED',))] -fn test_balance_of_batch_with_invalid_input() { - // reverts when input arrays don't match up - let (world, erc1155) = deploy_default(); - erc1155.balance_of_batch(array![USER1(), USER2()], array![0]); - erc1155.balance_of_batch(array![USER1()], array![0, 1, 2]); -} - -#[test] -#[available_gas(30000000)] -#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] -fn test_balance_of_batch_address_zero() { - // reverts when input arrays don't match up - let (world, erc1155) = deploy_default(); - erc1155.balance_of_batch(array![USER1(), ZERO()], array![0, 1]); -} - -#[test] -#[available_gas(30000000)] -fn test_balance_of_batch_empty_account() { - // when accounts don't own tokens - // returns zeros for each account - let (world, erc1155) = deploy_default(); - let balances = erc1155.balance_of_batch(array![USER1(), USER1(), USER1()], array![0, 1, 5]); - let bals = @balances; - assert(balances.len() == 3, 'should be 3'); - assert(bals[0] == @0_u256, 'should be 0'); - assert(bals[1] == @0_u256, 'should be 0'); - assert(bals[2] == @0_u256, 'should be 0'); -} - -#[test] -#[available_gas(30000000)] -fn test_balance_of_batch_with_tokens() { - // when accounts own some tokens - // returns amounts owned by each account in order passed - let (world, erc1155) = deploy_default(); - - erc1155.mint(USER1(), 0, 1, array![]); - erc1155.mint(USER1(), 69, 42, array![]); - erc1155.mint(USER2(), 69, 2, array![]); - - let balances = erc1155.balance_of_batch(array![USER1(), USER1(), USER2()], array![0, 69, 69]); - let bals = @balances; - assert(balances.len() == 3, 'should be 3'); - assert(bals[0] == @1_u256, 'should be 1'); - assert(bals[1] == @42_u256, 'should be 42'); - assert(bals[2] == @2_u256, 'should be 2'); -} - -#[test] -#[available_gas(30000000)] -fn test_balance_of_batch_with_tokens_2() { - // when accounts own some tokens - // returns multiple times the balance of the same address when asked - let (world, erc1155) = deploy_default(); - - erc1155.mint(USER1(), 0, 1, array![]); - erc1155.mint(USER2(), 69, 2, array![]); - - let balances = erc1155.balance_of_batch(array![USER1(), USER2(), USER1()], array![0, 69, 0]); - let bals = @balances; - assert(balances.len() == 3, 'should be 3'); - assert(bals[0] == @1_u256, 'should be 1'); - assert(bals[1] == @2_u256, 'should be 2'); - assert(bals[2] == @1_u256, 'should be 1'); -} - - -// -// balance_of_batch -// - -#[test] -#[available_gas(30000000)] -fn test_set_approval_for_all() { - // sets approval status which can be queried via is_approved_for_all - let (world, erc1155) = deploy_default(); - impersonate(USER1()); - - erc1155.set_approval_for_all(PROXY(), true); - assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); -} - -#[test] -#[available_gas(30000000)] -fn test_set_approval_for_all_emit_event() { - // set_approval_for_all emits ApprovalForAll event - let (world, erc1155) = deploy_default(); - impersonate(USER1()); - - erc1155.set_approval_for_all(PROXY(), true); - - // ApprovalForAll - assert( - @starknet::testing::pop_log(erc1155.contract_address) - .unwrap() == @Event::ApprovalForAll( - ApprovalForAll { owner: USER1(), operator: PROXY(), approved: true } - ), - 'invalid ApprovalForAll event' - ); -} - - -#[test] -#[available_gas(30000000)] -fn test_set_unset_approval_for_all() { - // sets approval status which can be queried via is_approved_for_all - let (world, erc1155) = deploy_default(); - impersonate(USER1()); - - erc1155.set_approval_for_all(PROXY(), true); - assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); - erc1155.set_approval_for_all(PROXY(), false); - assert(erc1155.is_approved_for_all(USER1(), PROXY()) == false, 'should be false'); -} - -#[test] -#[available_gas(30000000)] -#[should_panic()] -fn test_set_approval_for_all_on_self() { - // reverts if attempting to approve self as an operator - let (world, erc1155) = deploy_default(); - impersonate(USER1()); - - erc1155.set_approval_for_all(USER1(), true); // should panic -} - -// -// safe_transfer_from -// - -#[test] -#[available_gas(30000000)] -#[should_panic()] -fn test_safe_transfer_from_more_than_balance() { - // reverts when transferring more than balance - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - erc1155.safe_transfer_from(USER1(), USER2(), 1, 999, array![]); // should panic -} - -#[test] -#[available_gas(30000000)] -#[should_panic()] -fn test_safe_transfer_to_zero() { - // reverts when transferring to zero address - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - erc1155.safe_transfer_from(USER1(), ZERO(), 1, 1, array![]); // should panic -} - -#[test] -#[available_gas(50000000)] -fn test_safe_transfer_debit_sender() { - // debits transferred balance from sender - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - let balance_before = erc1155.balance_of(USER1(), 1); - erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); - let balance_after = erc1155.balance_of(USER1(), 1); - - assert(balance_after == balance_before - 1, 'invalid balance after'); -} - -#[test] -#[available_gas(50000000)] -fn test_safe_transfer_credit_receiver() { - // credits transferred balance to receiver - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - let balance_before = erc1155.balance_of(USER2(), 1); - erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); - let balance_after = erc1155.balance_of(USER2(), 1); - - assert(balance_after == balance_before + 1, 'invalid balance after'); -} - -#[test] -#[available_gas(50000000)] -fn test_safe_transfer_preserve_existing_balances() { - // preserves existing balances which are not transferred by multiTokenHolder - let (world, erc1155) = deploy_testcase1(); - - // impersonate user1 - impersonate(USER1()); - - let balance_before_2 = erc1155.balance_of(USER2(), 2); - let balance_before_3 = erc1155.balance_of(USER2(), 3); - erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); - let balance_after_2 = erc1155.balance_of(USER2(), 2); - let balance_after_3 = erc1155.balance_of(USER2(), 3); - - assert(balance_after_2 == balance_before_2, 'should be equal'); - assert(balance_after_3 == balance_before_3, 'should be equal'); -} - -#[test] -#[available_gas(30000000)] -#[should_panic()] -fn test_safe_transfer_from_unapproved_operator() { - // when called by an operator on behalf of the multiTokenHolder - // when operator is not approved by multiTokenHolder - - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER2()); - - erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); // should panic -} - -#[test] -#[available_gas(50000000)] -fn test_safe_transfer_from_approved_operator() { - // when called by an operator on behalf of the multiTokenHolder - // when operator is approved by multiTokenHolder - let (world, erc1155) = deploy_testcase1(); - - impersonate(PROXY()); - - let balance_before = erc1155.balance_of(USER1(), 1); - erc1155.safe_transfer_from(USER1(), USER2(), 1, 2, array![]); - let balance_after = erc1155.balance_of(USER1(), 1); - - assert(balance_after == balance_before - 2, 'invalid balance'); -} - -#[test] -#[available_gas(100000000)] -fn test_safe_transfer_from_approved_operator_preserve_operator_balance() { - // when called by an operator on behalf of the multiTokenHolder - // preserves operator's balances not involved in the transfer - let (world, erc1155) = deploy_testcase1(); - - impersonate(PROXY()); - - let balance_before_1 = erc1155.balance_of(PROXY(), 1); - let balance_before_2 = erc1155.balance_of(PROXY(), 2); - let balance_before_3 = erc1155.balance_of(PROXY(), 3); - erc1155.safe_transfer_from(USER1(), USER2(), 1, 2, array![]); - let balance_after_1 = erc1155.balance_of(PROXY(), 1); - let balance_after_2 = erc1155.balance_of(PROXY(), 2); - let balance_after_3 = erc1155.balance_of(PROXY(), 3); - - assert(balance_before_1 == balance_after_1, 'should be equal'); - assert(balance_before_2 == balance_after_2, 'should be equal'); - assert(balance_before_3 == balance_after_3, 'should be equal'); -} - - -#[test] -#[available_gas(50000000)] -#[should_panic] -fn test_safe_transfer_from_zero_address() { - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - erc1155.safe_transfer_from(ZERO(), USER1(), 1, 1, array![]); -} - -// -// safe_batch_transfer_from -// - -#[test] -#[available_gas(50000000)] -#[should_panic] -fn test_safe_batch_transfer_from_more_than_balance() { - // reverts when transferring amount more than any of balances - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - erc1155 - .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 999, 1], array![]); -} - - -#[test] -#[available_gas(50000000)] -#[should_panic] -fn test_safe_batch_transfer_from_mismatching_array_len() { - // reverts when ids array length doesn't match amounts array length - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 1], array![]); -} - - -#[test] -#[available_gas(50000000)] -#[should_panic] -fn test_safe_batch_transfer_from_to_zero_address() { - // reverts when transferring to zero address - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - erc1155.safe_batch_transfer_from(USER1(), ZERO(), array![1, 2], array![1, 1], array![]); -} - - -#[test] -#[available_gas(100000000)] -fn test_safe_batch_transfer_from_debits_sender() { - // debits transferred balances from sender - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - let balance_before_1 = erc1155.balance_of(USER1(), 1); - let balance_before_2 = erc1155.balance_of(USER1(), 2); - let balance_before_3 = erc1155.balance_of(USER1(), 3); - erc1155 - .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); - let balance_after_1 = erc1155.balance_of(USER1(), 1); - let balance_after_2 = erc1155.balance_of(USER1(), 2); - let balance_after_3 = erc1155.balance_of(USER1(), 3); - - assert(balance_before_1 - 1 == balance_after_1, 'invalid balance'); - assert(balance_before_2 - 10 == balance_after_2, 'invalid balance'); - assert(balance_before_3 - 20 == balance_after_3, 'invalid balance'); -} - - -#[test] -#[available_gas(100000000)] -fn test_safe_batch_transfer_from_credits_recipient() { - // credits transferred balances to receiver - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - let balance_before_1 = erc1155.balance_of(USER2(), 1); - let balance_before_2 = erc1155.balance_of(USER2(), 2); - let balance_before_3 = erc1155.balance_of(USER2(), 3); - erc1155 - .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); - let balance_after_1 = erc1155.balance_of(USER2(), 1); - let balance_after_2 = erc1155.balance_of(USER2(), 2); - let balance_after_3 = erc1155.balance_of(USER2(), 3); - - assert(balance_before_1 + 1 == balance_after_1, 'invalid balance'); - assert(balance_before_2 + 10 == balance_after_2, 'invalid balance'); - assert(balance_before_1 + 20 == balance_after_3, 'invalid balance'); -} - - -#[test] -#[available_gas(50000000)] -#[should_panic] -fn test_safe_batch_transfer_from_unapproved_operator() { - // when called by an operator on behalf of the multiTokenHolder - // when operator is not approved by multiTokenHolder - - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER2()); - - erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); -} - -#[test] -#[available_gas(100000000)] -fn test_safe_batch_transfer_from_approved_operator_preserve_operator_balance() { - // when called by an operator on behalf of the multiTokenHolder - // preserves operator's balances not involved in the transfer - - let (world, erc1155) = deploy_testcase1(); - - impersonate(PROXY()); - - let balance_before_1 = erc1155.balance_of(PROXY(), 1); - let balance_before_2 = erc1155.balance_of(PROXY(), 2); - let balance_before_3 = erc1155.balance_of(PROXY(), 3); - - erc1155 - .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); - - let balance_after_1 = erc1155.balance_of(PROXY(), 1); - let balance_after_2 = erc1155.balance_of(PROXY(), 2); - let balance_after_3 = erc1155.balance_of(PROXY(), 3); - - assert(balance_before_1 == balance_after_1, 'should be equal'); - assert(balance_before_2 == balance_after_2, 'should be equal'); - assert(balance_before_3 == balance_after_3, 'should be equal'); -} - -#[test] -#[available_gas(50000000)] -#[should_panic] -fn test_safe_batch_transfer_from_zero_address() { - let (world, erc1155) = deploy_testcase1(); - - impersonate(USER1()); - - erc1155.safe_batch_transfer_from(ZERO(), USER1(), array![1, 2], array![1, 1], array![]); -} - - -#[test] -#[available_gas(50000000)] -fn test_safe_batch_transfer_emit_transfer_batch_event() { - let (world, erc1155) = deploy_default(); - - // user1 token_id 1 x 10 - erc1155.mint(USER1(), 1, 10, array![]); - // user1 token_id 2 x 20 - erc1155.mint(USER1(), 2, 20, array![]); - - impersonate(USER1()); - - erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); - - let _: Event = starknet::testing::pop_log(erc1155.contract_address) - .unwrap(); // unpop erc1155.mint(USER1(), 1, 10, array![]); - let _: Event = starknet::testing::pop_log(erc1155.contract_address) - .unwrap(); // unpop erc1155.mint(USER1(), 2, 20, array![]); - - // TransferBatch - assert( - @starknet::testing::pop_log(erc1155.contract_address) - .unwrap() == @Event::TransferBatch( - TransferBatch { - operator: USER1(), - from: USER1(), - to: USER2(), - ids: array![1, 2], - values: array![1, 10] - } - ), - 'invalid TransferBatch event' - ); -} - - -// -// burn -// - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_burn_non_existing_token_id() { - //reverts when burning a non-existent token id - let (world, erc1155) = deploy_default(); - - impersonate(USER1()); - erc1155.burn(USER1(), 69, 1); // should panic -} - - -#[test] -#[available_gas(90000000)] -fn test_burn_emit_transfer_single_event() { - // burn should emit event - let (world, erc1155) = deploy_default(); - - erc1155.mint(USER1(), 69, 5, array![]); - assert(erc1155.balance_of(USER1(), 69) == 5, 'invalid balance'); - - impersonate(USER1()); - - erc1155.burn(USER1(), 69, 1); - assert(erc1155.balance_of(USER1(), 69) == 4, 'invalid balance'); - - let _: Event = starknet::testing::pop_log(erc1155.contract_address) - .unwrap(); // unpop erc1155.mint(USER1(), 69,5,array![]) - - // TransferSingle - assert( - @starknet::testing::pop_log(erc1155.contract_address) - .unwrap() == @Event::TransferSingle( - TransferSingle { operator: USER1(), from: USER1(), to: ZERO(), id: 69, value: 1 } - ), - 'invalid TransferSingle event' - ); -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_burn_more_than_owned() { - // reverts when burning more tokens than owned - let (world, erc1155) = deploy_default(); - erc1155.mint(USER1(), 69, 10, array![]); - - impersonate(USER1()); - - erc1155.burn(USER1(), 69, 1); - erc1155.burn(USER1(), 69, 10); // should panic -} -// TODO : to be continued - -// TODO : add test if we support IERC1155Receiver - - diff --git a/crates/dojo-erc/src/tests/test_erc1155_utils.cairo b/crates/dojo-erc/src/tests/test_erc1155_utils.cairo deleted file mode 100644 index 3feccda0b2..0000000000 --- a/crates/dojo-erc/src/tests/test_erc1155_utils.cairo +++ /dev/null @@ -1,131 +0,0 @@ -use traits::{Into, TryInto}; -use option::{Option, OptionTrait}; -use result::ResultTrait; -use array::ArrayTrait; - -use starknet::ContractAddress; -use starknet::syscalls::deploy_syscall; -use starknet::testing::set_contract_address; - -use dojo::test_utils::spawn_test_world; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; -use dojo_erc::tests::test_utils::impersonate; - -use dojo_erc::erc1155::erc1155::ERC1155; -use dojo_erc::erc1155::interface::{IERC1155A, IERC1155ADispatcher, IERC1155ADispatcherTrait}; - -use dojo_erc::erc1155::components::{erc_1155_balance, uri, operator_approval}; -use dojo_erc::erc1155::systems::{ - ERC1155SetApprovalForAll, ERC1155SetUri, ERC1155SafeTransferFrom, ERC1155SafeBatchTransferFrom, - ERC1155Mint, ERC1155Burn -}; - - -fn ZERO() -> ContractAddress { - starknet::contract_address_const::<0x0>() -} - -fn DEPLOYER() -> ContractAddress { - starknet::contract_address_const::<0x420>() -} - -fn USER1() -> ContractAddress { - starknet::contract_address_const::<0x111>() -} - -fn USER2() -> ContractAddress { - starknet::contract_address_const::<0x222>() -} - -fn USER3() -> ContractAddress { - starknet::contract_address_const::<0x333>() -} - -fn PROXY() -> ContractAddress { - starknet::contract_address_const::<0x999>() -} - -fn spawn_world(world_admin: ContractAddress) -> IWorldDispatcher { - impersonate(world_admin); - - // components - let mut components = array![ - erc_1155_balance::TEST_CLASS_HASH, uri::TEST_CLASS_HASH, operator_approval::TEST_CLASS_HASH, - ]; - - // systems - let mut systems = array![ - ERC1155SetApprovalForAll::TEST_CLASS_HASH, - ERC1155SetUri::TEST_CLASS_HASH, - ERC1155SafeTransferFrom::TEST_CLASS_HASH, - ERC1155SafeBatchTransferFrom::TEST_CLASS_HASH, - ERC1155Mint::TEST_CLASS_HASH, - ERC1155Burn::TEST_CLASS_HASH, - ]; - - let world = spawn_test_world(components, systems); - - // Grants writer rights for Component / System - - // erc_1155_balance - world.grant_writer('ERC1155Balance', 'ERC1155SafeTransferFrom'); - world.grant_writer('ERC1155Balance', 'ERC1155SafeBatchTransferFrom'); - world.grant_writer('ERC1155Balance', 'ERC1155Mint'); - world.grant_writer('ERC1155Balance', 'ERC1155Burn'); - - // uri - world.grant_writer('Uri', 'ERC1155SetUri'); - - // operator_approval - world.grant_writer('OperatorApproval', 'ERC1155SetApprovalForAll'); - - world -} - -fn deploy_erc1155( - world: IWorldDispatcher, deployer: ContractAddress, uri: felt252, seed: felt252 -) -> ContractAddress { - let constructor_calldata = array![world.contract_address.into(), deployer.into(), uri]; - let (deployed_address, _) = deploy_syscall( - ERC1155::TEST_CLASS_HASH.try_into().unwrap(), seed, constructor_calldata.span(), false - ) - .expect('error deploying ERC1155'); - - deployed_address -} - - -fn deploy_default() -> (IWorldDispatcher, IERC1155ADispatcher) { - let world = spawn_world(DEPLOYER()); - let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); - let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; - - (world, erc1155) -} - - -fn deploy_testcase1() -> (IWorldDispatcher, IERC1155ADispatcher) { - let world = spawn_world(DEPLOYER()); - let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); - let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; - - // proxy token_id 1 x 5 - erc1155.mint(PROXY(), 1, 5, array![]); - // proxy token_id 2 x 5 - erc1155.mint(PROXY(), 2, 5, array![]); - // proxy token_id 3 x 5 - erc1155.mint(PROXY(), 3, 5, array![]); - - // user1 token_id 1 x 10 - erc1155.mint(USER1(), 1, 10, array![]); - // user1 token_id 2 x 20 - erc1155.mint(USER1(), 2, 20, array![]); - // user1 token_id 3 x 30 - erc1155.mint(USER1(), 3, 30, array![]); - - impersonate(USER1()); - //user1 approve_for_all proxy - erc1155.set_approval_for_all(PROXY(), true); - - (world, erc1155) -} diff --git a/crates/dojo-erc/src/tests/test_erc20.cairo b/crates/dojo-erc/src/tests/test_erc20.cairo deleted file mode 100644 index 3c0811b7b7..0000000000 --- a/crates/dojo-erc/src/tests/test_erc20.cairo +++ /dev/null @@ -1,162 +0,0 @@ -use integer::BoundedInt; -use option::OptionTrait; -use result::ResultTrait; -use starknet::{ContractAddress, contract_address_const, get_caller_address, get_contract_address}; -use starknet::class_hash::ClassHash; -use starknet::class_hash::Felt252TryIntoClassHash; -use starknet::syscalls::deploy_syscall; -use starknet::SyscallResultTrait; -use starknet::testing::set_contract_address; -use traits::{Into, TryInto}; -use zeroable::Zeroable; - -use dojo_erc::erc20::erc20::ERC20; -use dojo_erc::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; -use dojo_erc::tests::test_erc20_utils::{ - NAME, SYMBOL, DECIMALS, OWNER, SPENDER, SUPPLY, RECIPIENT, VALUE, deploy_erc20 -}; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -#[test] -#[available_gas(200000000)] -fn test_constructor() { - let (world, erc20) = deploy_erc20(); - assert(erc20.balance_of(OWNER()) == SUPPLY, 'Should eq inital_supply'); - // assert(erc20.total_supply() == SUPPLY, 'Should eq inital_supply'); - // assert(erc20.name() == NAME, 'Name Should be NAME'); - // assert(erc20.symbol() == SYMBOL, 'Symbol Should be SYMBOL'); - // assert(erc20.decimals() == DECIMALS, 'Decimals Should be 18'); -} - -#[test] -#[available_gas(200000000)] -fn test_allowance() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - erc20.approve(SPENDER(), VALUE); - assert(erc20.allowance(OWNER(), SPENDER()) == VALUE, 'Should eq VALUE'); -} - -#[test] -#[available_gas(200000000)] -fn test_approve() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - assert(erc20.approve(SPENDER(), VALUE), 'Should return true'); - assert(erc20.allowance(OWNER(), SPENDER()) == VALUE, 'Spender not approved correctly'); -} - -#[test] -#[available_gas(200000000)] -#[should_panic(expected: ('ERC20: approve from 0', 'ENTRYPOINT_FAILED'))] -fn test_approve_from_zero() { - let (world, erc20) = deploy_erc20(); - erc20.approve(SPENDER(), VALUE); -} - -#[test] -#[available_gas(200000000)] -#[should_panic(expected: ('ERC20: approve to 0', 'ENTRYPOINT_FAILED'))] -fn test_approve_to_zero() { - set_contract_address(OWNER()); - let (world, erc20) = deploy_erc20(); - erc20.approve(Zeroable::zero(), VALUE); -} - -#[test] -#[available_gas(200000000)] -fn test_transfer() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - assert(erc20.transfer(RECIPIENT(), VALUE), 'Should return true'); -} - -#[test] -#[available_gas(2000000000)] -#[should_panic(expected: ('ERC20: not enough balance', 'ENTRYPOINT_FAILED'))] -fn test_transfer_not_enough_balance() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - let balance_plus_one = SUPPLY + 1; - erc20.transfer(RECIPIENT(), balance_plus_one.into()); -} - -#[test] -#[available_gas(2000000000)] -#[should_panic(expected: ('ERC20: transfer from 0', 'ENTRYPOINT_FAILED'))] -fn test_transfer_from_zero() { - let (world, erc20) = deploy_erc20(); - erc20.transfer(RECIPIENT(), VALUE); -} - -#[test] -#[available_gas(2000000000)] -#[should_panic(expected: ('ERC20: transfer to 0', 'ENTRYPOINT_FAILED'))] -fn test_transfer_to_zero() { - let (world, erc20) = deploy_erc20(); - set_contract_address(RECIPIENT()); - erc20.transfer(Zeroable::zero(), VALUE); -} - -#[test] -#[available_gas(200000000)] -fn test_transfer_from() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - erc20.approve(SPENDER(), VALUE); - - set_contract_address(SPENDER()); - assert(erc20.transfer_from(OWNER(), RECIPIENT(), VALUE), 'Should return true'); - assert(erc20.balance_of(RECIPIENT()) == VALUE, 'Should eq amount'); - assert(erc20.balance_of(OWNER()) == (SUPPLY - VALUE).into(), 'Should eq suppy - amount'); - assert(erc20.allowance(OWNER(), SPENDER()) == 0.into(), 'Should eq to 0'); - assert(erc20.total_supply() == SUPPLY, 'Total supply should not change'); -} - -#[test] -#[available_gas(200000000)] -fn test_transfer_from_doesnt_consume_infinite_allowance() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - erc20.approve(SPENDER(), BoundedInt::max()); - - set_contract_address(SPENDER()); - assert(erc20.transfer_from(OWNER(), RECIPIENT(), VALUE), 'Should return true'); - assert( - erc20.allowance(OWNER(), SPENDER()) == BoundedInt::max(), - 'allowance should not change' - ); -} - -#[test] -#[available_gas(200000000)] -#[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED'))] -fn test_transfer_from_greater_than_allowance() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - erc20.approve(SPENDER(), VALUE); - - set_contract_address(SPENDER()); - let allowance_plus_one = VALUE + 1; - erc20.transfer_from(OWNER(), RECIPIENT(), allowance_plus_one); -} - -#[test] -#[available_gas(200000000)] -#[should_panic(expected: ('ERC20: transfer to 0', 'ENTRYPOINT_FAILED'))] -fn test_transfer_from_to_zero_address() { - let (world, erc20) = deploy_erc20(); - set_contract_address(OWNER()); - erc20.approve(SPENDER(), VALUE); - - set_contract_address(SPENDER()); - erc20.transfer_from(OWNER(), Zeroable::zero(), VALUE); -} - -#[test] -#[available_gas(200000000)] -#[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED'))] -fn test_transfer_from_from_zero_address() { - let (world, erc20) = deploy_erc20(); - erc20.transfer_from(Zeroable::zero(), RECIPIENT(), VALUE); -} diff --git a/crates/dojo-erc/src/tests/test_erc20_utils.cairo b/crates/dojo-erc/src/tests/test_erc20_utils.cairo deleted file mode 100644 index 38eb3824c2..0000000000 --- a/crates/dojo-erc/src/tests/test_erc20_utils.cairo +++ /dev/null @@ -1,61 +0,0 @@ -use array::{ArrayTrait, SpanTrait}; -use option::OptionTrait; -use result::ResultTrait; -use starknet::SyscallResultTrait; -use starknet::{ - ClassHash, ContractAddress, syscalls::deploy_syscall, class_hash::Felt252TryIntoClassHash, - get_caller_address, contract_address_const -}; -use traits::{Into, TryInto}; - -use dojo::executor::executor; -use dojo::test_utils::spawn_test_world; -use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; -use dojo_erc::erc20::components::{allowance, balance, supply}; -use dojo_erc::erc20::erc20::ERC20; -use dojo_erc::erc20::systems::{erc20_approve, erc20_burn, erc20_mint, erc20_transfer_from}; -use dojo_erc::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - -const DECIMALS: u8 = 18; -const NAME: felt252 = 111; -const SUPPLY: u256 = 2000; -const SYMBOL: felt252 = 222; -const VALUE: u256 = 300; - -fn OWNER() -> ContractAddress { - contract_address_const::<0x5>() -} -fn RECIPIENT() -> ContractAddress { - contract_address_const::<0x7>() -} -fn SPENDER() -> ContractAddress { - contract_address_const::<0x6>() -} - -fn deploy_erc20() -> (IWorldDispatcher, IERC20Dispatcher) { - let mut systems = array![ - erc20_approve::TEST_CLASS_HASH, - erc20_burn::TEST_CLASS_HASH, - erc20_mint::TEST_CLASS_HASH, - erc20_transfer_from::TEST_CLASS_HASH - ]; - - let mut components = array![ - allowance::TEST_CLASS_HASH, balance::TEST_CLASS_HASH, supply::TEST_CLASS_HASH - ]; - let world = spawn_test_world(components, systems); - - let mut calldata: Array = array![ - world.contract_address.into(), - NAME, - SYMBOL, - DECIMALS.into(), - SUPPLY.try_into().unwrap(), - OWNER().into() - ]; - let (erc20_address, _) = deploy_syscall( - ERC20::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false - ) - .unwrap_syscall(); - return (world, IERC20Dispatcher { contract_address: erc20_address }); -} diff --git a/crates/dojo-erc/src/tests/test_erc721.cairo b/crates/dojo-erc/src/tests/test_erc721.cairo deleted file mode 100644 index 071e4abf74..0000000000 --- a/crates/dojo-erc/src/tests/test_erc721.cairo +++ /dev/null @@ -1,862 +0,0 @@ -use core::zeroable::Zeroable; -use core::traits::{Into, Default}; -use array::ArrayTrait; -use serde::Serde; -use starknet::ContractAddress; -use starknet::testing::set_contract_address; -use option::OptionTrait; - -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -use dojo_erc::tests::test_utils::impersonate; -use dojo_erc::tests::test_erc721_utils::{ - spawn_world, deploy_erc721, deploy_default, deploy_testcase1, USER1, USER2, USER3, DEPLOYER, - ZERO, PROXY -}; - - -use dojo_erc::erc165::interface::IERC165_ID; -use dojo_erc::erc721::interface::{ - IERC721, IERC721ADispatcher, IERC721ADispatcherTrait, IERC721_ID, IERC721_METADATA_ID -}; -use dojo_erc::erc721::erc721::ERC721::{Event, Transfer, Approval, ApprovalForAll}; -// actually it's possible to mint -> burn -> mint -> ... -// todo : add Minted component to keep track of minted ids - -#[test] -#[available_gas(30000000)] -fn test_deploy() { - let world = spawn_world(DEPLOYER()); - let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); - let erc721 = IERC721ADispatcher { contract_address: erc721_address }; - - assert(erc721.owner() == DEPLOYER(), 'invalid owner'); - assert(erc721.name() == 'name', 'invalid name'); - assert(erc721.symbol() == 'symbol', 'invalid symbol'); -} - - -#[test] -#[available_gas(30000000)] -fn test_deploy_default() { - let (world, erc721) = deploy_default(); - assert(erc721.name() == 'name', 'invalid name'); -} - -// -// supports_interface -// - -#[test] -#[available_gas(30000000)] -fn test_should_support_interfaces() { - let (world, erc721) = deploy_default(); - - assert(erc721.supports_interface(IERC165_ID) == true, 'should support erc165'); - assert(erc721.supports_interface(IERC721_ID) == true, 'should support erc721'); - assert( - erc721.supports_interface(IERC721_METADATA_ID) == true, 'should support erc721_metadata' - ); -} - - -// -// behaves like an ERC721 -// - -// -// balance_of -// - -use debug::PrintTrait; - -#[test] -#[available_gas(100000000)] -fn test_balance_of_with_tokens() { - // returns the amount of tokens owned by the given address - - let (world, erc721) = deploy_testcase1(); - assert(erc721.balance_of(USER1()) == 3, 'should be 3'); - assert(erc721.balance_of(PROXY()) == 4, 'should be 4'); -} - -#[test] -#[available_gas(100000000)] -fn test_balance_of_with_no_tokens() { - // when the given address does not own any tokens - - let (world, erc721) = deploy_testcase1(); - assert(erc721.balance_of(USER3()) == 0, 'should be 0'); -} - - -#[test] -#[available_gas(50000000)] -#[should_panic] -fn test_balance_of_zero_address() { - // when querying the zero address - - let (world, erc721) = deploy_testcase1(); - erc721.balance_of(ZERO()); -} - -// -// owner_of -// - -#[test] -#[available_gas(90000000)] -fn test_owner_of_existing_id() { - // when the given token ID was tracked by this token = for existing id - - let (world, erc721) = deploy_testcase1(); - assert(erc721.owner_of(1) == USER1(), 'should be user1'); - assert(erc721.owner_of(2) == USER1(), 'should be user1'); - assert(erc721.owner_of(3) == USER1(), 'should be user1'); - - assert(erc721.owner_of(10) == PROXY(), 'should be proxy'); - assert(erc721.owner_of(11) == PROXY(), 'should be proxy'); - assert(erc721.owner_of(12) == PROXY(), 'should be proxy'); - assert(erc721.owner_of(13) == PROXY(), 'should be proxy'); -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_owner_of_non_existing_id() { - // when the given token ID was not tracked by this token = non existing id - - let (world, erc721) = deploy_testcase1(); - let owner_of_0 = erc721.owner_of(0); // should panic -} - -// -// transfers -// - -#[test] -#[available_gas(90000000)] -fn test_transfer_ownership() { - // transfers the ownership of the given token ID to the given address - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - let owner_of_1 = erc721.owner_of(1); - // transfer token_id 1 to user2 - erc721.transfer(USER2(), 1); - assert(erc721.owner_of(1) == USER2(), 'invalid owner'); -} - -#[test] -#[available_gas(90000000)] -fn test_transfer_event() { - // emits a Transfer event - - let (world, erc721) = deploy_default(); - - // mint - erc721.mint(USER1(), 42); - - impersonate(USER1()); - - // transfer token_id 1 to user2 - erc721.transfer(USER2(), 42); - - impersonate(USER2()); - erc721.burn(42); - - // mint - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Transfer(Transfer { from: ZERO(), to: USER1(), token_id: 42 }), - 'invalid Transfer event' - ); - // transfer - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Transfer(Transfer { from: USER1(), to: USER2(), token_id: 42 }), - 'invalid Transfer event' - ); - // burn - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Transfer(Transfer { from: USER2(), to: ZERO(), token_id: 42 }), - 'invalid Transfer event' - ); -} - - -#[test] -#[available_gas(90000000)] -fn test_transfer_clear_approval() { - // clears the approval for the token ID - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - erc721.approve(PROXY(), 1); - assert(erc721.get_approved(1) == PROXY(), 'should be proxy'); - - // transfer token_id 1 to user2 - erc721.transfer(USER2(), 1); - assert(erc721.get_approved(1).is_zero(), 'should be zero'); -} - -#[test] -#[available_gas(90000000)] -fn test_transfer_adjusts_owners_balances() { - // adjusts owners balances - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - let balance_user1_before = erc721.balance_of(USER1()); - let balance_user2_before = erc721.balance_of(USER2()); - - // transfer token_id 1 to user2 - erc721.transfer(USER2(), 1); - - let balance_user1_after = erc721.balance_of(USER1()); - let balance_user2_after = erc721.balance_of(USER2()); - - assert(balance_user1_after == balance_user1_before - 1, 'invalid user1 balance'); - assert(balance_user2_after == balance_user2_before + 1, 'invalid user2 balance'); -} - - -#[test] -#[available_gas(90000000)] -fn test_transfer_from_approved() { - // when called by the approved individual - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - //user1 approve user2 for token_id 2 - erc721.approve(USER2(), 2); - - impersonate(USER2()); - - erc721.transfer_from(USER1(), USER2(), 2); - assert(erc721.owner_of(2) == USER2(), 'invalid owner'); -} - -#[test] -#[available_gas(90000000)] -fn test_transfer_from_approved_operator() { - // when called by the operator - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - //user1 set_approval_for_all for proxy - erc721.set_approval_for_all(PROXY(), true); - - impersonate(PROXY()); - - erc721.transfer_from(USER1(), USER2(), 2); - assert(erc721.owner_of(2) == USER2(), 'invalid owner'); -} - -#[test] -#[available_gas(90000000)] -fn test_transfer_from_owner_without_approved() { - // when called by the owner without an approved user - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - erc721.approve(ZERO(), 2); - - erc721.transfer_from(USER1(), USER2(), 2); - assert(erc721.owner_of(2) == USER2(), 'invalid owner'); -} - - -#[test] -#[available_gas(90000000)] -fn test_transfer_to_owner() { - // when sent to the owner - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - let balance_before = erc721.balance_of(USER1()); - - assert(erc721.owner_of(3) == USER1(), 'invalid owner'); - erc721.transfer(USER1(), 3); - - // keeps ownership of the token - assert(erc721.owner_of(3) == USER1(), 'invalid owner'); - - // clears the approval for the token ID - assert(erc721.get_approved(3) == ZERO(), 'invalid approved'); - - //emits only a transfer event : cumbersome to test with pop_log - - //keeps the owner balance - let balance_after = erc721.balance_of(USER1()); - assert(balance_before == balance_after, 'invalid balance') -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_transfer_when_previous_owner_is_incorrect() { - // when the address of the previous owner is incorrect - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - //user2 owner token_id 10 - erc721.transfer_from(USER1(), PROXY(), 10); // should panic -} - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_transfer_when_sender_not_authorized() { - // when the sender is not authorized for the token id - let (world, erc721) = deploy_testcase1(); - - impersonate(PROXY()); - - //proxy is not authorized for USER2 - erc721.transfer_from(USER2(), PROXY(), 20); // should panic -} - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_transfer_when_token_id_doesnt_exists() { - // when the sender is not authorized for the token id - let (world, erc721) = deploy_testcase1(); - - impersonate(PROXY()); - - //proxy is authorized for USER1 but token_id 50 doesnt exists - erc721.transfer_from(USER1(), PROXY(), 50); // should panic -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_transfer_to_address_zero() { - // when the address to transfer the token to is the zero address - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - erc721.transfer(ZERO(), 1); // should panic -} - -// -// approval -// - -// when clearing approval - -#[test] -#[available_gas(90000000)] -fn test_approval_when_clearing_with_prior_approval() { - // -when there was a prior approval - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 42); - - impersonate(USER1()); - - erc721.approve(PROXY(), 42); - - //revoke approve - erc721.approve(ZERO(), 42); - - // clears approval for the token - assert(erc721.get_approved(42) == ZERO(), 'invalid approved'); - - // emits an approval event - let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop approve PROXY - - // approve ZERO - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Approval(Approval { owner: USER1(), to: ZERO(), token_id: 42 }), - 'invalid Approval event' - ); -} - -#[test] -#[available_gas(90000000)] -fn test_approval_when_clearing_without_prior_approval() { - // when clearing approval - // -when there was no prior approval - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 42); - - impersonate(USER1()); - - //revoke approve - erc721.approve(ZERO(), 42); - - // updates approval for the token - assert(erc721.get_approved(42) == ZERO(), 'invalid approved'); - - let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint - - // approve ZERO - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Approval(Approval { owner: USER1(), to: ZERO(), token_id: 42 }), - 'invalid Approval event' - ); -} - - -// when approving a non-zero address - -#[test] -#[available_gas(90000000)] -fn test_approval_non_zero_address_with_prior_approval() { - // -when there was a prior approval - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 42); - - impersonate(USER1()); - erc721.approve(PROXY(), 42); - - // user1 approves user3 - erc721.approve(USER3(), 42); - - // set approval for the token - assert(erc721.get_approved(42) == USER3(), 'invalid approved'); - - // emits an approval event - let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop approve PROXY - - // approve USER3 - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER3(), token_id: 42 }), - 'invalid Approval event' - ); -} - -#[test] -#[available_gas(90000000)] -fn test_approval_non_zero_address_with_no_prior_approval() { - // -when there was no prior approval - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 42); - - impersonate(USER1()); - - // user1 approves user3 - erc721.approve(USER3(), 42); - - // set approval for the token - assert(erc721.get_approved(42) == USER3(), 'invalid approved'); - - // emits an approval event - let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint - - // approve USER3 - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER3(), token_id: 42 }), - 'invalid Approval event' - ); -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_approval_self_approve() { - // when the address that receives the approval is the owner - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 42); - - impersonate(USER1()); - - // user1 approves user1 - erc721.approve(USER1(), 42); // should panic -} - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_approval_not_owned() { - // when the sender does not own the given token ID - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - // user1 approves user2 for token 20 - erc721.approve(USER2(), 20); // should panic -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_approval_from_approved_sender() { - // when the sender is approved for the given token ID - - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - // user1 approve user3 - erc721.approve(USER3(), 1); - - impersonate(USER3()); - - // (ERC721: approve caller is not token owner or approved for all) - erc721.approve(USER2(), 1); // should panic -} - - -#[test] -#[available_gas(90000000)] -fn test_approval_from_approved_operator() { - // when the sender is an operator - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 50); - - impersonate(USER1()); - - erc721.set_approval_for_all(PROXY(), true); - - impersonate(PROXY()); - - // proxy approves user2 for token 20 - erc721.approve(USER2(), 50); - - assert(erc721.get_approved(50) == USER2(), 'invalid approval'); - - let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop set_approval_for_all - - // approve - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER2(), token_id: 50 }), - 'invalid Approval event' - ); -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_approval_unexisting_id() { - // when the given token ID does not exist - let (world, erc721) = deploy_testcase1(); - - impersonate(USER1()); - - // user1 approve user3 - erc721.approve(USER3(), 69); // should panic -} - -// -// approval_for_all -// - -#[test] -#[available_gas(90000000)] -fn test_approval_for_all_operator_is_not_owner_no_operator_approval() { - // when the operator willing to approve is not the owner - // -when there is no operator approval set by the sender - let (world, erc721) = deploy_default(); - - impersonate(USER2()); - - // user2 set_approval_for_all PROXY - erc721.set_approval_for_all(PROXY(), true); - - assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); - - // ApproveForAll - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::ApprovalForAll( - ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } - ), - 'invalid ApprovalForAll event' - ); -} - -#[test] -#[available_gas(90000000)] -fn test_approval_for_all_operator_is_not_owner_from_not_approved() { - // when the operator willing to approve is not the owner - // -when the operator was set as not approved - let (world, erc721) = deploy_default(); - - impersonate(USER2()); - - erc721.set_approval_for_all(PROXY(), false); - - // user2 set_approval_for_all PROXY - erc721.set_approval_for_all(PROXY(), true); - - assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); - - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop set_approval_for_all(PROXY(), false) - - // ApproveForAll - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::ApprovalForAll( - ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } - ), - 'invalid ApprovalForAll event' - ); -} - -#[test] -#[available_gas(90000000)] -fn test_approval_for_all_operator_is_not_owner_can_unset_approval_for_all() { - // when the operator willing to approve is not the owner - // can unset the operator approval - let (world, erc721) = deploy_default(); - - impersonate(USER2()); - - erc721.set_approval_for_all(PROXY(), false); - erc721.set_approval_for_all(PROXY(), true); - assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); - erc721.set_approval_for_all(PROXY(), false); - assert(erc721.is_approved_for_all(USER2(), PROXY()) == false, 'invalid is_approved_for_all'); - - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop set_approval_for_all(PROXY(), false) - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop set_approval_for_all(PROXY(), true) - - // ApproveForAll - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::ApprovalForAll( - ApprovalForAll { owner: USER2(), operator: PROXY(), approved: false } - ), - 'invalid ApprovalForAll event' - ); -} - -#[test] -#[available_gas(90000000)] -fn test_approval_for_all_operator_with_operator_already_approved() { - // when the operator willing to approve is not the owner - // when the operator was already approved - let (world, erc721) = deploy_default(); - - impersonate(USER2()); - - erc721.set_approval_for_all(PROXY(), true); - assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); - erc721.set_approval_for_all(PROXY(), true); - assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); - - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop set_approval_for_all(PROXY(), true) - - // ApproveForAll - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::ApprovalForAll( - ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } - ), - 'invalid ApprovalForAll event' - ); -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_approval_for_all_with_owner_as_operator() { - // when the operator is the owner - - let (world, erc721) = deploy_default(); - - impersonate(USER1()); - - erc721.set_approval_for_all(USER1(), true); // should panic -} - - -// -// get_approved -// - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_get_approved_unexisting_token() { - let (world, erc721) = deploy_default(); - - erc721.get_approved(420); // should panic -} - - -#[test] -#[available_gas(90000000)] -fn test_get_approved_with_existing_token() { - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 420); - assert(erc721.get_approved(420) == ZERO(), 'invalid get_approved'); -} - - -#[test] -#[available_gas(90000000)] -fn test_get_approved_with_existing_token_and_approval() { - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 420); - - impersonate(USER1()); - - erc721.approve(PROXY(), 420); - assert(erc721.get_approved(420) == PROXY(), 'invalid get_approved'); -} - -// -// mint -// - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_mint_to_address_zero() { - // reverts with a null destination address - - let (world, erc721) = deploy_default(); - - erc721.mint(ZERO(), 69); // should panic -} - - -#[test] -#[available_gas(90000000)] -fn test_mint() { - // reverts with a null destination address - - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 69); - - assert(erc721.balance_of(USER1()) == 1, 'invalid balance'); - - // Transfer - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Transfer(Transfer { from: ZERO(), to: USER1(), token_id: 69 }), - 'invalid Transfer event' - ); -} - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_mint_existing_token_id() { - // reverts with a null destination address - - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 69); - erc721.mint(USER1(), 69); //should panic -} - - -// -// burn -// - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_burn_non_existing_token_id() { - //reverts when burning a non-existent token id - let (world, erc721) = deploy_default(); - erc721.burn(69); // should panic -} - - -#[test] -#[available_gas(90000000)] -fn test_burn_emit_events() { - // burn should emit event - let (world, erc721) = deploy_default(); - - erc721.mint(USER1(), 69); - assert(erc721.balance_of(USER1()) == 1, 'invalid balance'); - - impersonate(USER1()); - - erc721.burn(69); - assert(erc721.balance_of(USER1()) == 0, 'invalid balance'); - - let _: Event = starknet::testing::pop_log(erc721.contract_address) - .unwrap(); // unpop erc721.mint(USER1(), 69) - - // Transfer - assert( - @starknet::testing::pop_log(erc721.contract_address) - .unwrap() == @Event::Transfer(Transfer { from: USER1(), to: ZERO(), token_id: 69 }), - 'invalid Transfer event' - ); -} - - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_burn_same_id_twice() { - // reverts when burning a token id that has been deleted - let (world, erc721) = deploy_default(); - erc721.mint(USER1(), 69); - erc721.burn(69); - erc721.burn(69); // should panic -} - -// -// token_uri -// - -#[test] -#[available_gas(90000000)] -#[should_panic] -fn test_token_uri_for_non_existing_token_id() { - // reverts when queried for non existent token id - let (world, erc721) = deploy_default(); - erc721.token_uri(1234); // should panic -} - diff --git a/crates/dojo-erc/src/tests/test_erc721_utils.cairo b/crates/dojo-erc/src/tests/test_erc721_utils.cairo deleted file mode 100644 index 46f28ac997..0000000000 --- a/crates/dojo-erc/src/tests/test_erc721_utils.cairo +++ /dev/null @@ -1,153 +0,0 @@ -use traits::{Into, TryInto}; -use option::{Option, OptionTrait}; -use result::ResultTrait; -use array::ArrayTrait; - -use starknet::ContractAddress; -use starknet::syscalls::deploy_syscall; -use starknet::testing::set_contract_address; - -use dojo::test_utils::spawn_test_world; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; -use dojo_erc::tests::test_utils::impersonate; - -use dojo_erc::erc721::erc721::ERC721; -use dojo_erc::erc721::interface::{IERC721, IERC721ADispatcher, IERC721ADispatcherTrait}; - -use dojo_erc::erc721::components::{ - erc_721_balance, erc_721_owner, erc_721_token_approval, operator_approval, base_uri -}; -use dojo_erc::erc721::systems::{ - ERC721Approve, ERC721SetApprovalForAll, ERC721TransferFrom, ERC721Mint, ERC721Burn, - ERC721SetBaseUri -}; - -fn DEPLOYER() -> ContractAddress { - starknet::contract_address_const::<0x420>() -} - -fn USER1() -> ContractAddress { - starknet::contract_address_const::<0x111>() -} - -fn USER2() -> ContractAddress { - starknet::contract_address_const::<0x222>() -} - -fn USER3() -> ContractAddress { - starknet::contract_address_const::<0x333>() -} - -fn ZERO() -> ContractAddress { - starknet::contract_address_const::<0x0>() -} - -fn PROXY() -> ContractAddress { - starknet::contract_address_const::<0x999>() -} - -fn spawn_world(world_admin: ContractAddress) -> IWorldDispatcher { - impersonate(world_admin); - - // components - let mut components = array![ - erc_721_balance::TEST_CLASS_HASH, - erc_721_owner::TEST_CLASS_HASH, - erc_721_token_approval::TEST_CLASS_HASH, - operator_approval::TEST_CLASS_HASH, - base_uri::TEST_CLASS_HASH, - ]; - - // systems - let mut systems = array![ - ERC721Approve::TEST_CLASS_HASH, - ERC721SetApprovalForAll::TEST_CLASS_HASH, - ERC721TransferFrom::TEST_CLASS_HASH, - ERC721Mint::TEST_CLASS_HASH, - ERC721Burn::TEST_CLASS_HASH, - ERC721SetBaseUri::TEST_CLASS_HASH, - ]; - - let world = spawn_test_world(components, systems); - - // Grants writer rights for Component / System - - // erc_721_balance - world.grant_writer('ERC721Balance', 'ERC721TransferFrom'); - world.grant_writer('ERC721Balance', 'ERC721Mint'); - world.grant_writer('ERC721Balance', 'ERC721Burn'); - - // erc_721_owner - world.grant_writer('ERC721Owner', 'ERC721TransferFrom'); - world.grant_writer('ERC721Owner', 'ERC721Mint'); - world.grant_writer('ERC721Owner', 'ERC721Burn'); - - // erc_721_token_approval - world.grant_writer('ERC721TokenApproval', 'ERC721Approve'); - world.grant_writer('ERC721TokenApproval', 'ERC721TransferFrom'); - - // operator_approval - world.grant_writer('OperatorApproval', 'ERC721SetApprovalForAll'); - - // base_uri - world.grant_writer('BaseUri', 'ERC721SetBaseUri'); - - world -} - - -fn deploy_erc721( - world: IWorldDispatcher, - deployer: ContractAddress, - name: felt252, - symbol: felt252, - uri: felt252, - seed: felt252 -) -> ContractAddress { - let constructor_calldata = array![ - world.contract_address.into(), deployer.into(), name, symbol, uri - ]; - let (deployed_address, _) = deploy_syscall( - ERC721::TEST_CLASS_HASH.try_into().unwrap(), seed, constructor_calldata.span(), false - ) - .expect('error deploying ERC721'); - - deployed_address -} - - -fn deploy_default() -> (IWorldDispatcher, IERC721ADispatcher) { - let world = spawn_world(DEPLOYER()); - let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); - let erc721 = IERC721ADispatcher { contract_address: erc721_address }; - - (world, erc721) -} - - -fn deploy_testcase1() -> (IWorldDispatcher, IERC721ADispatcher) { - let world = spawn_world(DEPLOYER()); - let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); - let erc721 = IERC721ADispatcher { contract_address: erc721_address }; - - // user1 owns id : 1,2,3 - erc721.mint(USER1(), 1); - erc721.mint(USER1(), 2); - erc721.mint(USER1(), 3); - - // proxy owns id : 10, 11,12,13 - erc721.mint(PROXY(), 10); - erc721.mint(PROXY(), 11); - erc721.mint(PROXY(), 12); - erc721.mint(PROXY(), 13); - - //user2 owns id : 20 - erc721.mint(USER2(), 20); - - impersonate(USER1()); - //user1 approve_for_all proxy - erc721.set_approval_for_all(PROXY(), true); - - (world, erc721) -} - diff --git a/crates/dojo-erc/src/tests/test_utils.cairo b/crates/dojo-erc/src/tests/test_utils.cairo deleted file mode 100644 index 3f71f16ff3..0000000000 --- a/crates/dojo-erc/src/tests/test_utils.cairo +++ /dev/null @@ -1,11 +0,0 @@ -use starknet::ContractAddress; -use starknet::testing::{set_contract_address, set_account_contract_address}; - -fn impersonate(address: ContractAddress) { - // world.cairo uses account_contract_address : - // - in constructor to define world owner - // - in assert_can_write to check ownership of world & component - - set_account_contract_address(address); - set_contract_address(address); -} diff --git a/crates/dojo-erc/src/tests/utils.cairo b/crates/dojo-erc/src/tests/utils.cairo new file mode 100644 index 0000000000..c42ce68c97 --- /dev/null +++ b/crates/dojo-erc/src/tests/utils.cairo @@ -0,0 +1,38 @@ +// mod dojo_erc::tests::constants; + +use array::ArrayTrait; +use array::SpanTrait; +use core::result::ResultTrait; +use option::OptionTrait; +use starknet::class_hash::Felt252TryIntoClassHash; +use starknet::ContractAddress; +use starknet::testing; +use traits::TryInto; + +fn deploy(contract_class_hash: felt252, calldata: Array) -> ContractAddress { + let (address, _) = starknet::deploy_syscall( + contract_class_hash.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap(); + address +} + +/// Pop the earliest unpopped logged event for the contract as the requested type +/// and checks there's no more data left on the event, preventing unaccounted params. +/// Indexed event members are currently not supported, so they are ignored. +fn pop_log, impl TEvent: starknet::Event>( + address: ContractAddress +) -> Option { + let (mut keys, mut data) = testing::pop_log_raw(address)?; + let ret = starknet::Event::deserialize(ref keys, ref data); + assert(data.is_empty(), 'Event has extra data'); + ret +} + +fn assert_no_events_left(address: ContractAddress) { + assert(testing::pop_log_raw(address).is_none(), 'Events remaining on queue'); +} + +fn drop_event(address: ContractAddress) { + testing::pop_log_raw(address); +} diff --git a/crates/dojo-erc/src/token/erc20.cairo b/crates/dojo-erc/src/token/erc20.cairo new file mode 100644 index 0000000000..3fb468a477 --- /dev/null +++ b/crates/dojo-erc/src/token/erc20.cairo @@ -0,0 +1,331 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IERC20 { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} + +trait IERC20CamelOnly { + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; +} + +#[starknet::contract] +mod ERC20 { + use dojo_erc::token::erc20_components::{ERC20Allowance, ERC20Balance, ERC20Meta}; + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + use integer::BoundedInt; + use super::IERC20; + use super::IERC20CamelOnly; + use starknet::ContractAddress; + use starknet::{get_caller_address, get_contract_address}; + use zeroable::Zeroable; + use debug::PrintTrait; + + + #[storage] + struct Storage { + _world: ContractAddress, + } + + #[event] + #[derive(Copy, Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + } + + #[derive(Copy, Drop, starknet::Event)] + struct Transfer { + from: ContractAddress, + to: ContractAddress, + value: u256 + } + + #[derive(Copy, Drop, starknet::Event)] + struct Approval { + owner: ContractAddress, + spender: ContractAddress, + value: u256 + } + + mod Errors { + const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0'; + const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0'; + const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0'; + const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0'; + const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; + const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + world: ContractAddress, + name: felt252, + symbol: felt252, + initial_supply: u256, + recipient: ContractAddress + ) { + self._world.write(world); + self.initializer(name, symbol); + self._mint(recipient, initial_supply); + } + + // + // External + // + + #[external(v0)] + impl ERC20Impl of IERC20 { + fn name(self: @ContractState) -> felt252 { + self.get_meta().name + } + + fn symbol(self: @ContractState) -> felt252 { + self.get_meta().symbol + } + + fn decimals(self: @ContractState) -> u8 { + 18 + } + + fn total_supply(self: @ContractState) -> u256 { + self.get_meta().total_supply + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.get_balance(account).amount + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.get_allowance(owner, spender).amount + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + true + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); + true + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let owner = get_caller_address(); + self + .set_allowance( + ERC20Allowance { token: get_contract_address(), owner, spender, amount } + ); + true + } + } + + #[external(v0)] + impl ERC20CamelOnlyImpl of IERC20CamelOnly { + fn totalSupply(self: @ContractState) -> u256 { + ERC20Impl::total_supply(self) + } + + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + ERC20Impl::balance_of(self, account) + } + + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + ERC20Impl::transfer_from(ref self, sender, recipient, amount) + } + } + + #[external(v0)] + fn increase_allowance( + ref self: ContractState, spender: ContractAddress, added_value: u256 + ) -> bool { + self.update_allowance(get_caller_address(), spender, 0, added_value); + true + } + + #[external(v0)] + fn increaseAllowance( + ref self: ContractState, spender: ContractAddress, addedValue: u256 + ) -> bool { + increase_allowance(ref self, spender, addedValue) + } + + #[external(v0)] + fn decrease_allowance( + ref self: ContractState, spender: ContractAddress, subtracted_value: u256 + ) -> bool { + self.update_allowance(get_caller_address(), spender, subtracted_value, 0); + true + } + + #[external(v0)] + fn decreaseAllowance( + ref self: ContractState, spender: ContractAddress, subtractedValue: u256 + ) -> bool { + decrease_allowance(ref self, spender, subtractedValue) + } + + // + // Internal + // + + #[generate_trait] + impl WorldInteractionsImpl of WorldInteractionsTrait { + fn world(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: self._world.read() } + } + + fn get_meta(self: @ContractState) -> ERC20Meta { + get!(self.world(), get_contract_address(), ERC20Meta) + } + + // Helper function to update total_supply component + fn update_total_supply(ref self: ContractState, subtract: u256, add: u256) { + let mut meta = self.get_meta(); + // adding and subtracting is fewer steps than if + meta.total_supply = meta.total_supply - subtract; + meta.total_supply = meta.total_supply + add; + set!(self.world(), (meta)); + } + + // Helper function for balance component + fn get_balance(self: @ContractState, account: ContractAddress) -> ERC20Balance { + get!(self.world(), (get_contract_address(), account), ERC20Balance) + } + + fn update_balance( + ref self: ContractState, account: ContractAddress, subtract: u256, add: u256 + ) { + let mut balance: ERC20Balance = self.get_balance(account); + // adding and subtracting is fewer steps than if + balance.amount = balance.amount - subtract; + balance.amount = balance.amount + add; + set!(self.world(), (balance)); + } + + // Helper function for allowance component + fn get_allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> ERC20Allowance { + get!(self.world(), (get_contract_address(), owner, spender), ERC20Allowance) + } + + fn update_allowance( + ref self: ContractState, + owner: ContractAddress, + spender: ContractAddress, + subtract: u256, + add: u256 + ) { + let mut allowance = self.get_allowance(owner, spender); + // adding and subtracting is fewer steps than if + allowance.amount = allowance.amount - subtract; + allowance.amount = allowance.amount + add; + self.set_allowance(allowance); + } + + fn set_allowance(ref self: ContractState, allowance: ERC20Allowance) { + assert(!allowance.owner.is_zero(), Errors::APPROVE_FROM_ZERO); + assert(!allowance.spender.is_zero(), Errors::APPROVE_TO_ZERO); + set!(self.world(), (allowance)); + self + .emit_event( + Approval { + owner: allowance.owner, spender: allowance.spender, value: allowance.amount + } + ); + } + + fn emit_event< + S, impl IntoImp: traits::Into, impl SDrop: Drop, impl SCopy: Copy + >( + ref self: ContractState, event: S + ) { + self.emit(event); + emit!(self.world(), event); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn initializer(ref self: ContractState, name: felt252, symbol: felt252) { + let meta = ERC20Meta { token: get_contract_address(), name, symbol, total_supply: 0 }; + set!(self.world(), (meta)); + } + + fn _mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + assert(!recipient.is_zero(), Errors::MINT_TO_ZERO); + self.update_total_supply(0, amount); + self.update_balance(recipient, 0, amount); + self.emit_event(Transfer { from: Zeroable::zero(), to: recipient, value: amount }); + } + + fn _burn(ref self: ContractState, account: ContractAddress, amount: u256) { + assert(!account.is_zero(), Errors::BURN_FROM_ZERO); + self.update_total_supply(amount, 0); + self.update_balance(account, amount, 0); + self.emit_event(Transfer { from: account, to: Zeroable::zero(), value: amount }); + } + + fn _approve( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + self + .set_allowance( + ERC20Allowance { token: get_contract_address(), owner, spender, amount } + ); + } + + fn _transfer( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); + assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO); + self.update_balance(sender, amount, 0); + self.update_balance(recipient, 0, amount); + self.emit_event(Transfer { from: sender, to: recipient, value: amount }); + } + + fn _spend_allowance( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + let current_allowance = self.get_allowance(owner, spender).amount; + if current_allowance != BoundedInt::max() { + self.update_allowance(owner, spender, amount, 0); + } + } + } +} diff --git a/crates/dojo-erc/src/erc20/components.cairo b/crates/dojo-erc/src/token/erc20_components.cairo similarity index 65% rename from crates/dojo-erc/src/erc20/components.cairo rename to crates/dojo-erc/src/token/erc20_components.cairo index 4266db3562..bbde98ef81 100644 --- a/crates/dojo-erc/src/erc20/components.cairo +++ b/crates/dojo-erc/src/token/erc20_components.cairo @@ -1,28 +1,30 @@ use starknet::ContractAddress; #[derive(Component, Copy, Drop, Serde)] -struct Allowance { +struct ERC20Balance { #[key] token: ContractAddress, #[key] - owner: ContractAddress, - #[key] - spender: ContractAddress, - amount: felt252, + account: ContractAddress, + amount: u256, } #[derive(Component, Copy, Drop, Serde)] -struct Balance { +struct ERC20Allowance { #[key] token: ContractAddress, #[key] - sender: ContractAddress, - amount: felt252, + owner: ContractAddress, + #[key] + spender: ContractAddress, + amount: u256, } #[derive(Component, Copy, Drop, Serde)] -struct Supply { +struct ERC20Meta { #[key] token: ContractAddress, - amount: felt252 + name: felt252, + symbol: felt252, + total_supply: u256, } diff --git a/crates/dojo-lang/src/compiler.rs b/crates/dojo-lang/src/compiler.rs index 41116b3d55..d41583355c 100644 --- a/crates/dojo-lang/src/compiler.rs +++ b/crates/dojo-lang/src/compiler.rs @@ -163,8 +163,6 @@ pub fn collect_core_crate_ids(db: &RootDatabase) -> Vec { [ ContractSelector("dojo::executor::executor".to_string()), ContractSelector("dojo::world::world".to_string()), - ContractSelector("dojo::world::library_call".to_string()), - ContractSelector("dojo::world_factory::world_factory".to_string()), ] .iter() .map(|selector| selector.package().into()) diff --git a/crates/dojo-lang/src/manifest.rs b/crates/dojo-lang/src/manifest.rs index 6efcf62b26..17b3cdc55a 100644 --- a/crates/dojo-lang/src/manifest.rs +++ b/crates/dojo-lang/src/manifest.rs @@ -1,20 +1,18 @@ use std::collections::HashMap; -use anyhow::{Context, Result}; +use anyhow::Context; use cairo_lang_defs::ids::{ModuleId, ModuleItemId}; use cairo_lang_filesystem::ids::CrateId; use cairo_lang_semantic::db::SemanticGroup; use cairo_lang_starknet::abi; +use cairo_lang_starknet::plugin::aux_data::StarkNetContractAuxData; use convert_case::{Case, Casing}; -use dojo_world::manifest::{ - Contract, Input, Output, System, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME, -}; -use itertools::Itertools; +use dojo_world::manifest::{Contract, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME}; use serde::Serialize; use smol_str::SmolStr; use starknet::core::types::FieldElement; -use crate::plugin::{DojoAuxData, SystemAuxData}; +use crate::plugin::DojoAuxData; #[derive(Default, Debug, Serialize)] pub(crate) struct Manifest(dojo_world::manifest::Manifest); @@ -26,7 +24,6 @@ impl Manifest { compiled_classes: HashMap)>, ) -> Self { let mut manifest = Manifest(dojo_world::manifest::Manifest::default()); - let (world, world_abi) = compiled_classes.get(WORLD_CONTRACT_NAME).unwrap_or_else(|| { panic!( "{}", @@ -73,14 +70,15 @@ impl Manifest { let Some(aux_data) = &generated_file_info.aux_data else { continue; }; - let Some(aux_data) = aux_data.0.as_any().downcast_ref() else { - continue; - }; - - manifest.find_components(db, aux_data, *module_id, &compiled_classes); - manifest.find_systems(db, aux_data, *module_id, &compiled_classes).unwrap(); + let aux_data = aux_data.0.as_any(); + if let Some(contracts) = aux_data.downcast_ref::() { + manifest.find_contracts(contracts, &compiled_classes); + } else if let Some(dojo_aux_data) = aux_data.downcast_ref() { + manifest.find_components(db, dojo_aux_data, *module_id, &compiled_classes); + } } } + manifest.filter_contracts(); } manifest @@ -118,70 +116,35 @@ impl Manifest { } } - fn find_systems( - &mut self, - db: &dyn SemanticGroup, - aux_data: &DojoAuxData, - module_id: ModuleId, - compiled_classes: &HashMap)>, - ) -> Result<()> { - for SystemAuxData { name, dependencies } in &aux_data.systems { - if let Ok(Some(ModuleItemId::Submodule(submodule_id))) = - db.module_item_by_name(module_id, name.clone()) - { - let defs_db = db.upcast(); - let fns = db.module_free_functions_ids(ModuleId::Submodule(submodule_id)).unwrap(); - for fn_id in fns.iter() { - if fn_id.name(defs_db) != "execute" { - continue; - } - let signature = db.free_function_signature(*fn_id).unwrap(); + // removes contracts with DojoAuxType + fn filter_contracts(&mut self) { + let mut components = HashMap::new(); - let mut inputs = vec![]; - let mut params = signature.params; + for component in &self.0.components { + components.insert(component.class_hash, true); + } - // Last arg is always the `world_address` which is provided by the executor. - params.pop(); - for param in params.into_iter() { - let ty = param.ty.format(db); - // Context is injected by the executor contract. - if ty == "dojo::world::Context" { - continue; - } + for i in (0..self.0.contracts.len()).rev() { + if components.get(&self.0.contracts[i].class_hash).is_some() { + self.0.contracts.remove(i); + } + } + } - inputs.push(Input { name: param.id.name(db.upcast()).into(), ty }); - } + fn find_contracts( + &mut self, + aux_data: &StarkNetContractAuxData, + compiled_classes: &HashMap)>, + ) { + for name in &aux_data.contracts { + if "world" == name.as_str() || "executor" == name.as_str() { + return; + } - let outputs = if signature.return_type.is_unit(db) { - vec![] - } else { - vec![Output { ty: signature.return_type.format(db) }] - }; + let (class_hash, abi) = compiled_classes.get(name).unwrap().clone(); - let (class_hash, class_abi) = compiled_classes - .get(name.as_str()) - .with_context(|| format!("System {name} not found in target.")) - .unwrap(); - - self.0.systems.push(System { - name: name.clone(), - inputs, - outputs, - class_hash: *class_hash, - dependencies: dependencies - .iter() - .sorted_by(|a, b| a.name.cmp(&b.name)) - .cloned() - .collect::>(), - abi: class_abi.clone(), - }); - } - } else { - panic!("System `{name}` was not found."); - } + self.0.contracts.push(Contract { name: name.clone(), address: None, class_hash, abi }); } - - Ok(()) } } diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 39b778347a..46dcffc92c 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -8,7 +8,7 @@ test_manifest_file "world": { "name": "world", "address": null, - "class_hash": "0x3f3bfd922a5e8d46f9182cd5b73d839159f1fc342347323df6edbebbc732449", + "class_hash": "0x370195d9fa250c4844f750f68ca77adf86c7f3293cc1cbac5c65d3404e2b2af", "abi": [ { "type": "impl", @@ -91,34 +91,6 @@ test_manifest_file "outputs": [], "state_mutability": "external" }, - { - "type": "function", - "name": "system", - "inputs": [ - { - "name": "name", - "type": "core::felt252" - } - ], - "outputs": [ - { - "type": "core::starknet::class_hash::ClassHash" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "register_system", - "inputs": [ - { - "name": "class_hash", - "type": "core::starknet::class_hash::ClassHash" - } - ], - "outputs": [], - "state_mutability": "external" - }, { "type": "function", "name": "uuid", @@ -146,26 +118,6 @@ test_manifest_file "outputs": [], "state_mutability": "view" }, - { - "type": "function", - "name": "execute", - "inputs": [ - { - "name": "system", - "type": "core::felt252" - }, - { - "name": "calldata", - "type": "core::array::Array::" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "state_mutability": "external" - }, { "type": "function", "name": "entity", @@ -293,28 +245,6 @@ test_manifest_file "outputs": [], "state_mutability": "external" }, - { - "type": "function", - "name": "origin", - "inputs": [], - "outputs": [ - { - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "caller_system", - "inputs": [], - "outputs": [ - { - "type": "core::felt252" - } - ], - "state_mutability": "view" - }, { "type": "function", "name": "is_owner", @@ -377,7 +307,7 @@ test_manifest_file }, { "name": "system", - "type": "core::felt252" + "type": "core::starknet::contract_address::ContractAddress" } ], "outputs": [ @@ -397,7 +327,7 @@ test_manifest_file }, { "name": "system", - "type": "core::felt252" + "type": "core::starknet::contract_address::ContractAddress" } ], "outputs": [], @@ -413,7 +343,7 @@ test_manifest_file }, { "name": "system", - "type": "core::felt252" + "type": "core::starknet::contract_address::ContractAddress" } ], "outputs": [], @@ -476,8 +406,8 @@ test_manifest_file "kind": "data" }, { - "name": "class_hash", - "type": "core::starknet::class_hash::ClassHash", + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress", "kind": "data" } ] @@ -563,7 +493,7 @@ test_manifest_file "executor": { "name": "executor", "address": null, - "class_hash": "0x24caf320f4df7648b6f150df66c16c62f51bcba009daecfec1f7622007ad04c", + "class_hash": "0x2b35dd4816731188ed1ad16caa73bde76075c9d9cb8cbfa3e447d3ab9b1ab33", "abi": [ { "type": "impl", @@ -584,26 +514,6 @@ test_manifest_file "type": "interface", "name": "dojo::executor::IExecutor", "items": [ - { - "type": "function", - "name": "execute", - "inputs": [ - { - "name": "class_hash", - "type": "core::starknet::class_hash::ClassHash" - }, - { - "name": "calldata", - "type": "core::array::Span::" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "state_mutability": "view" - }, { "type": "function", "name": "call", @@ -638,29 +548,17 @@ test_manifest_file } ] }, - "systems": [ + "systems": [], + "contracts": [ { - "name": "spawn", - "inputs": [ - { - "name": "self", - "type": "@dojo_examples::systems::spawn::ContractState" - } - ], - "outputs": [], - "class_hash": "0x1e214d9da41aa8fc88c0419f153ddc9b5e9395e3306bfdef972f0eeb36dca30", - "dependencies": [], + "name": "player_actions", + "address": null, + "class_hash": "0x3b55d1b5540a961f4b668fc2b8378b488b4c5f99e46542bfa3585ccd1a13f9c", "abi": [ { - "type": "function", - "name": "name", - "inputs": [], - "outputs": [ - { - "type": "core::felt252" - } - ], - "state_mutability": "view" + "type": "impl", + "name": "PlayerActionsImpl", + "interface_name": "dojo_examples::systems::with_decorator::IPlayerActions" }, { "type": "struct", @@ -672,75 +570,6 @@ test_manifest_file } ] }, - { - "type": "struct", - "name": "dojo::world::Context", - "members": [ - { - "name": "world", - "type": "dojo::world::IWorldDispatcher" - }, - { - "name": "origin", - "type": "core::starknet::contract_address::ContractAddress" - }, - { - "name": "system", - "type": "core::felt252" - }, - { - "name": "system_class_hash", - "type": "core::starknet::class_hash::ClassHash" - } - ] - }, - { - "type": "function", - "name": "execute", - "inputs": [ - { - "name": "ctx", - "type": "dojo::world::Context" - } - ], - "outputs": [], - "state_mutability": "view" - }, - { - "type": "event", - "name": "dojo_examples::systems::spawn::Event", - "kind": "enum", - "variants": [] - } - ] - }, - { - "name": "move", - "inputs": [ - { - "name": "self", - "type": "@dojo_examples::systems::move::ContractState" - }, - { - "name": "direction", - "type": "dojo_examples::components::Direction" - } - ], - "outputs": [], - "class_hash": "0x99300d16860db03c0901eeee801fa87f47fdd9744176a6ea7b41990c23e36e", - "dependencies": [], - "abi": [ - { - "type": "function", - "name": "name", - "inputs": [], - "outputs": [ - { - "type": "core::felt252" - } - ], - "state_mutability": "view" - }, { "type": "enum", "name": "dojo_examples::components::Direction", @@ -768,60 +597,57 @@ test_manifest_file ] }, { - "type": "struct", - "name": "dojo::world::IWorldDispatcher", - "members": [ - { - "name": "contract_address", - "type": "core::starknet::contract_address::ContractAddress" - } - ] - }, - { - "type": "struct", - "name": "dojo::world::Context", - "members": [ - { - "name": "world", - "type": "dojo::world::IWorldDispatcher" - }, - { - "name": "origin", - "type": "core::starknet::contract_address::ContractAddress" - }, - { - "name": "system", - "type": "core::felt252" + "type": "interface", + "name": "dojo_examples::systems::with_decorator::IPlayerActions", + "items": [ + { + "type": "function", + "name": "spawn", + "inputs": [ + { + "name": "world", + "type": "dojo::world::IWorldDispatcher" + } + ], + "outputs": [], + "state_mutability": "view" }, { - "name": "system_class_hash", - "type": "core::starknet::class_hash::ClassHash" + "type": "function", + "name": "move", + "inputs": [ + { + "name": "world", + "type": "dojo::world::IWorldDispatcher" + }, + { + "name": "direction", + "type": "dojo_examples::components::Direction" + } + ], + "outputs": [], + "state_mutability": "view" } ] }, { "type": "function", - "name": "execute", - "inputs": [ - { - "name": "direction", - "type": "dojo_examples::components::Direction" - }, + "name": "name", + "inputs": [], + "outputs": [ { - "name": "ctx", - "type": "dojo::world::Context" + "type": "core::felt252" } ], - "outputs": [], "state_mutability": "view" }, { "type": "event", - "name": "dojo_examples::systems::move::Moved", + "name": "dojo_examples::systems::with_decorator::player_actions::Moved", "kind": "struct", "members": [ { - "name": "address", + "name": "player", "type": "core::starknet::contract_address::ContractAddress", "kind": "data" }, @@ -834,12 +660,12 @@ test_manifest_file }, { "type": "event", - "name": "dojo_examples::systems::move::Event", + "name": "dojo_examples::systems::with_decorator::player_actions::Event", "kind": "enum", "variants": [ { "name": "Moved", - "type": "dojo_examples::systems::move::Moved", + "type": "dojo_examples::systems::with_decorator::player_actions::Moved", "kind": "nested" } ] @@ -847,53 +673,14 @@ test_manifest_file ] }, { - "name": "library_call", - "inputs": [ - { - "name": "self", - "type": "@dojo::world::library_call::ContractState" - }, - { - "name": "class_hash", - "type": "core::starknet::class_hash::ClassHash" - }, - { - "name": "entrypoint", - "type": "core::felt252" - }, - { - "name": "calladata", - "type": "core::array::Span::" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "class_hash": "0x3687cba423875df72318e609c4a5584b222862e01f624e80011c67b18661ae1", - "dependencies": [], + "name": "player_actions_external", + "address": null, + "class_hash": "0x713256dd40b9cb5ad2dedb4e638cb13ec94c7140ac92ebf6ec6feb8c088af79", "abi": [ { - "type": "function", - "name": "name", - "inputs": [], - "outputs": [ - { - "type": "core::felt252" - } - ], - "state_mutability": "view" - }, - { - "type": "struct", - "name": "core::array::Span::", - "members": [ - { - "name": "snapshot", - "type": "@core::array::Array::" - } - ] + "type": "impl", + "name": "PlayerActionsImpl", + "interface_name": "dojo_examples::systems::raw_contract::IPlayerActions" }, { "type": "struct", @@ -906,65 +693,97 @@ test_manifest_file ] }, { - "type": "struct", - "name": "dojo::world::Context", - "members": [ + "type": "enum", + "name": "dojo_examples::components::Direction", + "variants": [ { - "name": "world", - "type": "dojo::world::IWorldDispatcher" + "name": "None", + "type": "()" }, { - "name": "origin", - "type": "core::starknet::contract_address::ContractAddress" + "name": "Left", + "type": "()" }, { - "name": "system", - "type": "core::felt252" + "name": "Right", + "type": "()" + }, + { + "name": "Up", + "type": "()" }, { - "name": "system_class_hash", - "type": "core::starknet::class_hash::ClassHash" + "name": "Down", + "type": "()" } ] }, { - "type": "function", - "name": "execute", - "inputs": [ - { - "name": "class_hash", - "type": "core::starknet::class_hash::ClassHash" + "type": "interface", + "name": "dojo_examples::systems::raw_contract::IPlayerActions", + "items": [ + { + "type": "function", + "name": "spawn", + "inputs": [ + { + "name": "world", + "type": "dojo::world::IWorldDispatcher" + } + ], + "outputs": [], + "state_mutability": "view" }, { - "name": "entrypoint", - "type": "core::felt252" - }, + "type": "function", + "name": "move", + "inputs": [ + { + "name": "world", + "type": "dojo::world::IWorldDispatcher" + }, + { + "name": "direction", + "type": "dojo_examples::components::Direction" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "event", + "name": "dojo_examples::systems::raw_contract::player_actions_external::Moved", + "kind": "struct", + "members": [ { - "name": "calladata", - "type": "core::array::Span::" + "name": "player", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" }, { - "name": "_ctx", - "type": "dojo::world::Context" - } - ], - "outputs": [ - { - "type": "core::array::Span::" + "name": "direction", + "type": "dojo_examples::components::Direction", + "kind": "data" } - ], - "state_mutability": "view" + ] }, { "type": "event", - "name": "dojo::world::library_call::Event", + "name": "dojo_examples::systems::raw_contract::player_actions_external::Event", "kind": "enum", - "variants": [] + "variants": [ + { + "name": "Moved", + "type": "dojo_examples::systems::raw_contract::player_actions_external::Moved", + "kind": "nested" + } + ] } ] } ], - "contracts": [], "components": [ { "name": "Moves", diff --git a/crates/dojo-lang/src/plugin_test_data/system b/crates/dojo-lang/src/plugin_test_data/system index 18cbfa3378..09c2d21c93 100644 --- a/crates/dojo-lang/src/plugin_test_data/system +++ b/crates/dojo-lang/src/plugin_test_data/system @@ -50,7 +50,7 @@ mod spawn { use dojo::world::Context; #[external(v0)] - fn execute(self: @ContractState, name: felt252, ctx: Context) { + fn execute(self: @ContractState, ctx: Context, name: felt252) { return (); } } @@ -72,7 +72,7 @@ mod proxy { #[external(v0)] - fn execute(self: @ContractState, value: felt252, _ctx: dojo::world::Context) -> felt252 { + fn execute(self: @ContractState, value: felt252) -> felt252 { value } } @@ -96,7 +96,7 @@ mod ctxnamed { use dojo::world::Context; #[external(v0)] - fn execute(self: @ContractState, name: felt252, ctx2: Context) { + fn execute(self: @ContractState, ctx2: Context, name: felt252) { return (); } } diff --git a/crates/dojo-lang/src/system.rs b/crates/dojo-lang/src/system.rs index 1cb7a2a54b..cabb5f7516 100644 --- a/crates/dojo-lang/src/system.rs +++ b/crates/dojo-lang/src/system.rs @@ -4,8 +4,9 @@ use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; use cairo_lang_defs::plugin::{ DynGeneratedFileAuxData, PluginDiagnostic, PluginGeneratedFile, PluginResult, }; +// use cairo_lang_syntax::node::ast::{MaybeModuleBody, Param}; +use cairo_lang_syntax::node::ast::MaybeModuleBody; use cairo_lang_syntax::node::ast::OptionReturnTypeClause::ReturnTypeClause; -use cairo_lang_syntax::node::ast::{MaybeModuleBody, Param}; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; @@ -96,26 +97,25 @@ impl System { let signature = function_ast.declaration(db).signature(db); let parameters = signature.parameters(db); - let mut elements = parameters.elements(db); - - let mut context = "_ctx: dojo::world::Context".to_string(); - if let Some(first) = elements.first() { - // If context is first, move it to last. - if is_context(db, first) { - let ctx = elements.remove(0); - context = ctx.as_syntax_node().get_text(db); - } - } else if let Some(param) = elements.iter().find(|p| is_context(db, p)) { - // Context not the first element, but exists. - self.diagnostics.push(PluginDiagnostic { - message: "Context must be first parameter when provided".into(), - stable_ptr: param.stable_ptr().untyped(), - }); - } - - let mut params = - elements.iter().map(|e| e.as_syntax_node().get_text(db)).collect::>(); - params.push(context); + let elements = parameters.elements(db); + + // let mut context = "_ctx: dojo::world::Context".to_string(); + // if let Some(first) = elements.first() { + // // If context is first, move it to last. + // if is_context(db, first) { + // let ctx = elements.remove(0); + // context = ctx.as_syntax_node().get_text(db); + // } + // } else if let Some(param) = elements.iter().find(|p| is_context(db, p)) { + // // Context not the first element, but exists. + // self.diagnostics.push(PluginDiagnostic { + // message: "Context must be first parameter when provided".into(), + // stable_ptr: param.stable_ptr().untyped(), + // }); + // } + + let params = elements.iter().map(|e| e.as_syntax_node().get_text(db)).collect::>(); + // params.push(context); let params = params.join(", "); let ret_clause = if let ReturnTypeClause(clause) = signature.ret_ty(db) { @@ -143,6 +143,6 @@ impl System { } } -fn is_context(db: &dyn SyntaxGroup, param: &Param) -> bool { - param.type_clause(db).ty(db).as_syntax_node().get_text(db) == "Context" -} +// fn is_context(db: &dyn SyntaxGroup, param: &Param) -> bool { +// param.type_clause(db).ty(db).as_syntax_node().get_text(db) == "Context" +// } diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index 6b8275b144..f6268548c2 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -10,8 +10,7 @@ use starknet::accounts::{Account, AccountError, Call, ConnectedAccount, SingleOw use starknet::core::types::contract::{CompiledClass, SierraClass}; use starknet::core::types::{ BlockId, BlockTag, DeclareTransactionResult, FieldElement, FlattenedSierraClass, - InvokeTransactionResult, MaybePendingTransactionReceipt, StarknetError, - TransactionFinalityStatus, + InvokeTransactionResult, StarknetError, }; use starknet::core::utils::{get_contract_address, CairoShortStringToFeltError}; use starknet::macros::{felt, selector}; @@ -21,7 +20,7 @@ use starknet::providers::{ use starknet::signers::Signer; use thiserror::Error; -use crate::utils::{block_number_from_receipt, TransactionWaiter, TransactionWaitingError}; +use crate::utils::{TransactionWaiter, TransactionWaitingError}; pub mod class; pub mod contract; @@ -35,7 +34,6 @@ pub struct DeployOutput { pub transaction_hash: FieldElement, pub contract_address: FieldElement, pub declare: Option, - pub block_number: u64, } #[derive(Debug)] @@ -51,7 +49,7 @@ pub enum MigrationError { #[error("Class already declared.")] ClassAlreadyDeclared, #[error("Contract already deployed.")] - ContractAlreadyDeployed, + ContractAlreadyDeployed(FieldElement), #[error(transparent)] Migrator(#[from] AccountError), #[error(transparent)] @@ -189,7 +187,7 @@ pub trait Deployable: Declarable + Sync { .. })) => {} - Ok(_) => return Err(MigrationError::ContractAlreadyDeployed), + Ok(_) => return Err(MigrationError::ContractAlreadyDeployed(contract_address)), Err(e) => return Err(MigrationError::Provider(e)), } @@ -207,19 +205,9 @@ pub trait Deployable: Declarable + Sync { let InvokeTransactionResult { transaction_hash } = txn.send().await.map_err(MigrationError::Migrator)?; - // TODO: remove finality check once we can remove displaying the block number in the - // migration logs - let receipt = TransactionWaiter::new(transaction_hash, account.provider()) - .with_finality(TransactionFinalityStatus::AcceptedOnL2) - .await - .map_err(MigrationError::WaitingError)?; - - let block_number = match receipt { - MaybePendingTransactionReceipt::Receipt(receipt) => block_number_from_receipt(&receipt), - _ => panic!("Transaction was not accepted on L2"), - }; + TransactionWaiter::new(transaction_hash, account.provider()).await?; - Ok(DeployOutput { transaction_hash, contract_address, declare, block_number }) + Ok(DeployOutput { transaction_hash, contract_address, declare }) } fn salt(&self) -> FieldElement; diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index eb68067589..1c09bac90f 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -17,7 +17,7 @@ use super::{DeployOutput, MigrationType, RegisterOutput}; pub struct MigrationOutput { pub world: Option, pub executor: Option, - pub systems: Option, + pub contracts: Vec, pub components: Option, } @@ -26,7 +26,7 @@ pub struct MigrationStrategy { pub world_address: Option, pub world: Option, pub executor: Option, - pub systems: Vec, + pub contracts: Vec, pub components: Vec, } @@ -62,7 +62,7 @@ impl MigrationStrategy { } } - self.systems.iter().for_each(|item| match item.migration_type() { + self.contracts.iter().for_each(|item| match item.migration_type() { MigrationType::New => new += 1, MigrationType::Update => update += 1, }); @@ -111,9 +111,10 @@ where let mut world = evaluate_contract_to_migrate(&diff.world, &artifact_paths, false)?; let mut executor = evaluate_contract_to_migrate(&diff.executor, &artifact_paths, world.is_some())?; + let contracts = + evaluate_contracts_to_migrate(&diff.contracts, &artifact_paths, world.is_some())?; let components = evaluate_components_to_migrate(&diff.components, &artifact_paths, world.is_some())?; - let systems = evaluate_systems_to_migrate(&diff.systems, &artifact_paths, world.is_some())?; if let Some(executor) = &mut executor { executor.contract_address = @@ -134,45 +135,49 @@ where ); } - Ok(MigrationStrategy { world_address, world, executor, systems, components }) + Ok(MigrationStrategy { world_address, world, executor, contracts, components }) } -fn evaluate_systems_to_migrate( - systems: &[ClassDiff], +fn evaluate_components_to_migrate( + components: &[ClassDiff], artifact_paths: &HashMap, world_contract_will_migrate: bool, ) -> Result> { - let mut syst_to_migrate = vec![]; + let mut comps_to_migrate = vec![]; - for s in systems { - match s.remote { - Some(remote) if remote == s.local && !world_contract_will_migrate => continue, + for c in components { + match c.remote { + Some(remote) if remote == c.local && !world_contract_will_migrate => continue, _ => { - let path = find_artifact_path(&s.name, artifact_paths)?; - syst_to_migrate - .push(ClassMigration { diff: s.clone(), artifact_path: path.clone() }); + let path = + find_artifact_path(c.name.to_case(Case::Snake).as_str(), artifact_paths)?; + comps_to_migrate + .push(ClassMigration { diff: c.clone(), artifact_path: path.clone() }); } } } - Ok(syst_to_migrate) + Ok(comps_to_migrate) } -fn evaluate_components_to_migrate( - components: &[ClassDiff], +fn evaluate_contracts_to_migrate( + contracts: &[ContractDiff], artifact_paths: &HashMap, world_contract_will_migrate: bool, -) -> Result> { +) -> Result> { let mut comps_to_migrate = vec![]; - for c in components { + for c in contracts { match c.remote { Some(remote) if remote == c.local && !world_contract_will_migrate => continue, _ => { let path = find_artifact_path(c.name.to_case(Case::Snake).as_str(), artifact_paths)?; - comps_to_migrate - .push(ClassMigration { diff: c.clone(), artifact_path: path.clone() }); + comps_to_migrate.push(ContractMigration { + diff: c.clone(), + artifact_path: path.clone(), + ..Default::default() + }); } } } diff --git a/crates/dojo-world/src/migration/world.rs b/crates/dojo-world/src/migration/world.rs index 4ee8e55d62..c06d082e65 100644 --- a/crates/dojo-world/src/migration/world.rs +++ b/crates/dojo-world/src/migration/world.rs @@ -14,7 +14,7 @@ mod tests; pub struct WorldDiff { pub world: ContractDiff, pub executor: ContractDiff, - pub contracts: Vec, + pub contracts: Vec, pub components: Vec, pub systems: Vec, } @@ -52,7 +52,7 @@ impl WorldDiff { let contracts = local .contracts .iter() - .map(|contract| ClassDiff { + .map(|contract| ContractDiff { name: contract.name.to_string(), local: contract.class_hash, remote: None, diff --git a/crates/sozo/src/commands/register.rs b/crates/sozo/src/commands/register.rs index 7ebb45dbc4..0e04d94a18 100644 --- a/crates/sozo/src/commands/register.rs +++ b/crates/sozo/src/commands/register.rs @@ -34,24 +34,6 @@ pub enum RegisterCommand { #[command(flatten)] account: AccountOptions, }, - - #[command(about = "Register a system to a world.")] - System { - #[arg(num_args = 1..)] - #[arg(required = true)] - #[arg(value_name = "CLASS_HASH")] - #[arg(help = "The class hash of the systems to register.")] - systems: Vec, - - #[command(flatten)] - world: WorldOptions, - - #[command(flatten)] - starknet: StarknetOptions, - - #[command(flatten)] - account: AccountOptions, - }, } impl RegisterArgs { diff --git a/crates/sozo/src/ops/migration/mod.rs b/crates/sozo/src/ops/migration/mod.rs index 56a18c0437..87486cabd0 100644 --- a/crates/sozo/src/ops/migration/mod.rs +++ b/crates/sozo/src/ops/migration/mod.rs @@ -3,9 +3,12 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; use dojo_world::manifest::{Manifest, ManifestError}; use dojo_world::metadata::Environment; +use dojo_world::migration::contract::ContractMigration; use dojo_world::migration::strategy::{prepare_for_migration, MigrationStrategy}; use dojo_world::migration::world::WorldDiff; -use dojo_world::migration::{Declarable, Deployable, MigrationError, RegisterOutput, StateDiff}; +use dojo_world::migration::{ + Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, +}; use dojo_world::utils::TransactionWaiter; use scarb::core::Config; use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; @@ -246,38 +249,10 @@ where P: Provider + Sync + Send + 'static, S: Signer + Sync + Send + 'static, { - let mut block_height = None; match &strategy.executor { Some(executor) => { ws_config.ui().print_header("# Executor"); - - match executor - .deploy( - executor.diff.local, - vec![], - migrator, - txn_config.clone().map(|c| c.into()).unwrap_or_default(), - ) - .await - { - Ok(val) => { - if let Some(declare) = val.clone().declare { - ws_config.ui().print_hidden_sub(format!( - "Declare transaction: {:#x}", - declare.transaction_hash - )); - } - - ws_config.ui().print_hidden_sub(format!( - "Deploy transaction: {:#x}", - val.transaction_hash - )); - - Ok(()) - } - Err(MigrationError::ContractAlreadyDeployed) => Ok(()), - Err(e) => Err(anyhow!("Failed to migrate executor: {:?}", e)), - }?; + deploy_contract(executor, "executor", vec![], migrator, ws_config, &txn_config).await?; if strategy.world.is_none() { let addr = strategy.world_address()?; @@ -286,13 +261,7 @@ where .set_executor(executor.contract_address) .await?; - let _ = - TransactionWaiter::new(transaction_hash, migrator.provider()).await.map_err( - MigrationError::< - as Account>::SignError, -

::Error, - >::WaitingError, - )?; + TransactionWaiter::new(transaction_hash, migrator.provider()).await?; ws_config.ui().print_hidden_sub(format!("Updated at: {transaction_hash:#x}")); } @@ -305,39 +274,8 @@ where match &strategy.world { Some(world) => { ws_config.ui().print_header("# World"); - - match world - .deploy( - world.diff.local, - vec![strategy.executor.as_ref().unwrap().contract_address], - migrator, - txn_config.clone().map(|c| c.into()).unwrap_or_default(), - ) - .await - { - Ok(val) => { - if let Some(declare) = val.clone().declare { - ws_config.ui().print_hidden_sub(format!( - "Declare transaction: {:#x}", - declare.transaction_hash - )); - } - - ws_config.ui().print_hidden_sub(format!( - "Deploy transaction: {:#x}", - val.transaction_hash - )); - - block_height = Some(val.block_number); - - Ok(()) - } - Err(MigrationError::ContractAlreadyDeployed) => { - ws_config.ui().print_sub("World already exists, updating..."); - Ok(()) - } - Err(e) => Err(anyhow!("Failed to migrate world: {:?}", e)), - }?; + let calldata = vec![strategy.executor.as_ref().unwrap().contract_address]; + deploy_contract(world, "world", calldata, migrator, ws_config, &txn_config).await?; ws_config.ui().print_sub(format!("Contract address: {:#x}", world.contract_address)); } @@ -345,9 +283,59 @@ where }; register_components(strategy, migrator, ws_config, txn_config.clone()).await?; - register_systems(strategy, migrator, ws_config, txn_config).await?; + deploy_contracts(strategy, migrator, ws_config, txn_config).await?; + + // This gets current block numder if helpful + // let block_height = migrator.provider().block_number().await.ok(); - Ok(block_height) + Ok(None) +} + +enum ContractDeploymentOutput { + AlreadyDeployed(FieldElement), + Output(DeployOutput), +} + +async fn deploy_contract( + contract: &ContractMigration, + contract_id: &str, + constructor_calldata: Vec, + migrator: &SingleOwnerAccount, + ws_config: &Config, + txn_config: &Option, +) -> Result +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + match contract + .deploy( + contract.diff.local, + constructor_calldata, + migrator, + txn_config.clone().map(|c| c.into()).unwrap_or_default(), + ) + .await + { + Ok(val) => { + if let Some(declare) = val.clone().declare { + ws_config.ui().print_hidden_sub(format!( + "Declare transaction: {:#x}", + declare.transaction_hash + )); + } + + ws_config + .ui() + .print_hidden_sub(format!("Deploy transaction: {:#x}", val.transaction_hash)); + + Ok(ContractDeploymentOutput::Output(val)) + } + Err(MigrationError::ContractAlreadyDeployed(contract_address)) => { + Ok(ContractDeploymentOutput::AlreadyDeployed(contract_address)) + } + Err(e) => Err(anyhow!("Failed to migrate {}: {:?}", contract_id, e)), + } } async fn register_components( @@ -377,10 +365,9 @@ where c.declare(migrator, txn_config.clone().map(|c| c.into()).unwrap_or_default()).await; match res { Ok(output) => { - ws_config.ui().print_hidden_sub(format!( - "declare transaction: {:#x}", - output.transaction_hash - )); + ws_config + .ui() + .print_hidden_sub(format!("transaction_hash: {:#x}", output.transaction_hash)); declare_output.push(output); } @@ -393,7 +380,7 @@ where Err(e) => bail!("Failed to declare component {}: {e}", c.diff.name), } - ws_config.ui().print_sub(format!("class hash: {:#x}", c.diff.local)); + ws_config.ui().print_sub(format!("Class hash: {:#x}", c.diff.local)); } let world_address = strategy.world_address()?; @@ -403,79 +390,53 @@ where .await .map_err(|e| anyhow!("Failed to register components to World: {e}"))?; - TransactionWaiter::new(transaction_hash, migrator.provider()).await.map_err( - MigrationError::< - as Account>::SignError, -

::Error, - >::WaitingError, - )?; + TransactionWaiter::new(transaction_hash, migrator.provider()).await?; ws_config.ui().print_hidden_sub(format!("registered at: {transaction_hash:#x}")); Ok(Some(RegisterOutput { transaction_hash, declare_output })) } -async fn register_systems( +async fn deploy_contracts( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, ws_config: &Config, txn_config: Option, -) -> Result> +) -> Result>> where P: Provider + Sync + Send + 'static, S: Signer + Sync + Send + 'static, { - let systems = &strategy.systems; + let contracts = &strategy.contracts; - if systems.is_empty() { - return Ok(None); + if contracts.is_empty() { + return Ok(vec![]); } - ws_config.ui().print_header(format!("# Systems ({})", systems.len())); - - let mut declare_output = vec![]; + ws_config.ui().print_header(format!("# Contracts ({})", contracts.len())); - for s in strategy.systems.iter() { - ws_config.ui().print(italic_message(&s.diff.name).to_string()); + let mut deploy_output = vec![]; - let res = - s.declare(migrator, txn_config.clone().map(|c| c.into()).unwrap_or_default()).await; - match res { - Ok(output) => { + for contract in strategy.contracts.iter() { + let name = &contract.diff.name; + ws_config.ui().print(italic_message(name).to_string()); + match deploy_contract(contract, name, vec![], migrator, ws_config, &txn_config).await? { + ContractDeploymentOutput::Output(output) => { + ws_config + .ui() + .print_sub(format!("Contract address: {:#x}", output.contract_address)); ws_config.ui().print_hidden_sub(format!( - "Declare transaction: {:#x}", + "deploy transaction: {:#x}", output.transaction_hash )); - - declare_output.push(output); + deploy_output.push(Some(output)); } - - // Continue if system is already declared - Err(MigrationError::ClassAlreadyDeclared) => { - ws_config.ui().print_sub("Already declared"); - continue; + ContractDeploymentOutput::AlreadyDeployed(contract_address) => { + ws_config.ui().print_sub(format!("Already deployed: {:#x}", contract_address)); + deploy_output.push(None); } - Err(e) => bail!("Failed to declare system {}: {e}", s.diff.name), } - - ws_config.ui().print_sub(format!("class hash: {:#x}", s.diff.local)); } - let world_address = strategy.world_address()?; - - let InvokeTransactionResult { transaction_hash } = WorldContract::new(world_address, migrator) - .register_systems(&systems.iter().map(|s| s.diff.local).collect::>()) - .await - .map_err(|e| anyhow!("Failed to register systems to World: {e}"))?; - - TransactionWaiter::new(transaction_hash, migrator.provider()).await.map_err( - MigrationError::< - as Account>::SignError, -

::Error, - >::WaitingError, - )?; - - ws_config.ui().print_hidden_sub(format!("registered at: {transaction_hash:#x}")); - - Ok(Some(RegisterOutput { transaction_hash, declare_output })) + Ok(deploy_output) } diff --git a/crates/sozo/src/ops/register.rs b/crates/sozo/src/ops/register.rs index d5082989ff..9325f0dbab 100644 --- a/crates/sozo/src/ops/register.rs +++ b/crates/sozo/src/ops/register.rs @@ -20,21 +20,6 @@ pub async fn execute(command: RegisterCommand, env_metadata: Option println!("Components registered at transaction: {:#x}", res.transaction_hash) } - - RegisterCommand::System { systems, world, starknet, account } => { - let world_address = world.address(env_metadata.as_ref())?; - let provider = starknet.provider(env_metadata.as_ref())?; - - let account = account.account(provider, env_metadata.as_ref()).await?; - let world = WorldContract::new(world_address, &account); - - let res = world - .register_systems(&systems) - .await - .with_context(|| "Failed to send transaction")?; - - println!("Systems registered at transaction: {:#x}", res.transaction_hash) - } } Ok(()) } diff --git a/crates/torii/client/src/contract/component.rs b/crates/torii/client/src/contract/component.rs index 8f9cdec733..c132ed4608 100644 --- a/crates/torii/client/src/contract/component.rs +++ b/crates/torii/client/src/contract/component.rs @@ -74,15 +74,11 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { let res = self .world - .call( - "library_call", - vec![FieldElement::THREE, self.class_hash, entrypoint, FieldElement::ZERO], - block_id, - ) + .executor_call(self.class_hash, vec![entrypoint, FieldElement::ZERO], block_id) .await .map_err(ComponentError::ContractReaderError)?; - parse_ty::

(&res[2..]) + parse_ty::

(&res[1..]) } pub async fn size(&self, block_id: BlockId) -> Result> { @@ -90,11 +86,7 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { let res = self .world - .call( - "library_call", - vec![FieldElement::THREE, self.class_hash, entrypoint, FieldElement::ZERO], - block_id, - ) + .executor_call(self.class_hash, vec![entrypoint, FieldElement::ZERO], block_id) .await .map_err(ComponentError::ContractReaderError)?; @@ -109,11 +101,7 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { let res = self .world - .call( - "library_call", - vec![FieldElement::THREE, self.class_hash, entrypoint, FieldElement::ZERO], - block_id, - ) + .executor_call(self.class_hash, vec![entrypoint, FieldElement::ZERO], block_id) .await .map_err(ComponentError::ContractReaderError)?; @@ -200,6 +188,7 @@ pub fn unpack( } fn parse_ty(data: &[FieldElement]) -> Result> { + println!("{:#?}", data); let member_type: u8 = data[0].try_into().unwrap(); match member_type { 0 => parse_simple::

(&data[1..]), diff --git a/crates/torii/client/src/contract/system_test.rs b/crates/torii/client/src/contract/system_test.rs index 0251f5e43f..93ec05a141 100644 --- a/crates/torii/client/src/contract/system_test.rs +++ b/crates/torii/client/src/contract/system_test.rs @@ -1,58 +1,58 @@ -use std::time::Duration; +// use std::time::Duration; -use camino::Utf8PathBuf; -use dojo_test_utils::sequencer::{ - get_default_test_starknet_config, SequencerConfig, TestSequencer, -}; -use starknet::accounts::Account; -use starknet::core::types::{BlockId, BlockTag}; -use starknet_crypto::FieldElement; +// use camino::Utf8PathBuf; +// use dojo_test_utils::sequencer::{ +// get_default_test_starknet_config, SequencerConfig, TestSequencer, +// }; +// use starknet::accounts::Account; +// use starknet::core::types::{BlockId, BlockTag}; +// use starknet_crypto::FieldElement; -use crate::contract::world::test::deploy_world; -use crate::contract::world::WorldContract; +// use crate::contract::world::test::deploy_world; +// use crate::contract::world::WorldContract; -#[tokio::test(flavor = "multi_thread")] -async fn test_system() { - let sequencer = - TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; - let account = sequencer.account(); - let (world_address, _) = deploy_world( - &sequencer, - Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev".into()).unwrap(), - ) - .await; +// #[tokio::test(flavor = "multi_thread")] +// async fn test_system() { +// let sequencer = +// TestSequencer::start(SequencerConfig::default(), +// get_default_test_starknet_config()).await; let account = sequencer.account(); +// let (world_address, _) = deploy_world( +// &sequencer, +// Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev".into()).unwrap(), +// ) +// .await; - let block_id: BlockId = BlockId::Tag(BlockTag::Latest); - let world = WorldContract::new(world_address, &account); - let spawn = world.system("spawn", block_id).await.unwrap(); +// let block_id: BlockId = BlockId::Tag(BlockTag::Latest); +// let world = WorldContract::new(world_address, &account); +// let spawn = world.system("spawn", block_id).await.unwrap(); - let _ = spawn.execute(vec![]).await.unwrap(); +// let _ = spawn.execute(vec![]).await.unwrap(); - // wait for the tx to be mined - tokio::time::sleep(Duration::from_millis(250)).await; +// // wait for the tx to be mined +// tokio::time::sleep(Duration::from_millis(250)).await; - let component = world.component("Moves", block_id).await.unwrap(); - let moves = component.entity(vec![account.address()], block_id).await.unwrap(); +// let component = world.component("Moves", block_id).await.unwrap(); +// let moves = component.entity(vec![account.address()], block_id).await.unwrap(); - assert_eq!(moves, vec![10_u8.into(), FieldElement::ZERO]); +// assert_eq!(moves, vec![10_u8.into(), FieldElement::ZERO]); - let move_system = world.system("move", block_id).await.unwrap(); +// let move_system = world.system("move", block_id).await.unwrap(); - let _ = move_system.execute(vec![FieldElement::ONE]).await.unwrap(); - // wait for the tx to be mined - tokio::time::sleep(Duration::from_millis(250)).await; +// let _ = move_system.execute(vec![FieldElement::ONE]).await.unwrap(); +// // wait for the tx to be mined +// tokio::time::sleep(Duration::from_millis(250)).await; - let _ = move_system.execute(vec![FieldElement::THREE]).await.unwrap(); - // wait for the tx to be mined - tokio::time::sleep(Duration::from_millis(250)).await; +// let _ = move_system.execute(vec![FieldElement::THREE]).await.unwrap(); +// // wait for the tx to be mined +// tokio::time::sleep(Duration::from_millis(250)).await; - let moves = component.entity(vec![account.address()], block_id).await.unwrap(); +// let moves = component.entity(vec![account.address()], block_id).await.unwrap(); - assert_eq!(moves, vec![8_u8.into(), FieldElement::THREE]); +// assert_eq!(moves, vec![8_u8.into(), FieldElement::THREE]); - let position_component = world.component("Position", block_id).await.unwrap(); +// let position_component = world.component("Position", block_id).await.unwrap(); - let position = position_component.entity(vec![account.address()], block_id).await.unwrap(); +// let position = position_component.entity(vec![account.address()], block_id).await.unwrap(); - assert_eq!(position, vec![9_u8.into(), 9_u8.into()]); -} +// assert_eq!(position, vec![9_u8.into(), 9_u8.into()]); +// } diff --git a/crates/torii/client/src/contract/world.rs b/crates/torii/client/src/contract/world.rs index d13d945c3b..7c48e3919e 100644 --- a/crates/torii/client/src/contract/world.rs +++ b/crates/torii/client/src/contract/world.rs @@ -100,29 +100,6 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { self.account.execute(calls).send().await } - pub async fn register_systems( - &self, - systems: &[FieldElement], - ) -> Result::Error>> - { - let calls = systems - .iter() - .map(|s| Call { - to: self.address, - // function selector: "register_system" - selector: FieldElement::from_mont([ - 6581716859078500959, - 16871126355047595269, - 14219012428168968926, - 473332093618875024, - ]), - calldata: vec![*s], - }) - .collect::>(); - - self.account.execute(calls).send().await - } - pub async fn execute( &self, name: &str, @@ -271,6 +248,27 @@ impl<'a, P: Provider + Sync> WorldContractReader<'a, P> { Ok(res[0]) } + pub async fn executor_call( + &self, + class_hash: FieldElement, + mut calldata: Vec, + block_id: BlockId, + ) -> Result, ContractReaderError> { + calldata.insert(0, class_hash); + + self.provider + .call( + FunctionCall { + contract_address: self.executor(block_id).await.unwrap(), + calldata, + entry_point_selector: get_selector_from_name("call").unwrap(), + }, + block_id, + ) + .await + .map_err(ContractReaderError::ProviderError) + } + pub async fn call( &self, system: &str, diff --git a/crates/torii/client/src/contract/world_test.rs b/crates/torii/client/src/contract/world_test.rs index 0ec1e5b79c..27ec3c6130 100644 --- a/crates/torii/client/src/contract/world_test.rs +++ b/crates/torii/client/src/contract/world_test.rs @@ -87,23 +87,16 @@ pub async fn deploy_world( // wait for the tx to be mined tokio::time::sleep(Duration::from_millis(250)).await; - let mut declare_output = vec![]; - for system in strategy.systems { - let res = system.declare(&account, Default::default()).await.unwrap(); - declare_output.push(res); + for contract in strategy.contracts { + let declare_res = contract.declare(&account, Default::default()).await.unwrap(); + contract + .deploy(declare_res.class_hash, vec![], &account, Default::default()) + .await + .unwrap(); } // wait for the tx to be mined tokio::time::sleep(Duration::from_millis(250)).await; - let world = WorldContract::new(world_address, &account); - let _ = world - .register_systems(&declare_output.iter().map(|o| o.class_hash).collect::>()) - .await - .unwrap(); - - // wait for the tx to be mined - tokio::time::sleep(Duration::from_millis(250)).await; - (world_address, executor_address) } diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index bbe5d06bb1..b90c871b8f 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -24,9 +24,6 @@ async fn test_load_from_manifest(pool: SqlitePool) { let moves_models = sqlx::query("SELECT * FROM external_moves").fetch_all(&pool).await.unwrap(); assert_eq!(moves_models.len(), 0); - let systems = sqlx::query("SELECT * FROM systems").fetch_all(&pool).await.unwrap(); - assert_eq!(systems.len(), 3); - let mut world = state.world().await.unwrap(); assert_eq!(world.world_address.0, FieldElement::ZERO); diff --git a/examples/ecs/Scarb.toml b/examples/ecs/Scarb.toml index 1f51b8f574..daacfcdc19 100644 --- a/examples/ecs/Scarb.toml +++ b/examples/ecs/Scarb.toml @@ -8,20 +8,20 @@ sierra-replace-ids = true [dependencies] dojo = { path = "../../crates/dojo-core" } -dojo_erc = { path = "../../crates/dojo-erc" } +# dojo_erc = { path = "../../crates/dojo-erc" } [[target.dojo]] build-external-contracts = [ - "dojo_erc::erc721::components::Balance", - "dojo_erc::erc721::components::OperatorApproval", - "dojo_erc::erc721::components::Owner", - "dojo_erc::erc721::components::TokenApproval", - "dojo_erc::erc721::erc721::ERC721", - "dojo_erc::erc721::systems::erc721_approve", - "dojo_erc::erc721::systems::erc721_burn", - "dojo_erc::erc721::systems::erc721_mint", - "dojo_erc::erc721::systems::erc721_set_approval_for_all", - "dojo_erc::erc721::systems::erc721_transfer_from", + # "dojo_erc::erc721::components::Balance", + # "dojo_erc::erc721::components::OperatorApproval", + # "dojo_erc::erc721::components::Owner", + # "dojo_erc::erc721::components::TokenApproval", + # "dojo_erc::erc721::erc721::ERC721", + # "dojo_erc::erc721::systems::erc721_approve", + # "dojo_erc::erc721::systems::erc721_burn", + # "dojo_erc::erc721::systems::erc721_mint", + # "dojo_erc::erc721::systems::erc721_set_approval_for_all", + # "dojo_erc::erc721::systems::erc721_transfer_from", ] [tool.dojo] diff --git a/examples/ecs/src/components.cairo b/examples/ecs/src/components.cairo index 6e9ea3d010..bf62895b60 100644 --- a/examples/ecs/src/components.cairo +++ b/examples/ecs/src/components.cairo @@ -1,7 +1,9 @@ use array::ArrayTrait; use core::debug::PrintTrait; use starknet::ContractAddress; -use dojo::database::schema::{EnumMember, Member, Ty, Struct, SchemaIntrospection, serialize_member, serialize_member_type}; +use dojo::database::schema::{ + EnumMember, Member, Ty, Struct, SchemaIntrospection, serialize_member, serialize_member_type +}; #[derive(Serde, Copy, Drop)] enum Direction { @@ -25,17 +27,20 @@ impl DirectionSchemaIntrospectionImpl of SchemaIntrospection { #[inline(always)] fn ty() -> Ty { - Ty::Enum(EnumMember{ - name: 'Direction', - attrs: array![].span(), - values: array![ - serialize_member_type(@Ty::Simple('None')), - serialize_member_type(@Ty::Simple('Left')), - serialize_member_type(@Ty::Simple('Right')), - serialize_member_type(@Ty::Simple('Up')), - serialize_member_type(@Ty::Simple('Down')) - ].span() - }) + Ty::Enum( + EnumMember { + name: 'Direction', + attrs: array![].span(), + values: array![ + serialize_member_type(@Ty::Simple('None')), + serialize_member_type(@Ty::Simple('Left')), + serialize_member_type(@Ty::Simple('Right')), + serialize_member_type(@Ty::Simple('Up')), + serialize_member_type(@Ty::Simple('Down')) + ] + .span() + } + ) } } @@ -97,22 +102,25 @@ impl Vec2SchemaIntrospectionImpl of SchemaIntrospection { #[inline(always)] fn ty() -> Ty { - Ty::Struct(Struct { - name: 'Vec2', - attrs: array![].span(), - children: array![ - serialize_member(@Member { - name: 'x', - ty: SchemaIntrospection::::ty(), - attrs: array![].span(), - }), - serialize_member(@Member { - name: 'y', - ty: SchemaIntrospection::::ty(), - attrs: array![].span(), - }) - ].span() - }) + Ty::Struct( + Struct { + name: 'Vec2', + attrs: array![].span(), + children: array![ + serialize_member( + @Member { + name: 'x', ty: SchemaIntrospection::::ty(), attrs: array![].span(), + } + ), + serialize_member( + @Member { + name: 'y', ty: SchemaIntrospection::::ty(), attrs: array![].span(), + } + ) + ] + .span() + } + ) } } @@ -156,7 +164,6 @@ mod tests { #[available_gas(100000)] fn test_vec_is_equal() { let position = Vec2 { x: 420, y: 0 }; - position.print(); assert(position.is_equal(Vec2 { x: 420, y: 0 }), 'not equal'); } } diff --git a/examples/ecs/src/lib.cairo b/examples/ecs/src/lib.cairo index 98d784c920..1a585cb71d 100644 --- a/examples/ecs/src/lib.cairo +++ b/examples/ecs/src/lib.cairo @@ -1,2 +1,11 @@ mod components; -mod systems; + +mod systems { + // example with #[system] decorator + mod with_decorator; + + // raw example with #[starknet::contract] decorator + mod raw_contract; +} + +mod utils; diff --git a/examples/ecs/src/systems.cairo b/examples/ecs/src/systems.cairo deleted file mode 100644 index f1e1e41801..0000000000 --- a/examples/ecs/src/systems.cairo +++ /dev/null @@ -1,140 +0,0 @@ -#[system] -mod spawn { - use array::ArrayTrait; - use box::BoxTrait; - use traits::Into; - use dojo::world::Context; - - use dojo_examples::components::Position; - use dojo_examples::components::Vec2; - use dojo_examples::components::Moves; - use dojo_examples::components::Direction; - - fn execute(ctx: Context) { - let position = get !(ctx.world, ctx.origin, (Position)); - set !( - ctx.world, - ( - Moves { - player: ctx.origin, remaining: 10, last_direction: Direction::None(()) - }, Position { - player: ctx.origin, vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10 } - }, - ) - ); - return (); - } -} - -#[system] -mod move { - use starknet::ContractAddress; - use array::ArrayTrait; - use box::BoxTrait; - use traits::Into; - use dojo::world::Context; - - use dojo_examples::components::Position; - use dojo_examples::components::Moves; - use dojo_examples::components::Direction; - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - Moved: Moved, - } - - #[derive(Drop, starknet::Event)] - struct Moved { - address: ContractAddress, - direction: Direction - } - - fn execute(ctx: Context, direction: Direction) { - let (mut position, mut moves) = get!(ctx.world, ctx.origin, (Position, Moves)); - moves.remaining -= 1; - moves.last_direction = direction; - let next = next_position(position, direction); - set!(ctx.world, (moves, next)); - emit!(ctx.world, Moved { address: ctx.origin, direction }); - return (); - } - - fn next_position(mut position: Position, direction: Direction) -> Position { - match direction { - Direction::None(()) => { - return position; - }, - Direction::Left(()) => { - position.vec.x -= 1; - }, - Direction::Right(()) => { - position.vec.x += 1; - }, - Direction::Up(()) => { - position.vec.y -= 1; - }, - Direction::Down(()) => { - position.vec.y += 1; - }, - }; - - position - } -} - -#[cfg(test)] -mod tests { - use core::traits::Into; - use array::ArrayTrait; - use debug::PrintTrait; - - use dojo::world::IWorldDispatcherTrait; - use dojo::database::schema::SchemaIntrospection; - - use dojo::test_utils::spawn_test_world; - - use dojo_examples::components::position; - use dojo_examples::components::Position; - use dojo_examples::components::moves; - use dojo_examples::components::Moves; - use dojo_examples::systems::spawn; - use dojo_examples::systems::move; - - - #[test] - #[available_gas(30000000)] - fn test_move() { - let caller = starknet::contract_address_const::<0x0>(); - - // components - let mut components = array::ArrayTrait::new(); - components.append(position::TEST_CLASS_HASH); - components.append(moves::TEST_CLASS_HASH); - // components.append(dojo_erc::erc20::components::balance::TEST_CLASS_HASH); - // systems - let mut systems = array::ArrayTrait::new(); - systems.append(spawn::TEST_CLASS_HASH); - systems.append(move::TEST_CLASS_HASH); - - // deploy executor, world and register components/systems - let world = spawn_test_world(components, systems); - - let spawn_call_data = array::ArrayTrait::new(); - world.execute('spawn', spawn_call_data); - - let mut move_calldata = array::ArrayTrait::new(); - move_calldata.append(move::Direction::Right(()).into()); - world.execute('move', move_calldata); - let mut keys = array::ArrayTrait::new(); - keys.append(caller.into()); - - let moves = world.entity('Moves', keys.span(), 0, SchemaIntrospection::::size(), array![8, 8].span()); - assert(*moves[0] == 9, 'updated packed value is wrong'); - assert(*moves[1] == 2, 'updated packed value is wrong'); - let new_position = world - .entity('Position', keys.span(), 0, SchemaIntrospection::::size(), array![32, 32].span()); - assert(*new_position[0] == 11, 'position x is wrong'); - assert(*new_position[1] == 10, 'position y is wrong'); - } -} diff --git a/examples/ecs/src/systems/raw_contract.cairo b/examples/ecs/src/systems/raw_contract.cairo new file mode 100644 index 0000000000..26e751330b --- /dev/null +++ b/examples/ecs/src/systems/raw_contract.cairo @@ -0,0 +1,108 @@ +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo_examples::components::{Direction}; + +// trait: specify functions to implement +#[starknet::interface] +trait IPlayerActions { + fn spawn(self: @TContractState, world: IWorldDispatcher); + fn move(self: @TContractState, world: IWorldDispatcher, direction: Direction); +} + +// exact same functionality as examples/ecs/src/systems/with_decorator.cairo +// requires some additional code without using system decorator +#[starknet::contract] +mod player_actions_external { + use starknet::{ContractAddress, get_caller_address}; + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + use dojo_examples::components::{Position, Moves, Direction, Vec2}; + use dojo_examples::utils::next_position; + use super::IPlayerActions; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Moved: Moved, + } + + #[derive(Drop, starknet::Event)] + struct Moved { + player: ContractAddress, + direction: Direction + } + + // impl: implement functions specified in trait + #[external(v0)] + impl PlayerActionsImpl of IPlayerActions { + fn spawn(self: @ContractState, world: IWorldDispatcher) { + let player = get_caller_address(); + let position = get!(world, player, (Position)); + set!( + world, + ( + Moves { player, remaining: 10, last_direction: Direction::None(()) }, + Position { player, vec: Vec2 { x: 10, y: 10 } }, + ) + ); + } + + fn move(self: @ContractState, world: IWorldDispatcher, direction: Direction) { + let player = get_caller_address(); + let (mut position, mut moves) = get!(world, player, (Position, Moves)); + moves.remaining -= 1; + moves.last_direction = direction; + let next = next_position(position, direction); + set!(world, (moves, next)); + emit!(world, Moved { player, direction }); + return (); + } + } +} + +#[cfg(test)] +mod tests { + use core::traits::Into; + use array::{ArrayTrait}; + + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + use dojo::test_utils::{spawn_test_world, deploy_contract}; + + use dojo_examples::components::{position, moves}; + use dojo_examples::components::{Position, Moves, Direction, Vec2}; + + use super::{ + IPlayerActionsDispatcher, IPlayerActionsDispatcherTrait, + player_actions_external as player_actions + }; + + #[test] + #[available_gas(30000000)] + fn test_move() { + let caller = starknet::contract_address_const::<0x0>(); + + // components + let mut components = array![position::TEST_CLASS_HASH, moves::TEST_CLASS_HASH,]; + // deploy world with components + let world = spawn_test_world(components); + + // deploy systems contract + let contract_address = deploy_contract(player_actions::TEST_CLASS_HASH, array![].span()); + let player_actions_system = IPlayerActionsDispatcher { contract_address }; + + // System calls + player_actions_system.spawn(world); + player_actions_system.move(world, Direction::Right(())); + + let moves = get!(world, caller, Moves); + let right_dir_felt: felt252 = Direction::Right(()).into(); + + assert(moves.remaining == 9, 'moves is wrong'); + assert(moves.last_direction.into() == right_dir_felt, 'last direction is wrong'); + + let new_position = get!(world, caller, Position); + assert(new_position.vec.x == 11, 'position x is wrong'); + assert(new_position.vec.y == 10, 'position y is wrong'); + } +} diff --git a/examples/ecs/src/systems/with_decorator.cairo b/examples/ecs/src/systems/with_decorator.cairo new file mode 100644 index 0000000000..0bad00ab9b --- /dev/null +++ b/examples/ecs/src/systems/with_decorator.cairo @@ -0,0 +1,101 @@ +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo_examples::components::{Position, Moves, Direction}; +use starknet::{ContractAddress, ClassHash}; + +// trait: specify functions to implement +#[starknet::interface] +trait IPlayerActions { + fn spawn(self: @TContractState, world: IWorldDispatcher); + fn move(self: @TContractState, world: IWorldDispatcher, direction: Direction); +} + +#[system] +mod player_actions { + use starknet::{ContractAddress, get_caller_address}; + use dojo_examples::components::{Position, Moves, Direction, Vec2}; + use dojo_examples::utils::next_position; + use super::IPlayerActions; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Moved: Moved, + } + + #[derive(Drop, starknet::Event)] + struct Moved { + player: ContractAddress, + direction: Direction + } + + // impl: implement functions specified in trait + #[external(v0)] + impl PlayerActionsImpl of IPlayerActions { + // ContractState is defined by system decorator expansion + fn spawn(self: @ContractState, world: IWorldDispatcher) { + let player = get_caller_address(); + let position = get!(world, player, (Position)); + set!( + world, + ( + Moves { player, remaining: 10, last_direction: Direction::None(()) }, + Position { player, vec: Vec2 { x: 10, y: 10 } }, + ) + ); + } + + fn move(self: @ContractState, world: IWorldDispatcher, direction: Direction) { + let player = get_caller_address(); + let (mut position, mut moves) = get!(world, player, (Position, Moves)); + moves.remaining -= 1; + moves.last_direction = direction; + let next = next_position(position, direction); + set!(world, (moves, next)); + emit!(world, Moved { player, direction }); + return (); + } + } +} + +#[cfg(test)] +mod tests { + use core::traits::Into; + use array::{ArrayTrait}; + + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + + use dojo::test_utils::{spawn_test_world, deploy_contract}; + + use dojo_examples::components::{position, moves}; + use dojo_examples::components::{Position, Moves, Direction, Vec2}; + use super::{player_actions, IPlayerActionsDispatcher, IPlayerActionsDispatcherTrait}; + + #[test] + #[available_gas(30000000)] + fn test_move() { + let caller = starknet::contract_address_const::<0x0>(); + + // components + let mut components = array![position::TEST_CLASS_HASH, moves::TEST_CLASS_HASH,]; + // deploy world with components + let world = spawn_test_world(components); + + // deploy systems contract + let contract_address = deploy_contract(player_actions::TEST_CLASS_HASH, array![].span()); + let player_actions_system = IPlayerActionsDispatcher { contract_address }; + + // System calls + player_actions_system.spawn(world); + player_actions_system.move(world, Direction::Right(())); + + let moves = get!(world, caller, Moves); + let right_dir_felt: felt252 = Direction::Right(()).into(); + + assert(moves.remaining == 9, 'moves is wrong'); + assert(moves.last_direction.into() == right_dir_felt, 'last direction is wrong'); + + let new_position = get!(world, caller, Position); + assert(new_position.vec.x == 11, 'position x is wrong'); + assert(new_position.vec.y == 10, 'position y is wrong'); + } +} diff --git a/examples/ecs/src/utils.cairo b/examples/ecs/src/utils.cairo new file mode 100644 index 0000000000..586cea42ce --- /dev/null +++ b/examples/ecs/src/utils.cairo @@ -0,0 +1,23 @@ +use dojo_examples::components::{Position, Direction}; + +fn next_position(mut position: Position, direction: Direction) -> Position { + match direction { + Direction::None(()) => { + return position; + }, + Direction::Left(()) => { + position.vec.x -= 1; + }, + Direction::Right(()) => { + position.vec.x += 1; + }, + Direction::Up(()) => { + position.vec.y -= 1; + }, + Direction::Down(()) => { + position.vec.y += 1; + }, + }; + + position +} diff --git a/scripts/cairo_test.sh b/scripts/cairo_test.sh index ea08ab6c25..393bb96c61 100755 --- a/scripts/cairo_test.sh +++ b/scripts/cairo_test.sh @@ -1,8 +1,8 @@ #!/bin/bash set -euxo pipefail -cargo run --bin sozo -- --manifest-path crates/dojo-core/Scarb.toml test +cargo run -r --bin sozo -- --manifest-path crates/dojo-core/Scarb.toml test # cargo run --bin sozo -- test crates/dojo-physics -cargo run --bin sozo -- --manifest-path crates/dojo-erc/Scarb.toml test +cargo run -r --bin sozo -- --manifest-path crates/dojo-erc/Scarb.toml test # cargo run --bin sozo -- test crates/dojo-defi -cargo run --bin sozo -- --manifest-path examples/ecs/Scarb.toml test +cargo run -r --bin sozo -- --manifest-path examples/ecs/Scarb.toml test From 933026be4b3a0edfb3fdf9db7274fa6e44cee08d Mon Sep 17 00:00:00 2001 From: Shramee Srivastav Date: Mon, 25 Sep 2023 01:50:46 +0800 Subject: [PATCH 03/14] lang: keep origin struct (#918) --- crates/dojo-lang/src/component.rs | 8 ------ crates/dojo-lang/src/plugin.rs | 2 +- .../dojo-lang/src/plugin_test_data/component | 13 +++++++--- crates/dojo-lang/src/plugin_test_data/print | 26 +++++++++++++++++++ 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/crates/dojo-lang/src/component.rs b/crates/dojo-lang/src/component.rs index fd1b1099eb..53f82e33cc 100644 --- a/crates/dojo-lang/src/component.rs +++ b/crates/dojo-lang/src/component.rs @@ -108,10 +108,6 @@ pub fn handle_component_struct( ( RewriteNode::interpolate_patched( " - struct $type_name$ { - $members$ - } - impl $type_name$Component of dojo::component::Component<$type_name$> { #[inline(always)] fn name(self: @$type_name$) -> felt252 { @@ -203,10 +199,6 @@ pub fn handle_component_struct( "type_name".to_string(), RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node()), ), - ( - "members".to_string(), - RewriteNode::Copied(struct_ast.members(db).as_syntax_node()), - ), ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), ("layout".to_string(), RewriteNode::new_modified(layout)), diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 0475aff566..414a0eab00 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -185,7 +185,7 @@ impl MacroPlugin for DojoPlugin { diagnostics_mappings: builder.diagnostics_mappings, }), diagnostics, - remove_original_item: true, + remove_original_item: false, } } _ => PluginResult::default(), diff --git a/crates/dojo-lang/src/plugin_test_data/component b/crates/dojo-lang/src/plugin_test_data/component index 03426f902c..1009724cea 100644 --- a/crates/dojo-lang/src/plugin_test_data/component +++ b/crates/dojo-lang/src/plugin_test_data/component @@ -10,7 +10,6 @@ use serde::Serde; struct Position { #[key] id: felt252, - x: felt252, y: felt252 } @@ -53,6 +52,8 @@ struct Player { //! > generated_cairo_code use serde::Serde; + +#[derive(Component, Copy, Drop, Serde)] struct Position { #[key] id: felt252, @@ -193,6 +194,8 @@ impl PositionImpl of PositionTrait { } } + +#[derive(Component, Serde)] struct Roles { role_ids: Array } @@ -296,14 +299,16 @@ mod roles { use starknet::ContractAddress; + +#[derive(Component, Copy, Drop, Serde)] struct Player { #[key] game: felt252, #[key] player: ContractAddress, - name: felt252, -} + name: felt252, +} impl PlayerComponent of dojo::component::Component { #[inline(always)] fn name(self: @Player) -> felt252 { @@ -416,6 +421,6 @@ mod player { //! > expected_diagnostics error: Component must define atleast one #[key] attribute - --> dummy_file.cairo:31:8 + --> dummy_file.cairo:30:8 struct Roles { ^***^ diff --git a/crates/dojo-lang/src/plugin_test_data/print b/crates/dojo-lang/src/plugin_test_data/print index 534fcae8ca..b428f50a9d 100644 --- a/crates/dojo-lang/src/plugin_test_data/print +++ b/crates/dojo-lang/src/plugin_test_data/print @@ -35,6 +35,16 @@ struct Player { //! > generated_cairo_code use serde::Serde; + +#[derive(Print, Copy, Drop, Serde)] +struct Position { + #[key] + id: felt252, + + x: felt252, + y: felt252 +} + #[cfg(test)] impl PositionPrintImpl of debug::PrintTrait { fn print(self: Position) { @@ -47,6 +57,12 @@ impl PositionPrintImpl of debug::PrintTrait { } } + +#[derive(Print, Serde)] +struct Roles { + role_ids: Array +} + #[cfg(test)] impl RolesPrintImpl of debug::PrintTrait { fn print(self: Roles) { @@ -58,6 +74,16 @@ impl RolesPrintImpl of debug::PrintTrait { use starknet::ContractAddress; + +#[derive(Print, Copy, Drop, Serde)] +struct Player { + #[key] + game: felt252, + #[key] + player: ContractAddress, + + name: felt252, +} #[cfg(test)] impl PlayerPrintImpl of debug::PrintTrait { fn print(self: Player) { From 97162926c52d9c1ad294267462e217c09ea01477 Mon Sep 17 00:00:00 2001 From: Shramee Srivastav Date: Mon, 25 Sep 2023 04:11:14 +0800 Subject: [PATCH 04/14] Schema macro (#920) --- crates/dojo-lang/src/component.rs | 84 +----- crates/dojo-lang/src/introspect.rs | 112 ++++++++ crates/dojo-lang/src/lib.rs | 1 + .../dojo-lang/src/manifest_test_data/manifest | 6 +- crates/dojo-lang/src/plugin.rs | 5 + crates/dojo-lang/src/plugin_test.rs | 1 + .../dojo-lang/src/plugin_test_data/introspect | 239 ++++++++++++++++++ .../client/src/contract/component_test.rs | 2 +- examples/ecs/src/components.cairo | 44 +--- 9 files changed, 366 insertions(+), 128 deletions(-) create mode 100644 crates/dojo-lang/src/introspect.rs create mode 100644 crates/dojo-lang/src/plugin_test_data/introspect diff --git a/crates/dojo-lang/src/component.rs b/crates/dojo-lang/src/component.rs index 53f82e33cc..c05beeef88 100644 --- a/crates/dojo-lang/src/component.rs +++ b/crates/dojo-lang/src/component.rs @@ -7,8 +7,8 @@ use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; use convert_case::{Case, Casing}; use dojo_world::manifest::Member; -use itertools::Itertools; +use crate::introspect::handle_introspect_struct; use crate::plugin::{Component, DojoAuxData}; /// A handler for Dojo code that modifies a component struct. @@ -67,41 +67,6 @@ pub fn handle_component_struct( let serialized_values: Vec<_> = members.iter().filter_map(|m| serialize_member(m, false)).collect::<_>(); - let layout: Vec<_> = members - .iter() - .filter_map(|m| { - if m.key { - return None; - } - - Some(RewriteNode::Text(format!( - "dojo::database::schema::SchemaIntrospection::<{}>::layout(ref layout);\n", - m.ty - ))) - }) - .collect::<_>(); - - let member_types: Vec<_> = members - .iter() - .map(|m| { - let mut attrs = vec![]; - if m.key { - attrs.push("'key'") - } - - format!( - "dojo::database::schema::serialize_member(@dojo::database::schema::Member {{ - name: '{}', - ty: dojo::database::schema::SchemaIntrospection::<{}>::ty(), - attrs: array![{}].span() - }})", - m.name, - m.ty, - attrs.join(","), - ) - }) - .collect::<_>(); - let name = struct_ast.name(db).text(db); aux_data.components.push(Component { name: name.to_string(), members: members.to_vec() }); @@ -135,29 +100,7 @@ pub fn handle_component_struct( array::ArrayTrait::span(@layout) } } - - impl $type_name$SchemaIntrospection of \ - dojo::database::schema::SchemaIntrospection<$type_name$> { - #[inline(always)] - fn size() -> usize { - $size$ - } - - #[inline(always)] - fn layout(ref layout: Array) { - $layout$ - } - - #[inline(always)] - fn ty() -> dojo::database::schema::Ty { - dojo::database::schema::Ty::Struct(dojo::database::schema::Struct { - name: '$type_name$', - attrs: array![].span(), - children: array![$member_types$].span() - }) - } - } - + $schema_introspection$ #[starknet::interface] trait I$type_name$ { fn name(self: @T) -> felt252; @@ -199,30 +142,9 @@ pub fn handle_component_struct( "type_name".to_string(), RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node()), ), + ("schema_introspection".to_string(), handle_introspect_struct(db, struct_ast)), ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), - ("layout".to_string(), RewriteNode::new_modified(layout)), - ("member_types".to_string(), RewriteNode::Text(member_types.join(","))), - ( - "size".to_string(), - RewriteNode::Text( - struct_ast - .members(db) - .elements(db) - .iter() - .filter_map(|member| { - if member.has_attr(db, "key") { - return None; - } - - Some(format!( - "dojo::database::schema::SchemaIntrospection::<{}>::size()", - member.type_clause(db).ty(db).as_syntax_node().get_text(db), - )) - }) - .join(" + "), - ), - ), ]), ), diagnostics, diff --git a/crates/dojo-lang/src/introspect.rs b/crates/dojo-lang/src/introspect.rs new file mode 100644 index 0000000000..eeb5cc4b4d --- /dev/null +++ b/crates/dojo-lang/src/introspect.rs @@ -0,0 +1,112 @@ +use cairo_lang_defs::patcher::RewriteNode; +use cairo_lang_syntax::node::ast::ItemStruct; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; +use dojo_world::manifest::Member; +use itertools::Itertools; + +/// A handler for Dojo code derives Introspect for a struct +/// Parameters: +/// * db: The semantic database. +/// * struct_ast: The AST of the struct. +/// Returns: +/// * A RewriteNode containing the generated code. +pub fn handle_introspect_struct(db: &dyn SyntaxGroup, struct_ast: ItemStruct) -> RewriteNode { + let members: Vec<_> = struct_ast + .members(db) + .elements(db) + .iter() + .map(|member| Member { + name: member.name(db).text(db).to_string(), + ty: member.type_clause(db).ty(db).as_syntax_node().get_text(db).trim().to_string(), + key: member.has_attr(db, "key"), + }) + .collect::<_>(); + + let layout: Vec<_> = members + .iter() + .filter_map(|m| { + if m.key { + return None; + } + + Some(RewriteNode::Text(format!( + "dojo::database::schema::SchemaIntrospection::<{}>::layout(ref layout);\n", + m.ty + ))) + }) + .collect::<_>(); + + let member_types: Vec<_> = members + .iter() + .map(|m| { + let mut attrs = vec![]; + if m.key { + attrs.push("'key'") + } + + format!( + "dojo::database::schema::serialize_member(@dojo::database::schema::Member {{ + name: '{}', + ty: dojo::database::schema::SchemaIntrospection::<{}>::ty(), + attrs: array![{}].span() + }})", + m.name, + m.ty, + attrs.join(","), + ) + }) + .collect::<_>(); + + RewriteNode::interpolate_patched( + " + impl $name$SchemaIntrospection of dojo::database::schema::SchemaIntrospection<$name$> { + #[inline(always)] + fn size() -> usize { + $size$ + } + + #[inline(always)] + fn layout(ref layout: Array) { + $layout$ + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Struct(dojo::database::schema::Struct { + name: '$name$', + attrs: array![].span(), + children: array![$member_types$].span() + }) + } + } + ", + UnorderedHashMap::from([ + ("name".to_string(), RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node())), + ( + "size".to_string(), + RewriteNode::Text( + struct_ast + .members(db) + .elements(db) + .iter() + .filter_map(|member| { + if member.has_attr(db, "key") { + return None; + } + + Some(format!( + "dojo::database::schema::SchemaIntrospection::<{}>::size()", + member.type_clause(db).ty(db).as_syntax_node().get_text(db), + )) + }) + .join(" + "), + ), + ), + ("layout".to_string(), RewriteNode::new_modified(layout)), + ("member_types".to_string(), RewriteNode::Text(member_types.join(","))), + ]), + ) +} diff --git a/crates/dojo-lang/src/lib.rs b/crates/dojo-lang/src/lib.rs index fc7c109d19..ba4f83d75c 100644 --- a/crates/dojo-lang/src/lib.rs +++ b/crates/dojo-lang/src/lib.rs @@ -6,6 +6,7 @@ pub mod compiler; pub mod component; pub mod inline_macros; +pub mod introspect; mod manifest; pub mod plugin; pub mod print; diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 46dcffc92c..40193e2937 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -553,7 +553,7 @@ test_manifest_file { "name": "player_actions", "address": null, - "class_hash": "0x3b55d1b5540a961f4b668fc2b8378b488b4c5f99e46542bfa3585ccd1a13f9c", + "class_hash": "0x48465b92d0b49ae15999132b88c619b84a0ac7877f92e134a2fef77d387035e", "abi": [ { "type": "impl", @@ -675,7 +675,7 @@ test_manifest_file { "name": "player_actions_external", "address": null, - "class_hash": "0x713256dd40b9cb5ad2dedb4e638cb13ec94c7140ac92ebf6ec6feb8c088af79", + "class_hash": "0x17a570b3ae75ace7bad6da4581f55356dc3325fa2b454913372f6976d8d5b40", "abi": [ { "type": "impl", @@ -956,7 +956,7 @@ test_manifest_file "key": false } ], - "class_hash": "0x71a5cb83c2d96f4b97a015d36f936de6f93166a1434bbacd29147933c4ac314", + "class_hash": "0x69889772f44397619cd8965660e1c8e80ba5f0c917ba40df29b2ffa5b440745", "abi": [ { "type": "function", diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 414a0eab00..36db6b5c2d 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -25,6 +25,7 @@ use crate::component::handle_component_struct; use crate::inline_macros::emit::EmitMacro; use crate::inline_macros::get::GetMacro; use crate::inline_macros::set::SetMacro; +use crate::introspect::handle_introspect_struct; use crate::print::derive_print; use crate::system::System; @@ -162,6 +163,10 @@ impl MacroPlugin for DojoPlugin { "Print" => { rewrite_nodes.push(derive_print(db, struct_ast.clone())); } + "Introspect" => { + rewrite_nodes + .push(handle_introspect_struct(db, struct_ast.clone())); + } _ => continue, } } diff --git a/crates/dojo-lang/src/plugin_test.rs b/crates/dojo-lang/src/plugin_test.rs index 4aba2c860f..be26d616c2 100644 --- a/crates/dojo-lang/src/plugin_test.rs +++ b/crates/dojo-lang/src/plugin_test.rs @@ -18,6 +18,7 @@ cairo_lang_test_utils::test_file_test!( { component: "component", print: "print", + introspect: "introspect", system: "system", }, test_expand_plugin diff --git a/crates/dojo-lang/src/plugin_test_data/introspect b/crates/dojo-lang/src/plugin_test_data/introspect new file mode 100644 index 0000000000..07e7434705 --- /dev/null +++ b/crates/dojo-lang/src/plugin_test_data/introspect @@ -0,0 +1,239 @@ +//! > Test expansion of the component contract. + +//! > test_runner_name +test_expand_plugin + +//! > cairo_code +use serde::Serde; + +#[derive(Copy, Drop, Serde, Print, Introspect)] +struct Vec2 { + x: u32, + y: u32 +} + +#[derive(Component, Copy, Drop, Print, Introspect)] +struct Position { + #[key] + player: ContractAddress, + vec: Vec2, +} + +//! > generated_cairo_code +use serde::Serde; + + +#[derive(Copy, Drop, Serde, Print, Introspect)] +struct Vec2 { + x: u32, + y: u32 +} + +#[cfg(test)] +impl Vec2PrintImpl of debug::PrintTrait { + fn print(self: Vec2) { + debug::PrintTrait::print('x'); + debug::PrintTrait::print(self.x); + debug::PrintTrait::print('y'); + debug::PrintTrait::print(self.y); + } +} +impl Vec2SchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + dojo::database::schema::SchemaIntrospection::::size() + + dojo::database::schema::SchemaIntrospection::::size() + } + + #[inline(always)] + fn layout(ref layout: Array) { + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Struct( + dojo::database::schema::Struct { + name: 'Vec2', + attrs: array![].span(), + children: array![ + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'x', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array![].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'y', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array![].span() + } + ) + ] + .span() + } + ) + } +} + + + +#[derive(Component, Copy, Drop, Print, Introspect)] +struct Position { + #[key] + player: ContractAddress, + vec: Vec2, +} +impl PositionComponent of dojo::component::Component { + #[inline(always)] + fn name(self: @Position) -> felt252 { + 'Position' + } + + #[inline(always)] + fn keys(self: @Position) -> Span { + let mut serialized = ArrayTrait::new(); + serde::Serde::serialize(self.player, ref serialized); + array::ArrayTrait::span(@serialized) + } + + #[inline(always)] + fn values(self: @Position) -> Span { + let mut serialized = ArrayTrait::new(); + serde::Serde::serialize(self.vec, ref serialized); + array::ArrayTrait::span(@serialized) + } + + #[inline(always)] + fn layout(self: @Position) -> Span { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + array::ArrayTrait::span(@layout) + } +} + +impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + dojo::database::schema::SchemaIntrospection::::size() + } + + #[inline(always)] + fn layout(ref layout: Array) { + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Struct( + dojo::database::schema::Struct { + name: 'Position', + attrs: array![].span(), + children: array![ + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'player', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array!['key'].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'vec', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array![].span() + } + ) + ] + .span() + } + ) + } +} + +#[starknet::interface] +trait IPosition { + fn name(self: @T) -> felt252; +} + +#[starknet::contract] +mod position { + use super::Position; + + #[storage] + struct Storage {} + + #[external(v0)] + fn name(self: @ContractState) -> felt252 { + 'Position' + } + + #[external(v0)] + fn size(self: @ContractState) -> usize { + dojo::database::schema::SchemaIntrospection::::size() + } + + #[external(v0)] + fn layout(self: @ContractState) -> Span { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + array::ArrayTrait::span(@layout) + } + + #[external(v0)] + fn schema(self: @ContractState) -> dojo::database::schema::Ty { + dojo::database::schema::SchemaIntrospection::::ty() + } +} +#[cfg(test)] +impl PositionPrintImpl of debug::PrintTrait { + fn print(self: Position) { + debug::PrintTrait::print('player'); + debug::PrintTrait::print(self.player); + debug::PrintTrait::print('vec'); + debug::PrintTrait::print(self.vec); + } +} +impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + dojo::database::schema::SchemaIntrospection::::size() + } + + #[inline(always)] + fn layout(ref layout: Array) { + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Struct( + dojo::database::schema::Struct { + name: 'Position', + attrs: array![].span(), + children: array![ + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'player', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array!['key'].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'vec', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array![].span() + } + ) + ] + .span() + } + ) + } +} + +//! > expected_diagnostics diff --git a/crates/torii/client/src/contract/component_test.rs b/crates/torii/client/src/contract/component_test.rs index 4ff7379584..4f0acea412 100644 --- a/crates/torii/client/src/contract/component_test.rs +++ b/crates/torii/client/src/contract/component_test.rs @@ -62,7 +62,7 @@ async fn test_component() { assert_eq!( position.class_hash(), FieldElement::from_hex_be( - "0x071a5cb83c2d96f4b97a015d36f936de6f93166a1434bbacd29147933c4ac314" + "0x069889772f44397619cd8965660e1c8e80ba5f0c917ba40df29b2ffa5b440745" ) .unwrap() ); diff --git a/examples/ecs/src/components.cairo b/examples/ecs/src/components.cairo index bf62895b60..0fa9761372 100644 --- a/examples/ecs/src/components.cairo +++ b/examples/ecs/src/components.cairo @@ -76,54 +76,12 @@ struct Moves { last_direction: Direction } -#[derive(Copy, Drop, Serde)] +#[derive(Copy, Drop, Serde, Print, Introspect)] struct Vec2 { x: u32, y: u32 } -impl Vec2PrintImpl of PrintTrait { - fn print(self: Vec2) { - self.x.print(); - } -} - -impl Vec2SchemaIntrospectionImpl of SchemaIntrospection { - #[inline(always)] - fn size() -> usize { - 2 - } - - #[inline(always)] - fn layout(ref layout: Array) { - layout.append(32); - layout.append(32); - } - - #[inline(always)] - fn ty() -> Ty { - Ty::Struct( - Struct { - name: 'Vec2', - attrs: array![].span(), - children: array![ - serialize_member( - @Member { - name: 'x', ty: SchemaIntrospection::::ty(), attrs: array![].span(), - } - ), - serialize_member( - @Member { - name: 'y', ty: SchemaIntrospection::::ty(), attrs: array![].span(), - } - ) - ] - .span() - } - ) - } -} - #[derive(Component, Copy, Drop, Print, Serde)] struct Position { #[key] From 345bba109a82f79d80da7d8342ae2393cf4710b7 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Tue, 26 Sep 2023 09:38:25 -0400 Subject: [PATCH 05/14] Remove println --- crates/torii/client/src/contract/component.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/torii/client/src/contract/component.rs b/crates/torii/client/src/contract/component.rs index c132ed4608..9400ce6525 100644 --- a/crates/torii/client/src/contract/component.rs +++ b/crates/torii/client/src/contract/component.rs @@ -188,7 +188,6 @@ pub fn unpack( } fn parse_ty(data: &[FieldElement]) -> Result> { - println!("{:#?}", data); let member_type: u8 = data[0].try_into().unwrap(); match member_type { 0 => parse_simple::

(&data[1..]), From 68b5faab716c36152f9326d124d0be11b35ef464 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 26 Sep 2023 20:39:02 +0700 Subject: [PATCH 06/14] refactor(torii-server): combine `tonic` and `warp` (#926) --- Cargo.lock | 72 ++++++++++++------- Cargo.toml | 9 +++ crates/torii/grpc/Cargo.toml | 7 +- crates/torii/grpc/src/lib.rs | 56 ++++++++++++++- crates/torii/grpc/src/route.rs | 90 ----------------------- crates/torii/server/Cargo.toml | 10 ++- crates/torii/server/src/cli.rs | 32 ++++----- crates/torii/server/src/server.rs | 116 ++++++++++++++++++++++++++++++ 8 files changed, 254 insertions(+), 138 deletions(-) delete mode 100644 crates/torii/grpc/src/route.rs create mode 100644 crates/torii/server/src/server.rs diff --git a/Cargo.lock b/Cargo.lock index 06c761edf2..cd2a793654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5013,12 +5013,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.1.25" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 1.0.109", + "syn 2.0.32", ] [[package]] @@ -5089,9 +5089,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.9" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" dependencies = [ "bytes", "prost-derive", @@ -5099,44 +5099,44 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.11.9" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", - "lazy_static", + "itertools 0.11.0", "log", "multimap", + "once_cell", "petgraph", "prettyplease", "prost", "prost-types", "regex", - "syn 1.0.109", + "syn 2.0.32", "tempfile", "which", ] [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.32", ] [[package]] name = "prost-types" -version = "0.11.9" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" dependencies = [ "prost", ] @@ -6802,16 +6802,15 @@ dependencies = [ [[package]] name = "tonic" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +checksum = "14c00bc15e49625f3d2f20b17082601e5e17cf27ead69e805174026c194b6664" dependencies = [ + "async-stream", "async-trait", "axum", "base64 0.21.4", "bytes", - "futures-core", - "futures-util", "h2", "http", "http-body", @@ -6830,15 +6829,35 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6fdaae4c2c638bb70fe42803a26fbd6fc6ac8c72f5c59f67ecc2a2dcabf4b07" +checksum = "c9d37bb15da06ae9bb945963066baca6561b505af93a52e949a85d28558459a2" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "quote", - "syn 1.0.109", + "syn 2.0.32", +] + +[[package]] +name = "tonic-web" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2953fe95664e86519e0d1c4bdd65007d93bc47a59c9af512280977aa9e46b871" +dependencies = [ + "base64 0.21.4", + "bytes", + "http", + "http-body", + "hyper", + "pin-project", + "tokio-stream", + "tonic", + "tower-http", + "tower-layer", + "tower-service", + "tracing", ] [[package]] @@ -6924,7 +6943,6 @@ dependencies = [ name = "torii-grpc" version = "0.2.1" dependencies = [ - "bytes", "prost", "sqlx", "tonic", @@ -6946,6 +6964,10 @@ dependencies = [ "ctrlc", "dojo-types", "dojo-world", + "either", + "http", + "http-body", + "hyper", "indexmap 1.9.3", "poem", "scarb", @@ -6957,10 +6979,12 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", + "tonic-web", "torii-client", "torii-core", "torii-graphql", "torii-grpc", + "tower", "tracing", "tracing-subscriber", "url", diff --git a/Cargo.toml b/Cargo.toml index 5f4afc9399..60515d099e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,8 +85,17 @@ toml = "0.7.4" tracing = "0.1.34" tracing-subscriber = { version = "0.3.16", features = [ "env-filter" ] } url = "2.4.0" + +# server +hyper = "0.14.27" warp = "0.3" +# gRPC +prost = "0.12" +tonic = "0.10" +tonic-build = "0.10" +tonic-web = "0.10.1" + [patch."https://github.com/starkware-libs/blockifier"] blockifier = { git = "https://github.com/dojoengine/blockifier", rev = "f7df9ba" } diff --git a/crates/torii/grpc/Cargo.toml b/crates/torii/grpc/Cargo.toml index 8b25f0694c..7c86677292 100644 --- a/crates/torii/grpc/Cargo.toml +++ b/crates/torii/grpc/Cargo.toml @@ -6,11 +6,10 @@ repository.workspace = true version.workspace = true [dependencies] -bytes = "1.0" -prost = "0.11" +prost.workspace = true sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } -tonic = "0.9" +tonic.workspace = true warp.workspace = true [build-dependencies] -tonic-build = "0.9" +tonic-build.workspace = true diff --git a/crates/torii/grpc/src/lib.rs b/crates/torii/grpc/src/lib.rs index b9bb57fef0..1b956ac6ee 100644 --- a/crates/torii/grpc/src/lib.rs +++ b/crates/torii/grpc/src/lib.rs @@ -1 +1,55 @@ -pub mod route; +use sqlx::{Pool, Sqlite}; +use tonic::{Request, Response, Status}; +use world::world_server::World; +use world::{MetaReply, MetaRequest}; + +pub mod world { + tonic::include_proto!("world"); +} + +#[derive(Clone, Debug)] +pub struct DojoWorld { + pool: Pool, +} + +impl DojoWorld { + pub fn new(pool: Pool) -> Self { + Self { pool } + } +} + +#[tonic::async_trait] +impl World for DojoWorld { + async fn meta( + &self, + request: Request, // Accept request of type MetaRequest + ) -> Result, Status> { + let id = request.into_inner().id; + + let (world_address, world_class_hash, executor_address, executor_class_hash): ( + String, + String, + String, + String, + ) = sqlx::query_as( + "SELECT world_address, world_class_hash, executor_address, executor_class_hash FROM \ + worlds WHERE id = ?", + ) + .bind(id) + .fetch_one(&self.pool) + .await + .map_err(|e| match e { + sqlx::Error::RowNotFound => Status::not_found("World not found"), + _ => Status::internal("Internal error"), + })?; + + let reply = world::MetaReply { + world_address, + world_class_hash, + executor_address, + executor_class_hash, + }; + + Ok(Response::new(reply)) // Send back our formatted greeting + } +} diff --git a/crates/torii/grpc/src/route.rs b/crates/torii/grpc/src/route.rs deleted file mode 100644 index e7ddf7de16..0000000000 --- a/crates/torii/grpc/src/route.rs +++ /dev/null @@ -1,90 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use prost::Message; -use sqlx::{Pool, Sqlite}; -use tonic::{Request, Response, Status}; -use warp::Filter; -use world::world_server::World; -use world::{MetaReply, MetaRequest}; - -pub mod world { - tonic::include_proto!("world"); -} - -#[derive(Clone, Debug)] -pub struct DojoWorld { - pool: Pool, -} - -impl DojoWorld { - fn new(pool: Pool) -> Self { - Self { pool } - } -} - -#[tonic::async_trait] -impl World for DojoWorld { - async fn meta( - &self, - request: Request, // Accept request of type MetaRequest - ) -> Result, Status> { - let id = request.into_inner().id; - - let (world_address, world_class_hash, executor_address, executor_class_hash): ( - String, - String, - String, - String, - ) = sqlx::query_as( - "SELECT world_address, world_class_hash, executor_address, executor_class_hash FROM \ - worlds WHERE id = ?", - ) - .bind(id) - .fetch_one(&self.pool) - .await - .map_err(|e| match e { - sqlx::Error::RowNotFound => Status::not_found("World not found"), - _ => Status::internal("Internal error"), - })?; - - let reply = world::MetaReply { - world_address, - world_class_hash, - executor_address, - executor_class_hash, - }; - - Ok(Response::new(reply)) // Send back our formatted greeting - } -} - -#[derive(Debug)] -struct InvalidProtobufError; - -impl warp::reject::Reject for InvalidProtobufError {} - -pub fn filter( - pool: &Pool, -) -> impl Filter + Clone { - let world = DojoWorld::new(pool.clone()); - - warp::path("grpc").and(warp::post()).and(warp::body::bytes()).and_then(move |body: Bytes| { - let world = world.clone(); - async move { - let request = match MetaRequest::decode(body) { - Ok(req) => req, - Err(_) => return Err(warp::reject::custom(InvalidProtobufError)), - }; - let response = world.meta(tonic::Request::new(request)).await.unwrap(); - let meta_reply = response.into_inner(); - - let mut bytes = BytesMut::new(); - meta_reply.encode(&mut bytes).unwrap(); - - Ok::<_, warp::reject::Rejection>(warp::reply::with_header( - bytes.to_vec(), - "content-type", - "application/octet-stream", - )) - } - }) -} diff --git a/crates/torii/server/Cargo.toml b/crates/torii/server/Cargo.toml index c19c43946a..7ecd3f7e45 100644 --- a/crates/torii/server/Cargo.toml +++ b/crates/torii/server/Cargo.toml @@ -16,6 +16,8 @@ clap.workspace = true ctrlc = "3.2.5" dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } +http = "0.2.9" +hyper.workspace = true indexmap = "1.9.3" poem = "1.3.48" scarb.workspace = true @@ -27,21 +29,25 @@ starknet.workspace = true tokio-stream = "0.1.11" tokio-util = "0.7.7" tokio.workspace = true +tonic-web.workspace = true torii-client = { path = "../client" } torii-core = { path = "../core" } torii-graphql = { path = "../graphql" } torii-grpc = { path = "../grpc" } +tower = "0.4.13" tracing-subscriber.workspace = true tracing.workspace = true url.workspace = true warp.workspace = true +http-body = "0.4.5" +either = "1.9.0" [dev-dependencies] camino.workspace = true [features] -default = [ "sqlite" ] -sqlite = [ "sqlx/sqlite" ] +default = ["sqlite"] +sqlite = ["sqlx/sqlite"] [[bin]] name = "torii" diff --git a/crates/torii/server/src/cli.rs b/crates/torii/server/src/cli.rs index 70461c4b07..9388e7ca43 100644 --- a/crates/torii/server/src/cli.rs +++ b/crates/torii/server/src/cli.rs @@ -1,4 +1,5 @@ use std::env; +use std::net::SocketAddr; use std::str::FromStr; use anyhow::{anyhow, Context}; @@ -21,13 +22,13 @@ use torii_core::sql::Sql; use tracing::error; use tracing_subscriber::fmt; use url::Url; -use warp::Filter; use crate::engine::Processors; use crate::indexer::Indexer; mod engine; mod indexer; +mod server; /// Dojo World Indexer #[derive(Parser, Debug)] @@ -51,12 +52,9 @@ struct Args { /// Host address for GraphQL/gRPC endpoints #[arg(long, default_value = "0.0.0.0")] host: String, - /// Port number for GraphQL endpoint + /// Port number for GraphQL/gRPC endpoints #[arg(long, default_value = "8080")] - graphql_port: u16, - /// Port number for gRPC endpoint - #[arg(long, default_value = "50051")] - grpc_port: u16, + port: u16, } #[tokio::main] @@ -108,23 +106,23 @@ async fn main() -> anyhow::Result<()> { let indexer = Indexer::new(&world, &db, &provider, processors, manifest, world_address, args.start_block); - let base_route = warp::path::end() - .and(warp::get()) - .map(|| warp::reply::json(&serde_json::json!({ "success": true }))); - let routes = torii_graphql::route::filter(&pool) - .await - .or(torii_grpc::route::filter(&pool)) - .or(base_route); - let server = warp::serve(routes); - let server = server.run((args.host.parse::()?, args.graphql_port)); + let addr = format!("{}:{}", args.host, args.port) + .parse::() + .expect("able to parse address"); tokio::select! { res = indexer.start() => { if let Err(e) = res { - error!("Indexer failed with error: {:?}", e); + error!("Indexer failed with error: {e}"); } } - _ = server => {} + + res = server::spawn_server(&addr, &pool) => { + if let Err(e) = res { + error!("Server failed with error: {e}"); + } + } + _ = tokio::signal::ctrl_c() => { println!("Received Ctrl+C, shutting down"); } diff --git a/crates/torii/server/src/server.rs b/crates/torii/server/src/server.rs new file mode 100644 index 0000000000..bfe61bab4c --- /dev/null +++ b/crates/torii/server/src/server.rs @@ -0,0 +1,116 @@ +use std::convert::Infallible; +use std::net::SocketAddr; +use std::pin::Pin; +use std::str::FromStr; +use std::task::Poll; + +use either::Either; +use hyper::service::{make_service_fn, Service}; +use hyper::Uri; +use sqlx::{Pool, Sqlite}; +use warp::Filter; + +type Error = Box; + +// TODO: check if there's a nicer way to implement this +pub async fn spawn_server(addr: &SocketAddr, pool: &Pool) -> anyhow::Result<()> { + let world_server = torii_grpc::DojoWorld::new(pool.clone()); + + let base_route = warp::path::end() + .and(warp::get()) + .map(|| warp::reply::json(&serde_json::json!({ "success": true }))); + let routes = torii_graphql::route::filter(pool).await.or(base_route); + + let warp = warp::service(routes); + let tonic = tonic_web::enable(torii_grpc::world::world_server::WorldServer::new(world_server)); + + hyper::Server::bind(addr) + .serve(make_service_fn(move |_| { + let mut tonic = tonic.clone(); + let mut warp = warp.clone(); + + std::future::ready(Ok::<_, Infallible>(tower::service_fn( + move |mut req: hyper::Request| { + let mut path_iter = req.uri().path().split('/').skip(1); + + // check the base path + match path_iter.next() { + // There's a bug in tonic client where the URI path is not respected in + // `Endpoint`, but this issue doesn't exist if `torii-client` is compiled to + // `wasm32-unknown-unknown`. See: https://github.com/hyperium/tonic/issues/1314 + Some("grpc") => { + let grpc_method = path_iter.collect::>().join("/"); + *req.uri_mut() = + Uri::from_str(&format!("/{grpc_method}")).expect("valid uri"); + + Either::Right({ + let res = tonic.call(req); + Box::pin(async move { + let res = res.await.map(|res| res.map(EitherBody::Right))?; + Ok::<_, Error>(res) + }) + }) + } + + _ => Either::Left({ + let res = warp.call(req); + Box::pin(async move { + let res = res.await.map(|res| res.map(EitherBody::Left))?; + Ok::<_, Error>(res) + }) + }), + } + }, + ))) + })) + .await?; + + Ok(()) +} + +enum EitherBody { + Left(A), + Right(B), +} + +impl http_body::Body for EitherBody +where + A: http_body::Body + Send + Unpin, + B: http_body::Body + Send + Unpin, + A::Error: Into, + B::Error: Into, +{ + type Data = A::Data; + type Error = Box; + + fn is_end_stream(&self) -> bool { + match self { + EitherBody::Left(b) => b.is_end_stream(), + EitherBody::Right(b) => b.is_end_stream(), + } + } + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll>> { + match self.get_mut() { + EitherBody::Left(b) => Pin::new(b).poll_data(cx).map(map_option_err), + EitherBody::Right(b) => Pin::new(b).poll_data(cx).map(map_option_err), + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll, Self::Error>> { + match self.get_mut() { + EitherBody::Left(b) => Pin::new(b).poll_trailers(cx).map_err(Into::into), + EitherBody::Right(b) => Pin::new(b).poll_trailers(cx).map_err(Into::into), + } + } +} + +fn map_option_err>(err: Option>) -> Option> { + err.map(|e| e.map_err(Into::into)) +} From 3a02cc6e2cd63c41ec508eb2a4a5f95406803836 Mon Sep 17 00:00:00 2001 From: notV4l <122404722+notV4l@users.noreply.github.com> Date: Tue, 26 Sep 2023 17:33:36 +0200 Subject: [PATCH 07/14] Erc721 dojotized (#921) * erc721 dojotized * fix: increase gas on test --- crates/dojo-erc/src/lib.cairo | 3 + crates/dojo-erc/src/tests/erc20_tests.cairo | 2 - crates/dojo-erc/src/tests/erc721_tests.cairo | 1454 +++++++++++++++++ crates/dojo-erc/src/token/erc721.cairo | 5 + .../src/token/erc721/components.cairo | 48 + crates/dojo-erc/src/token/erc721/erc721.cairo | 443 +++++ .../dojo-erc/src/token/erc721/interface.cairo | 60 + 7 files changed, 2013 insertions(+), 2 deletions(-) create mode 100644 crates/dojo-erc/src/tests/erc721_tests.cairo create mode 100644 crates/dojo-erc/src/token/erc721.cairo create mode 100644 crates/dojo-erc/src/token/erc721/components.cairo create mode 100644 crates/dojo-erc/src/token/erc721/erc721.cairo create mode 100644 crates/dojo-erc/src/token/erc721/interface.cairo diff --git a/crates/dojo-erc/src/lib.cairo b/crates/dojo-erc/src/lib.cairo index 810dc43dd8..8d7760a12b 100644 --- a/crates/dojo-erc/src/lib.cairo +++ b/crates/dojo-erc/src/lib.cairo @@ -1,6 +1,8 @@ mod token { mod erc20; mod erc20_components; + mod erc721; + } #[cfg(test)] @@ -9,4 +11,5 @@ mod tests { mod utils; mod erc20_tests; + mod erc721_tests; } diff --git a/crates/dojo-erc/src/tests/erc20_tests.cairo b/crates/dojo-erc/src/tests/erc20_tests.cairo index 666021c0b9..490964c017 100644 --- a/crates/dojo-erc/src/tests/erc20_tests.cairo +++ b/crates/dojo-erc/src/tests/erc20_tests.cairo @@ -10,11 +10,9 @@ use dojo_erc::token::erc20::ERC20::ERC20Impl; use dojo_erc::token::erc20::ERC20::InternalImpl; use dojo_erc::token::erc20::ERC20::Transfer; use dojo_erc::token::erc20::ERC20; -use option::OptionTrait; use starknet::ContractAddress; use starknet::contract_address_const; use starknet::testing; -use traits::Into; use zeroable::Zeroable; use dojo::test_utils::spawn_test_world; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; diff --git a/crates/dojo-erc/src/tests/erc721_tests.cairo b/crates/dojo-erc/src/tests/erc721_tests.cairo new file mode 100644 index 0000000000..371403b092 --- /dev/null +++ b/crates/dojo-erc/src/tests/erc721_tests.cairo @@ -0,0 +1,1454 @@ +use integer::BoundedInt; +use integer::u256; +use integer::u256_from_felt252; +use dojo_erc::tests::utils; +use dojo_erc::tests::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, OPERATOR, OTHER, NAME, SYMBOL, URI, TOKEN_ID +}; + +use dojo_erc::token::erc721::ERC721::ERC721Impl; +use dojo_erc::token::erc721::ERC721::ERC721CamelOnlyImpl; +use dojo_erc::token::erc721::ERC721::ERC721MetadataImpl; +use dojo_erc::token::erc721::ERC721::InternalImpl; +use dojo_erc::token::erc721::ERC721::WorldInteractionsImpl; +use dojo_erc::token::erc721::ERC721::{Approval, ApprovalForAll, Transfer}; +use dojo_erc::token::erc721::ERC721; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::testing; +use zeroable::Zeroable; +use dojo::test_utils::spawn_test_world; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::token::erc721::components::{ + ERC721Meta, erc_721_meta, ERC721OperatorApproval, erc_721_operator_approval, ERC721Owner, + erc_721_owner, ERC721Balance, erc_721_balance, ERC721TokenApproval, erc_721_token_approval +}; +use dojo_erc::token::erc721::ERC721::_worldContractMemberStateTrait; +use debug::PrintTrait; + +// +// Setup +// + +fn STATE() -> (IWorldDispatcher, ERC721::ContractState) { + let world = spawn_test_world( + array![ + erc_721_meta::TEST_CLASS_HASH, + erc_721_operator_approval::TEST_CLASS_HASH, + erc_721_owner::TEST_CLASS_HASH, + erc_721_balance::TEST_CLASS_HASH, + erc_721_token_approval::TEST_CLASS_HASH, + ] + ); + let mut state = ERC721::contract_state_for_testing(); + state._world.write(world.contract_address); + (world, state) +} + +fn setup() -> ERC721::ContractState { + let (world, mut state) = STATE(); + ERC721::constructor(ref state, world.contract_address, NAME, SYMBOL, URI, OWNER(), TOKEN_ID); + utils::drop_event(ZERO()); + state +} + +// fn setup_receiver() -> ContractAddress { +// utils::deploy(ERC721Receiver::TEST_CLASS_HASH, array![]) +// } + +// fn setup_camel_receiver() -> ContractAddress { +// utils::deploy(CamelERC721ReceiverMock::TEST_CLASS_HASH, array![]) +// } + +// fn setup_account() -> ContractAddress { +// let mut calldata = array![PUBKEY]; +// utils::deploy(Account::TEST_CLASS_HASH, calldata) +// } + +// fn setup_camel_account() -> ContractAddress { +// let mut calldata = array![PUBKEY]; +// utils::deploy(CamelAccountMock::TEST_CLASS_HASH, calldata) +// } + +// +// initializer & constructor +// + +#[test] +#[available_gas(20000000)] +fn test_constructor() { + let (world, mut state) = STATE(); + ERC721::constructor(ref state, world.contract_address, NAME, SYMBOL, URI, OWNER(), TOKEN_ID); + + assert(ERC721MetadataImpl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC721MetadataImpl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance should be one'); + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'OWNER should be owner'); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_ID), 'Missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_METADATA_ID), +// 'missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, introspection::interface::ISRC5_ID), +// 'missing interface ID' +// ); +} + +#[test] +#[available_gas(10000000)] +fn test_initializer() { + let (world, mut state) = STATE(); + InternalImpl::initializer(ref state, NAME, SYMBOL, URI); + + assert(ERC721MetadataImpl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC721MetadataImpl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + + assert(ERC721Impl::balance_of(@state, OWNER()) == 0, 'Balance should be zero'); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_ID), 'Missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_METADATA_ID), +// 'missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, introspection::interface::ISRC5_ID), +// 'missing interface ID' +// ); +} + + +// +// Getters +// + +#[test] +#[available_gas(20000000)] +fn test_balance_of() { + let state = setup(); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Should return balance'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid account',))] +fn test_balance_of_zero() { + let state = setup(); + ERC721Impl::balance_of(@state, ZERO()); +} + +#[test] +#[available_gas(20000000)] +fn test_owner_of() { + let state = setup(); + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Should return owner'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_owner_of_non_minted() { + let state = setup(); + ERC721Impl::owner_of(@state, u256_from_felt252(7)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_token_uri_non_minted() { + let state = setup(); + ERC721MetadataImpl::token_uri(@state, u256_from_felt252(7)); +} + +#[test] +#[available_gas(20000000)] +fn test_get_approved() { + let mut state = setup(); + let spender = SPENDER(); + let token_id = TOKEN_ID; + + assert(ERC721Impl::get_approved(@state, token_id) == ZERO(), 'Should return non-approval'); + InternalImpl::_approve(ref state, spender, token_id); + assert(ERC721Impl::get_approved(@state, token_id) == spender, 'Should return approval'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_get_approved_nonexistent() { + let mut state = setup(); + ERC721Impl::get_approved(@state, u256_from_felt252(7)); +} + +#[test] +#[available_gas(20000000)] +fn test__exists() { + let (world, mut state) = STATE(); + let token_id = TOKEN_ID; + + assert(!InternalImpl::_exists(@state, token_id), 'Token should not exist'); + assert( + WorldInteractionsImpl::get_owner_of(@state, token_id).address == ZERO(), 'Invalid owner' + ); + + InternalImpl::_mint(ref state, RECIPIENT(), token_id); + + assert(InternalImpl::_exists(@state, token_id), 'Token should exist'); + assert( + WorldInteractionsImpl::get_owner_of(@state, token_id).address == RECIPIENT(), + 'Invalid owner' + ); + + InternalImpl::_burn(ref state, token_id); + + assert(!InternalImpl::_exists(@state, token_id), 'Token should not exist'); + assert( + WorldInteractionsImpl::get_owner_of(@state, token_id).address == ZERO(), 'Invalid owner' + ); +} + + +// +// approve & _approve +// + +#[test] +#[available_gas(20000000)] +fn test_approve_from_owner() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); + assert_event_approval(OWNER(), SPENDER(), TOKEN_ID); + + assert( + ERC721Impl::get_approved(@state, TOKEN_ID) == SPENDER(), 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +fn test_approve_from_operator() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); + assert_event_approval(OWNER(), SPENDER(), TOKEN_ID); + + assert( + ERC721Impl::get_approved(@state, TOKEN_ID) == SPENDER(), 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_approve_from_unauthorized() { + let mut state = setup(); + + testing::set_caller_address(OTHER()); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: approval to owner',))] +fn test_approve_to_owner() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721Impl::approve(ref state, OWNER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_approve_nonexistent() { + // let mut state = STATE(); + let (world, mut state) = STATE(); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +fn test__approve() { + let mut state = setup(); + InternalImpl::_approve(ref state, SPENDER(), TOKEN_ID); + assert_event_approval(OWNER(), SPENDER(), TOKEN_ID); + + assert( + ERC721Impl::get_approved(@state, TOKEN_ID) == SPENDER(), 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: approval to owner',))] +fn test__approve_to_owner() { + let mut state = setup(); + InternalImpl::_approve(ref state, OWNER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test__approve_nonexistent() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + InternalImpl::_approve(ref state, SPENDER(), TOKEN_ID); +} + +// +// set_approval_for_all & _set_approval_for_all +// + +#[test] +#[available_gas(20000000)] +fn test_set_approval_for_all() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + + assert(!ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), 'Invalid default value'); + + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); + + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Approval not revoked correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_true() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + ERC721Impl::set_approval_for_all(ref state, OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_false() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + ERC721Impl::set_approval_for_all(ref state, OWNER(), false); +} + +#[test] +#[available_gas(20000000)] +fn test__set_approval_for_all() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + assert(!ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), 'Invalid default value'); + + InternalImpl::_set_approval_for_all(ref state, OWNER(), OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); + + InternalImpl::_set_approval_for_all(ref state, OWNER(), OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_true() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + InternalImpl::_set_approval_for_all(ref state, OWNER(), OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_false() { + // let mut state = STATE(); + let (world, mut state) = STATE(); + InternalImpl::_set_approval_for_all(ref state, OWNER(), OWNER(), false); +} + + +// +// transfer_from & transferFrom +// + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_owner() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + // set approval to check reset + InternalImpl::_approve(ref state, OTHER(), token_id); + utils::drop_event(ZERO()); + + assert_state_before_transfer(@state, owner, recipient, token_id); + assert(ERC721Impl::get_approved(@state, token_id) == OTHER(), 'Approval not implicitly reset'); + + testing::set_caller_address(owner); + ERC721Impl::transfer_from(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_owner() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + // set approval to check reset + InternalImpl::_approve(ref state, OTHER(), token_id); + utils::drop_event(ZERO()); + + assert_state_before_transfer(@state, owner, recipient, token_id); + assert(ERC721Impl::get_approved(@state, token_id) == OTHER(), 'Approval not implicitly reset'); + + testing::set_caller_address(owner); + ERC721CamelOnlyImpl::transferFrom(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_transfer_from_nonexistent() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + ERC721Impl::transfer_from(ref state, ZERO(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_transferFrom_nonexistent() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + ERC721CamelOnlyImpl::transferFrom(ref state, ZERO(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test_transfer_from_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC721Impl::transfer_from(ref state, OWNER(), ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test_transferFrom_to_zero() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721CamelOnlyImpl::transferFrom(ref state, OWNER(), ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_to_owner() { + let mut state = setup(); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership before'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner before'); + + testing::set_caller_address(OWNER()); + ERC721Impl::transfer_from(ref state, OWNER(), OWNER(), TOKEN_ID); + assert_event_transfer(OWNER(), OWNER(), TOKEN_ID); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership after'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner after'); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_to_owner() { + let mut state = setup(); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership before'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner before'); + + testing::set_caller_address(OWNER()); + ERC721CamelOnlyImpl::transferFrom(ref state, OWNER(), OWNER(), TOKEN_ID); + assert_event_transfer( OWNER(), OWNER(), TOKEN_ID); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership after'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner after'); +} + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_approved() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + assert_state_before_transfer(@state, owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::approve(ref state, OPERATOR(), token_id); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721Impl::transfer_from(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_approved() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + assert_state_before_transfer(@state,owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::approve(ref state, OPERATOR(), token_id); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721CamelOnlyImpl::transferFrom(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state,owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_approved_for_all() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721Impl::transfer_from(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_approved_for_all() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state,owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721CamelOnlyImpl::transferFrom(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state,owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_transfer_from_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + ERC721Impl::transfer_from(ref state, OWNER(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_transferFrom_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + ERC721CamelOnlyImpl::transferFrom(ref state, OWNER(), RECIPIENT(), TOKEN_ID); +} + +// // +// // safe_transfer_from & safeTransferFrom +// // + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_account() { +// let mut state = setup(); +// let account = setup_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_account() { +// let mut state = setup(); +// let account = setup_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_account_camel() { +// let mut state = setup(); +// let account = setup_camel_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_account_camel() { +// let mut state = setup(); +// let account = setup_camel_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_receiver() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_receiver() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_receiver_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_receiver_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safe_transfer_from_to_receiver_failure() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safeTransferFrom_to_receiver_failure() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safe_transfer_from_to_receiver_failure_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safeTransferFrom_to_receiver_failure_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +// fn test_safe_transfer_from_to_non_receiver() { +// let mut state = setup(); +// let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, recipient, token_id, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +// fn test_safeTransferFrom_to_non_receiver() { +// let mut state = setup(); +// let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, recipient, token_id, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid token ID',))] +// fn test_safe_transfer_from_nonexistent() { +// let mut state = STATE(); +// ERC721Impl::safe_transfer_from(ref state, ZERO(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid token ID',))] +// fn test_safeTransferFrom_nonexistent() { +// let mut state = STATE(); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, ZERO(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid receiver',))] +// fn test_safe_transfer_from_to_zero() { +// let mut state = setup(); +// testing::set_caller_address(OWNER()); +// ERC721Impl::safe_transfer_from(ref state, OWNER(), ZERO(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid receiver',))] +// fn test_safeTransferFrom_to_zero() { +// let mut state = setup(); +// testing::set_caller_address(OWNER()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, OWNER(), ZERO(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_owner() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_owner() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_owner_camel() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_camel_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_owner_camel() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_camel_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved_for_all() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved_for_all() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved_for_all_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved_for_all_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: unauthorized caller',))] +// fn test_safe_transfer_from_unauthorized() { +// let mut state = setup(); +// testing::set_caller_address(OTHER()); +// ERC721Impl::safe_transfer_from(ref state, OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: unauthorized caller',))] +// fn test_safeTransferFrom_unauthorized() { +// let mut state = setup(); +// testing::set_caller_address(OTHER()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// +// _transfer +// + +#[test] +#[available_gas(50000000)] +fn test__transfer() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, token_id); + + InternalImpl::_transfer(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test__transfer_nonexistent() { + let (world, mut state) = STATE(); + InternalImpl::_transfer(ref state, ZERO(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test__transfer_to_zero() { + let mut state = setup(); + InternalImpl::_transfer(ref state, OWNER(), ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: wrong sender',))] +fn test__transfer_from_invalid_owner() { + let mut state = setup(); + InternalImpl::_transfer(ref state, RECIPIENT(), OWNER(), TOKEN_ID); +} + +// +// _mint +// + +#[test] +#[available_gas(20000000)] +fn test__mint() { + let (world, mut state) = STATE(); + let recipient = RECIPIENT(); + let token_id = TOKEN_ID; + + assert_state_before_mint(@state, recipient); + InternalImpl::_mint(ref state, recipient, TOKEN_ID); + assert_event_transfer(ZERO(), recipient, token_id); + + assert_state_after_mint(@state, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test__mint_to_zero() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: token already minted',))] +fn test__mint_already_exist() { + let mut state = setup(); + InternalImpl::_mint(ref state, RECIPIENT(), TOKEN_ID); +} + +// // +// // _safe_mint +// // + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_receiver() { +// let mut state = STATE(); +// let recipient = setup_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(true)); +// assert_event_transfer(ZERO(), recipient, token_id); + +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_receiver_camel() { +// let mut state = STATE(); +// let recipient = setup_camel_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(true)); +// assert_event_transfer(ZERO(), recipient, token_id); + +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_account() { +// let mut state = STATE(); +// let account = setup_account(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,account); +// InternalImpl::_safe_mint(ref state, account, token_id, DATA(true)); +// assert_event_transfer(ZERO(), account, token_id); + +// assert_state_after_mint(@state,account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_account_camel() { +// let mut state = STATE(); +// let account = setup_camel_account(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,account); +// InternalImpl::_safe_mint(ref state, account, token_id, DATA(true)); +// assert_event_transfer(ZERO(), account, token_id); + +// assert_state_after_mint(@state,account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +// fn test__safe_mint_to_non_receiver() { +// let mut state = STATE(); +// let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(true)); +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe mint failed',))] +// fn test__safe_mint_to_receiver_failure() { +// let mut state = STATE(); +// let recipient = setup_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(false)); +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe mint failed',))] +// fn test__safe_mint_to_receiver_failure_camel() { +// let mut state = STATE(); +// let recipient = setup_camel_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(false)); +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid receiver',))] +// fn test__safe_mint_to_zero() { +// let mut state = STATE(); +// InternalImpl::_safe_mint(ref state, ZERO(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: token already minted',))] +// fn test__safe_mint_already_exist() { +// let mut state = setup(); +// InternalImpl::_safe_mint(ref state, RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// +// _burn +// + +#[test] +#[available_gas(20000000)] +fn test__burn() { + let mut state = setup(); + + InternalImpl::_approve(ref state, OTHER(), TOKEN_ID); + utils::drop_event(ZERO()); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership before'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner before'); + assert(ERC721Impl::get_approved(@state, TOKEN_ID) == OTHER(), 'Approval before'); + + InternalImpl::_burn(ref state, TOKEN_ID); + assert_event_transfer(OWNER(), ZERO(), TOKEN_ID); + + assert( + WorldInteractionsImpl::get_owner_of(@state, TOKEN_ID).address == ZERO(), 'Ownership after' + ); + assert(ERC721Impl::balance_of(@state, OWNER()) == 0, 'Balance of owner after'); + assert( + WorldInteractionsImpl::get_token_approval(@state, TOKEN_ID).address == ZERO(), + 'Approval after' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test__burn_nonexistent() { + let (mut world, mut state) = STATE(); + InternalImpl::_burn(ref state, TOKEN_ID); +} + +// +// _set_token_uri +// + +// #[test] +// #[available_gas(20000000)] +// fn test__set_token_uri() { +// let mut state = setup(); + +// assert(ERC721MetadataImpl::token_uri(@state, TOKEN_ID) == 0, 'URI should be 0'); +// InternalImpl::_set_token_uri(ref state, TOKEN_ID, URI); +// assert(ERC721MetadataImpl::token_uri(@state, TOKEN_ID) == URI, 'URI should be set'); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid token ID',))] +// fn test__set_token_uri_nonexistent() { +// let mut state = STATE(); +// InternalImpl::_set_token_uri(ref state, TOKEN_ID, URI); +// } + +// +// Helpers +// + +fn assert_state_before_transfer( + state: @ERC721::ContractState, + owner: ContractAddress, + recipient: ContractAddress, + token_id: u256 +) { + assert(ERC721Impl::owner_of(state, token_id) == owner, 'Ownership before'); + assert(ERC721Impl::balance_of(state, owner) == 1, 'Balance of owner before'); + assert(ERC721Impl::balance_of(state, recipient) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_transfer( + state: @ERC721::ContractState, + owner: ContractAddress, + recipient: ContractAddress, + token_id: u256 +) { + assert(ERC721Impl::owner_of(state, token_id) == recipient, 'Ownership after'); + assert(ERC721Impl::balance_of(state, owner) == 0, 'Balance of owner after'); + assert(ERC721Impl::balance_of(state, recipient) == 1, 'Balance of recipient after'); + assert(ERC721Impl::get_approved(state, token_id) == ZERO(), 'Approval not implicitly reset'); +} + +fn assert_state_before_mint(state: @ERC721::ContractState, recipient: ContractAddress) { + assert(ERC721Impl::balance_of(state, recipient) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_mint( + state: @ERC721::ContractState, recipient: ContractAddress, token_id: u256 +) { + assert(ERC721Impl::owner_of(state, token_id) == recipient, 'Ownership after'); + assert(ERC721Impl::balance_of(state, recipient) == 1, 'Balance of recipient after'); + assert(ERC721Impl::get_approved(state, token_id) == ZERO(), 'Approval implicitly set'); +} + +fn assert_event_approval_for_all( + owner: ContractAddress, operator: ContractAddress, approved: bool +) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.operator == operator, 'Invalid `operator`'); + assert(event.approved == approved, 'Invalid `approved`'); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_approval(owner: ContractAddress, approved: ContractAddress, token_id: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.approved == approved, 'Invalid `approved`'); + assert(event.token_id == token_id, 'Invalid `token_id`'); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_transfer(from: ContractAddress, to: ContractAddress, token_id: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.token_id == token_id, 'Invalid `token_id`'); + utils::assert_no_events_left(ZERO()); +} diff --git a/crates/dojo-erc/src/token/erc721.cairo b/crates/dojo-erc/src/token/erc721.cairo new file mode 100644 index 0000000000..92af52a460 --- /dev/null +++ b/crates/dojo-erc/src/token/erc721.cairo @@ -0,0 +1,5 @@ +mod erc721; +mod components; +mod interface; + +use erc721::ERC721; \ No newline at end of file diff --git a/crates/dojo-erc/src/token/erc721/components.cairo b/crates/dojo-erc/src/token/erc721/components.cairo new file mode 100644 index 0000000000..d42d1b040d --- /dev/null +++ b/crates/dojo-erc/src/token/erc721/components.cairo @@ -0,0 +1,48 @@ +use starknet::ContractAddress; + +#[derive(Component, Copy, Drop, Serde)] +struct ERC721Meta { + #[key] + token: ContractAddress, + name: felt252, + symbol: felt252, + base_uri: felt252, +} + +#[derive(Component, Copy, Drop, Serde)] +struct ERC721OperatorApproval { + #[key] + token: ContractAddress, + #[key] + owner: ContractAddress, + #[key] + operator: ContractAddress, + approved: bool +} + +#[derive(Component, Copy, Drop, Serde)] +struct ERC721Owner { + #[key] + token: ContractAddress, + #[key] + token_id: u256, + address: ContractAddress +} + +#[derive(Component, Copy, Drop, Serde)] +struct ERC721Balance { + #[key] + token: ContractAddress, + #[key] + account: ContractAddress, + amount: u256, +} + +#[derive(Component, Copy, Drop, Serde)] +struct ERC721TokenApproval { + #[key] + token: ContractAddress, + #[key] + token_id: u256, + address: ContractAddress, +} \ No newline at end of file diff --git a/crates/dojo-erc/src/token/erc721/erc721.cairo b/crates/dojo-erc/src/token/erc721/erc721.cairo new file mode 100644 index 0000000000..7809ef6616 --- /dev/null +++ b/crates/dojo-erc/src/token/erc721/erc721.cairo @@ -0,0 +1,443 @@ +#[starknet::contract] +mod ERC721 { + use dojo_erc::token::erc721::components::{ + ERC721Meta, ERC721OperatorApproval, ERC721Owner, ERC721Balance, ERC721TokenApproval + }; + use dojo_erc::token::erc721::interface; + use dojo_erc::token::erc721::interface::{IERC721, IERC721CamelOnly}; + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + use integer::BoundedInt; + use starknet::ContractAddress; + use starknet::{get_caller_address, get_contract_address}; + use zeroable::Zeroable; + use debug::PrintTrait; + + + #[storage] + struct Storage { + _world: ContractAddress, + } + + #[event] + #[derive(Copy, Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + ApprovalForAll: ApprovalForAll + } + + #[derive(Copy, Drop, starknet::Event)] + struct Transfer { + from: ContractAddress, + to: ContractAddress, + token_id: u256 + } + + #[derive(Copy, Drop, starknet::Event)] + struct Approval { + owner: ContractAddress, + approved: ContractAddress, + token_id: u256 + } + + #[derive(Copy, Drop, starknet::Event)] + struct ApprovalForAll { + owner: ContractAddress, + operator: ContractAddress, + approved: bool + } + + mod Errors { + const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID'; + const INVALID_ACCOUNT: felt252 = 'ERC721: invalid account'; + const UNAUTHORIZED: felt252 = 'ERC721: unauthorized caller'; + const APPROVAL_TO_OWNER: felt252 = 'ERC721: approval to owner'; + const SELF_APPROVAL: felt252 = 'ERC721: self approval'; + const INVALID_RECEIVER: felt252 = 'ERC721: invalid receiver'; + const ALREADY_MINTED: felt252 = 'ERC721: token already minted'; + const WRONG_SENDER: felt252 = 'ERC721: wrong sender'; + const SAFE_MINT_FAILED: felt252 = 'ERC721: safe mint failed'; + const SAFE_TRANSFER_FAILED: felt252 = 'ERC721: safe transfer failed'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + world: ContractAddress, + name: felt252, + symbol: felt252, + base_uri: felt252, + recipient: ContractAddress, + token_id: u256 + ) { + self._world.write(world); + self.initializer(name, symbol, base_uri); + self._mint(recipient, token_id); + } + + // + // External + // + + // #[external(v0)] + // impl SRC5Impl of ISRC5 { + // fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + // let unsafe_state = src5::SRC5::unsafe_new_contract_state(); + // src5::SRC5::SRC5Impl::supports_interface(@unsafe_state, interface_id) + // } + // } + + // #[external(v0)] + // impl SRC5CamelImpl of ISRC5Camel { + // fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { + // let unsafe_state = src5::SRC5::unsafe_new_contract_state(); + // src5::SRC5::SRC5CamelImpl::supportsInterface(@unsafe_state, interfaceId) + // } + // } + + #[external(v0)] + impl ERC721MetadataImpl of interface::IERC721Metadata { + fn name(self: @ContractState) -> felt252 { + self.get_meta().name + } + + fn symbol(self: @ContractState) -> felt252 { + self.get_meta().symbol + } + + fn token_uri(self: @ContractState, token_id: u256) -> felt252 { + assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + // TODO : concat with id + self.get_uri(token_id) + } + } + + #[external(v0)] + impl ERC721MetadataCamelOnlyImpl of interface::IERC721MetadataCamelOnly { + fn tokenURI(self: @ContractState, tokenId: u256) -> felt252 { + assert(self._exists(tokenId), Errors::INVALID_TOKEN_ID); + self.get_uri(tokenId) + } + } + + #[external(v0)] + impl ERC721Impl of interface::IERC721 { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + assert(account.is_non_zero(), Errors::INVALID_ACCOUNT); + self.get_balance(account).amount + } + + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + self._owner_of(token_id) + } + + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + self.get_token_approval(token_id).address + } + + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.get_operator_approval(owner, operator).approved + } + + fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + let owner = self._owner_of(token_id); + + let caller = get_caller_address(); + assert( + owner == caller || ERC721Impl::is_approved_for_all(@self, owner, caller), + Errors::UNAUTHORIZED + ); + self._approve(to, token_id); + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool + ) { + self._set_approval_for_all(get_caller_address(), operator, approved) + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + assert( + self._is_approved_or_owner(get_caller_address(), token_id), Errors::UNAUTHORIZED + ); + self._transfer(from, to, token_id); + } + + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) { + assert( + self._is_approved_or_owner(get_caller_address(), token_id), Errors::UNAUTHORIZED + ); + self._safe_transfer(from, to, token_id, data); + } + } + + #[external(v0)] + impl ERC721CamelOnlyImpl of interface::IERC721CamelOnly { + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + ERC721Impl::balance_of(self, account) + } + + fn ownerOf(self: @ContractState, tokenId: u256) -> ContractAddress { + ERC721Impl::owner_of(self, tokenId) + } + + fn getApproved(self: @ContractState, tokenId: u256) -> ContractAddress { + ERC721Impl::get_approved(self, tokenId) + } + + fn isApprovedForAll( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + ERC721Impl::is_approved_for_all(self, owner, operator) + } + + fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { + ERC721Impl::set_approval_for_all(ref self, operator, approved) + } + + fn transferFrom( + ref self: ContractState, from: ContractAddress, to: ContractAddress, tokenId: u256 + ) { + ERC721Impl::transfer_from(ref self, from, to, tokenId) + } + + fn safeTransferFrom( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + data: Span + ) { + ERC721Impl::safe_transfer_from(ref self, from, to, tokenId, data) + } + } + + // + // Internal + // + + #[generate_trait] + impl WorldInteractionsImpl of WorldInteractionsTrait { + fn world(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: self._world.read() } + } + + fn get_meta(self: @ContractState) -> ERC721Meta { + get!(self.world(), get_contract_address(), ERC721Meta) + } + + fn get_uri(self: @ContractState, token_id: u256) -> felt252 { + // TODO : concat with id when we have string type + self.get_meta().base_uri + } + + fn get_balance(self: @ContractState, account: ContractAddress) -> ERC721Balance { + get!(self.world(), (get_contract_address(), account), ERC721Balance) + } + + fn get_owner_of(self: @ContractState, token_id: u256) -> ERC721Owner { + get!(self.world(), (get_contract_address(), token_id), ERC721Owner) + } + + fn get_token_approval(self: @ContractState, token_id: u256) -> ERC721TokenApproval { + get!(self.world(), (get_contract_address(), token_id), ERC721TokenApproval) + } + + fn get_operator_approval( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> ERC721OperatorApproval { + get!(self.world(), (get_contract_address(), owner, operator), ERC721OperatorApproval) + } + + fn set_token_approval( + ref self: ContractState, + owner: ContractAddress, + to: ContractAddress, + token_id: u256, + emit: bool + ) { + set!( + self.world(), + ERC721TokenApproval { token: get_contract_address(), token_id, address: to, } + ); + if emit { + self.emit_event(Approval { owner, approved: to, token_id }); + } + } + + fn set_operator_approval( + ref self: ContractState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + set!( + self.world(), + ERC721OperatorApproval { token: get_contract_address(), owner, operator, approved } + ); + self.emit_event(ApprovalForAll { owner, operator, approved }); + } + + fn set_balance(ref self: ContractState, account: ContractAddress, amount: u256) { + set!(self.world(), ERC721Balance { token: get_contract_address(), account, amount }); + } + + fn set_owner(ref self: ContractState, token_id: u256, address: ContractAddress) { + set!(self.world(), ERC721Owner { token: get_contract_address(), token_id, address }); + } + + fn emit_event< + S, impl IntoImp: traits::Into, impl SDrop: Drop, impl SCopy: Copy + >( + ref self: ContractState, event: S + ) { + self.emit(event); + emit!(self.world(), event); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn initializer(ref self: ContractState, name: felt252, symbol: felt252, base_uri: felt252) { + let meta = ERC721Meta { token: get_contract_address(), name, symbol, base_uri }; + set!(self.world(), (meta)); + // let mut unsafe_state = src5::SRC5::unsafe_new_contract_state(); + // src5::SRC5::InternalImpl::register_interface(ref unsafe_state, interface::IERC721_ID); + // src5::SRC5::InternalImpl::register_interface( + // ref unsafe_state, interface::IERC721_METADATA_ID + // ); + } + + fn _owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + let owner = self.get_owner_of(token_id).address; + match owner.is_zero() { + bool::False(()) => owner, + bool::True(()) => panic_with_felt252(Errors::INVALID_TOKEN_ID) + } + } + + fn _exists(self: @ContractState, token_id: u256) -> bool { + let owner = self.get_owner_of(token_id).address; + owner.is_non_zero() + } + + fn _is_approved_or_owner( + self: @ContractState, spender: ContractAddress, token_id: u256 + ) -> bool { + let owner = self._owner_of(token_id); + let is_approved_for_all = ERC721Impl::is_approved_for_all(self, owner, spender); + owner == spender + || is_approved_for_all + || spender == ERC721Impl::get_approved(self, token_id) + } + + fn _approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + let owner = self._owner_of(token_id); + assert(owner != to, Errors::APPROVAL_TO_OWNER); + + self.set_token_approval(owner, to, token_id, true); + } + + fn _set_approval_for_all( + ref self: ContractState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + assert(owner != operator, Errors::SELF_APPROVAL); + self.set_operator_approval(owner, operator, approved); + } + + fn _mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + assert(!self._exists(token_id), Errors::ALREADY_MINTED); + + self.set_balance(to, self.get_balance(to).amount + 1); + self.set_owner(token_id, to); + + self.emit_event(Transfer { from: Zeroable::zero(), to, token_id }); + } + + fn _transfer( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + let owner = self._owner_of(token_id); + assert(from == owner, Errors::WRONG_SENDER); + + // Implicit clear approvals, no need to emit an event + self.set_token_approval(owner, Zeroable::zero(), token_id, false); + + self.set_balance(from, self.get_balance(from).amount - 1); + self.set_balance(to, self.get_balance(to).amount + 1); + self.set_owner(token_id, to); + + self.emit_event(Transfer { from, to, token_id }); + } + + fn _burn(ref self: ContractState, token_id: u256) { + let owner = self._owner_of(token_id); + + // Implicit clear approvals, no need to emit an event + self.set_token_approval(owner, Zeroable::zero(), token_id, false); + + self.set_balance(owner, self.get_balance(owner).amount - 1); + self.set_owner(token_id, Zeroable::zero()); + + self.emit_event(Transfer { from: owner, to: Zeroable::zero(), token_id }); + } + + fn _safe_mint( + ref self: ContractState, to: ContractAddress, token_id: u256, data: Span + ) { + self._mint(to, token_id); + // assert( + // _check_on_erc721_received(Zeroable::zero(), to, token_id, data), + // Errors::SAFE_MINT_FAILED + // ); + } + + fn _safe_transfer( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) { + self._transfer(from, to, token_id); + // assert( + // _check_on_erc721_received(from, to, token_id, data), Errors::SAFE_TRANSFER_FAILED + // ); + } + // fn _set_token_uri(ref self: ContractState, token_id: u256, token_uri: felt252) { + // assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + // self._token_uri.write(token_id, token_uri) + // } + } + +//#[internal] +// fn _check_on_erc721_received( +// from: ContractAddress, to: ContractAddress, token_id: u256, data: Span +// ) -> bool { +// if (DualCaseSRC5 { contract_address: to } +// .supports_interface(interface::IERC721_RECEIVER_ID)) { +// DualCaseERC721Receiver { contract_address: to } +// .on_erc721_received( +// get_caller_address(), from, token_id, data +// ) == interface::IERC721_RECEIVER_ID +// } else { +// DualCaseSRC5 { contract_address: to }.supports_interface(account::interface::ISRC6_ID) +// } +// } +} diff --git a/crates/dojo-erc/src/token/erc721/interface.cairo b/crates/dojo-erc/src/token/erc721/interface.cairo new file mode 100644 index 0000000000..730ad2e027 --- /dev/null +++ b/crates/dojo-erc/src/token/erc721/interface.cairo @@ -0,0 +1,60 @@ +use starknet::ContractAddress; + +const IERC721_ID: felt252 = 0x33eb2f84c309543403fd69f0d0f363781ef06ef6faeb0131ff16ea3175bd943; +const IERC721_METADATA_ID: felt252 = + 0x6069a70848f907fa57668ba1875164eb4dcee693952468581406d131081bbd; +const IERC721_RECEIVER_ID: felt252 = + 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; + +#[starknet::interface] +trait IERC721 { + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn owner_of(self: @TState, token_id: u256) -> ContractAddress; + fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256); + fn safe_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ); + fn approve(ref self: TState, to: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); + fn get_approved(self: @TState, token_id: u256) -> ContractAddress; + fn is_approved_for_all( + self: @TState, owner: ContractAddress, operator: ContractAddress + ) -> bool; +} + +#[starknet::interface] +trait IERC721CamelOnly { + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn ownerOf(self: @TState, tokenId: u256) -> ContractAddress; + fn transferFrom(ref self: TState, from: ContractAddress, to: ContractAddress, tokenId: u256); + fn safeTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + data: Span + ); + fn setApprovalForAll(ref self: TState, operator: ContractAddress, approved: bool); + fn getApproved(self: @TState, tokenId: u256) -> ContractAddress; + fn isApprovedForAll(self: @TState, owner: ContractAddress, operator: ContractAddress) -> bool; +} + +// +// IERC721Metadata +// + +#[starknet::interface] +trait IERC721Metadata { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn token_uri(self: @TState, token_id: u256) -> felt252; +} + +#[starknet::interface] +trait IERC721MetadataCamelOnly { + fn tokenURI(self: @TState, tokenId: u256) -> felt252; +} From 3eda0434e10dd8b350896156a5ec48b25aa0ad62 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 27 Sep 2023 17:06:54 +0700 Subject: [PATCH 08/14] fix: size return values invalid index (#929) --- crates/torii/client/src/contract/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/torii/client/src/contract/component.rs b/crates/torii/client/src/contract/component.rs index 9400ce6525..7c3f293057 100644 --- a/crates/torii/client/src/contract/component.rs +++ b/crates/torii/client/src/contract/component.rs @@ -90,7 +90,7 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { .await .map_err(ComponentError::ContractReaderError)?; - Ok(res[2]) + Ok(res[1]) } pub async fn layout( From 03c2f5b966860c44626412c129333ea51bd34252 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Wed, 27 Sep 2023 09:50:08 -0400 Subject: [PATCH 09/14] Rename some usages of component to model (#932) --- crates/dojo-core/src/executor_test.cairo | 2 +- crates/dojo-core/src/lib.cairo | 2 +- .../src/{component.cairo => model.cairo} | 9 ++------- crates/dojo-core/src/world_test.cairo | 4 ++-- crates/dojo-defi/src/market/components.cairo | 10 +++++----- .../dojo-erc/src/token/erc20_components.cairo | 6 +++--- .../src/token/erc721/components.cairo | 10 +++++----- crates/dojo-lang/src/inline_macros/set.rs | 10 +++++----- crates/dojo-lang/src/lib.rs | 2 +- crates/dojo-lang/src/manifest.rs | 2 +- .../dojo-lang/src/{component.rs => model.rs} | 14 ++++++------- crates/dojo-lang/src/plugin.rs | 14 ++++++------- crates/dojo-lang/src/plugin_test.rs | 2 +- .../dojo-lang/src/plugin_test_data/introspect | 6 +++--- .../src/plugin_test_data/{component => model} | 20 +++++++++---------- crates/dojo-lang/src/system.rs | 2 +- examples/ecs/src/components.cairo | 4 ++-- 17 files changed, 57 insertions(+), 62 deletions(-) rename crates/dojo-core/src/{component.cairo => model.cairo} (72%) rename crates/dojo-lang/src/{component.rs => model.rs} (91%) rename crates/dojo-lang/src/plugin_test_data/{component => model} (96%) diff --git a/crates/dojo-core/src/executor_test.cairo b/crates/dojo-core/src/executor_test.cairo index 0efbdf852b..cfb0f5f4fe 100644 --- a/crates/dojo-core/src/executor_test.cairo +++ b/crates/dojo-core/src/executor_test.cairo @@ -10,7 +10,7 @@ use starknet::class_hash::Felt252TryIntoClassHash; use dojo::executor::{executor, IExecutorDispatcher, IExecutorDispatcherTrait}; use dojo::world::{IWorldDispatcher}; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Foo { #[key] id: felt252, diff --git a/crates/dojo-core/src/lib.cairo b/crates/dojo-core/src/lib.cairo index 9370cb4f6a..450842abc6 100644 --- a/crates/dojo-core/src/lib.cairo +++ b/crates/dojo-core/src/lib.cairo @@ -4,7 +4,7 @@ mod database_test; mod executor; #[cfg(test)] mod executor_test; -mod component; +mod model; mod packing; #[cfg(test)] mod packing_test; diff --git a/crates/dojo-core/src/component.cairo b/crates/dojo-core/src/model.cairo similarity index 72% rename from crates/dojo-core/src/component.cairo rename to crates/dojo-core/src/model.cairo index 7366a0b869..d7e9ef7b42 100644 --- a/crates/dojo-core/src/component.cairo +++ b/crates/dojo-core/src/model.cairo @@ -1,4 +1,4 @@ -trait Component { +trait Model { fn name(self: @T) -> felt252; fn keys(self: @T) -> Span; fn values(self: @T) -> Span; @@ -6,13 +6,8 @@ trait Component { } #[starknet::interface] -trait IComponent { +trait IModel { fn name(self: @T) -> felt252; fn layout(self: @T) -> Span; fn schema(self: @T) -> Span; } - -#[starknet::interface] -trait ISystem { - fn name(self: @T) -> felt252; -} diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index 4259a98ce8..ef433e8ca2 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -14,7 +14,7 @@ use dojo::test_utils::{spawn_test_world, deploy_with_world_address}; // Components and Systems -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Foo { #[key] caller: ContractAddress, @@ -22,7 +22,7 @@ struct Foo { b: u128, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Fizz { #[key] caller: ContractAddress, diff --git a/crates/dojo-defi/src/market/components.cairo b/crates/dojo-defi/src/market/components.cairo index 1619ce5e5c..c7da959dfe 100644 --- a/crates/dojo-defi/src/market/components.cairo +++ b/crates/dojo-defi/src/market/components.cairo @@ -1,5 +1,5 @@ use starknet::ContractAddress; -use dojo::component::StorageIntrospection; +use dojo::model::StorageIntrospection; // Cubit fixed point math library use cubit::f128::types::fixed::Fixed; @@ -24,14 +24,14 @@ impl SchemaIntrospectionFixed of SchemaIntrospection { } } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Cash { #[key] player: ContractAddress, amount: u128, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Item { #[key] player: ContractAddress, @@ -40,7 +40,7 @@ struct Item { quantity: u128, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Liquidity { #[key] player: ContractAddress, @@ -49,7 +49,7 @@ struct Liquidity { shares: Fixed, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Market { #[key] item_id: u32, diff --git a/crates/dojo-erc/src/token/erc20_components.cairo b/crates/dojo-erc/src/token/erc20_components.cairo index bbde98ef81..ab68718dc9 100644 --- a/crates/dojo-erc/src/token/erc20_components.cairo +++ b/crates/dojo-erc/src/token/erc20_components.cairo @@ -1,6 +1,6 @@ use starknet::ContractAddress; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC20Balance { #[key] token: ContractAddress, @@ -9,7 +9,7 @@ struct ERC20Balance { amount: u256, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC20Allowance { #[key] token: ContractAddress, @@ -20,7 +20,7 @@ struct ERC20Allowance { amount: u256, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC20Meta { #[key] token: ContractAddress, diff --git a/crates/dojo-erc/src/token/erc721/components.cairo b/crates/dojo-erc/src/token/erc721/components.cairo index d42d1b040d..1bc4bcadd6 100644 --- a/crates/dojo-erc/src/token/erc721/components.cairo +++ b/crates/dojo-erc/src/token/erc721/components.cairo @@ -1,6 +1,6 @@ use starknet::ContractAddress; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC721Meta { #[key] token: ContractAddress, @@ -9,7 +9,7 @@ struct ERC721Meta { base_uri: felt252, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC721OperatorApproval { #[key] token: ContractAddress, @@ -20,7 +20,7 @@ struct ERC721OperatorApproval { approved: bool } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC721Owner { #[key] token: ContractAddress, @@ -29,7 +29,7 @@ struct ERC721Owner { address: ContractAddress } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC721Balance { #[key] token: ContractAddress, @@ -38,7 +38,7 @@ struct ERC721Balance { amount: u256, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct ERC721TokenApproval { #[key] token: ContractAddress, diff --git a/crates/dojo-lang/src/inline_macros/set.rs b/crates/dojo-lang/src/inline_macros/set.rs index 8595c11366..0542affd95 100644 --- a/crates/dojo-lang/src/inline_macros/set.rs +++ b/crates/dojo-lang/src/inline_macros/set.rs @@ -68,7 +68,7 @@ impl InlineMacroExprPlugin for SetMacro { return InlinePluginResult { code: None, diagnostics: vec![PluginDiagnostic { - message: "Invalid arguments: No components provided.".to_string(), + message: "Invalid arguments: No models provided.".to_string(), stable_ptr: arg_list.args(db).stable_ptr().untyped(), }], }; @@ -77,10 +77,10 @@ impl InlineMacroExprPlugin for SetMacro { for entity in bundle { builder.add_str(&format!( "\n let __set_macro_value__ = {}; - {}.set_entity(dojo::component::Component::name(@__set_macro_value__), \ - dojo::component::Component::keys(@__set_macro_value__), 0_u8, \ - dojo::component::Component::values(@__set_macro_value__), \ - dojo::component::Component::layout(@__set_macro_value__));", + {}.set_entity(dojo::model::Model::name(@__set_macro_value__), \ + dojo::model::Model::keys(@__set_macro_value__), 0_u8, \ + dojo::model::Model::values(@__set_macro_value__), \ + dojo::model::Model::layout(@__set_macro_value__));", entity, world.as_syntax_node().get_text(db), )); diff --git a/crates/dojo-lang/src/lib.rs b/crates/dojo-lang/src/lib.rs index ba4f83d75c..e761ad00f4 100644 --- a/crates/dojo-lang/src/lib.rs +++ b/crates/dojo-lang/src/lib.rs @@ -4,10 +4,10 @@ //! //! Learn more at [dojoengine.gg](http://dojoengine.gg). pub mod compiler; -pub mod component; pub mod inline_macros; pub mod introspect; mod manifest; +pub mod model; pub mod plugin; pub mod print; pub mod system; diff --git a/crates/dojo-lang/src/manifest.rs b/crates/dojo-lang/src/manifest.rs index 17b3cdc55a..04dde184a4 100644 --- a/crates/dojo-lang/src/manifest.rs +++ b/crates/dojo-lang/src/manifest.rs @@ -93,7 +93,7 @@ impl Manifest { module_id: ModuleId, compiled_classes: &HashMap)>, ) { - for component in &aux_data.components { + for component in &aux_data.models { let component = component.clone(); let name: SmolStr = component.name.clone().into(); if let Ok(Some(ModuleItemId::Struct(_))) = diff --git a/crates/dojo-lang/src/component.rs b/crates/dojo-lang/src/model.rs similarity index 91% rename from crates/dojo-lang/src/component.rs rename to crates/dojo-lang/src/model.rs index c05beeef88..ba84e76a50 100644 --- a/crates/dojo-lang/src/component.rs +++ b/crates/dojo-lang/src/model.rs @@ -9,15 +9,15 @@ use convert_case::{Case, Casing}; use dojo_world::manifest::Member; use crate::introspect::handle_introspect_struct; -use crate::plugin::{Component, DojoAuxData}; +use crate::plugin::{DojoAuxData, Model}; -/// A handler for Dojo code that modifies a component struct. +/// A handler for Dojo code that modifies a model struct. /// Parameters: /// * db: The semantic database. -/// * struct_ast: The AST of the component struct. +/// * struct_ast: The AST of the model struct. /// Returns: /// * A RewriteNode containing the generated code. -pub fn handle_component_struct( +pub fn handle_model_struct( db: &dyn SyntaxGroup, aux_data: &mut DojoAuxData, struct_ast: ItemStruct, @@ -38,7 +38,7 @@ pub fn handle_component_struct( if keys.is_empty() { diagnostics.push(PluginDiagnostic { - message: "Component must define atleast one #[key] attribute".into(), + message: "Model must define atleast one #[key] attribute".into(), stable_ptr: struct_ast.name(db).stable_ptr().untyped(), }); } @@ -68,12 +68,12 @@ pub fn handle_component_struct( members.iter().filter_map(|m| serialize_member(m, false)).collect::<_>(); let name = struct_ast.name(db).text(db); - aux_data.components.push(Component { name: name.to_string(), members: members.to_vec() }); + aux_data.models.push(Model { name: name.to_string(), members: members.to_vec() }); ( RewriteNode::interpolate_patched( " - impl $type_name$Component of dojo::component::Component<$type_name$> { + impl $type_name$Model of dojo::model::Model<$type_name$> { #[inline(always)] fn name(self: @$type_name$) -> felt252 { '$type_name$' diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 36db6b5c2d..767fdf068f 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -21,18 +21,18 @@ use semver::Version; use smol_str::SmolStr; use url::Url; -use crate::component::handle_component_struct; use crate::inline_macros::emit::EmitMacro; use crate::inline_macros::get::GetMacro; use crate::inline_macros::set::SetMacro; use crate::introspect::handle_introspect_struct; +use crate::model::handle_model_struct; use crate::print::derive_print; use crate::system::System; const SYSTEM_ATTR: &str = "system"; #[derive(Clone, Debug, PartialEq)] -pub struct Component { +pub struct Model { pub name: String, pub members: Vec, } @@ -46,8 +46,8 @@ pub struct SystemAuxData { /// Dojo related auxiliary data of the Dojo plugin. #[derive(Debug, Default, PartialEq)] pub struct DojoAuxData { - /// A list of components that were processed by the plugin. - pub components: Vec, + /// A list of models that were processed by the plugin. + pub models: Vec, /// A list of systems that were processed by the plugin and their component dependencies. pub systems: Vec, } @@ -150,13 +150,13 @@ impl MacroPlugin for DojoPlugin { continue; }; - // Get the text of the segment and check if it is "Component" + // Get the text of the segment and check if it is "Model" let derived = segment.ident(db).text(db); match derived.as_str() { - "Component" => { + "Model" => { let (component_rewrite_nodes, component_diagnostics) = - handle_component_struct(db, &mut aux_data, struct_ast.clone()); + handle_model_struct(db, &mut aux_data, struct_ast.clone()); rewrite_nodes.push(component_rewrite_nodes); diagnostics.extend(component_diagnostics); } diff --git a/crates/dojo-lang/src/plugin_test.rs b/crates/dojo-lang/src/plugin_test.rs index be26d616c2..7429e9c7dd 100644 --- a/crates/dojo-lang/src/plugin_test.rs +++ b/crates/dojo-lang/src/plugin_test.rs @@ -16,7 +16,7 @@ cairo_lang_test_utils::test_file_test!( expand_plugin, "src/plugin_test_data", { - component: "component", + model: "model", print: "print", introspect: "introspect", system: "system", diff --git a/crates/dojo-lang/src/plugin_test_data/introspect b/crates/dojo-lang/src/plugin_test_data/introspect index 07e7434705..81bb75b6ea 100644 --- a/crates/dojo-lang/src/plugin_test_data/introspect +++ b/crates/dojo-lang/src/plugin_test_data/introspect @@ -12,7 +12,7 @@ struct Vec2 { y: u32 } -#[derive(Component, Copy, Drop, Print, Introspect)] +#[derive(Model, Copy, Drop, Print, Introspect)] struct Position { #[key] player: ContractAddress, @@ -81,13 +81,13 @@ impl Vec2SchemaIntrospection of dojo::database::schema::SchemaIntrospection { +impl PositionModel of dojo::model::Model { #[inline(always)] fn name(self: @Position) -> felt252 { 'Position' diff --git a/crates/dojo-lang/src/plugin_test_data/component b/crates/dojo-lang/src/plugin_test_data/model similarity index 96% rename from crates/dojo-lang/src/plugin_test_data/component rename to crates/dojo-lang/src/plugin_test_data/model index 1009724cea..d2d3a2c57b 100644 --- a/crates/dojo-lang/src/plugin_test_data/component +++ b/crates/dojo-lang/src/plugin_test_data/model @@ -6,7 +6,7 @@ test_expand_plugin //! > cairo_code use serde::Serde; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Position { #[key] id: felt252, @@ -32,14 +32,14 @@ impl PositionImpl of PositionTrait { } } -#[derive(Component, Serde)] +#[derive(Model, Serde)] struct Roles { role_ids: Array } use starknet::ContractAddress; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Player { #[key] game: felt252, @@ -53,7 +53,7 @@ struct Player { use serde::Serde; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Position { #[key] id: felt252, @@ -61,7 +61,7 @@ struct Position { y: felt252 } -impl PositionComponent of dojo::component::Component { +impl PositionModel of dojo::model::Model { #[inline(always)] fn name(self: @Position) -> felt252 { 'Position' @@ -195,12 +195,12 @@ impl PositionImpl of PositionTrait { } -#[derive(Component, Serde)] +#[derive(Model, Serde)] struct Roles { role_ids: Array } -impl RolesComponent of dojo::component::Component { +impl RolesModel of dojo::model::Model { #[inline(always)] fn name(self: @Roles) -> felt252 { 'Roles' @@ -300,7 +300,7 @@ mod roles { use starknet::ContractAddress; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Player { #[key] game: felt252, @@ -309,7 +309,7 @@ struct Player { name: felt252, } -impl PlayerComponent of dojo::component::Component { +impl PlayerModel of dojo::model::Model { #[inline(always)] fn name(self: @Player) -> felt252 { 'Player' @@ -420,7 +420,7 @@ mod player { } //! > expected_diagnostics -error: Component must define atleast one #[key] attribute +error: Model must define atleast one #[key] attribute --> dummy_file.cairo:30:8 struct Roles { ^***^ diff --git a/crates/dojo-lang/src/system.rs b/crates/dojo-lang/src/system.rs index cabb5f7516..dd22a75068 100644 --- a/crates/dojo-lang/src/system.rs +++ b/crates/dojo-lang/src/system.rs @@ -71,7 +71,7 @@ impl System { name: name.clone(), content: builder.code, aux_data: Some(DynGeneratedFileAuxData::new(DojoAuxData { - components: vec![], + models: vec![], systems: vec![SystemAuxData { name, dependencies: system.dependencies.values().cloned().collect(), diff --git a/examples/ecs/src/components.cairo b/examples/ecs/src/components.cairo index 0fa9761372..df6b9d1155 100644 --- a/examples/ecs/src/components.cairo +++ b/examples/ecs/src/components.cairo @@ -68,7 +68,7 @@ impl DirectionIntoFelt252 of Into { } } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Model, Copy, Drop, Serde)] struct Moves { #[key] player: ContractAddress, @@ -82,7 +82,7 @@ struct Vec2 { y: u32 } -#[derive(Component, Copy, Drop, Print, Serde)] +#[derive(Model, Copy, Drop, Print, Serde)] struct Position { #[key] player: ContractAddress, From a9c0c63e7cb470e04054ae938ea9e8c32b474377 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Wed, 27 Sep 2023 11:20:52 -0400 Subject: [PATCH 10/14] Implement nested model schema storage (#916) * Implement nested model schema storage * Refactor for integration testing * Torii integration test work --- Cargo.lock | 6 + crates/dojo-test-utils/src/lib.rs | 1 + crates/dojo-test-utils/src/migration.rs | 15 + crates/dojo-types/src/component.rs | 148 ++++++---- crates/dojo-world/src/manifest.rs | 6 + crates/sozo/src/commands/completions.rs | 2 +- crates/sozo/src/lib.rs | 3 + crates/sozo/src/main.rs | 9 +- .../sozo/src/ops/migration/migration_test.rs | 57 +--- crates/sozo/src/ops/migration/mod.rs | 68 ++--- crates/torii/client/src/contract/component.rs | 2 +- .../client/src/contract/component_test.rs | 20 +- crates/torii/core/Cargo.toml | 2 + crates/torii/{server => core}/src/engine.rs | 110 ++++---- crates/torii/core/src/lib.rs | 2 +- crates/torii/core/src/processors/mod.rs | 2 +- .../core/src/processors/register_model.rs | 9 +- crates/torii/core/src/sql.rs | 256 +++++++++--------- crates/torii/core/src/sql_test.rs | 62 +++-- crates/torii/graphql/Cargo.toml | 4 + crates/torii/graphql/src/tests/common/mod.rs | 152 ----------- .../torii/graphql/src/tests/entities_test.rs | 37 ++- crates/torii/graphql/src/tests/mod.rs | 205 +++++++++++++- crates/torii/graphql/src/tests/models_test.rs | 4 +- .../graphql/src/tests/subscription_test.rs | 40 +-- .../torii/migrations/20230316154230_setup.sql | 12 +- crates/torii/server/src/cli.rs | 20 +- crates/torii/server/src/indexer.rs | 45 --- 28 files changed, 692 insertions(+), 607 deletions(-) create mode 100644 crates/dojo-test-utils/src/migration.rs create mode 100644 crates/sozo/src/lib.rs rename crates/torii/{server => core}/src/engine.rs (69%) delete mode 100644 crates/torii/graphql/src/tests/common/mod.rs delete mode 100644 crates/torii/server/src/indexer.rs diff --git a/Cargo.lock b/Cargo.lock index cd2a793654..8e4008272a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6896,6 +6896,8 @@ dependencies = [ "dojo-world", "futures-channel", "futures-util", + "hex", + "lazy_static", "log", "once_cell", "serde", @@ -6922,17 +6924,21 @@ dependencies = [ "base64 0.21.4", "camino", "chrono", + "dojo-test-utils", "dojo-types", "dojo-world", "indexmap 1.9.3", + "scarb-ui", "serde", "serde_json", + "sozo", "sqlx", "starknet", "starknet-crypto 0.6.0", "tokio", "tokio-stream", "tokio-util", + "torii-client", "torii-core", "tracing", "url", diff --git a/crates/dojo-test-utils/src/lib.rs b/crates/dojo-test-utils/src/lib.rs index e2f7b4af16..f8f7c63e82 100644 --- a/crates/dojo-test-utils/src/lib.rs +++ b/crates/dojo-test-utils/src/lib.rs @@ -1,4 +1,5 @@ pub mod compiler; +pub mod migration; pub mod rpc; pub mod sequencer; diff --git a/crates/dojo-test-utils/src/migration.rs b/crates/dojo-test-utils/src/migration.rs new file mode 100644 index 0000000000..4e5c001183 --- /dev/null +++ b/crates/dojo-test-utils/src/migration.rs @@ -0,0 +1,15 @@ +use std::path::PathBuf; + +use anyhow::Result; +use camino::Utf8PathBuf; +use dojo_world::manifest::Manifest; +use dojo_world::migration::strategy::{prepare_for_migration, MigrationStrategy}; +use dojo_world::migration::world::WorldDiff; +use starknet::macros::felt; + +pub fn prepare_migration(path: PathBuf) -> Result { + let target_dir = Utf8PathBuf::from_path_buf(path).unwrap(); + let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); + let world = WorldDiff::compute(manifest, None); + prepare_for_migration(None, Some(felt!("0x12345")), target_dir, world) +} diff --git a/crates/dojo-types/src/component.rs b/crates/dojo-types/src/component.rs index 82ecd13166..a1ed74fcbe 100644 --- a/crates/dojo-types/src/component.rs +++ b/crates/dojo-types/src/component.rs @@ -10,16 +10,101 @@ pub struct Member { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum Ty { - Simple(String), + Name(String), Struct(Struct), Enum(Enum), } +impl Ty { + pub fn name(&self) -> String { + match self { + Ty::Name(s) => s.clone(), + Ty::Struct(s) => s.name.clone(), + Ty::Enum(e) => e.name.clone(), + } + } + + pub fn flatten(&self) -> Vec { + let mut flattened = Ty::flatten_ty(self.clone()); + flattened.reverse(); + flattened + } + + fn flatten_ty(ty: Ty) -> Vec { + let mut items = vec![]; + match ty { + Ty::Name(_) => { + items.push(ty.clone()); + } + Ty::Struct(mut s) => { + for (i, member) in s.children.clone().iter().enumerate() { + match member.ty { + Ty::Struct(_) => { + items.extend(Ty::flatten_ty(member.ty.clone())); + } + Ty::Enum(_) => { + items.extend(Ty::flatten_ty(member.ty.clone())); + } + _ => {} + } + + s.children[i].ty = Ty::Name(member.ty.name()); + } + + items.push(Ty::Struct(s)) + } + Ty::Enum(mut e) => { + for (i, ty) in e.values.clone().iter().enumerate() { + match ty { + Ty::Struct(_) => { + items.extend(Ty::flatten_ty(ty.clone())); + } + Ty::Enum(_) => { + items.extend(Ty::flatten_ty(ty.clone())); + } + _ => {} + } + + e.values[i] = Ty::Name(ty.name()); + } + + items.push(Ty::Enum(e)) + } + }; + + items + } +} + impl std::fmt::Display for Ty { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut items = print_ty(self); + let mut items = self.flatten(); items.reverse(); - write!(f, "{}", items.join("\n\n")) + let str = items + .iter() + .map(|ty| match ty { + Ty::Name(s) => s.to_string(), + Ty::Struct(s) => { + let mut struct_str = format!("struct {} {{\n", s.name); + for member in &s.children { + struct_str.push_str(&format!("{},\n", format_member(member))); + } + struct_str.push('}'); + struct_str + } + Ty::Enum(e) => { + let mut enum_str = format!("enum {} {{\n", e.name); + for ty in &e.values { + enum_str.push_str(&format!(" {}\n", ty.name())); + } + enum_str.push('}'); + enum_str + } + }) + .collect::>() + .join("\n\n"); + + write!(f, "{}", str) } } @@ -35,63 +120,10 @@ pub struct Enum { pub values: Vec, } -fn format_name(ty: &Ty) -> String { - match ty { - Ty::Simple(s) => s.clone(), - Ty::Struct(s) => s.name.clone(), - Ty::Enum(e) => e.name.clone(), - } -} - fn format_member(m: &Member) -> String { if m.key { - format!(" #[key]\n {}: {}", m.name, format_name(&m.ty)) + format!(" #[key]\n {}: {}", m.name, m.ty.name()) } else { - format!(" {}: {}", m.name, format_name(&m.ty)) + format!(" {}: {}", m.name, m.ty.name()) } } - -fn print_ty(ty: &Ty) -> Vec { - let mut items = vec![]; - match ty { - Ty::Simple(s) => println!("{}", s), - Ty::Struct(s) => { - let mut struct_str = format!("struct {} {{\n", s.name); - for member in &s.children { - match member.ty { - Ty::Struct(_) => { - items.extend(print_ty(&member.ty)); - } - Ty::Enum(_) => { - items.extend(print_ty(&member.ty)); - } - _ => {} - } - - struct_str.push_str(&format!("{},\n", format_member(member))); - } - struct_str.push('}'); - items.push(struct_str); - } - Ty::Enum(e) => { - let mut enum_str = format!("enum {} {{\n", e.name); - for ty in &e.values { - match ty { - Ty::Struct(_) => { - items.extend(print_ty(ty)); - } - Ty::Enum(_) => { - items.extend(print_ty(ty)); - } - _ => {} - } - - enum_str.push_str(&format!(" {}\n", format_name(ty))); - } - enum_str.push('}'); - items.push(enum_str); - } - }; - - items -} diff --git a/crates/dojo-world/src/manifest.rs b/crates/dojo-world/src/manifest.rs index 892ca365c8..3189f9c0e0 100644 --- a/crates/dojo-world/src/manifest.rs +++ b/crates/dojo-world/src/manifest.rs @@ -49,6 +49,12 @@ pub struct Member { pub key: bool, } +impl From for Member { + fn from(m: dojo_types::component::Member) -> Self { + Self { name: m.name, ty: m.ty.name(), key: m.key } + } +} + /// Represents a declaration of a component. #[serde_as] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] diff --git a/crates/sozo/src/commands/completions.rs b/crates/sozo/src/commands/completions.rs index 123cb04659..65c99ac2bb 100644 --- a/crates/sozo/src/commands/completions.rs +++ b/crates/sozo/src/commands/completions.rs @@ -4,7 +4,7 @@ use anyhow::Result; use clap::{Args, CommandFactory}; use clap_complete::{generate, Shell}; -use crate::SozoArgs; +use crate::args::SozoArgs; #[derive(Args, Debug)] pub struct CompletionsArgs { diff --git a/crates/sozo/src/lib.rs b/crates/sozo/src/lib.rs new file mode 100644 index 0000000000..fc9ec51d87 --- /dev/null +++ b/crates/sozo/src/lib.rs @@ -0,0 +1,3 @@ +pub mod args; +pub mod commands; +pub mod ops; diff --git a/crates/sozo/src/main.rs b/crates/sozo/src/main.rs index b21d47e172..ac5d6c212d 100644 --- a/crates/sozo/src/main.rs +++ b/crates/sozo/src/main.rs @@ -8,12 +8,7 @@ use dojo_lang::plugin::CairoPluginRepository; use scarb::compiler::CompilerRepository; use scarb::core::Config; use scarb_ui::{OutputFormat, Ui}; - -mod args; -mod commands; -mod ops; - -use args::{Commands, SozoArgs}; +use sozo::args::{Commands, SozoArgs}; fn main() { let args = SozoArgs::parse(); @@ -46,5 +41,5 @@ fn cli_main(args: SozoArgs) -> Result<()> { .compilers(compilers) .build()?; - commands::run(args.command, &config) + sozo::commands::run(args.command, &config) } diff --git a/crates/sozo/src/ops/migration/migration_test.rs b/crates/sozo/src/ops/migration/migration_test.rs index 790c5188a7..94d6975146 100644 --- a/crates/sozo/src/ops/migration/migration_test.rs +++ b/crates/sozo/src/ops/migration/migration_test.rs @@ -1,12 +1,12 @@ use camino::Utf8PathBuf; +use dojo_test_utils::migration::prepare_migration; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, StarknetConfig, TestSequencer, }; use dojo_world::manifest::Manifest; use dojo_world::migration::strategy::prepare_for_migration; use dojo_world::migration::world::WorldDiff; -use scarb::core::Config; -use scarb_ui::Verbosity; +use scarb_ui::{OutputFormat, Ui, Verbosity}; use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::chain_id; use starknet::core::types::{BlockId, BlockTag}; @@ -20,7 +20,8 @@ use crate::ops::migration::execute_strategy; #[tokio::test(flavor = "multi_thread")] async fn migrate_with_auto_mine() { - let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); + let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); + let migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -28,23 +29,15 @@ async fn migrate_with_auto_mine() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) - .ui_verbosity(Verbosity::Quiet) - .build() - .unwrap(); - - let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); - let world = WorldDiff::compute(manifest, None); - - let migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir, world).unwrap(); - execute_strategy(&migration, &account, &config, None).await.unwrap(); + execute_strategy(&migration, &account, &ui, None).await.unwrap(); sequencer.stop().unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn migrate_with_block_time() { - let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); + let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); + let migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( SequencerConfig { block_time: Some(1000), ..Default::default() }, @@ -55,21 +48,14 @@ async fn migrate_with_block_time() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) - .ui_verbosity(Verbosity::Quiet) - .build() - .unwrap(); - - let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); - let world = WorldDiff::compute(manifest, None); - - let migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir, world).unwrap(); - execute_strategy(&migration, &account, &config, None).await.unwrap(); + execute_strategy(&migration, &account, &ui, None).await.unwrap(); + sequencer.stop().unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn migrate_with_small_fee_multiplier_will_fail() { - let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); + let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); + let migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( Default::default(), @@ -87,26 +73,17 @@ async fn migrate_with_small_fee_multiplier_will_fail() { ExecutionEncoding::Legacy, ); - let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) - .ui_verbosity(Verbosity::Quiet) - .build() - .unwrap(); - - let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); - let world = WorldDiff::compute(manifest, None); - - let migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir, world).unwrap(); - assert!( execute_strategy( &migration, &account, - &config, + &ui, Some(TransactionOptions { fee_estimate_multiplier: Some(0.2f64) }), ) .await .is_err() ); + sequencer.stop().unwrap(); } #[test] @@ -121,6 +98,7 @@ fn migrate_world_without_seed_will_fail() { #[ignore] #[tokio::test] async fn migration_from_remote() { + let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = @@ -136,18 +114,13 @@ async fn migration_from_remote() { ExecutionEncoding::Legacy, ); - let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) - .ui_verbosity(Verbosity::Quiet) - .build() - .unwrap(); - let manifest = Manifest::load_from_path(target_dir.clone()).unwrap(); let world = WorldDiff::compute(manifest, None); let migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir.clone(), world).unwrap(); - execute_strategy(&migration, &account, &config, None).await.unwrap(); + execute_strategy(&migration, &account, &ui, None).await.unwrap(); let local_manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); let remote_manifest = Manifest::from_remote( diff --git a/crates/sozo/src/ops/migration/mod.rs b/crates/sozo/src/ops/migration/mod.rs index 87486cabd0..62dcf9de00 100644 --- a/crates/sozo/src/ops/migration/mod.rs +++ b/crates/sozo/src/ops/migration/mod.rs @@ -11,6 +11,7 @@ use dojo_world::migration::{ }; use dojo_world::utils::TransactionWaiter; use scarb::core::Config; +use scarb_ui::Ui; use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; use starknet::core::types::{ BlockId, BlockTag, FieldElement, InvokeTransactionResult, StarknetError, @@ -94,7 +95,7 @@ where println!(" "); - let block_height = execute_strategy(&strategy, account, config, txn_config) + let block_height = execute_strategy(&strategy, account, config.ui(), txn_config) .await .map_err(|e| anyhow!(e)) .with_context(|| "Problem trying to migrate.")?; @@ -239,10 +240,10 @@ where // returns the Some(block number) at which migration world is deployed, returns none if world was // not redeployed -async fn execute_strategy( +pub async fn execute_strategy( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, - ws_config: &Config, + ui: &Ui, txn_config: Option, ) -> Result> where @@ -251,8 +252,8 @@ where { match &strategy.executor { Some(executor) => { - ws_config.ui().print_header("# Executor"); - deploy_contract(executor, "executor", vec![], migrator, ws_config, &txn_config).await?; + ui.print_header("# Executor"); + deploy_contract(executor, "executor", vec![], migrator, ui, &txn_config).await?; if strategy.world.is_none() { let addr = strategy.world_address()?; @@ -263,27 +264,27 @@ where TransactionWaiter::new(transaction_hash, migrator.provider()).await?; - ws_config.ui().print_hidden_sub(format!("Updated at: {transaction_hash:#x}")); + ui.print_hidden_sub(format!("Updated at: {transaction_hash:#x}")); } - ws_config.ui().print_sub(format!("Contract address: {:#x}", executor.contract_address)); + ui.print_sub(format!("Contract address: {:#x}", executor.contract_address)); } None => {} }; match &strategy.world { Some(world) => { - ws_config.ui().print_header("# World"); + ui.print_header("# World"); let calldata = vec![strategy.executor.as_ref().unwrap().contract_address]; - deploy_contract(world, "world", calldata, migrator, ws_config, &txn_config).await?; + deploy_contract(world, "world", calldata, migrator, ui, &txn_config).await?; - ws_config.ui().print_sub(format!("Contract address: {:#x}", world.contract_address)); + ui.print_sub(format!("Contract address: {:#x}", world.contract_address)); } None => {} }; - register_components(strategy, migrator, ws_config, txn_config.clone()).await?; - deploy_contracts(strategy, migrator, ws_config, txn_config).await?; + register_components(strategy, migrator, ui, txn_config.clone()).await?; + deploy_contracts(strategy, migrator, ui, txn_config).await?; // This gets current block numder if helpful // let block_height = migrator.provider().block_number().await.ok(); @@ -301,7 +302,7 @@ async fn deploy_contract( contract_id: &str, constructor_calldata: Vec, migrator: &SingleOwnerAccount, - ws_config: &Config, + ui: &Ui, txn_config: &Option, ) -> Result where @@ -319,15 +320,13 @@ where { Ok(val) => { if let Some(declare) = val.clone().declare { - ws_config.ui().print_hidden_sub(format!( + ui.print_hidden_sub(format!( "Declare transaction: {:#x}", declare.transaction_hash )); } - ws_config - .ui() - .print_hidden_sub(format!("Deploy transaction: {:#x}", val.transaction_hash)); + ui.print_hidden_sub(format!("Deploy transaction: {:#x}", val.transaction_hash)); Ok(ContractDeploymentOutput::Output(val)) } @@ -341,7 +340,7 @@ where async fn register_components( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, - ws_config: &Config, + ui: &Ui, txn_config: Option, ) -> Result> where @@ -354,33 +353,31 @@ where return Ok(None); } - ws_config.ui().print_header(format!("# Models ({})", components.len())); + ui.print_header(format!("# Models ({})", components.len())); let mut declare_output = vec![]; for c in components.iter() { - ws_config.ui().print(italic_message(&c.diff.name).to_string()); + ui.print(italic_message(&c.diff.name).to_string()); let res = c.declare(migrator, txn_config.clone().map(|c| c.into()).unwrap_or_default()).await; match res { Ok(output) => { - ws_config - .ui() - .print_hidden_sub(format!("transaction_hash: {:#x}", output.transaction_hash)); + ui.print_hidden_sub(format!("transaction_hash: {:#x}", output.transaction_hash)); declare_output.push(output); } // Continue if component is already declared Err(MigrationError::ClassAlreadyDeclared) => { - ws_config.ui().print_sub("Already declared"); + ui.print_sub("Already declared"); continue; } Err(e) => bail!("Failed to declare component {}: {e}", c.diff.name), } - ws_config.ui().print_sub(format!("Class hash: {:#x}", c.diff.local)); + ui.print_sub(format!("Class hash: {:#x}", c.diff.local)); } let world_address = strategy.world_address()?; @@ -392,7 +389,7 @@ where TransactionWaiter::new(transaction_hash, migrator.provider()).await?; - ws_config.ui().print_hidden_sub(format!("registered at: {transaction_hash:#x}")); + ui.print_hidden_sub(format!("registered at: {transaction_hash:#x}")); Ok(Some(RegisterOutput { transaction_hash, declare_output })) } @@ -400,7 +397,7 @@ where async fn deploy_contracts( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, - ws_config: &Config, + ui: &Ui, txn_config: Option, ) -> Result>> where @@ -413,26 +410,21 @@ where return Ok(vec![]); } - ws_config.ui().print_header(format!("# Contracts ({})", contracts.len())); + ui.print_header(format!("# Contracts ({})", contracts.len())); let mut deploy_output = vec![]; for contract in strategy.contracts.iter() { let name = &contract.diff.name; - ws_config.ui().print(italic_message(name).to_string()); - match deploy_contract(contract, name, vec![], migrator, ws_config, &txn_config).await? { + ui.print(italic_message(name).to_string()); + match deploy_contract(contract, name, vec![], migrator, ui, &txn_config).await? { ContractDeploymentOutput::Output(output) => { - ws_config - .ui() - .print_sub(format!("Contract address: {:#x}", output.contract_address)); - ws_config.ui().print_hidden_sub(format!( - "deploy transaction: {:#x}", - output.transaction_hash - )); + ui.print_sub(format!("Contract address: {:#x}", output.contract_address)); + ui.print_hidden_sub(format!("deploy transaction: {:#x}", output.transaction_hash)); deploy_output.push(Some(output)); } ContractDeploymentOutput::AlreadyDeployed(contract_address) => { - ws_config.ui().print_sub(format!("Already deployed: {:#x}", contract_address)); + ui.print_sub(format!("Already deployed: {:#x}", contract_address)); deploy_output.push(None); } } diff --git a/crates/torii/client/src/contract/component.rs b/crates/torii/client/src/contract/component.rs index 7c3f293057..967a0fde23 100644 --- a/crates/torii/client/src/contract/component.rs +++ b/crates/torii/client/src/contract/component.rs @@ -200,7 +200,7 @@ fn parse_ty(data: &[FieldElement]) -> Result(data: &[FieldElement]) -> Result> { let ty = parse_cairo_short_string(&data[0]).map_err(ComponentError::ParseCairoShortStringError)?; - Ok(Ty::Simple(ty)) + Ok(Ty::Name(ty)) } fn parse_struct(data: &[FieldElement]) -> Result> { diff --git a/crates/torii/client/src/contract/component_test.rs b/crates/torii/client/src/contract/component_test.rs index 4f0acea412..6e6b8ace6e 100644 --- a/crates/torii/client/src/contract/component_test.rs +++ b/crates/torii/client/src/contract/component_test.rs @@ -33,7 +33,7 @@ async fn test_component() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Simple("ContractAddress".to_string()), + ty: Ty::Name("ContractAddress".to_string()), key: true }, Member { @@ -43,12 +43,12 @@ async fn test_component() { children: vec![ Member { name: "x".to_string(), - ty: Ty::Simple("u32".to_string()), + ty: Ty::Name("u32".to_string()), key: false }, Member { name: "y".to_string(), - ty: Ty::Simple("u32".to_string()), + ty: Ty::Name("u32".to_string()), key: false } ] @@ -77,12 +77,12 @@ async fn test_component() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Simple("ContractAddress".to_string()), + ty: Ty::Name("ContractAddress".to_string()), key: true }, Member { name: "remaining".to_string(), - ty: Ty::Simple("u8".to_string()), + ty: Ty::Name("u8".to_string()), key: false }, Member { @@ -90,11 +90,11 @@ async fn test_component() { ty: Ty::Enum(Enum { name: "Direction".to_string(), values: vec![ - Ty::Simple("None".to_string()), - Ty::Simple("Left".to_string()), - Ty::Simple("Right".to_string()), - Ty::Simple("Up".to_string()), - Ty::Simple("Down".to_string()) + Ty::Name("None".to_string()), + Ty::Name("Left".to_string()), + Ty::Name("Right".to_string()), + Ty::Name("Up".to_string()), + Ty::Name("Down".to_string()) ] }), key: false diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index dcd128f0e6..166ea4d4ea 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -17,6 +17,8 @@ dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } futures-channel = "0.3.0" futures-util = "0.3.0" +hex = "0.4.3" +lazy_static = "1.4.0" log = "0.4.17" once_cell.workspace = true serde.workspace = true diff --git a/crates/torii/server/src/engine.rs b/crates/torii/core/src/engine.rs similarity index 69% rename from crates/torii/server/src/engine.rs rename to crates/torii/core/src/engine.rs index b7e95fe833..05597acabc 100644 --- a/crates/torii/server/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -7,43 +7,42 @@ use starknet::core::types::{ }; use starknet::core::utils::get_selector_from_name; use starknet::providers::Provider; -use starknet_crypto::FieldElement; use tokio::time::sleep; +use tokio_util::sync::CancellationToken; use torii_client::contract::world::WorldContractReader; -use torii_core::processors::{BlockProcessor, EventProcessor, TransactionProcessor}; -use torii_core::sql::{Executable, Sql}; use tracing::{error, info, warn}; -pub struct Processors { +use crate::processors::{BlockProcessor, EventProcessor, TransactionProcessor}; +use crate::sql::{Executable, Sql}; + +pub struct Processors { pub block: Vec>>, pub transaction: Vec>>, pub event: Vec>>, } -impl Default for Processors

{ +impl Default for Processors

{ fn default() -> Self { - Self { block: vec![], transaction: vec![], event: vec![] } + Self { block: vec![], event: vec![], transaction: vec![] } } } #[derive(Debug)] pub struct EngineConfig { pub block_time: Duration, - pub world_address: FieldElement, pub start_block: u64, } impl Default for EngineConfig { fn default() -> Self { - Self { - block_time: Duration::from_secs(1), - world_address: FieldElement::ZERO, - start_block: 0, - } + Self { block_time: Duration::from_secs(1), start_block: 0 } } } -pub struct Engine<'a, P: Provider + Sync + Send> { +pub struct Engine<'a, P: Provider + Sync> +where + P::Error: 'static, +{ world: &'a WorldContractReader<'a, P>, db: &'a Sql, provider: &'a P, @@ -51,7 +50,10 @@ pub struct Engine<'a, P: Provider + Sync + Send> { config: EngineConfig, } -impl<'a, P: Provider + Sync + Send> Engine<'a, P> { +impl<'a, P: Provider + Sync> Engine<'a, P> +where + P::Error: 'static, +{ pub fn new( world: &'a WorldContractReader<'a, P>, db: &'a Sql, @@ -62,10 +64,10 @@ impl<'a, P: Provider + Sync + Send> Engine<'a, P> { Self { world, db, provider, processors, config } } - pub async fn start(&self) -> Result<(), Box> { + pub async fn start(&self, cts: CancellationToken) -> Result<(), Box> { let db_head = self.db.head().await?; - let mut current_block_number = match db_head { + let current_block_number = match db_head { 0 => self.config.start_block, _ => { if self.config.start_block != 0 { @@ -76,45 +78,57 @@ impl<'a, P: Provider + Sync + Send> Engine<'a, P> { }; loop { + if cts.is_cancelled() { + break Ok(()); + } + sleep(self.config.block_time).await; + match self.sync_to_head(current_block_number).await { + Ok(block_with_txs) => block_with_txs, + Err(e) => { + error!("getting block: {}", e); + continue; + } + }; + } + } - let latest_block_with_txs = - match self.provider.get_block_with_txs(BlockId::Tag(BlockTag::Latest)).await { - Ok(block_with_txs) => block_with_txs, - Err(e) => { - error!("getting block: {}", e); - continue; - } - }; + pub async fn sync_to_head(&self, from: u64) -> Result> { + let latest_block_with_txs = + self.provider.get_block_with_txs(BlockId::Tag(BlockTag::Latest)).await?; + + let latest_block_number = match latest_block_with_txs { + MaybePendingBlockWithTxs::Block(latest_block_with_txs) => { + latest_block_with_txs.block_number + } + _ => return Err(anyhow::anyhow!("Getting latest block number").into()), + }; + + self.sync_range(from, latest_block_number).await?; + + Ok(latest_block_number) + } - let latest_block_number = match latest_block_with_txs { - MaybePendingBlockWithTxs::Block(latest_block_with_txs) => { - latest_block_with_txs.block_number + pub async fn sync_range(&self, mut from: u64, to: u64) -> Result<(), Box> { + // Process all blocks from current to latest. + while from <= to { + let block_with_txs = match self.provider.get_block_with_txs(BlockId::Number(from)).await + { + Ok(block_with_txs) => block_with_txs, + Err(e) => { + error!("getting block: {}", e); + continue; } - _ => continue, }; - // Process all blocks from current to latest. - while current_block_number <= latest_block_number { - let block_with_txs = match self - .provider - .get_block_with_txs(BlockId::Number(current_block_number)) - .await - { - Ok(block_with_txs) => block_with_txs, - Err(e) => { - error!("getting block: {}", e); - continue; - } - }; - - self.process(block_with_txs).await?; + self.process(block_with_txs).await?; - self.db.set_head(current_block_number).await?; - self.db.execute().await?; - current_block_number += 1; - } + self.db.set_head(from).await?; + self.db.execute().await?; + from += 1; } + + Ok(()) } async fn process(&self, block: MaybePendingBlockWithTxs) -> Result<(), Box> { @@ -152,7 +166,7 @@ impl<'a, P: Provider + Sync + Send> Engine<'a, P> { if let TransactionReceipt::Invoke(invoke_receipt) = receipt.clone() { for (event_idx, event) in invoke_receipt.events.iter().enumerate() { - if event.from_address != self.config.world_address { + if event.from_address != self.world.address { continue; } diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs index 515c3cef05..668937bca8 100644 --- a/crates/torii/core/src/lib.rs +++ b/crates/torii/core/src/lib.rs @@ -3,7 +3,7 @@ use sqlx::FromRow; use crate::types::SQLFieldElement; -// pub mod memory; +pub mod engine; pub mod processors; pub mod simple_broker; pub mod sql; diff --git a/crates/torii/core/src/processors/mod.rs b/crates/torii/core/src/processors/mod.rs index 85c006005b..71cf21d5ff 100644 --- a/crates/torii/core/src/processors/mod.rs +++ b/crates/torii/core/src/processors/mod.rs @@ -9,7 +9,7 @@ use crate::sql::Sql; pub mod register_model; pub mod register_system; pub mod store_set_record; -pub mod store_system_call; +// pub mod store_system_call; #[async_trait] pub trait EventProcessor { diff --git a/crates/torii/core/src/processors/register_model.rs b/crates/torii/core/src/processors/register_model.rs index 71275b8b0f..288996db59 100644 --- a/crates/torii/core/src/processors/register_model.rs +++ b/crates/torii/core/src/processors/register_model.rs @@ -1,6 +1,5 @@ use anyhow::{Error, Ok, Result}; use async_trait::async_trait; -use dojo_world::manifest::Model; use starknet::core::types::{BlockId, BlockTag, BlockWithTxs, Event, InvokeTransactionReceipt}; use starknet::core::utils::parse_cairo_short_string; use starknet::providers::Provider; @@ -30,10 +29,12 @@ impl EventProcessor

for RegisterModelProcessor ) -> Result<(), Error> { let name = parse_cairo_short_string(&event.data[0])?; let model = world.component(&name, BlockId::Tag(BlockTag::Latest)).await?; - let _schema = model.schema(BlockId::Tag(BlockTag::Latest)).await?; - info!("registered model: {}", name); + let schema = model.schema(BlockId::Tag(BlockTag::Latest)).await?; + let layout = model.layout(BlockId::Tag(BlockTag::Latest)).await?; + info!("Registered model: {}", name); + + db.register_model(schema, layout, event.data[1]).await?; - db.register_model(Model { name, class_hash: event.data[1], ..Default::default() }).await?; Ok(()) } } diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index c778a60769..59572fbfaa 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use anyhow::Result; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use dojo_world::manifest::{Manifest, Model, System}; +use dojo_types::component::Ty; +use dojo_world::manifest::{Manifest, System}; +use lazy_static::lazy_static; use sqlx::pool::PoolConnection; use sqlx::sqlite::SqliteRow; use sqlx::{Executor, Pool, Row, Sqlite}; @@ -19,6 +21,26 @@ use crate::types::{Entity, Model as ModelType}; #[path = "sql_test.rs"] mod test; +lazy_static! { + static ref CAIRO_TO_SQL_TYPE: HashMap = { + let mut m = HashMap::new(); + m.insert("u8".to_string(), "INTEGER".to_string()); + m.insert("u16".to_string(), "INTEGER".to_string()); + m.insert("u32".to_string(), "INTEGER".to_string()); + m.insert("u64".to_string(), "INTEGER".to_string()); + m.insert("u128".to_string(), "TEXT".to_string()); + m.insert("u256".to_string(), "TEXT".to_string()); + m.insert("usize".to_string(), "INTEGER".to_string()); + m.insert("bool".to_string(), "INTEGER".to_string()); + // m.insert("Cursor".to_string(), "TEXT".to_string()); + m.insert("ContractAddress".to_string(), "TEXT".to_string()); + m.insert("ClassHash".to_string(), "TEXT".to_string()); + // m.insert("DateTime".to_string(), "TEXT".to_string()); + m.insert("felt252".to_string(), "TEXT".to_string()); + m + }; +} + #[async_trait] pub trait Executable { async fn execute(&self) -> Result<()>; @@ -29,7 +51,6 @@ pub struct Sql { world_address: FieldElement, pool: Pool, query_queue: Mutex>, - sql_types: Mutex>, } impl Sql { @@ -53,29 +74,7 @@ impl Sql { tx.commit().await?; - let sql_types = HashMap::from([ - ("u8".to_string(), "INTEGER"), - ("u16".to_string(), "INTEGER"), - ("u32".to_string(), "INTEGER"), - ("u64".to_string(), "INTEGER"), - ("u128".to_string(), "TEXT"), - ("u256".to_string(), "TEXT"), - ("usize".to_string(), "INTEGER"), - ("bool".to_string(), "INTEGER"), - ("Cursor".to_string(), "TEXT"), - ("ContractAddress".to_string(), "TEXT"), - ("ClassHash".to_string(), "TEXT"), - ("DateTime".to_string(), "TEXT"), - ("felt252".to_string(), "TEXT"), - ("Enum".to_string(), "INTEGER"), - ]); - - Ok(Self { - pool, - world_address, - query_queue: Mutex::new(vec![]), - sql_types: Mutex::new(sql_types), - }) + Ok(Self { pool, world_address, query_queue: Mutex::new(vec![]) }) } pub async fn load_from_manifest(&self, manifest: Manifest) -> Result<()> { @@ -96,10 +95,6 @@ impl Sql { )]) .await; - for model in manifest.components { - self.register_model(model).await?; - } - for system in manifest.systems { self.register_system(system).await?; } @@ -151,53 +146,39 @@ impl Sql { Ok(()) } - pub async fn register_model(&self, model: Model) -> Result<()> { - let mut sql_types = self.sql_types.lock().await; + pub async fn register_model( + &self, + schema: Ty, + layout: Vec, + class_hash: FieldElement, + ) -> Result<()> { + let types = schema.flatten(); + + let root = types.first().unwrap(); + let root_name = root.name(); - let model_id = model.name.to_lowercase(); + let layout_blob = layout.iter().map(|x| (*x).try_into().unwrap()).collect::>(); let mut queries = vec![format!( - "INSERT INTO models (id, name, class_hash) VALUES ('{}', '{}', '{:#x}') ON \ - CONFLICT(id) DO UPDATE SET class_hash='{:#x}'", - model_id, model.name, model.class_hash, model.class_hash + "INSERT INTO models (id, name, class_hash, layout) VALUES ('{}', '{}', '{:#x}', '{}') \ + ON CONFLICT(id) DO UPDATE SET class_hash='{:#x}'", + root_name, + root_name, + class_hash, + hex::encode(&layout_blob), + class_hash )]; + queries.extend(build_model_query(root, 0, None)); - let mut model_table_query = format!( - "CREATE TABLE IF NOT EXISTS external_{} (entity_id TEXT NOT NULL PRIMARY KEY, ", - model.name.to_lowercase() - ); - - for member in &model.members { - // FIXME: defaults all unknown model types to Enum for now until we support nested - // models - let (sql_type, member_type) = match sql_types.get(&member.ty) { - Some(sql_type) => (*sql_type, member.ty.as_str()), - None => { - sql_types.insert(member.ty.clone(), "INTEGER"); - ("INTEGER", "Enum") - } - }; - - queries.push(format!( - "INSERT OR IGNORE INTO model_members (model_id, name, type, key) VALUES ('{}', \ - '{}', '{}', {})", - model_id, member.name, member_type, member.key, - )); - - model_table_query.push_str(&format!("external_{} {}, ", member.name, sql_type)); + for (model_idx, ty) in types[1..].iter().enumerate() { + queries.extend(build_model_query(ty, model_idx + 1, Some(root_name.clone()))); } - model_table_query.push_str( - "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (entity_id) REFERENCES entities(id));", - ); - queries.push(model_table_query); - self.queue(queries).await; // Since previous query has not been executed, we have to make sure created_at exists let created_at: DateTime = match sqlx::query("SELECT created_at FROM models WHERE id = ?") - .bind(model_id.clone()) + .bind(root_name.clone()) .fetch_one(&self.pool) .await { @@ -206,9 +187,9 @@ impl Sql { }; SimpleBroker::publish(ModelType { - id: model_id, - name: model.name, - class_hash: format!("{:#x}", model.class_hash), + id: root_name.clone(), + name: root_name, + class_hash: format!("{:#x}", class_hash), transaction_hash: "0x0".to_string(), created_at, }); @@ -219,10 +200,7 @@ impl Sql { let query = format!( "INSERT INTO systems (id, name, class_hash) VALUES ('{}', '{}', '{:#x}') ON \ CONFLICT(id) DO UPDATE SET class_hash='{:#x}'", - system.name.to_lowercase(), - system.name, - system.class_hash, - system.class_hash + system.name, system.name, system.class_hash, system.class_hash ); self.queue(vec![query]).await; Ok(()) @@ -242,39 +220,45 @@ impl Sql { let keys_str = felts_sql_string(&keys); let model_names = model_names_sql_string(entity_result, &model)?; - let insert_entities = format!( + let mut queries = vec![format!( "INSERT INTO entities (id, keys, model_names) VALUES ('{}', '{}', '{}') ON \ - CONFLICT(id) DO UPDATE SET - model_names=excluded.model_names, + CONFLICT(id) DO UPDATE SET model_names=excluded.model_names, \ updated_at=CURRENT_TIMESTAMP", entity_id, keys_str, model_names - ); + )]; - let member_names_result = - sqlx::query("SELECT * FROM model_members WHERE model_id = ? ORDER BY id ASC") - .bind(model.to_lowercase()) - .fetch_all(&self.pool) - .await?; + 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 (primitive_members, _): (Vec<_>, Vec<_>) = + members.into_iter().partition(|member| CAIRO_TO_SQL_TYPE.contains_key(&member.2)); // keys are part of model members, so combine keys and model values array let mut member_values: Vec = Vec::new(); member_values.extend(keys); member_values.extend(values); - let sql_types = self.sql_types.lock().await; - let names_str = members_sql_string(&member_names_result)?; - let values_str = values_sql_string(&member_names_result, &member_values, &sql_types)?; + 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}' {})", + format_value(&ty, &value).unwrap() + ) + }) + .collect(); - let insert_models = format!( - "INSERT OR REPLACE INTO external_{} (entity_id {}) VALUES ('{}' {})", - model.to_lowercase(), - names_str, - entity_id, - values_str - ); + queries.extend(insert_models); // tx commit required - self.queue(vec![insert_entities, insert_models]).await; + self.queue(queries).await; self.execute().await?; let query_result = sqlx::query("SELECT created_at FROM entities WHERE id = ?") @@ -325,7 +309,7 @@ impl Sql { '{:#x}', '{}')", calldata.iter().map(|c| format!("{:#x}", c)).collect::>().join(","), transaction_hash, - system.to_lowercase() + system ); self.queue(vec![query]).await; Ok(()) @@ -368,7 +352,6 @@ impl Executable for Sql { } let mut tx = self.pool.begin().await?; - for query in queries { tx.execute(sqlx::query(&query)).await?; } @@ -395,40 +378,61 @@ fn model_names_sql_string(entity_result: Option, new_model: &str) -> Ok(model_names) } -fn values_sql_string( - member_results: &[SqliteRow], - values: &[FieldElement], - sql_types: &HashMap, -) -> Result { - let types: Result> = - member_results.iter().map(|row| Ok(row.try_get::("type")?)).collect(); - - // format according to type - let values: Result> = values - .iter() - .zip(types?.iter()) - .map(|(value, ty)| match sql_types.get(ty).copied() { - Some("INTEGER") => Ok(format!(",'{}'", value)), - Some("TEXT") => Ok(format!(",'{:#x}'", value)), - _ => Err(anyhow::anyhow!("Unsupported type {}", ty)), - }) - .collect(); - - Ok(values?.join("")) -} - -fn members_sql_string(member_results: &[SqliteRow]) -> Result { - let names: Result> = member_results - .iter() - .map(|row| { - let name = row.try_get::("name")?; - Ok(format!(",external_{}", name)) - }) - .collect(); - - Ok(names?.join("")) +fn format_value(ty: &str, value: &FieldElement) -> Result { + match CAIRO_TO_SQL_TYPE.get(ty) { + Some(sql_type) => match sql_type.as_str() { + "INTEGER" => Ok(format!(", '{}'", value)), + "TEXT" => Ok(format!(", '{:#x}'", value)), + _ => Err(anyhow::anyhow!("Format not supported for type: {}", ty)), + }, + _ => Err(anyhow::anyhow!("Format not supported for type: {}", ty)), + } } fn felts_sql_string(felts: &[FieldElement]) -> String { felts.iter().map(|k| format!("{:#x}", k)).collect::>().join("/") + "/" } + +fn build_model_query(model: &Ty, model_idx: usize, parent_id: Option) -> Vec { + let name = if let Some(parent_id) = parent_id.clone() { + format!("{parent_id}${}", model.name()) + } else { + model.name() + }; + let model_id = if let Some(parent_id) = parent_id.clone() { parent_id } else { model.name() }; + + let mut queries = vec![]; + let mut query = + format!("CREATE TABLE IF NOT EXISTS [{}] (entity_id TEXT NOT NULL PRIMARY KEY, ", name); + + match model { + Ty::Struct(s) => { + for (member_idx, member) in s.children.iter().enumerate() { + if let Some(sql_type) = CAIRO_TO_SQL_TYPE.get(&member.ty.name()) { + query.push_str(&format!("external_{} {}, ", member.name, sql_type)); + }; + + queries.push(format!( + "INSERT OR IGNORE INTO model_members (id, model_id, model_idx, member_idx, \ + name, type, key) VALUES ('{name}', '{model_id}', '{model_idx}', \ + '{member_idx}', '{}', '{}', {})", + member.name, + member.ty.name(), + member.key, + )); + } + } + Ty::Enum(_) => {} + _ => {} + } + + query.push_str("created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); + + if let Some(id) = parent_id { + query.push_str(&format!("FOREIGN KEY (entity_id) REFERENCES {id} (entity_id), ")); + }; + + query.push_str("FOREIGN KEY (entity_id) REFERENCES entities(id));"); + queries.push(query); + queries +} diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index b90c871b8f..72111069f1 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -1,7 +1,6 @@ -use std::str::FromStr; - use camino::Utf8PathBuf; -use dojo_world::manifest::{Member, Model, System}; +use dojo_types::component::{Member, Struct, Ty}; +use dojo_world::manifest::System; use sqlx::sqlite::SqlitePool; use starknet::core::types::{Event, FieldElement}; @@ -19,10 +18,7 @@ async fn test_load_from_manifest(pool: SqlitePool) { state.load_from_manifest(manifest.clone()).await.unwrap(); let models = sqlx::query("SELECT * FROM models").fetch_all(&pool).await.unwrap(); - assert_eq!(models.len(), 2); - - let moves_models = sqlx::query("SELECT * FROM external_moves").fetch_all(&pool).await.unwrap(); - assert_eq!(moves_models.len(), 0); + assert_eq!(models.len(), 0); let mut world = state.world().await.unwrap(); @@ -48,32 +44,38 @@ async fn test_load_from_manifest(pool: SqlitePool) { assert_eq!(head, 1); state - .register_model(Model { - name: "Test".into(), - members: vec![Member { name: "test".into(), ty: "u32".into(), key: false }], - class_hash: FieldElement::TWO, - ..Default::default() - }) + .register_model( + Ty::Struct(Struct { + name: "Position".into(), + children: vec![Member { + name: "test".into(), + ty: Ty::Name("u32".to_string()), + key: false, + }], + }), + vec![], + FieldElement::TWO, + ) .await .unwrap(); state.execute().await.unwrap(); let (id, name, class_hash): (String, String, String) = - sqlx::query_as("SELECT id, name, class_hash FROM models WHERE id = 'test'") + sqlx::query_as("SELECT id, name, class_hash FROM models WHERE id = 'Position'") .fetch_one(&pool) .await .unwrap(); - assert_eq!(id, "test"); - assert_eq!(name, "Test"); + assert_eq!(id, "Position"); + assert_eq!(name, "Position"); assert_eq!(class_hash, format!("{:#x}", FieldElement::TWO)); - let test_models = sqlx::query("SELECT * FROM external_test").fetch_all(&pool).await.unwrap(); - assert_eq!(test_models.len(), 0); + let position_models = sqlx::query("SELECT * FROM [Position]").fetch_all(&pool).await.unwrap(); + assert_eq!(position_models.len(), 0); state .register_system(System { - name: "Test".into(), + name: "Position".into(), inputs: vec![], outputs: vec![], class_hash: FieldElement::THREE, @@ -85,13 +87,13 @@ async fn test_load_from_manifest(pool: SqlitePool) { state.execute().await.unwrap(); let (id, name, class_hash): (String, String, String) = - sqlx::query_as("SELECT id, name, class_hash FROM systems WHERE id = 'test'") + sqlx::query_as("SELECT id, name, class_hash FROM systems WHERE id = 'Position'") .fetch_one(&pool) .await .unwrap(); - assert_eq!(id, "test"); - assert_eq!(name, "Test"); + assert_eq!(id, "Position"); + assert_eq!(name, "Position"); assert_eq!(class_hash, format!("{:#x}", FieldElement::THREE)); state @@ -107,14 +109,14 @@ async fn test_load_from_manifest(pool: SqlitePool) { .await .unwrap(); - state - .store_system_call( - "Test".into(), - FieldElement::from_str("0x4").unwrap(), - &[FieldElement::ONE, FieldElement::TWO, FieldElement::THREE], - ) - .await - .unwrap(); + // state + // .store_system_call( + // "Test".into(), + // FieldElement::from_str("0x4").unwrap(), + // &[FieldElement::ONE, FieldElement::TWO, FieldElement::THREE], + // ) + // .await + // .unwrap(); state .store_event( diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 5847608b98..05e33083c2 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -16,8 +16,10 @@ async-trait.workspace = true base64 = "0.21.2" chrono.workspace = true 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" @@ -29,7 +31,9 @@ warp.workspace = true [dev-dependencies] camino.workspace = true +dojo-test-utils = { path = "../../dojo-test-utils" } dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } starknet-crypto.workspace = true starknet.workspace = true +torii-client = { path = "../client" } diff --git a/crates/torii/graphql/src/tests/common/mod.rs b/crates/torii/graphql/src/tests/common/mod.rs deleted file mode 100644 index 0ddb2d947a..0000000000 --- a/crates/torii/graphql/src/tests/common/mod.rs +++ /dev/null @@ -1,152 +0,0 @@ -use camino::Utf8PathBuf; -use serde::Deserialize; -use serde_json::Value; -use sqlx::SqlitePool; -use starknet::core::types::FieldElement; -use tokio_stream::StreamExt; -use torii_core::sql::{Executable, Sql}; - -use crate::schema::build_schema; - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Connection { - pub total_count: i64, - pub edges: Vec>, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Edge { - pub node: T, - pub cursor: String, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Entity { - pub model_names: String, - pub keys: Option>, - pub created_at: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Moves { - pub __typename: String, - pub remaining: u32, - pub last_direction: u8, - pub entity: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Position { - pub __typename: String, - pub x: u32, - pub y: u32, - pub entity: Option, -} - -pub enum Paginate { - Forward, - 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; - - assert!(res.errors.is_empty(), "GraphQL query returned errors: {:?}", res.errors); - serde_json::to_value(res.data).expect("Failed to serialize GraphQL response") -} - -pub async fn run_graphql_subscription( - pool: &SqlitePool, - subscription: &str, -) -> async_graphql::Value { - // Build dynamic schema - let schema = build_schema(pool).await.unwrap(); - schema.execute_stream(subscription).next().await.unwrap().into_result().unwrap().data - // fn subscribe() is called from inside dynamic subscription -} - -pub async fn entity_fixtures(pool: &SqlitePool) { - let state = init(pool).await; - - // 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]; - state.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(), - ]; - state.set_entity("Position".to_string(), key, position_values.clone()).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(), - ]; - state.set_entity("Moves".to_string(), key.clone(), moves_values).await.unwrap(); - state.set_entity("Position".to_string(), key, position_values).await.unwrap(); - - state.execute().await.unwrap(); -} - -pub async fn init(pool: &SqlitePool) -> Sql { - let manifest = dojo_world::manifest::Manifest::load_from_path( - Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev/manifest.json".into()) - .unwrap(), - ) - .unwrap(); - - let state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - state.load_from_manifest(manifest).await.unwrap(); - state -} - -pub async fn paginate( - pool: &SqlitePool, - cursor: Option, - direction: Paginate, - page_size: usize, -) -> Connection { - let (first_last, before_after) = match direction { - Paginate::Forward => ("first", "after"), - Paginate::Backward => ("last", "before"), - }; - - let cursor = cursor.map_or(String::new(), |c| format!(", {before_after}: \"{c}\"")); - let query = format!( - " - {{ - entities ({first_last}: {page_size} {cursor}) - {{ - totalCount - edges {{ - cursor - node {{ - modelNames - }} - }} - }} - }} - " - ); - - let value = run_graphql_query(pool, &query).await; - let entities = value.get("entities").ok_or("entities not found").unwrap(); - serde_json::from_value(entities.clone()).unwrap() -} diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index 3acf973c7f..6ea97362e9 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -1,15 +1,36 @@ #[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::common::{ - entity_fixtures, paginate, run_graphql_query, Entity, Moves, Paginate, Position, + use crate::tests::{ + bootstrap_engine, create_pool, entity_fixtures, paginate, run_graphql_query, Entity, Moves, + Paginate, Position, }; - #[sqlx::test(migrations = "../migrations")] - async fn test_entity(pool: SqlitePool) { - entity_fixtures(&pool).await; + #[tokio::test(flavor = "multi_thread")] + async fn test_entity() { + let pool = create_pool().await; + let 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, &db, &provider, &migration, &sequencer).await; + + entity_fixtures(&db).await; + let entity_id = poseidon_hash_many(&[FieldElement::ONE]); let query = format!( r#" @@ -31,7 +52,8 @@ mod tests { #[ignore] #[sqlx::test(migrations = "../migrations")] async fn test_entity_models(pool: SqlitePool) { - entity_fixtures(&pool).await; + let db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); + entity_fixtures(&db).await; let entity_id = poseidon_hash_many(&[FieldElement::THREE]); let query = format!( @@ -70,7 +92,8 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn test_entities_pagination(pool: SqlitePool) { - entity_fixtures(&pool).await; + let db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); + entity_fixtures(&db).await; let page_size = 2; diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index b578891237..e5ed72f2c5 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -1,4 +1,203 @@ -mod common; +use camino::Utf8PathBuf; +use dojo_test_utils::sequencer::TestSequencer; +use dojo_world::migration::strategy::MigrationStrategy; +use scarb_ui::{OutputFormat, Ui, Verbosity}; +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 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::{Executable, Sql}; + mod entities_test; -mod models_test; -mod subscription_test; +// mod models_test; +// mod subscription_test; + +use crate::schema::build_schema; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Connection { + pub total_count: i64, + pub edges: Vec>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Edge { + pub node: T, + pub cursor: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Entity { + pub model_names: String, + pub keys: Option>, + pub created_at: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Moves { + pub __typename: String, + pub remaining: u32, + pub last_direction: u8, + pub entity: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Position { + pub __typename: String, + pub x: u32, + pub y: u32, + pub entity: Option, +} + +pub enum Paginate { + Forward, + 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; + + assert!(res.errors.is_empty(), "GraphQL query returned errors: {:?}", res.errors); + 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 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 engine = Engine::new( + world, + db, + provider, + Processors { + event: vec![ + Box::new(RegisterModelProcessor), + Box::new(RegisterSystemProcessor), + Box::new(StoreSetRecordProcessor), + ], + ..Processors::default() + }, + EngineConfig::default(), + ); + + let _ = engine.sync_to_head(0).await?; + + Ok(engine) +} + +#[allow(dead_code)] +pub async fn run_graphql_subscription( + pool: &SqlitePool, + subscription: &str, +) -> async_graphql::Value { + // Build dynamic schema + let schema = build_schema(pool).await.unwrap(); + schema.execute_stream(subscription).next().await.unwrap().into_result().unwrap().data + // fn subscribe() is called from inside dynamic subscription +} + +pub async fn entity_fixtures(db: &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(); + + // 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.execute().await.unwrap(); +} + +pub async fn paginate( + pool: &SqlitePool, + cursor: Option, + direction: Paginate, + page_size: usize, +) -> Connection { + let (first_last, before_after) = match direction { + Paginate::Forward => ("first", "after"), + Paginate::Backward => ("last", "before"), + }; + + let cursor = cursor.map_or(String::new(), |c| format!(", {before_after}: \"{c}\"")); + let query = format!( + " + {{ + entities ({first_last}: {page_size} {cursor}) + {{ + totalCount + edges {{ + cursor + node {{ + modelNames + }} + }} + }} + }} + " + ); + + let value = run_graphql_query(pool, &query).await; + let entities = value.get("entities").ok_or("entities not found").unwrap(); + serde_json::from_value(entities.clone()).unwrap() +} diff --git a/crates/torii/graphql/src/tests/models_test.rs b/crates/torii/graphql/src/tests/models_test.rs index 824e8487be..4d61e5a08d 100644 --- a/crates/torii/graphql/src/tests/models_test.rs +++ b/crates/torii/graphql/src/tests/models_test.rs @@ -2,9 +2,7 @@ mod tests { use sqlx::SqlitePool; - use crate::tests::common::{ - entity_fixtures, run_graphql_query, Connection, Edge, Moves, Position, - }; + use crate::tests::{entity_fixtures, run_graphql_query, Connection, Edge, Moves, Position}; type OrderTestFn = dyn Fn(&Vec>) -> bool; diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index f58d567350..dea292bdb2 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -3,19 +3,21 @@ mod tests { use std::time::Duration; use async_graphql::value; - use dojo_world::manifest::{Member, Model}; + use dojo_types::component::{Member, Struct, Ty}; use sqlx::SqlitePool; use starknet_crypto::{poseidon_hash_many, FieldElement}; use tokio::sync::mpsc; + use tokio_util::sync::CancellationToken; use torii_core::sql::Sql; - use crate::tests::common::{init, run_graphql_subscription}; + use crate::tests::{init, run_graphql_subscription}; #[sqlx::test(migrations = "../migrations")] async fn test_entity_subscription(pool: SqlitePool) { // Sleep in order to run this test in a single thread tokio::time::sleep(Duration::from_secs(1)).await; - let state = init(&pool).await; + let cts = CancellationToken::new(); + let state = init(cts, &pool).await; // 0. Preprocess expected entity value let key = vec![FieldElement::ONE]; let entity_id = format!("{:#x}", poseidon_hash_many(&key)); @@ -59,7 +61,8 @@ mod tests { async fn test_entity_subscription_with_id(pool: SqlitePool) { // Sleep in order to run this test in a single thread tokio::time::sleep(Duration::from_secs(1)).await; - let state = init(&pool).await; + let cts = CancellationToken::new(); + let state = init(cts, &pool).await; // 0. Preprocess expected entity value let key = vec![FieldElement::ONE]; let entity_id = format!("{:#x}", poseidon_hash_many(&key)); @@ -119,13 +122,16 @@ mod tests { // 1. Open process and sleep.Go to execute subscription tokio::time::sleep(Duration::from_secs(1)).await; - let model = Model { + let model = Ty::Struct(Struct { name, - members: vec![Member { name: "test".into(), ty: "u32".into(), key: false }], - class_hash, - ..Default::default() - }; - state.register_model(model).await.unwrap(); + children: vec![Member { + name: "test".into(), + ty: Ty::Name("u32".to_string()), + key: false, + }], + }); + state.register_model(model, vec![], class_hash).await.unwrap(); + // 3. fn publish() is called from state.set_entity() tx.send(()).await.unwrap(); @@ -168,13 +174,15 @@ mod tests { // 1. Open process and sleep.Go to execute subscription tokio::time::sleep(Duration::from_secs(1)).await; - let model = Model { + let model = Ty::Struct(Struct { name, - members: vec![Member { name: "test".into(), ty: "u32".into(), key: false }], - class_hash, - ..Default::default() - }; - state.register_model(model).await.unwrap(); + children: vec![Member { + name: "test".into(), + ty: Ty::Name("u32".to_string()), + key: false, + }], + }); + state.register_model(model, vec![], class_hash).await.unwrap(); // 3. fn publish() is called from state.set_entity() tx.send(()).await.unwrap(); diff --git a/crates/torii/migrations/20230316154230_setup.sql b/crates/torii/migrations/20230316154230_setup.sql index 280596cec8..6d06ca0aef 100644 --- a/crates/torii/migrations/20230316154230_setup.sql +++ b/crates/torii/migrations/20230316154230_setup.sql @@ -18,20 +18,22 @@ CREATE TABLE models ( name TEXT NOT NULL, class_hash TEXT NOT NULL, transaction_hash TEXT, + layout BLOB NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_models_created_at ON models (created_at); CREATE TABLE model_members( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id TEXT NOT NULL, + model_idx INTEGER NOT NULL, + member_idx INTEGER NOT NULL, model_id TEXT NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL, key BOOLEAN NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (model_id) REFERENCES models(id) - UNIQUE (model_id, name) + PRIMARY KEY (id, model_idx) FOREIGN KEY (model_id) REFERENCES models(id) UNIQUE (id, member_idx) ); CREATE INDEX idx_model_members_model_id ON model_members (model_id); @@ -44,7 +46,7 @@ CREATE TABLE system_calls ( created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (system_id) REFERENCES systems(id), UNIQUE (transaction_hash) -); +); CREATE INDEX idx_system_calls_created_at ON system_calls (created_at); @@ -67,6 +69,7 @@ CREATE TABLE entities ( ); CREATE INDEX idx_entities_keys ON entities (keys); + CREATE INDEX idx_entities_keys_create_on ON entities (keys, created_at); CREATE TABLE events ( @@ -78,4 +81,5 @@ CREATE TABLE events ( ); CREATE INDEX idx_events_keys ON events (keys); + CREATE INDEX idx_events_created_at ON events (created_at); \ No newline at end of file diff --git a/crates/torii/server/src/cli.rs b/crates/torii/server/src/cli.rs index 9388e7ca43..80f73dd460 100644 --- a/crates/torii/server/src/cli.rs +++ b/crates/torii/server/src/cli.rs @@ -14,20 +14,15 @@ use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use tokio_util::sync::CancellationToken; 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::processors::store_system_call::StoreSystemCallProcessor; use torii_core::sql::Sql; use tracing::error; use tracing_subscriber::fmt; use url::Url; -use crate::engine::Processors; -use crate::indexer::Indexer; - -mod engine; -mod indexer; mod server; /// Dojo World Indexer @@ -99,19 +94,24 @@ async fn main() -> anyhow::Result<()> { Box::new(RegisterSystemProcessor), Box::new(StoreSetRecordProcessor), ], - transaction: vec![Box::new(StoreSystemCallProcessor)], + // transaction: vec![Box::new(StoreSystemCallProcessor)], ..Processors::default() }; - let indexer = - Indexer::new(&world, &db, &provider, processors, manifest, world_address, args.start_block); + let engine = Engine::new( + &world, + &db, + &provider, + processors, + EngineConfig { start_block: args.start_block, ..Default::default() }, + ); let addr = format!("{}:{}", args.host, args.port) .parse::() .expect("able to parse address"); tokio::select! { - res = indexer.start() => { + res = engine.start(cts) => { if let Err(e) = res { error!("Indexer failed with error: {e}"); } diff --git a/crates/torii/server/src/indexer.rs b/crates/torii/server/src/indexer.rs deleted file mode 100644 index 649f35c91d..0000000000 --- a/crates/torii/server/src/indexer.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::error::Error; - -use dojo_world::manifest::Manifest; -use starknet::providers::Provider; -use starknet_crypto::FieldElement; -use torii_client::contract::world::WorldContractReader; -use torii_core::sql::Sql; -use tracing::info; - -use crate::engine::{Engine, EngineConfig, Processors}; - -#[allow(dead_code)] -pub struct Indexer<'a, P: Provider + Sync + Send> { - world: &'a WorldContractReader<'a, P>, - db: &'a Sql, - provider: &'a P, - engine: Engine<'a, P>, - manifest: Manifest, -} - -impl<'a, P: Provider + Sync + Send> Indexer<'a, P> { - pub fn new( - world: &'a WorldContractReader<'a, P>, - db: &'a Sql, - provider: &'a P, - processors: Processors

, - manifest: Manifest, - world_address: FieldElement, - start_block: u64, - ) -> Self { - let engine = Engine::new( - world, - db, - provider, - processors, - EngineConfig { world_address, start_block, ..Default::default() }, - ); - Self { world, db, provider, engine, manifest } - } - - pub async fn start(&self) -> Result<(), Box> { - info!("starting indexer"); - self.engine.start().await - } -} From 28eebb8bd85cf24c3f27d649383bcb7577c7e709 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Wed, 27 Sep 2023 12:44:07 -0400 Subject: [PATCH 11/14] Compute packed size (#933) --- crates/dojo-core/src/model.cairo | 1 + crates/dojo-core/src/packing.cairo | 24 ++++++++++++++- crates/dojo-core/src/packing_test.cairo | 30 ++++++++++++++++++- crates/dojo-erc/src/tests/erc20_tests.cairo | 2 +- crates/dojo-erc/src/tests/erc721_tests.cairo | 6 ++-- crates/dojo-lang/src/inline_macros/get.rs | 9 ++++-- .../dojo-lang/src/manifest_test_data/manifest | 4 +-- crates/dojo-lang/src/model.rs | 8 +++++ .../dojo-lang/src/plugin_test_data/introspect | 8 +++++ crates/dojo-lang/src/plugin_test_data/model | 24 +++++++++++++++ crates/torii/graphql/src/object/model.rs | 1 + 11 files changed, 106 insertions(+), 11 deletions(-) diff --git a/crates/dojo-core/src/model.cairo b/crates/dojo-core/src/model.cairo index d7e9ef7b42..2547a9275a 100644 --- a/crates/dojo-core/src/model.cairo +++ b/crates/dojo-core/src/model.cairo @@ -3,6 +3,7 @@ trait Model { fn keys(self: @T) -> Span; fn values(self: @T) -> Span; fn layout(self: @T) -> Span; + fn packed_size(self: @T) -> usize; } #[starknet::interface] diff --git a/crates/dojo-core/src/packing.cairo b/crates/dojo-core/src/packing.cairo index ccef3b0164..2d7bff76a6 100644 --- a/crates/dojo-core/src/packing.cairo +++ b/crates/dojo-core/src/packing.cairo @@ -3,7 +3,6 @@ use array::{ArrayTrait, SpanTrait}; use traits::{Into, TryInto}; use integer::{U256BitAnd, U256BitOr, U256BitXor, upcast, downcast, BoundedInt}; use option::OptionTrait; -use debug::PrintTrait; fn pack(ref packed: Array, ref unpacked: Span, ref layout: Span) { assert(unpacked.len() == layout.len(), 'mismatched input lens'); @@ -22,6 +21,29 @@ fn pack(ref packed: Array, ref unpacked: Span, ref layout: Spa packed.append(packing); } +fn calculate_packed_size(ref layout: Span) -> usize { + let mut size = 1; + let mut partial = 0_usize; + + loop { + match layout.pop_front() { + Option::Some(item) => { + let item_size: usize = (*item).into(); + partial += item_size; + if (partial > 251) { + size += 1; + partial = item_size; + } + }, + Option::None(_) => { + break; + } + }; + }; + + size +} + fn unpack(ref unpacked: Array, ref packed: Span, ref layout: Span) { let mut unpacking: felt252 = 0x0; let mut offset: u8 = 251; diff --git a/crates/dojo-core/src/packing_test.cairo b/crates/dojo-core/src/packing_test.cairo index bc67691133..4935e9bc84 100644 --- a/crates/dojo-core/src/packing_test.cairo +++ b/crates/dojo-core/src/packing_test.cairo @@ -1,6 +1,6 @@ use array::{ArrayTrait, SpanTrait}; use starknet::{ClassHash, ContractAddress, Felt252TryIntoContractAddress, Felt252TryIntoClassHash}; -use dojo::packing::{shl, shr, fpow, pack, unpack, pack_inner, unpack_inner}; +use dojo::packing::{shl, shr, fpow, pack, unpack, pack_inner, unpack_inner, calculate_packed_size}; use integer::U256BitAnd; use option::OptionTrait; use debug::PrintTrait; @@ -312,3 +312,31 @@ fn test_pack_unpack_felt252_single() { let output = serde::Serde::::deserialize(ref unpacked_span).unwrap(); assert(input == output, 'invalid output'); } + +#[test] +#[available_gas(9000000)] +fn test_calculate_packed_size() { + let mut layout = array![128, 32].span(); + let got = calculate_packed_size(ref layout); + assert(got == 1, 'invalid length for [128, 32]'); + + let mut layout = array![128, 128].span(); + let got = calculate_packed_size(ref layout); + assert(got == 2, 'invalid length for [128, 128]'); + + let mut layout = array![251, 251].span(); + let got = calculate_packed_size(ref layout); + assert(got == 2, 'invalid length for [251, 251]'); + + let mut layout = array![251].span(); + let got = calculate_packed_size(ref layout); + assert(got == 1, 'invalid length for [251]'); + + let mut layout = array![32, 64, 128, 27].span(); + let got = calculate_packed_size(ref layout); + assert(got == 1, 'invalid length'); + + let mut layout = array![32, 64, 128, 28].span(); + let got = calculate_packed_size(ref layout); + assert(got == 2, 'invalid length'); +} diff --git a/crates/dojo-erc/src/tests/erc20_tests.cairo b/crates/dojo-erc/src/tests/erc20_tests.cairo index 490964c017..a75f625038 100644 --- a/crates/dojo-erc/src/tests/erc20_tests.cairo +++ b/crates/dojo-erc/src/tests/erc20_tests.cairo @@ -236,7 +236,7 @@ fn test__transfer_to_zero() { // #[test] -#[available_gas(25000000)] +#[available_gas(30000000)] fn test_transfer_from() { let mut state = setup(); testing::set_caller_address(OWNER()); diff --git a/crates/dojo-erc/src/tests/erc721_tests.cairo b/crates/dojo-erc/src/tests/erc721_tests.cairo index 371403b092..65bc34df19 100644 --- a/crates/dojo-erc/src/tests/erc721_tests.cairo +++ b/crates/dojo-erc/src/tests/erc721_tests.cairo @@ -404,7 +404,7 @@ fn test__set_approval_for_all_owner_equal_operator_false() { // #[test] -#[available_gas(50000000)] +#[available_gas(60000000)] fn test_transfer_from_owner() { let mut state = setup(); let token_id = TOKEN_ID; @@ -1334,7 +1334,7 @@ fn test__mint_already_exist() { // #[test] -#[available_gas(20000000)] +#[available_gas(25000000)] fn test__burn() { let mut state = setup(); @@ -1359,7 +1359,7 @@ fn test__burn() { } #[test] -#[available_gas(20000000)] +#[available_gas(30000000)] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test__burn_nonexistent() { let (mut world, mut state) = STATE(); diff --git a/crates/dojo-lang/src/inline_macros/get.rs b/crates/dojo-lang/src/inline_macros/get.rs index 05cd8207df..5209dfd5bd 100644 --- a/crates/dojo-lang/src/inline_macros/get.rs +++ b/crates/dojo-lang/src/inline_macros/get.rs @@ -88,11 +88,14 @@ impl InlineMacroExprPlugin for GetMacro { "\n let mut __{component}_layout__ = array::ArrayTrait::new(); dojo::database::schema::SchemaIntrospection::<{component}>::layout(ref \ __{component}_layout__); - let __{component}_layout_span__ = \ + let mut __{component}_layout_clone__ = __{component}_layout__.clone(); + let mut __{component}_layout_span__ = \ array::ArrayTrait::span(@__{component}_layout__); + let mut __{component}_layout_clone_span__ = \ + array::ArrayTrait::span(@__{component}_layout_clone__); let mut __{component}_values__ = {}.entity('{component}', __get_macro_keys__, \ - 0_u8, dojo::database::schema::SchemaIntrospection::<{component}>::size(), \ - __{component}_layout_span__); + 0_u8, dojo::packing::calculate_packed_size(ref \ + __{component}_layout_clone_span__), __{component}_layout_span__); let mut __{component}_component__ = array::ArrayTrait::new(); array::serialize_array_helper(__get_macro_keys__, ref __{component}_component__); array::serialize_array_helper(__{component}_values__, ref \ diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 40193e2937..f8b3c1ff2b 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -553,7 +553,7 @@ test_manifest_file { "name": "player_actions", "address": null, - "class_hash": "0x48465b92d0b49ae15999132b88c619b84a0ac7877f92e134a2fef77d387035e", + "class_hash": "0x76e85c6abaa082687466201db4edc0427ca814c6a89d962b4c884ba365bee17", "abi": [ { "type": "impl", @@ -675,7 +675,7 @@ test_manifest_file { "name": "player_actions_external", "address": null, - "class_hash": "0x17a570b3ae75ace7bad6da4581f55356dc3325fa2b454913372f6976d8d5b40", + "class_hash": "0x66af9023e04e381743a83314fbe1bb7dc63c9aeebee6bbeb96578423fe53a9d", "abi": [ { "type": "impl", diff --git a/crates/dojo-lang/src/model.rs b/crates/dojo-lang/src/model.rs index ba84e76a50..7ce01e3b6b 100644 --- a/crates/dojo-lang/src/model.rs +++ b/crates/dojo-lang/src/model.rs @@ -99,8 +99,16 @@ pub fn handle_model_struct( dojo::database::schema::SchemaIntrospection::<$type_name$>::layout(ref layout); array::ArrayTrait::span(@layout) } + + #[inline(always)] + fn packed_size(self: @$type_name$) -> usize { + let mut layout = self.layout(); + dojo::packing::calculate_packed_size(ref layout) + } } + $schema_introspection$ + #[starknet::interface] trait I$type_name$ { fn name(self: @T) -> felt252; diff --git a/crates/dojo-lang/src/plugin_test_data/introspect b/crates/dojo-lang/src/plugin_test_data/introspect index 81bb75b6ea..a8b6c7343c 100644 --- a/crates/dojo-lang/src/plugin_test_data/introspect +++ b/crates/dojo-lang/src/plugin_test_data/introspect @@ -113,8 +113,15 @@ impl PositionModel of dojo::model::Model { dojo::database::schema::SchemaIntrospection::::layout(ref layout); array::ArrayTrait::span(@layout) } + + #[inline(always)] + fn packed_size(self: @Position) -> usize { + let mut layout = self.layout(); + dojo::packing::calculate_packed_size(ref layout) + } } + impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection { #[inline(always)] fn size() -> usize { @@ -154,6 +161,7 @@ impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection< } } + #[starknet::interface] trait IPosition { fn name(self: @T) -> felt252; diff --git a/crates/dojo-lang/src/plugin_test_data/model b/crates/dojo-lang/src/plugin_test_data/model index d2d3a2c57b..8d8c1bc826 100644 --- a/crates/dojo-lang/src/plugin_test_data/model +++ b/crates/dojo-lang/src/plugin_test_data/model @@ -88,8 +88,15 @@ impl PositionModel of dojo::model::Model { dojo::database::schema::SchemaIntrospection::::layout(ref layout); array::ArrayTrait::span(@layout) } + + #[inline(always)] + fn packed_size(self: @Position) -> usize { + let mut layout = self.layout(); + dojo::packing::calculate_packed_size(ref layout) + } } + impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection { #[inline(always)] fn size() -> usize { @@ -138,6 +145,7 @@ impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection< } } + #[starknet::interface] trait IPosition { fn name(self: @T) -> felt252; @@ -226,8 +234,15 @@ impl RolesModel of dojo::model::Model { dojo::database::schema::SchemaIntrospection::::layout(ref layout); array::ArrayTrait::span(@layout) } + + #[inline(always)] + fn packed_size(self: @Roles) -> usize { + let mut layout = self.layout(); + dojo::packing::calculate_packed_size(ref layout) + } } + impl RolesSchemaIntrospection of dojo::database::schema::SchemaIntrospection { #[inline(always)] fn size() -> usize { @@ -260,6 +275,7 @@ impl RolesSchemaIntrospection of dojo::database::schema::SchemaIntrospection { fn name(self: @T) -> felt252; @@ -336,8 +352,15 @@ impl PlayerModel of dojo::model::Model { dojo::database::schema::SchemaIntrospection::::layout(ref layout); array::ArrayTrait::span(@layout) } + + #[inline(always)] + fn packed_size(self: @Player) -> usize { + let mut layout = self.layout(); + dojo::packing::calculate_packed_size(ref layout) + } } + impl PlayerSchemaIntrospection of dojo::database::schema::SchemaIntrospection { #[inline(always)] fn size() -> usize { @@ -384,6 +407,7 @@ impl PlayerSchemaIntrospection of dojo::database::schema::SchemaIntrospection { fn name(self: @T) -> felt252; diff --git a/crates/torii/graphql/src/object/model.rs b/crates/torii/graphql/src/object/model.rs index 6090b5d105..35a5ada2ce 100644 --- a/crates/torii/graphql/src/object/model.rs +++ b/crates/torii/graphql/src/object/model.rs @@ -32,6 +32,7 @@ impl Default for ModelObject { } } } + impl ModelObject { pub fn value_mapping(model: Model) -> ValueMapping { IndexMap::from([ From 554c7e1b4456e5c83f788c0e831e827a10afe079 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Wed, 27 Sep 2023 14:08:24 -0400 Subject: [PATCH 12/14] Update execute to work with contracts (#934) --- crates/sozo/src/args.rs | 3 - crates/sozo/src/commands/execute.rs | 11 +- crates/sozo/src/commands/mod.rs | 2 - crates/sozo/src/commands/system.rs | 61 ------ crates/sozo/src/ops/execute.rs | 18 +- crates/sozo/src/ops/mod.rs | 1 - crates/sozo/src/ops/system.rs | 66 ------- crates/torii/client/src/contract/mod.rs | 1 - crates/torii/client/src/contract/system.rs | 179 ------------------ .../torii/client/src/contract/system_test.rs | 58 ------ crates/torii/client/src/contract/world.rs | 42 ---- crates/torii/client/src/provider/jsonrpc.rs | 13 -- crates/torii/client/src/provider/mod.rs | 3 - 13 files changed, 17 insertions(+), 441 deletions(-) delete mode 100644 crates/sozo/src/commands/system.rs delete mode 100644 crates/sozo/src/ops/system.rs delete mode 100644 crates/torii/client/src/contract/system.rs delete mode 100644 crates/torii/client/src/contract/system_test.rs diff --git a/crates/sozo/src/args.rs b/crates/sozo/src/args.rs index dacb0b6007..f5b81ab298 100644 --- a/crates/sozo/src/args.rs +++ b/crates/sozo/src/args.rs @@ -17,7 +17,6 @@ use crate::commands::execute::ExecuteArgs; use crate::commands::init::InitArgs; use crate::commands::migrate::MigrateArgs; use crate::commands::register::RegisterArgs; -use crate::commands::system::SystemArgs; use crate::commands::test::TestArgs; #[derive(Parser)] @@ -66,8 +65,6 @@ pub enum Commands { Execute(ExecuteArgs), #[command(about = "Interact with a worlds components")] Component(ComponentArgs), - #[command(about = "Interact with a worlds systems")] - System(SystemArgs), #[command(about = "Register new systems and components")] Register(RegisterArgs), #[command(about = "Queries world events")] diff --git a/crates/sozo/src/commands/execute.rs b/crates/sozo/src/commands/execute.rs index d0a463bb15..0e11a2077c 100644 --- a/crates/sozo/src/commands/execute.rs +++ b/crates/sozo/src/commands/execute.rs @@ -6,14 +6,16 @@ use starknet::core::types::FieldElement; use super::options::account::AccountOptions; use super::options::starknet::StarknetOptions; -use super::options::world::WorldOptions; use crate::ops::execute; #[derive(Debug, Args)] #[command(about = "Execute a system with the given calldata.")] pub struct ExecuteArgs { - #[arg(help = "The name of the system to be executed.")] - pub system: String, + #[arg(help = "The address of the contract to be executed.")] + pub contract: FieldElement, + + #[arg(help = "The name of the entrypoint to be executed.")] + pub entrypoint: String, #[arg(short, long)] #[arg(value_delimiter = ',')] @@ -21,9 +23,6 @@ pub struct ExecuteArgs { 0x12345,0x69420.")] pub calldata: Vec, - #[command(flatten)] - pub world: WorldOptions, - #[command(flatten)] pub starknet: StarknetOptions, diff --git a/crates/sozo/src/commands/mod.rs b/crates/sozo/src/commands/mod.rs index 9d3ce0dbd7..7ce24a257e 100644 --- a/crates/sozo/src/commands/mod.rs +++ b/crates/sozo/src/commands/mod.rs @@ -14,7 +14,6 @@ pub(crate) mod init; pub(crate) mod migrate; pub(crate) mod options; pub(crate) mod register; -pub(crate) mod system; pub(crate) mod test; // copy of non pub functions from scarb @@ -30,7 +29,6 @@ pub fn run(command: Commands, config: &Config) -> Result<()> { Commands::Auth(args) => args.run(config), Commands::Execute(args) => args.run(config), Commands::Component(args) => args.run(config), - Commands::System(args) => args.run(config), Commands::Register(args) => args.run(config), Commands::Events(args) => args.run(config), Commands::Completions(args) => args.run(), diff --git a/crates/sozo/src/commands/system.rs b/crates/sozo/src/commands/system.rs deleted file mode 100644 index 74a7c484db..0000000000 --- a/crates/sozo/src/commands/system.rs +++ /dev/null @@ -1,61 +0,0 @@ -use anyhow::Result; -use clap::{Args, Subcommand}; -use dojo_world::metadata::dojo_metadata_from_workspace; -use scarb::core::Config; - -use super::options::starknet::StarknetOptions; -use super::options::world::WorldOptions; -use crate::ops::system; - -#[derive(Debug, Args)] -pub struct SystemArgs { - #[command(subcommand)] - command: SystemCommands, -} - -#[derive(Debug, Subcommand)] -pub enum SystemCommands { - #[command(about = "Get the class hash of a system.")] - Get { - #[arg(help = "The name of the system.")] - name: String, - - #[command(flatten)] - world: WorldOptions, - - #[command(flatten)] - starknet: StarknetOptions, - }, - - #[command(alias = "dep")] - #[command(about = "Retrieve the component dependencies of a system.")] - Dependency { - #[arg(help = "The name of the system.")] - name: String, - - #[arg(short = 'j', long = "json")] - #[arg(help_heading = "Display options")] - to_json: bool, - - #[command(flatten)] - world: WorldOptions, - - #[command(flatten)] - starknet: StarknetOptions, - }, -} - -impl SystemArgs { - pub fn run(self, config: &Config) -> Result<()> { - let env_metadata = if config.manifest_path().exists() { - let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; - - // TODO: Check the updated scarb way to read profile specific values - dojo_metadata_from_workspace(&ws).and_then(|inner| inner.env().cloned()) - } else { - None - }; - - config.tokio_handle().block_on(system::execute(self.command, env_metadata)) - } -} diff --git a/crates/sozo/src/ops/execute.rs b/crates/sozo/src/ops/execute.rs index 8dbda3b1e4..3d8fe31a64 100644 --- a/crates/sozo/src/ops/execute.rs +++ b/crates/sozo/src/ops/execute.rs @@ -1,20 +1,26 @@ use anyhow::{Context, Result}; use dojo_world::metadata::Environment; -use torii_client::contract::world::WorldContract; +use starknet::accounts::{Account, Call}; +use starknet::core::utils::get_selector_from_name; use crate::commands::execute::ExecuteArgs; pub async fn execute(args: ExecuteArgs, env_metadata: Option) -> Result<()> { - let ExecuteArgs { system, calldata, world, starknet, account } = args; + let ExecuteArgs { contract, entrypoint, calldata, starknet, account } = args; - let world_address = world.address(env_metadata.as_ref())?; let provider = starknet.provider(env_metadata.as_ref())?; let account = account.account(provider, env_metadata.as_ref()).await?; - let world = WorldContract::new(world_address, &account); - let res = - world.execute(&system, calldata).await.with_context(|| "Failed to send transaction")?; + let res = account + .execute(vec![Call { + calldata, + to: contract, + selector: get_selector_from_name(&entrypoint).unwrap(), + }]) + .send() + .await + .with_context(|| "Failed to send transaction")?; println!("Transaction: {:#x}", res.transaction_hash); diff --git a/crates/sozo/src/ops/mod.rs b/crates/sozo/src/ops/mod.rs index c1e215ddfb..02302ca2f2 100644 --- a/crates/sozo/src/ops/mod.rs +++ b/crates/sozo/src/ops/mod.rs @@ -4,4 +4,3 @@ pub mod events; pub mod execute; pub mod migration; pub mod register; -pub mod system; diff --git a/crates/sozo/src/ops/system.rs b/crates/sozo/src/ops/system.rs deleted file mode 100644 index 8b2612bbc3..0000000000 --- a/crates/sozo/src/ops/system.rs +++ /dev/null @@ -1,66 +0,0 @@ -use anyhow::Result; -use console::Style; -use dojo_world::metadata::Environment; -use starknet::core::types::{BlockId, BlockTag}; -use torii_client::contract::world::WorldContractReader; - -use crate::commands::system::SystemCommands; - -pub async fn execute(command: SystemCommands, env_metadata: Option) -> Result<()> { - match command { - SystemCommands::Get { name, world, starknet } => { - let world_address = world.address(env_metadata.as_ref())?; - let provider = starknet.provider(env_metadata.as_ref())?; - - let world = WorldContractReader::new(world_address, &provider); - let system = world.system(&name, BlockId::Tag(BlockTag::Pending)).await?; - - println!("{:#x}", system.class_hash()) - } - - SystemCommands::Dependency { name, to_json, world, starknet } => { - let world_address = world.address(env_metadata.as_ref())?; - let provider = starknet.provider(env_metadata.as_ref())?; - - let world = WorldContractReader::new(world_address, &provider); - let system = world.system(&name, BlockId::Tag(BlockTag::Pending)).await?; - - let deps = system.dependencies(BlockId::Tag(BlockTag::Pending)).await?; - - if to_json { - println!("{}", serde_json::to_string_pretty(&deps)?); - } else { - let read = deps - .iter() - .enumerate() - .filter_map(|(i, d)| { - if d.read { Some(format!("{}.{}", i + 1, d.name.clone())) } else { None } - }) - .collect::>(); - - let write = deps - .iter() - .enumerate() - .filter_map(|(i, d)| { - if d.write { Some(format!("{}. {}", i + 1, d.name.clone())) } else { None } - }) - .collect::>(); - - let output = format!( - r"{} -{} -{} -{}", - Style::from_dotted_str("bold.underlined").apply_to("Read:"), - read.join("\n"), - Style::from_dotted_str("bold.underlined").apply_to("Write:"), - write.join("\n"), - ); - - println!("{output}") - } - } - } - - Ok(()) -} diff --git a/crates/torii/client/src/contract/mod.rs b/crates/torii/client/src/contract/mod.rs index 0131b090a4..94b8f34a34 100644 --- a/crates/torii/client/src/contract/mod.rs +++ b/crates/torii/client/src/contract/mod.rs @@ -1,3 +1,2 @@ pub mod component; -pub mod system; pub mod world; diff --git a/crates/torii/client/src/contract/system.rs b/crates/torii/client/src/contract/system.rs deleted file mode 100644 index 9717beb8a5..0000000000 --- a/crates/torii/client/src/contract/system.rs +++ /dev/null @@ -1,179 +0,0 @@ -use dojo_types::system::Dependency; -use starknet::accounts::ConnectedAccount; -use starknet::core::types::{BlockId, FieldElement, FunctionCall, InvokeTransactionResult}; -use starknet::core::utils::{ - cairo_short_string_to_felt, get_selector_from_name, parse_cairo_short_string, - CairoShortStringToFeltError, ParseCairoShortStringError, -}; -use starknet::providers::{Provider, ProviderError}; - -use crate::contract::world::{ - ContractReaderError, WorldContract, WorldContractError, WorldContractReader, -}; - -#[cfg(test)] -#[path = "system_test.rs"] -mod test; - -#[derive(Debug, thiserror::Error)] -pub enum SystemError { - #[error(transparent)] - WorldError(WorldContractError), - #[error(transparent)] - ProviderError(ProviderError

), - #[error(transparent)] - ParseCairoShortStringError(ParseCairoShortStringError), - #[error(transparent)] - CairoShortStringToFeltError(CairoShortStringToFeltError), - #[error(transparent)] - ContractReaderError(ContractReaderError

), - #[error(transparent)] - ReaderError(SystemReaderError

), -} - -pub struct System<'a, A: ConnectedAccount + Sync> { - world: &'a WorldContract<'a, A>, - reader: SystemReader<'a, A::Provider>, - name: String, -} - -impl<'a, A: ConnectedAccount + Sync> System<'a, A> { - pub async fn new( - world: &'a WorldContract<'a, A>, - name: String, - block_id: BlockId, - ) -> Result, SystemError::Error>> { - Ok(Self { - name: name.clone(), - world, - reader: SystemReader::new(&world.reader, name, block_id) - .await - .map_err(SystemError::ReaderError)?, - }) - } - - pub fn class_hash(&self) -> FieldElement { - self.reader.class_hash - } - - pub async fn call( - &self, - calldata: Vec, - block_id: BlockId, - ) -> Result, SystemError::Error>> - { - self.reader.call(calldata, block_id).await.map_err(SystemError::ReaderError) - } - - pub async fn execute( - &self, - calldata: Vec, - ) -> Result::Error>> - { - let res = - self.world.execute(&self.name, calldata).await.map_err(SystemError::WorldError)?; - - Ok(res) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum SystemReaderError

{ - #[error(transparent)] - ProviderError(ProviderError

), - #[error(transparent)] - ParseCairoShortStringError(ParseCairoShortStringError), - #[error(transparent)] - CairoShortStringToFeltError(CairoShortStringToFeltError), - #[error(transparent)] - ContractReaderError(ContractReaderError

), - #[error("Invalid dependency length")] - InvalidDependencyLength, -} - -pub struct SystemReader<'a, P: Provider + Sync> { - world: &'a WorldContractReader<'a, P>, - name: String, - class_hash: FieldElement, -} - -impl<'a, P: Provider + Sync> SystemReader<'a, P> { - pub async fn new( - world: &'a WorldContractReader<'a, P>, - name: String, - block_id: BlockId, - ) -> Result, SystemReaderError> { - let res = world - .provider - .call( - FunctionCall { - contract_address: world.address, - calldata: vec![ - cairo_short_string_to_felt(&name) - .map_err(SystemReaderError::CairoShortStringToFeltError)?, - ], - entry_point_selector: get_selector_from_name("system").unwrap(), - }, - block_id, - ) - .await - .map_err(SystemReaderError::ProviderError)?; - - Ok(Self { name, world, class_hash: res[0] }) - } - - pub fn class_hash(&self) -> FieldElement { - self.class_hash - } - - pub async fn call( - &self, - mut calldata: Vec, - block_id: BlockId, - ) -> Result, SystemReaderError> { - calldata.insert(0, (calldata.len() as u64).into()); - - let res = self - .world - .call(&self.name, calldata, block_id) - .await - .map_err(SystemReaderError::ContractReaderError)?; - - Ok(res) - } - - pub async fn dependencies( - &self, - block_id: BlockId, - ) -> Result, SystemReaderError> { - let entrypoint = get_selector_from_name("dependencies").unwrap(); - - let res = self - .world - .call( - "library_call", - vec![FieldElement::THREE, self.class_hash, entrypoint, FieldElement::ZERO], - block_id, - ) - .await - .map_err(SystemReaderError::ContractReaderError)?; - - let mut dependencies = vec![]; - for chunk in res[3..].chunks(2) { - if chunk.len() != 2 { - return Err(SystemReaderError::InvalidDependencyLength); - } - - let is_write: bool = chunk[1] == FieldElement::ONE; - - dependencies.push(Dependency { - name: parse_cairo_short_string(&chunk[0]) - .map_err(SystemReaderError::ParseCairoShortStringError)?, - read: !is_write, - write: is_write, - }); - } - - Ok(dependencies) - } -} diff --git a/crates/torii/client/src/contract/system_test.rs b/crates/torii/client/src/contract/system_test.rs deleted file mode 100644 index 93ec05a141..0000000000 --- a/crates/torii/client/src/contract/system_test.rs +++ /dev/null @@ -1,58 +0,0 @@ -// use std::time::Duration; - -// use camino::Utf8PathBuf; -// use dojo_test_utils::sequencer::{ -// get_default_test_starknet_config, SequencerConfig, TestSequencer, -// }; -// use starknet::accounts::Account; -// use starknet::core::types::{BlockId, BlockTag}; -// use starknet_crypto::FieldElement; - -// use crate::contract::world::test::deploy_world; -// use crate::contract::world::WorldContract; - -// #[tokio::test(flavor = "multi_thread")] -// async fn test_system() { -// let sequencer = -// TestSequencer::start(SequencerConfig::default(), -// get_default_test_starknet_config()).await; let account = sequencer.account(); -// let (world_address, _) = deploy_world( -// &sequencer, -// Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev".into()).unwrap(), -// ) -// .await; - -// let block_id: BlockId = BlockId::Tag(BlockTag::Latest); -// let world = WorldContract::new(world_address, &account); -// let spawn = world.system("spawn", block_id).await.unwrap(); - -// let _ = spawn.execute(vec![]).await.unwrap(); - -// // wait for the tx to be mined -// tokio::time::sleep(Duration::from_millis(250)).await; - -// let component = world.component("Moves", block_id).await.unwrap(); -// let moves = component.entity(vec![account.address()], block_id).await.unwrap(); - -// assert_eq!(moves, vec![10_u8.into(), FieldElement::ZERO]); - -// let move_system = world.system("move", block_id).await.unwrap(); - -// let _ = move_system.execute(vec![FieldElement::ONE]).await.unwrap(); -// // wait for the tx to be mined -// tokio::time::sleep(Duration::from_millis(250)).await; - -// let _ = move_system.execute(vec![FieldElement::THREE]).await.unwrap(); -// // wait for the tx to be mined -// tokio::time::sleep(Duration::from_millis(250)).await; - -// let moves = component.entity(vec![account.address()], block_id).await.unwrap(); - -// assert_eq!(moves, vec![8_u8.into(), FieldElement::THREE]); - -// let position_component = world.component("Position", block_id).await.unwrap(); - -// let position = position_component.entity(vec![account.address()], block_id).await.unwrap(); - -// assert_eq!(position, vec![9_u8.into(), 9_u8.into()]); -// } diff --git a/crates/torii/client/src/contract/world.rs b/crates/torii/client/src/contract/world.rs index 7c48e3919e..a1a68ee1f5 100644 --- a/crates/torii/client/src/contract/world.rs +++ b/crates/torii/client/src/contract/world.rs @@ -8,7 +8,6 @@ use starknet::core::utils::{ use starknet::providers::{Provider, ProviderError}; use crate::contract::component::{ComponentError, ComponentReader}; -use crate::contract::system::{System, SystemError, SystemReader, SystemReaderError}; #[cfg(test)] #[path = "world_test.rs"] @@ -100,31 +99,6 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { self.account.execute(calls).send().await } - pub async fn execute( - &self, - name: &str, - mut calldata: Vec, - ) -> Result< - InvokeTransactionResult, - WorldContractError::Error>, - > { - calldata.insert(0, (calldata.len() as u64).into()); - calldata.insert( - 0, - cairo_short_string_to_felt(name) - .map_err(WorldContractError::CairoShortStringToFeltError)?, - ); - self.account - .execute(vec![Call { - calldata, - to: self.address, - selector: get_selector_from_name("execute").unwrap(), - }]) - .send() - .await - .map_err(WorldContractError::AccountError) - } - pub async fn executor( &self, block_id: BlockId, @@ -149,14 +123,6 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { { self.reader.component(name, block_id).await } - - pub async fn system( - &'a self, - name: &str, - block_id: BlockId, - ) -> Result, SystemError::Error>> { - System::new(self, name.to_string(), block_id).await - } } #[derive(Debug, thiserror::Error)] @@ -300,12 +266,4 @@ impl<'a, P: Provider + Sync> WorldContractReader<'a, P> { ) -> Result, ComponentError> { ComponentReader::new(self, name.to_string(), block_id).await } - - pub async fn system( - &'a self, - name: &str, - block_id: BlockId, - ) -> Result, SystemReaderError> { - SystemReader::new(self, name.to_string(), block_id).await - } } diff --git a/crates/torii/client/src/provider/jsonrpc.rs b/crates/torii/client/src/provider/jsonrpc.rs index 67ccdab552..e71290d633 100644 --- a/crates/torii/client/src/provider/jsonrpc.rs +++ b/crates/torii/client/src/provider/jsonrpc.rs @@ -6,13 +6,10 @@ use starknet_crypto::FieldElement; use super::Provider; use crate::contract::component::ComponentError; -use crate::contract::system::SystemReaderError; use crate::contract::world::WorldContractReader; #[derive(Debug, thiserror::Error)] pub enum JsonRpcProviderError

{ - #[error(transparent)] - SystemReader(SystemReaderError

), #[error(transparent)] ComponetReader(ComponentError

), } @@ -48,16 +45,6 @@ where { type Error = JsonRpcProviderError>; - async fn system(&self, name: &str) -> Result { - let world = self.world(); - let class_hash = world - .system(name, self.block_id) - .await - .map_err(JsonRpcProviderError::SystemReader)? - .class_hash(); - Ok(class_hash) - } - async fn component(&self, name: &str) -> Result { let world = self.world(); let class_hash = world diff --git a/crates/torii/client/src/provider/mod.rs b/crates/torii/client/src/provider/mod.rs index 26fd8eab12..e7ffa683d6 100644 --- a/crates/torii/client/src/provider/mod.rs +++ b/crates/torii/client/src/provider/mod.rs @@ -13,9 +13,6 @@ pub trait Provider { /// Get the class hash of a component. async fn component(&self, name: &str) -> Result; - /// Get the class hash of a system. - async fn system(&self, name: &str) -> Result; - /// Get the component values of an entity. async fn entity( &self, From b64e5aa3acb5b605bd0439d5707090bb0cd20e8a Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Wed, 27 Sep 2023 16:45:15 -0400 Subject: [PATCH 13/14] Update sozo model (#935) --- Cargo.lock | 2 + .../dojo-lang/src/manifest_test_data/manifest | 30 +++++- crates/dojo-lang/src/model.rs | 10 +- .../dojo-lang/src/plugin_test_data/introspect | 10 +- crates/dojo-lang/src/plugin_test_data/model | 30 +++++- crates/dojo-types/Cargo.toml | 2 + crates/dojo-types/src/component.rs | 12 +-- crates/dojo-types/src/core.rs | 97 +++++++++++++++++++ crates/dojo-types/src/lib.rs | 1 + crates/sozo/src/args.rs | 4 +- crates/sozo/src/commands/mod.rs | 4 +- .../src/commands/{component.rs => model.rs} | 26 ++--- crates/sozo/src/ops/mod.rs | 2 +- .../sozo/src/ops/{component.rs => model.rs} | 16 +-- crates/torii/client/src/contract/mod.rs | 2 +- .../src/contract/{component.rs => model.rs} | 90 +++++++++-------- .../{component_test.rs => model_test.rs} | 28 +++--- crates/torii/client/src/contract/world.rs | 17 ++-- crates/torii/client/src/provider/jsonrpc.rs | 8 +- crates/torii/client/wasm/Cargo.lock | 2 + .../core/src/processors/register_model.rs | 2 +- crates/torii/core/src/sql.rs | 47 ++------- crates/torii/core/src/sql_test.rs | 2 +- 23 files changed, 298 insertions(+), 146 deletions(-) create mode 100644 crates/dojo-types/src/core.rs rename crates/sozo/src/commands/{component.rs => model.rs} (73%) rename crates/sozo/src/ops/{component.rs => model.rs} (69%) rename crates/torii/client/src/contract/{component.rs => model.rs} (74%) rename crates/torii/client/src/contract/{component_test.rs => model_test.rs} (76%) diff --git a/Cargo.lock b/Cargo.lock index 8e4008272a..766167ad1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2164,8 +2164,10 @@ dependencies = [ name = "dojo-types" version = "0.2.1" dependencies = [ + "hex", "serde", "starknet", + "thiserror", ] [[package]] diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index f8b3c1ff2b..d07c4fa928 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -804,7 +804,7 @@ test_manifest_file "key": false } ], - "class_hash": "0x3682ef3ef44d8db8c8bfe72533e2e3b17e7b80efd6550b365deed7b4b3f8597", + "class_hash": "0x3dedf3d747db10aee693c0d034fcde1064ec66c84d285e60630f860dc9df566", "abi": [ { "type": "function", @@ -819,7 +819,18 @@ test_manifest_file }, { "type": "function", - "name": "size", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", "inputs": [], "outputs": [ { @@ -956,7 +967,7 @@ test_manifest_file "key": false } ], - "class_hash": "0x69889772f44397619cd8965660e1c8e80ba5f0c917ba40df29b2ffa5b440745", + "class_hash": "0x1b5f19668b9299cea232978c59856b16da649e6aa4dfc3f2a42aa8435e06bc7", "abi": [ { "type": "function", @@ -971,7 +982,18 @@ test_manifest_file }, { "type": "function", - "name": "size", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", "inputs": [], "outputs": [ { diff --git a/crates/dojo-lang/src/model.rs b/crates/dojo-lang/src/model.rs index 7ce01e3b6b..fc62785b54 100644 --- a/crates/dojo-lang/src/model.rs +++ b/crates/dojo-lang/src/model.rs @@ -127,10 +127,18 @@ pub fn handle_model_struct( } #[external(v0)] - fn size(self: @ContractState) -> usize { + fn unpacked_size(self: @ContractState) -> usize { dojo::database::schema::SchemaIntrospection::<$type_name$>::size() } + #[external(v0)] + fn packed_size(self: @ContractState) -> usize { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::<$type_name$>::layout(ref layout); + let mut layout_span = layout.span(); + dojo::packing::calculate_packed_size(ref layout_span) + } + #[external(v0)] fn layout(self: @ContractState) -> Span { let mut layout = ArrayTrait::new(); diff --git a/crates/dojo-lang/src/plugin_test_data/introspect b/crates/dojo-lang/src/plugin_test_data/introspect index a8b6c7343c..aeed661cdf 100644 --- a/crates/dojo-lang/src/plugin_test_data/introspect +++ b/crates/dojo-lang/src/plugin_test_data/introspect @@ -180,10 +180,18 @@ mod position { } #[external(v0)] - fn size(self: @ContractState) -> usize { + fn unpacked_size(self: @ContractState) -> usize { dojo::database::schema::SchemaIntrospection::::size() } + #[external(v0)] + fn packed_size(self: @ContractState) -> usize { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + let mut layout_span = layout.span(); + dojo::packing::calculate_packed_size(ref layout_span) + } + #[external(v0)] fn layout(self: @ContractState) -> Span { let mut layout = ArrayTrait::new(); diff --git a/crates/dojo-lang/src/plugin_test_data/model b/crates/dojo-lang/src/plugin_test_data/model index 8d8c1bc826..a5c09cb890 100644 --- a/crates/dojo-lang/src/plugin_test_data/model +++ b/crates/dojo-lang/src/plugin_test_data/model @@ -164,10 +164,18 @@ mod position { } #[external(v0)] - fn size(self: @ContractState) -> usize { + fn unpacked_size(self: @ContractState) -> usize { dojo::database::schema::SchemaIntrospection::::size() } + #[external(v0)] + fn packed_size(self: @ContractState) -> usize { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + let mut layout_span = layout.span(); + dojo::packing::calculate_packed_size(ref layout_span) + } + #[external(v0)] fn layout(self: @ContractState) -> Span { let mut layout = ArrayTrait::new(); @@ -294,10 +302,18 @@ mod roles { } #[external(v0)] - fn size(self: @ContractState) -> usize { + fn unpacked_size(self: @ContractState) -> usize { dojo::database::schema::SchemaIntrospection::::size() } + #[external(v0)] + fn packed_size(self: @ContractState) -> usize { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + let mut layout_span = layout.span(); + dojo::packing::calculate_packed_size(ref layout_span) + } + #[external(v0)] fn layout(self: @ContractState) -> Span { let mut layout = ArrayTrait::new(); @@ -426,10 +442,18 @@ mod player { } #[external(v0)] - fn size(self: @ContractState) -> usize { + fn unpacked_size(self: @ContractState) -> usize { dojo::database::schema::SchemaIntrospection::::size() } + #[external(v0)] + fn packed_size(self: @ContractState) -> usize { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + let mut layout_span = layout.span(); + dojo::packing::calculate_packed_size(ref layout_span) + } + #[external(v0)] fn layout(self: @ContractState) -> Span { let mut layout = ArrayTrait::new(); diff --git a/crates/dojo-types/Cargo.toml b/crates/dojo-types/Cargo.toml index 55983f0290..0606c9d2e4 100644 --- a/crates/dojo-types/Cargo.toml +++ b/crates/dojo-types/Cargo.toml @@ -6,5 +6,7 @@ version = "0.2.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hex = "0.4.3" serde.workspace = true starknet.workspace = true +thiserror.workspace = true diff --git a/crates/dojo-types/src/component.rs b/crates/dojo-types/src/component.rs index a1ed74fcbe..d454639700 100644 --- a/crates/dojo-types/src/component.rs +++ b/crates/dojo-types/src/component.rs @@ -10,7 +10,7 @@ pub struct Member { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum Ty { - Name(String), + Terminal(String), Struct(Struct), Enum(Enum), } @@ -18,7 +18,7 @@ pub enum Ty { impl Ty { pub fn name(&self) -> String { match self { - Ty::Name(s) => s.clone(), + Ty::Terminal(s) => s.clone(), Ty::Struct(s) => s.name.clone(), Ty::Enum(e) => e.name.clone(), } @@ -33,7 +33,7 @@ impl Ty { fn flatten_ty(ty: Ty) -> Vec { let mut items = vec![]; match ty { - Ty::Name(_) => { + Ty::Terminal(_) => { items.push(ty.clone()); } Ty::Struct(mut s) => { @@ -48,7 +48,7 @@ impl Ty { _ => {} } - s.children[i].ty = Ty::Name(member.ty.name()); + s.children[i].ty = Ty::Terminal(member.ty.name()); } items.push(Ty::Struct(s)) @@ -65,7 +65,7 @@ impl Ty { _ => {} } - e.values[i] = Ty::Name(ty.name()); + e.values[i] = Ty::Terminal(ty.name()); } items.push(Ty::Enum(e)) @@ -83,7 +83,7 @@ impl std::fmt::Display for Ty { let str = items .iter() .map(|ty| match ty { - Ty::Name(s) => s.to_string(), + Ty::Terminal(s) => s.to_string(), Ty::Struct(s) => { let mut struct_str = format!("struct {} {{\n", s.name); for member in &s.children { diff --git a/crates/dojo-types/src/core.rs b/crates/dojo-types/src/core.rs new file mode 100644 index 0000000000..d234cbc246 --- /dev/null +++ b/crates/dojo-types/src/core.rs @@ -0,0 +1,97 @@ +use std::str::FromStr; + +use starknet::core::types::FieldElement; + +pub enum CairoType { + U8, + U16, + U32, + U64, + U128, + U256, + USize, + Bool, + ContractAddress, + ClassHash, + Felt252, +} + +#[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, +} + +impl FromStr for CairoType { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "u8" => Ok(CairoType::U8), + "u16" => Ok(CairoType::U16), + "u32" => Ok(CairoType::U32), + "u64" => Ok(CairoType::U64), + "u128" => Ok(CairoType::U128), + "u256" => Ok(CairoType::U256), + "usize" => Ok(CairoType::USize), + "bool" => Ok(CairoType::Bool), + "ContractAddress" => Ok(CairoType::ContractAddress), + "ClassHash" => Ok(CairoType::ClassHash), + "felt252" => Ok(CairoType::Felt252), + _ => Err(()), + } + } +} + +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))) + } + } + } + } +} diff --git a/crates/dojo-types/src/lib.rs b/crates/dojo-types/src/lib.rs index f0ff6617a9..a52c4b61ee 100644 --- a/crates/dojo-types/src/lib.rs +++ b/crates/dojo-types/src/lib.rs @@ -1,4 +1,5 @@ pub mod component; +pub mod core; pub mod event; pub mod storage; pub mod system; diff --git a/crates/sozo/src/args.rs b/crates/sozo/src/args.rs index f5b81ab298..e52fd7b907 100644 --- a/crates/sozo/src/args.rs +++ b/crates/sozo/src/args.rs @@ -10,12 +10,12 @@ use tracing_log::AsTrace; use crate::commands::auth::AuthArgs; use crate::commands::build::BuildArgs; use crate::commands::completions::CompletionsArgs; -use crate::commands::component::ComponentArgs; use crate::commands::dev::DevArgs; use crate::commands::events::EventsArgs; use crate::commands::execute::ExecuteArgs; use crate::commands::init::InitArgs; use crate::commands::migrate::MigrateArgs; +use crate::commands::model::ModelArgs; use crate::commands::register::RegisterArgs; use crate::commands::test::TestArgs; @@ -64,7 +64,7 @@ pub enum Commands { #[command(about = "Execute a world's system")] Execute(ExecuteArgs), #[command(about = "Interact with a worlds components")] - Component(ComponentArgs), + Model(ModelArgs), #[command(about = "Register new systems and components")] Register(RegisterArgs), #[command(about = "Queries world events")] diff --git a/crates/sozo/src/commands/mod.rs b/crates/sozo/src/commands/mod.rs index 7ce24a257e..e093ab8ab8 100644 --- a/crates/sozo/src/commands/mod.rs +++ b/crates/sozo/src/commands/mod.rs @@ -6,12 +6,12 @@ use crate::args::Commands; pub(crate) mod auth; pub(crate) mod build; pub(crate) mod completions; -pub(crate) mod component; pub(crate) mod dev; pub(crate) mod events; pub(crate) mod execute; pub(crate) mod init; pub(crate) mod migrate; +pub(crate) mod model; pub(crate) mod options; pub(crate) mod register; pub(crate) mod test; @@ -28,7 +28,7 @@ pub fn run(command: Commands, config: &Config) -> Result<()> { Commands::Dev(args) => args.run(config), Commands::Auth(args) => args.run(config), Commands::Execute(args) => args.run(config), - Commands::Component(args) => args.run(config), + Commands::Model(args) => args.run(config), Commands::Register(args) => args.run(config), Commands::Events(args) => args.run(config), Commands::Completions(args) => args.run(), diff --git a/crates/sozo/src/commands/component.rs b/crates/sozo/src/commands/model.rs similarity index 73% rename from crates/sozo/src/commands/component.rs rename to crates/sozo/src/commands/model.rs index 67046a817d..a460b00679 100644 --- a/crates/sozo/src/commands/component.rs +++ b/crates/sozo/src/commands/model.rs @@ -6,19 +6,19 @@ use starknet::core::types::FieldElement; use super::options::starknet::StarknetOptions; use super::options::world::WorldOptions; -use crate::ops::component; +use crate::ops::model; #[derive(Debug, Args)] -pub struct ComponentArgs { +pub struct ModelArgs { #[command(subcommand)] - command: ComponentCommands, + command: ModelCommands, } #[derive(Debug, Subcommand)] -pub enum ComponentCommands { - #[command(about = "Get the class hash of a component")] - Get { - #[arg(help = "The name of the component")] +pub enum ModelCommands { + #[command(about = "Retrieve the class hash of a model")] + ClassHash { + #[arg(help = "The name of the model")] name: String, #[command(flatten)] @@ -28,9 +28,9 @@ pub enum ComponentCommands { starknet: StarknetOptions, }, - #[command(about = "Retrieve the schema for a component")] + #[command(about = "Retrieve the schema for a model")] Schema { - #[arg(help = "The name of the component")] + #[arg(help = "The name of the model")] name: String, #[command(flatten)] @@ -44,9 +44,9 @@ pub enum ComponentCommands { to_json: bool, }, - #[command(about = "Get the component value for an entity")] + #[command(about = "Retrieve the model value for an entity")] Entity { - #[arg(help = "The name of the component")] + #[arg(help = "The name of the model")] name: String, #[arg(value_name = "KEYS")] @@ -62,7 +62,7 @@ pub enum ComponentCommands { }, } -impl ComponentArgs { +impl ModelArgs { pub fn run(self, config: &Config) -> Result<()> { let env_metadata = if config.manifest_path().exists() { let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; @@ -73,6 +73,6 @@ impl ComponentArgs { None }; - config.tokio_handle().block_on(component::execute(self.command, env_metadata)) + config.tokio_handle().block_on(model::execute(self.command, env_metadata)) } } diff --git a/crates/sozo/src/ops/mod.rs b/crates/sozo/src/ops/mod.rs index 02302ca2f2..abf4eb9c5c 100644 --- a/crates/sozo/src/ops/mod.rs +++ b/crates/sozo/src/ops/mod.rs @@ -1,6 +1,6 @@ pub mod auth; -pub mod component; pub mod events; pub mod execute; pub mod migration; +pub mod model; pub mod register; diff --git a/crates/sozo/src/ops/component.rs b/crates/sozo/src/ops/model.rs similarity index 69% rename from crates/sozo/src/ops/component.rs rename to crates/sozo/src/ops/model.rs index 830fc89031..b2eb532341 100644 --- a/crates/sozo/src/ops/component.rs +++ b/crates/sozo/src/ops/model.rs @@ -3,26 +3,26 @@ use dojo_world::metadata::Environment; use starknet::core::types::{BlockId, BlockTag}; use torii_client::contract::world::WorldContractReader; -use crate::commands::component::ComponentCommands; +use crate::commands::model::ModelCommands; -pub async fn execute(command: ComponentCommands, env_metadata: Option) -> Result<()> { +pub async fn execute(command: ModelCommands, env_metadata: Option) -> Result<()> { match command { - ComponentCommands::Get { name, world, starknet } => { + ModelCommands::ClassHash { name, world, starknet } => { let world_address = world.address(env_metadata.as_ref())?; let provider = starknet.provider(env_metadata.as_ref())?; let world = WorldContractReader::new(world_address, &provider); - let component = world.component(&name, BlockId::Tag(BlockTag::Pending)).await?; + let component = world.model(&name, BlockId::Tag(BlockTag::Pending)).await?; println!("{:#x}", component.class_hash()); } - ComponentCommands::Schema { name, world, starknet, to_json } => { + ModelCommands::Schema { name, world, starknet, to_json } => { let world_address = world.address(env_metadata.as_ref())?; let provider = starknet.provider(env_metadata.as_ref())?; let world = WorldContractReader::new(world_address, &provider); - let component = world.component(&name, BlockId::Tag(BlockTag::Pending)).await?; + let component = world.model(&name, BlockId::Tag(BlockTag::Pending)).await?; let schema = component.schema(BlockId::Tag(BlockTag::Pending)).await?; @@ -33,12 +33,12 @@ pub async fn execute(command: ComponentCommands, env_metadata: Option { + ModelCommands::Entity { name, keys, starknet, world, .. } => { let world_address = world.address(env_metadata.as_ref())?; let provider = starknet.provider(env_metadata.as_ref())?; let world = WorldContractReader::new(world_address, &provider); - let component = world.component(&name, BlockId::Tag(BlockTag::Pending)).await?; + let component = world.model(&name, BlockId::Tag(BlockTag::Pending)).await?; let entity = component.entity(keys, BlockId::Tag(BlockTag::Pending)).await?; diff --git a/crates/torii/client/src/contract/mod.rs b/crates/torii/client/src/contract/mod.rs index 94b8f34a34..a994f340ec 100644 --- a/crates/torii/client/src/contract/mod.rs +++ b/crates/torii/client/src/contract/mod.rs @@ -1,2 +1,2 @@ -pub mod component; +pub mod model; pub mod world; diff --git a/crates/torii/client/src/contract/component.rs b/crates/torii/client/src/contract/model.rs similarity index 74% rename from crates/torii/client/src/contract/component.rs rename to crates/torii/client/src/contract/model.rs index 967a0fde23..48700ed8ee 100644 --- a/crates/torii/client/src/contract/component.rs +++ b/crates/torii/client/src/contract/model.rs @@ -14,11 +14,11 @@ use starknet_crypto::poseidon_hash_many; use crate::contract::world::{ContractReaderError, WorldContractReader}; #[cfg(test)] -#[path = "component_test.rs"] -mod test; +#[path = "model_test.rs"] +mod model_test; #[derive(Debug, thiserror::Error)] -pub enum ComponentError

{ +pub enum ModelError

{ #[error(transparent)] ProviderError(ProviderError

), #[error("Invalid schema")] @@ -35,20 +35,20 @@ pub enum ComponentError

{ ContractReaderError(ContractReaderError

), } -pub struct ComponentReader<'a, P: Provider + Sync> { +pub struct ModelReader<'a, P: Provider + Sync> { world: &'a WorldContractReader<'a, P>, class_hash: FieldElement, name: FieldElement, } -impl<'a, P: Provider + Sync> ComponentReader<'a, P> { +impl<'a, P: Provider + Sync> ModelReader<'a, P> { pub async fn new( world: &'a WorldContractReader<'a, P>, name: String, block_id: BlockId, - ) -> Result, ComponentError> { - let name = cairo_short_string_to_felt(&name) - .map_err(ComponentError::CairoShortStringToFeltError)?; + ) -> Result, ModelError> { + let name = + cairo_short_string_to_felt(&name).map_err(ModelError::CairoShortStringToFeltError)?; let res = world .provider .call( @@ -60,7 +60,7 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { block_id, ) .await - .map_err(ComponentError::ProviderError)?; + .map_err(ModelError::ProviderError)?; Ok(Self { world, class_hash: res[0], name }) } @@ -69,26 +69,41 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { self.class_hash } - pub async fn schema(&self, block_id: BlockId) -> Result> { + pub async fn schema(&self, block_id: BlockId) -> Result> { let entrypoint = get_selector_from_name("schema").unwrap(); let res = self .world .executor_call(self.class_hash, vec![entrypoint, FieldElement::ZERO], block_id) .await - .map_err(ComponentError::ContractReaderError)?; + .map_err(ModelError::ContractReaderError)?; parse_ty::

(&res[1..]) } - pub async fn size(&self, block_id: BlockId) -> Result> { + pub async fn packed_size( + &self, + block_id: BlockId, + ) -> Result> { + let entrypoint = get_selector_from_name("packed_size").unwrap(); + + let res = self + .world + .executor_call(self.class_hash, vec![entrypoint, FieldElement::ZERO], block_id) + .await + .map_err(ModelError::ContractReaderError)?; + + Ok(res[1]) + } + + pub async fn size(&self, block_id: BlockId) -> Result> { let entrypoint = get_selector_from_name("size").unwrap(); let res = self .world .executor_call(self.class_hash, vec![entrypoint, FieldElement::ZERO], block_id) .await - .map_err(ComponentError::ContractReaderError)?; + .map_err(ModelError::ContractReaderError)?; Ok(res[1]) } @@ -96,42 +111,42 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { pub async fn layout( &self, block_id: BlockId, - ) -> Result, ComponentError> { + ) -> Result, ModelError> { let entrypoint = get_selector_from_name("layout").unwrap(); let res = self .world .executor_call(self.class_hash, vec![entrypoint, FieldElement::ZERO], block_id) .await - .map_err(ComponentError::ContractReaderError)?; + .map_err(ModelError::ContractReaderError)?; - Ok(res[3..].into()) + Ok(res[2..].into()) } pub async fn entity( &self, keys: Vec, block_id: BlockId, - ) -> Result, ComponentError> { - let size: u8 = self.size(block_id).await?.try_into().unwrap(); + ) -> Result, ModelError> { + let packed_size: u8 = self.packed_size(block_id).await?.try_into().unwrap(); let layout = self.layout(block_id).await?; let key = poseidon_hash_many(&keys); let key = poseidon_hash_many(&[short_string!("dojo_storage"), self.name, key]); let mut packed = vec![]; - for slot in 0..size { + for slot in 0..packed_size { let value = self .world .provider .get_storage_at(self.world.address, key + slot.into(), block_id) .await - .map_err(ComponentError::ProviderError)?; + .map_err(ModelError::ProviderError)?; packed.push(value); } - let unpacked = unpack::

(packed, layout)?; + let unpacked = unpack::

(packed, layout.clone())?; Ok(unpacked) } @@ -151,22 +166,22 @@ impl<'a, P: Provider + Sync> ComponentReader<'a, P> { pub fn unpack( mut packed: Vec, layout: Vec, -) -> Result, ComponentError> { +) -> Result, ModelError> { packed.reverse(); let mut unpacked = vec![]; - let mut unpacking: U256 = packed.pop().ok_or(ComponentError::UnpackingEntity)?.as_ref().into(); + let mut unpacking: U256 = packed.pop().ok_or(ModelError::UnpackingEntity)?.as_ref().into(); let mut offset = 0; // Iterate over the layout. for size in layout { - let size: u8 = size.try_into().map_err(|_| ComponentError::ConvertingFelt)?; + let size: u8 = size.try_into().map_err(|_| ModelError::ConvertingFelt)?; let size: usize = size.into(); let remaining_bits = 251 - offset; // If there are less remaining bits than the size, move to the next felt for unpacking. if remaining_bits < size { - unpacking = packed.pop().ok_or(ComponentError::UnpackingEntity)?.as_ref().into(); + unpacking = packed.pop().ok_or(ModelError::UnpackingEntity)?.as_ref().into(); offset = 0; } @@ -177,7 +192,7 @@ pub fn unpack( let result = mask & (unpacking >> offset); let result_fe = FieldElement::from_hex_be(&result.to_string()) - .map_err(|_| ComponentError::ConvertingFelt)?; + .map_err(|_| ModelError::ConvertingFelt)?; unpacked.push(result_fe); // Update unpacking to be the shifted value after extracting the result. @@ -187,25 +202,24 @@ pub fn unpack( Ok(unpacked) } -fn parse_ty(data: &[FieldElement]) -> Result> { +fn parse_ty(data: &[FieldElement]) -> Result> { let member_type: u8 = data[0].try_into().unwrap(); match member_type { 0 => parse_simple::

(&data[1..]), 1 => parse_struct::

(&data[1..]), 2 => parse_enum::

(&data[1..]), - _ => Err(ComponentError::InvalidSchema), + _ => Err(ModelError::InvalidSchema), } } -fn parse_simple(data: &[FieldElement]) -> Result> { - let ty = - parse_cairo_short_string(&data[0]).map_err(ComponentError::ParseCairoShortStringError)?; - Ok(Ty::Name(ty)) +fn parse_simple(data: &[FieldElement]) -> Result> { + let ty = parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; + Ok(Ty::Terminal(ty)) } -fn parse_struct(data: &[FieldElement]) -> Result> { +fn parse_struct(data: &[FieldElement]) -> Result> { let name = - parse_cairo_short_string(&data[0]).map_err(ComponentError::ParseCairoShortStringError)?; + parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; let attrs_len: u32 = data[1].try_into().unwrap(); let attrs_slice_start = 2; @@ -230,9 +244,9 @@ fn parse_struct(data: &[FieldElement]) -> Result(data: &[FieldElement]) -> Result> { +fn parse_member(data: &[FieldElement]) -> Result> { let name = - parse_cairo_short_string(&data[0]).map_err(ComponentError::ParseCairoShortStringError)?; + parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; let attributes_len: u32 = data[1].try_into().unwrap(); let slice_start = 2; @@ -246,9 +260,9 @@ fn parse_member(data: &[FieldElement]) -> Result(data: &[FieldElement]) -> Result> { +fn parse_enum(data: &[FieldElement]) -> Result> { let name = - parse_cairo_short_string(&data[0]).map_err(ComponentError::ParseCairoShortStringError)?; + parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; let attrs_len: u32 = data[1].try_into().unwrap(); let attrs_slice_start = 2; diff --git a/crates/torii/client/src/contract/component_test.rs b/crates/torii/client/src/contract/model_test.rs similarity index 76% rename from crates/torii/client/src/contract/component_test.rs rename to crates/torii/client/src/contract/model_test.rs index 6e6b8ace6e..b69080b733 100644 --- a/crates/torii/client/src/contract/component_test.rs +++ b/crates/torii/client/src/contract/model_test.rs @@ -10,7 +10,7 @@ use crate::contract::world::test::deploy_world; use crate::contract::world::WorldContractReader; #[tokio::test(flavor = "multi_thread")] -async fn test_component() { +async fn test_model() { let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; let account = sequencer.account(); @@ -23,7 +23,7 @@ async fn test_component() { let block_id = BlockId::Tag(BlockTag::Latest); let world = WorldContractReader::new(world_address, provider); - let position = world.component("Position", block_id).await.unwrap(); + let position = world.model("Position", block_id).await.unwrap(); let schema = position.schema(block_id).await.unwrap(); assert_eq!( @@ -33,7 +33,7 @@ async fn test_component() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Name("ContractAddress".to_string()), + ty: Ty::Terminal("ContractAddress".to_string()), key: true }, Member { @@ -43,12 +43,12 @@ async fn test_component() { children: vec![ Member { name: "x".to_string(), - ty: Ty::Name("u32".to_string()), + ty: Ty::Terminal("u32".to_string()), key: false }, Member { name: "y".to_string(), - ty: Ty::Name("u32".to_string()), + ty: Ty::Terminal("u32".to_string()), key: false } ] @@ -62,12 +62,12 @@ async fn test_component() { assert_eq!( position.class_hash(), FieldElement::from_hex_be( - "0x069889772f44397619cd8965660e1c8e80ba5f0c917ba40df29b2ffa5b440745" + "0x01b5f19668b9299cea232978c59856b16da649e6aa4dfc3f2a42aa8435e06bc7" ) .unwrap() ); - let moves = world.component("Moves", block_id).await.unwrap(); + let moves = world.model("Moves", block_id).await.unwrap(); let schema = moves.schema(block_id).await.unwrap(); assert_eq!( @@ -77,12 +77,12 @@ async fn test_component() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Name("ContractAddress".to_string()), + ty: Ty::Terminal("ContractAddress".to_string()), key: true }, Member { name: "remaining".to_string(), - ty: Ty::Name("u8".to_string()), + ty: Ty::Terminal("u8".to_string()), key: false }, Member { @@ -90,11 +90,11 @@ async fn test_component() { ty: Ty::Enum(Enum { name: "Direction".to_string(), values: vec![ - Ty::Name("None".to_string()), - Ty::Name("Left".to_string()), - Ty::Name("Right".to_string()), - Ty::Name("Up".to_string()), - Ty::Name("Down".to_string()) + Ty::Terminal("None".to_string()), + Ty::Terminal("Left".to_string()), + Ty::Terminal("Right".to_string()), + Ty::Terminal("Up".to_string()), + Ty::Terminal("Down".to_string()) ] }), key: false diff --git a/crates/torii/client/src/contract/world.rs b/crates/torii/client/src/contract/world.rs index a1a68ee1f5..2bcf7d21cb 100644 --- a/crates/torii/client/src/contract/world.rs +++ b/crates/torii/client/src/contract/world.rs @@ -7,7 +7,7 @@ use starknet::core::utils::{ }; use starknet::providers::{Provider, ProviderError}; -use crate::contract::component::{ComponentError, ComponentReader}; +use crate::contract::model::{ModelError, ModelReader}; #[cfg(test)] #[path = "world_test.rs"] @@ -54,13 +54,13 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { pub async fn grant_writer( &self, - component: &str, + model: &str, system: &str, ) -> Result< InvokeTransactionResult, WorldContractError::Error>, > { - let component = cairo_short_string_to_felt(component) + let component = cairo_short_string_to_felt(model) .map_err(WorldContractError::CairoShortStringToFeltError)?; let system = cairo_short_string_to_felt(system) .map_err(WorldContractError::CairoShortStringToFeltError)?; @@ -119,9 +119,8 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { &'a self, name: &str, block_id: BlockId, - ) -> Result, ComponentError<::Error>> - { - self.reader.component(name, block_id).await + ) -> Result, ModelError<::Error>> { + self.reader.model(name, block_id).await } } @@ -259,11 +258,11 @@ impl<'a, P: Provider + Sync> WorldContractReader<'a, P> { .map_err(ContractReaderError::ProviderError) } - pub async fn component( + pub async fn model( &'a self, name: &str, block_id: BlockId, - ) -> Result, ComponentError> { - ComponentReader::new(self, name.to_string(), block_id).await + ) -> Result, ModelError> { + ModelReader::new(self, name.to_string(), block_id).await } } diff --git a/crates/torii/client/src/provider/jsonrpc.rs b/crates/torii/client/src/provider/jsonrpc.rs index e71290d633..6235eccc5e 100644 --- a/crates/torii/client/src/provider/jsonrpc.rs +++ b/crates/torii/client/src/provider/jsonrpc.rs @@ -5,13 +5,13 @@ use starknet::providers::JsonRpcClient; use starknet_crypto::FieldElement; use super::Provider; -use crate::contract::component::ComponentError; +use crate::contract::model::ModelError; use crate::contract::world::WorldContractReader; #[derive(Debug, thiserror::Error)] pub enum JsonRpcProviderError

{ #[error(transparent)] - ComponetReader(ComponentError

), + ComponetReader(ModelError

), } /// An implementation of [Provider] which uses a Starknet [JsonRpcClient] to query the World. @@ -48,7 +48,7 @@ where async fn component(&self, name: &str) -> Result { let world = self.world(); let class_hash = world - .component(name, self.block_id) + .model(name, self.block_id) .await .map_err(JsonRpcProviderError::ComponetReader)? .class_hash(); @@ -62,7 +62,7 @@ where ) -> Result, Self::Error> { let world = self.world(); let component = world - .component(component, self.block_id) + .model(component, self.block_id) .await .map_err(JsonRpcProviderError::ComponetReader)?; component.entity(keys, self.block_id).await.map_err(JsonRpcProviderError::ComponetReader) diff --git a/crates/torii/client/wasm/Cargo.lock b/crates/torii/client/wasm/Cargo.lock index b43a52a9ff..367d14c307 100644 --- a/crates/torii/client/wasm/Cargo.lock +++ b/crates/torii/client/wasm/Cargo.lock @@ -469,8 +469,10 @@ dependencies = [ name = "dojo-types" version = "0.2.1" dependencies = [ + "hex", "serde", "starknet", + "thiserror", ] [[package]] diff --git a/crates/torii/core/src/processors/register_model.rs b/crates/torii/core/src/processors/register_model.rs index 288996db59..e9e754e39c 100644 --- a/crates/torii/core/src/processors/register_model.rs +++ b/crates/torii/core/src/processors/register_model.rs @@ -28,7 +28,7 @@ impl EventProcessor

for RegisterModelProcessor event: &Event, ) -> Result<(), Error> { let name = parse_cairo_short_string(&event.data[0])?; - let model = world.component(&name, BlockId::Tag(BlockTag::Latest)).await?; + let model = world.model(&name, BlockId::Tag(BlockTag::Latest)).await?; let schema = model.schema(BlockId::Tag(BlockTag::Latest)).await?; let layout = model.layout(BlockId::Tag(BlockTag::Latest)).await?; info!("Registered model: {}", name); diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 59572fbfaa..7d9d100c87 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; +use std::str::FromStr; use anyhow::Result; use async_trait::async_trait; use chrono::{DateTime, Utc}; use dojo_types::component::Ty; +use dojo_types::core::CairoType; use dojo_world::manifest::{Manifest, System}; -use lazy_static::lazy_static; use sqlx::pool::PoolConnection; use sqlx::sqlite::SqliteRow; use sqlx::{Executor, Pool, Row, Sqlite}; @@ -21,26 +21,6 @@ use crate::types::{Entity, Model as ModelType}; #[path = "sql_test.rs"] mod test; -lazy_static! { - static ref CAIRO_TO_SQL_TYPE: HashMap = { - let mut m = HashMap::new(); - m.insert("u8".to_string(), "INTEGER".to_string()); - m.insert("u16".to_string(), "INTEGER".to_string()); - m.insert("u32".to_string(), "INTEGER".to_string()); - m.insert("u64".to_string(), "INTEGER".to_string()); - m.insert("u128".to_string(), "TEXT".to_string()); - m.insert("u256".to_string(), "TEXT".to_string()); - m.insert("usize".to_string(), "INTEGER".to_string()); - m.insert("bool".to_string(), "INTEGER".to_string()); - // m.insert("Cursor".to_string(), "TEXT".to_string()); - m.insert("ContractAddress".to_string(), "TEXT".to_string()); - m.insert("ClassHash".to_string(), "TEXT".to_string()); - // m.insert("DateTime".to_string(), "TEXT".to_string()); - m.insert("felt252".to_string(), "TEXT".to_string()); - m - }; -} - #[async_trait] pub trait Executable { async fn execute(&self) -> Result<()>; @@ -236,7 +216,7 @@ impl Sql { .await?; let (primitive_members, _): (Vec<_>, Vec<_>) = - members.into_iter().partition(|member| CAIRO_TO_SQL_TYPE.contains_key(&member.2)); + 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(); @@ -250,7 +230,7 @@ impl Sql { format!( "INSERT OR REPLACE INTO [{id}] (entity_id, external_{name}) VALUES \ ('{entity_id}' {})", - format_value(&ty, &value).unwrap() + CairoType::from_str(&ty).unwrap().format_for_sql(vec![&value]).unwrap() ) }) .collect(); @@ -378,17 +358,6 @@ fn model_names_sql_string(entity_result: Option, new_model: &str) -> Ok(model_names) } -fn format_value(ty: &str, value: &FieldElement) -> Result { - match CAIRO_TO_SQL_TYPE.get(ty) { - Some(sql_type) => match sql_type.as_str() { - "INTEGER" => Ok(format!(", '{}'", value)), - "TEXT" => Ok(format!(", '{:#x}'", value)), - _ => Err(anyhow::anyhow!("Format not supported for type: {}", ty)), - }, - _ => Err(anyhow::anyhow!("Format not supported for type: {}", ty)), - } -} - fn felts_sql_string(felts: &[FieldElement]) -> String { felts.iter().map(|k| format!("{:#x}", k)).collect::>().join("/") + "/" } @@ -408,8 +377,12 @@ fn build_model_query(model: &Ty, model_idx: usize, parent_id: Option) -> match model { Ty::Struct(s) => { for (member_idx, member) in s.children.iter().enumerate() { - if let Some(sql_type) = CAIRO_TO_SQL_TYPE.get(&member.ty.name()) { - query.push_str(&format!("external_{} {}, ", member.name, sql_type)); + if let Ok(cairo_type) = CairoType::from_str(&member.ty.name()) { + query.push_str(&format!( + "external_{} {}, ", + member.name, + cairo_type.to_sql_type() + )); }; queries.push(format!( diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index 72111069f1..5a1c9d36e3 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -49,7 +49,7 @@ async fn test_load_from_manifest(pool: SqlitePool) { name: "Position".into(), children: vec![Member { name: "test".into(), - ty: Ty::Name("u32".to_string()), + ty: Ty::Terminal("u32".to_string()), key: false, }], }), From 00dd22a633ffdb683ff33b1e990b13e88eaa3b19 Mon Sep 17 00:00:00 2001 From: Kariy Date: Thu, 28 Sep 2023 15:54:17 +0700 Subject: [PATCH 14/14] fix sync loop --- crates/torii/core/src/engine.rs | 38 +++++++++++++-------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/crates/torii/core/src/engine.rs b/crates/torii/core/src/engine.rs index 05597acabc..398b17902a 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -2,7 +2,7 @@ use std::error::Error; use std::time::Duration; use starknet::core::types::{ - BlockId, BlockTag, BlockWithTxs, Event, InvokeTransaction, InvokeTransactionReceipt, + BlockId, BlockWithTxs, Event, InvokeTransaction, InvokeTransactionReceipt, MaybePendingBlockWithTxs, MaybePendingTransactionReceipt, Transaction, TransactionReceipt, }; use starknet::core::utils::get_selector_from_name; @@ -65,47 +65,39 @@ where } pub async fn start(&self, cts: CancellationToken) -> Result<(), Box> { - let db_head = self.db.head().await?; - - let current_block_number = match db_head { - 0 => self.config.start_block, - _ => { - if self.config.start_block != 0 { - warn!("start block ignored, stored head exists and will be used instead"); - } - db_head - } - }; + if self.db.head().await? == 0 { + self.db.set_head(self.config.start_block).await?; + } else if self.config.start_block != 0 { + warn!("start block ignored, stored head exists and will be used instead"); + } loop { if cts.is_cancelled() { break Ok(()); } - sleep(self.config.block_time).await; - match self.sync_to_head(current_block_number).await { + let head = self.db.head().await?; + match self.sync_to_head(head).await { Ok(block_with_txs) => block_with_txs, Err(e) => { error!("getting block: {}", e); continue; } }; + + sleep(self.config.block_time).await; } } pub async fn sync_to_head(&self, from: u64) -> Result> { - let latest_block_with_txs = - self.provider.get_block_with_txs(BlockId::Tag(BlockTag::Latest)).await?; + let latest_block_number = self.provider.block_hash_and_number().await?.block_number; - let latest_block_number = match latest_block_with_txs { - MaybePendingBlockWithTxs::Block(latest_block_with_txs) => { - latest_block_with_txs.block_number - } - _ => return Err(anyhow::anyhow!("Getting latest block number").into()), + if from < latest_block_number { + // if `from` == 0, then the block may or may not be processed yet. + let from = if from == 0 { from } else { from + 1 }; + self.sync_range(from, latest_block_number).await?; }; - self.sync_range(from, latest_block_number).await?; - Ok(latest_block_number) }