From d955c1a2881571e01bbe0665a525a8a24ada0f85 Mon Sep 17 00:00:00 2001 From: notV4l <122404722+notV4l@users.noreply.github.com> Date: Tue, 21 Nov 2023 21:13:54 +0100 Subject: [PATCH 1/4] Upgradeable component (#1147) * core: upgradeable component wip * #[dojo::contract] merge Event, update upgradeable * update base_test.cairo & dojo-lang plugin_test_data * add Upgraded event in upgradeable component * move erc20, fix build issue, delete old tests * update erc20 to use #[dojo::contract] * revert changes on erc20 * revert changes on erc20 * fully qualify component * handle when Event is not defined in contract * follow clippy recomandation * torii: update records_contract address * trigger CI * trigger CI * rebase main * update manifest_test_data * use right records_contract address --- crates/dojo-core/src/base.cairo | 39 ++- crates/dojo-core/src/base_test.cairo | 33 ++- crates/dojo-core/src/components.cairo | 1 + .../src/components/upgradeable.cairo | 56 ++++ crates/dojo-core/src/lib.cairo | 5 +- crates/dojo-core/src/upgradable.cairo | 19 -- crates/dojo-core/src/world.cairo | 14 +- crates/dojo-lang/src/contract.rs | 106 ++++--- .../dojo-lang/src/manifest_test_data/manifest | 156 ++++++++-- crates/dojo-lang/src/plugin_test_data/system | 280 +++++++++++++++--- crates/torii/graphql/src/tests/mod.rs | 2 +- 11 files changed, 531 insertions(+), 180 deletions(-) create mode 100644 crates/dojo-core/src/components.cairo create mode 100644 crates/dojo-core/src/components/upgradeable.cairo delete mode 100644 crates/dojo-core/src/upgradable.cairo diff --git a/crates/dojo-core/src/base.cairo b/crates/dojo-core/src/base.cairo index 9b6a58f621..d5265b31bd 100644 --- a/crates/dojo-core/src/base.cairo +++ b/crates/dojo-core/src/base.cairo @@ -1,20 +1,26 @@ use dojo::world::IWorldDispatcher; -#[starknet::interface] -trait IBase { - fn world(self: @T) -> IWorldDispatcher; -} - #[starknet::contract] mod base { use starknet::{ClassHash, get_caller_address}; - - use dojo::upgradable::{IUpgradeable, UpgradeableTrait}; use dojo::world::IWorldDispatcher; + use dojo::world::IWorldProvider; + + use dojo::components::upgradeable::upgradeable as upgradeable_component; + + component!(path: upgradeable_component, storage: upgradeable, event: UpgradeableEvent); + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_component::Event + } #[storage] struct Storage { world_dispatcher: IWorldDispatcher, + #[substorage(v0)] + upgradeable: upgradeable_component::Storage, } #[constructor] @@ -23,19 +29,12 @@ mod base { } #[external(v0)] - fn world(self: @ContractState) -> IWorldDispatcher { - self.world_dispatcher.read() - } - - #[external(v0)] - impl Upgradeable of IUpgradeable { - /// Upgrade contract implementation to new_class_hash - /// - /// # Arguments - /// - /// * `new_class_hash` - The new implementation class hahs. - fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { - UpgradeableTrait::upgrade(new_class_hash); + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + self.world_dispatcher.read() } } + + #[abi(embed_v0)] + impl UpgradableImpl = upgradeable_component::UpgradableImpl; } diff --git a/crates/dojo-core/src/base_test.cairo b/crates/dojo-core/src/base_test.cairo index 691ad3bac6..2a86667870 100644 --- a/crates/dojo-core/src/base_test.cairo +++ b/crates/dojo-core/src/base_test.cairo @@ -3,8 +3,10 @@ use starknet::ClassHash; use traits::TryInto; use dojo::base::base; -use dojo::upgradable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; -use dojo::test_utils::deploy_contract; +use dojo::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; +use dojo::test_utils::{spawn_test_world}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + #[starknet::contract] mod contract_upgrade { @@ -29,15 +31,34 @@ mod contract_upgrade { use contract_upgrade::{IQuantumLeapDispatcher, IQuantumLeapDispatcherTrait}; +// Utils +fn deploy_world() -> IWorldDispatcher { + spawn_test_world(array![]) +} + #[test] #[available_gas(6000000)] -fn test_upgrade() { - let base_address = deploy_contract(base::TEST_CLASS_HASH, array![].span()); - let upgradable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; +fn test_upgrade_from_world() { + let world = deploy_world(); + let base_address = world.deploy_contract('salt', base::TEST_CLASS_HASH.try_into().unwrap()); let new_class_hash: ClassHash = contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - upgradable_dispatcher.upgrade(new_class_hash); + + world.upgrade_contract(base_address, new_class_hash); let quantum_dispatcher = IQuantumLeapDispatcher { contract_address: base_address }; assert(quantum_dispatcher.plz_more_tps() == 'daddy', 'quantum leap failed'); } + +#[test] +#[available_gas(6000000)] +#[should_panic(expected: ('must be called by world', 'ENTRYPOINT_FAILED'))] +fn test_upgrade_direct() { + let world = deploy_world(); + + let base_address = world.deploy_contract('salt', base::TEST_CLASS_HASH.try_into().unwrap()); + let new_class_hash: ClassHash = contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + + let upgradeable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; + upgradeable_dispatcher.upgrade(new_class_hash); +} diff --git a/crates/dojo-core/src/components.cairo b/crates/dojo-core/src/components.cairo new file mode 100644 index 0000000000..bd830eac21 --- /dev/null +++ b/crates/dojo-core/src/components.cairo @@ -0,0 +1 @@ +mod upgradeable; \ No newline at end of file diff --git a/crates/dojo-core/src/components/upgradeable.cairo b/crates/dojo-core/src/components/upgradeable.cairo new file mode 100644 index 0000000000..c5a030f12f --- /dev/null +++ b/crates/dojo-core/src/components/upgradeable.cairo @@ -0,0 +1,56 @@ +use starknet::ClassHash; + +#[starknet::interface] +trait IUpgradeable { + fn upgrade(ref self: T, new_class_hash: ClassHash); +} + +#[starknet::component] +mod upgradeable { + use starknet::ClassHash; + use starknet::ContractAddress; + use starknet::get_caller_address; + use starknet::syscalls::replace_class_syscall; + use dojo::world::{IWorldProvider, IWorldProviderDispatcher, IWorldDispatcher}; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Upgraded: Upgraded, + } + + #[derive(Drop, starknet::Event)] + struct Upgraded { + class_hash: ClassHash + } + + mod Errors { + const INVALID_CLASS: felt252 = 'class_hash cannot be zero'; + const INVALID_CALLER: felt252 = 'must be called by world'; + const INVALID_WORLD_ADDRESS: felt252 = 'invalid world address'; + } + + #[embeddable_as(UpgradableImpl)] + impl Upgradable< + TContractState, +HasComponent, +IWorldProvider + > of super::IUpgradeable> { + fn upgrade(ref self: ComponentState, new_class_hash: ClassHash) { + assert( + self.get_contract().world().contract_address.is_non_zero(), + Errors::INVALID_WORLD_ADDRESS + ); + assert( + get_caller_address() == self.get_contract().world().contract_address, + Errors::INVALID_CALLER + ); + assert(new_class_hash.is_non_zero(), Errors::INVALID_CLASS); + + replace_class_syscall(new_class_hash).unwrap(); + + self.emit(Upgraded { class_hash: new_class_hash }); + } + } +} diff --git a/crates/dojo-core/src/lib.cairo b/crates/dojo-core/src/lib.cairo index 6e3301c6ee..8a8ba0918d 100644 --- a/crates/dojo-core/src/lib.cairo +++ b/crates/dojo-core/src/lib.cairo @@ -14,10 +14,11 @@ mod packing_test; mod world; #[cfg(test)] mod world_test; -mod upgradable; #[cfg(test)] mod test_utils; #[cfg(test)] -mod benchmarks; \ No newline at end of file +mod benchmarks; + +mod components; \ No newline at end of file diff --git a/crates/dojo-core/src/upgradable.cairo b/crates/dojo-core/src/upgradable.cairo deleted file mode 100644 index 1a446770d8..0000000000 --- a/crates/dojo-core/src/upgradable.cairo +++ /dev/null @@ -1,19 +0,0 @@ -use starknet::{ClassHash, SyscallResult, SyscallResultTrait}; -use zeroable::Zeroable; -use result::ResultTrait; - -#[starknet::interface] -trait IUpgradeable { - fn upgrade(ref self: T, new_class_hash: ClassHash); -} - -trait UpgradeableTrait { - fn upgrade(new_class_hash: ClassHash); -} - -impl UpgradeableTraitImpl of UpgradeableTrait { - fn upgrade(new_class_hash: ClassHash) { - assert(new_class_hash.is_non_zero(), 'class_hash cannot be zero'); - starknet::replace_class_syscall(new_class_hash).unwrap_syscall(); - } -} diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index d38df35d3f..53094323c8 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -45,6 +45,12 @@ trait IWorld { fn revoke_writer(ref self: T, model: felt252, system: ContractAddress); } +#[starknet::interface] +trait IWorldProvider { + fn world(self: @T) -> IWorldDispatcher; +} + + #[starknet::contract] mod world { use core::traits::TryInto; @@ -66,9 +72,9 @@ mod world { use dojo::database; use dojo::database::index::WhereCondition; use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; - use dojo::upgradable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; use dojo::world::{IWorldDispatcher, IWorld}; - + + use dojo::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; const NAME_ENTRYPOINT: felt252 = 0x0361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60; @@ -404,8 +410,8 @@ mod world { self.contract_base.read(), salt, array![].span(), false ) .unwrap_syscall(); - let upgradable_dispatcher = IUpgradeableDispatcher { contract_address }; - upgradable_dispatcher.upgrade(class_hash); + let upgradeable_dispatcher = IUpgradeableDispatcher { contract_address }; + upgradeable_dispatcher.upgrade(class_hash); self.owners.write((contract_address.into(), get_caller_address()), true); diff --git a/crates/dojo-lang/src/contract.rs b/crates/dojo-lang/src/contract.rs index 8436eeb735..9880b4efc6 100644 --- a/crates/dojo-lang/src/contract.rs +++ b/crates/dojo-lang/src/contract.rs @@ -6,7 +6,7 @@ use cairo_lang_defs::plugin::{ }; // 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::OptionReturnTypeClause::ReturnTypeClause; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; @@ -23,16 +23,18 @@ impl DojoContract { pub fn from_module(db: &dyn SyntaxGroup, module_ast: ast::ItemModule) -> PluginResult { let name = module_ast.name(db).text(db); let mut system = DojoContract { diagnostics: vec![], dependencies: HashMap::new() }; + let mut has_event = false; if let MaybeModuleBody::Some(body) = module_ast.body(db) { - let body_nodes = body + let mut body_nodes: Vec<_> = body .items(db) .elements(db) .iter() .flat_map(|el| { - if let ast::Item::FreeFunction(fn_ast) = el { - if fn_ast.declaration(db).name(db).text(db).to_string() == "execute" { - return system.handle_execute(db, fn_ast.clone()); + if let ast::Item::Enum(enum_ast) = el { + if enum_ast.name(db).text(db).to_string() == "Event" { + has_event = true; + return system.merge_event(db, enum_ast.clone()); } } @@ -40,6 +42,10 @@ impl DojoContract { }) .collect(); + if !has_event { + body_nodes.append(&mut system.create_event()) + } + let mut builder = PatchBuilder::new(db); builder.add_modified(RewriteNode::interpolate_patched( " @@ -48,10 +54,17 @@ impl DojoContract { use dojo::world; use dojo::world::IWorldDispatcher; use dojo::world::IWorldDispatcherTrait; + use dojo::world::IWorldProvider; + + component!(path: dojo::components::upgradeable::upgradeable, storage: \ + upgradeable, event: UpgradeableEvent); + #[storage] struct Storage { world_dispatcher: IWorldDispatcher, + #[substorage(v0)] + upgradeable: dojo::components::upgradeable::upgradeable::Storage, } #[external(v0)] @@ -60,17 +73,16 @@ impl DojoContract { } #[external(v0)] - impl Upgradeable of dojo::upgradable::IUpgradeable { - fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { - let caller = starknet::get_caller_address(); - assert( - self.world_dispatcher.read().contract_address == caller, 'only \ - World can upgrade' - ); - dojo::upgradable::UpgradeableTrait::upgrade(new_class_hash); + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + self.world_dispatcher.read() } } + #[abi(embed_v0)] + impl UpgradableImpl = \ + dojo::components::upgradeable::upgradeable::UpgradableImpl; + $body$ } ", @@ -101,60 +113,44 @@ impl DojoContract { PluginResult::default() } - pub fn handle_execute( + pub fn merge_event( &mut self, db: &dyn SyntaxGroup, - function_ast: ast::FunctionWithBody, + enum_ast: ast::ItemEnum, ) -> Vec { let mut rewrite_nodes = vec![]; - let signature = function_ast.declaration(db).signature(db); - - let parameters = signature.parameters(db); - 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) { - RewriteNode::new_trimmed(clause.as_syntax_node()) - } else { - RewriteNode::Text("".to_string()) - }; + let elements = enum_ast.variants(db).elements(db); + + let variants = elements.iter().map(|e| e.as_syntax_node().get_text(db)).collect::>(); + let variants = variants.join(", "); rewrite_nodes.push(RewriteNode::interpolate_patched( " - #[external(v0)] - fn execute(self: @ContractState, $params$) $ret_clause$ $body$ + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: dojo::components::upgradeable::upgradeable::Event, + $variants$ + } ", - &UnorderedHashMap::from([ - ("params".to_string(), RewriteNode::Text(params)), - ( - "body".to_string(), - RewriteNode::new_trimmed(function_ast.body(db).as_syntax_node()), - ), - ("ret_clause".to_string(), ret_clause), - ]), + &UnorderedHashMap::from([("variants".to_string(), RewriteNode::Text(variants))]), )); - rewrite_nodes } + + pub fn create_event(&mut self) -> Vec { + vec![RewriteNode::Text( + " + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: dojo::components::upgradeable::upgradeable::Event, + } + " + .to_string(), + )] + } } // fn is_context(db: &dyn SyntaxGroup, param: &Param) -> bool { diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 1393fe2cdf..5a491c57f8 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": "0x481a9c7b128847a520bb234d93ef8a83b462de03fce7089eb4ed9135d2f31d9", + "class_hash": "0xb3e374b8087dca92601afbb9881fed855ac0d568e3bf878a876fca5ffcb479", "abi": [ { "type": "impl", @@ -811,16 +811,48 @@ test_manifest_file }, "base": { "name": "base", - "class_hash": "0x5a2c567ed06c8059c8d1199684796a0a0ef614f9a2ab628700e804524816b5c", + "class_hash": "0x77638e9a645209ac1e32e143bfdbfe9caf723c4f7645fcf465c38967545ea2f", "abi": [ { "type": "impl", - "name": "Upgradeable", - "interface_name": "dojo::upgradable::IUpgradeable" + "name": "WorldProviderImpl", + "interface_name": "dojo::world::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::world::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradableImpl", + "interface_name": "dojo::components::upgradeable::IUpgradeable" }, { "type": "interface", - "name": "dojo::upgradable::IUpgradeable", + "name": "dojo::components::upgradeable::IUpgradeable", "items": [ { "type": "function", @@ -842,31 +874,40 @@ test_manifest_file "inputs": [] }, { - "type": "struct", - "name": "dojo::world::IWorldDispatcher", + "type": "event", + "name": "dojo::components::upgradeable::upgradeable::Upgraded", + "kind": "struct", "members": [ { - "name": "contract_address", - "type": "core::starknet::contract_address::ContractAddress" + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" } ] }, { - "type": "function", - "name": "world", - "inputs": [], - "outputs": [ + "type": "event", + "name": "dojo::components::upgradeable::upgradeable::Event", + "kind": "enum", + "variants": [ { - "type": "dojo::world::IWorldDispatcher" + "name": "Upgraded", + "type": "dojo::components::upgradeable::upgradeable::Upgraded", + "kind": "nested" } - ], - "state_mutability": "view" + ] }, { "type": "event", "name": "dojo::base::base::Event", "kind": "enum", - "variants": [] + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::components::upgradeable::upgradeable::Event", + "kind": "nested" + } + ] } ] }, @@ -874,28 +915,37 @@ test_manifest_file { "name": "actions", "address": null, - "class_hash": "0x308ad5fcd288ea5ded168bbc502261d75458c13fa1df552bd41ca3d86347dfe", + "class_hash": "0x1f740b30fc835ecf509a40e8dc8e4eb7ada046243833d2060f17ab961e4e154", "abi": [ { "type": "impl", - "name": "Upgradeable", - "interface_name": "dojo::upgradable::IUpgradeable" + "name": "WorldProviderImpl", + "interface_name": "dojo::world::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] }, { "type": "interface", - "name": "dojo::upgradable::IUpgradeable", + "name": "dojo::world::IWorldProvider", "items": [ { "type": "function", - "name": "upgrade", - "inputs": [ + "name": "world", + "inputs": [], + "outputs": [ { - "name": "new_class_hash", - "type": "core::starknet::class_hash::ClassHash" + "type": "dojo::world::IWorldDispatcher" } ], - "outputs": [], - "state_mutability": "external" + "state_mutability": "view" } ] }, @@ -955,6 +1005,29 @@ test_manifest_file } ] }, + { + "type": "impl", + "name": "UpgradableImpl", + "interface_name": "dojo::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, { "type": "function", "name": "dojo_resource", @@ -1026,6 +1099,30 @@ test_manifest_file ], "state_mutability": "view" }, + { + "type": "event", + "name": "dojo::components::upgradeable::upgradeable::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::components::upgradeable::upgradeable::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::components::upgradeable::upgradeable::Upgraded", + "kind": "nested" + } + ] + }, { "type": "event", "name": "dojo_examples::actions::actions::Moved", @@ -1048,6 +1145,11 @@ test_manifest_file "name": "dojo_examples::actions::actions::Event", "kind": "enum", "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::components::upgradeable::upgradeable::Event", + "kind": "nested" + }, { "name": "Moved", "type": "dojo_examples::actions::actions::Moved", diff --git a/crates/dojo-lang/src/plugin_test_data/system b/crates/dojo-lang/src/plugin_test_data/system index e588abd684..bb252973ac 100644 --- a/crates/dojo-lang/src/plugin_test_data/system +++ b/crates/dojo-lang/src/plugin_test_data/system @@ -31,6 +31,26 @@ mod ctxnamed { } } +#[dojo::contract] +mod withevent { + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + TestEvent: TestEvent, + } + + #[derive(Drop, starknet::Event)] + struct TestEvent { + address: ContractAddress, + } + + #[external(v0)] + fn test(value: felt252) -> value { + value + } +} + //! > generated_cairo_code #[starknet::contract] mod spawn { @@ -157,64 +177,154 @@ error: Unsupported attribute. ^*******************^ error: Unsupported attribute. - --> test_src/lib.cairo[spawn]:8:21 + --> test_src/lib.cairo[withevent]:2:17 + #[starknet::contract] + ^*******************^ + +error: Unknown inline item macro: 'component'. + --> test_src/lib.cairo[spawn]:9:21 + component!(path: dojo::components::upgradeable::upgradeable, storage: upgradeable, event: UpgradeableEvent); + ^**********************************************************************************************************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[spawn]:12:21 #[storage] ^********^ error: Unsupported attribute. - --> test_src/lib.cairo[spawn]:13:21 + --> test_src/lib.cairo[spawn]:15:25 + #[substorage(v0)] + ^***************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[spawn]:19:21 #[external(v0)] ^*************^ error: Unsupported attribute. - --> test_src/lib.cairo[spawn]:18:21 + --> test_src/lib.cairo[spawn]:24:21 #[external(v0)] ^*************^ error: Unsupported attribute. - --> test_src/lib.cairo[spawn]:32:17 - #[external(v0)] - ^*************^ + --> test_src/lib.cairo[spawn]:31:21 + #[abi(embed_v0)] + ^**************^ error: Unsupported attribute. - --> test_src/lib.cairo[proxy]:8:21 + --> test_src/lib.cairo[spawn]:41:13 + #[event] + ^******^ + +error: Unknown inline item macro: 'component'. + --> test_src/lib.cairo[proxy]:9:21 + component!(path: dojo::components::upgradeable::upgradeable, storage: upgradeable, event: UpgradeableEvent); + ^**********************************************************************************************************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[proxy]:12:21 #[storage] ^********^ error: Unsupported attribute. - --> test_src/lib.cairo[proxy]:13:21 + --> test_src/lib.cairo[proxy]:15:25 + #[substorage(v0)] + ^***************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[proxy]:19:21 #[external(v0)] ^*************^ error: Unsupported attribute. - --> test_src/lib.cairo[proxy]:18:21 + --> test_src/lib.cairo[proxy]:24:21 #[external(v0)] ^*************^ error: Unsupported attribute. - --> test_src/lib.cairo[proxy]:30:17 - #[external(v0)] - ^*************^ + --> test_src/lib.cairo[proxy]:31:21 + #[abi(embed_v0)] + ^**************^ error: Unsupported attribute. - --> test_src/lib.cairo[ctxnamed]:8:21 + --> test_src/lib.cairo[proxy]:38:13 + #[event] + ^******^ + +error: Unknown inline item macro: 'component'. + --> test_src/lib.cairo[ctxnamed]:9:21 + component!(path: dojo::components::upgradeable::upgradeable, storage: upgradeable, event: UpgradeableEvent); + ^**********************************************************************************************************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[ctxnamed]:12:21 #[storage] ^********^ error: Unsupported attribute. - --> test_src/lib.cairo[ctxnamed]:13:21 + --> test_src/lib.cairo[ctxnamed]:15:25 + #[substorage(v0)] + ^***************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[ctxnamed]:19:21 + #[external(v0)] + ^*************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[ctxnamed]:24:21 + #[external(v0)] + ^*************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[ctxnamed]:31:21 + #[abi(embed_v0)] + ^**************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[ctxnamed]:41:13 + #[event] + ^******^ + +error: Unknown inline item macro: 'component'. + --> test_src/lib.cairo[withevent]:9:21 + component!(path: dojo::components::upgradeable::upgradeable, storage: upgradeable, event: UpgradeableEvent); + ^**********************************************************************************************************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[withevent]:12:21 + #[storage] + ^********^ + +error: Unsupported attribute. + --> test_src/lib.cairo[withevent]:15:25 + #[substorage(v0)] + ^***************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[withevent]:19:21 #[external(v0)] ^*************^ error: Unsupported attribute. - --> test_src/lib.cairo[ctxnamed]:18:21 + --> test_src/lib.cairo[withevent]:24:21 #[external(v0)] ^*************^ error: Unsupported attribute. - --> test_src/lib.cairo[ctxnamed]:32:17 - #[external(v0)] - ^*************^ + --> test_src/lib.cairo[withevent]:31:21 + #[abi(embed_v0)] + ^**************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[withevent]:35:13 + #[event] + ^******^ + +error: Unsupported attribute. + --> test_src/lib.cairo[withevent]:47:5 + #[external(v0)] + ^*************^ //! > expanded_cairo_code #[starknet::contract] @@ -222,10 +332,14 @@ error: Unsupported attribute. use dojo::world; use dojo::world::IWorldDispatcher; use dojo::world::IWorldDispatcherTrait; + use dojo::world::IWorldProvider; + #[storage] struct Storage { world_dispatcher: IWorldDispatcher, + #[substorage(v0)] + upgradeable: dojo::components::upgradeable::upgradeable::Storage, } #[external(v0)] @@ -234,23 +348,28 @@ error: Unsupported attribute. } #[external(v0)] - impl Upgradeable of dojo::upgradable::IUpgradeable { - fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { - let caller = starknet::get_caller_address(); - assert( - self.world_dispatcher.read().contract_address == caller, 'only World can upgrade' - ); - dojo::upgradable::UpgradeableTrait::upgrade(new_class_hash); + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + self.world_dispatcher.read() } } + #[abi(embed_v0)] + impl UpgradableImpl = dojo::components::upgradeable::upgradeable::UpgradableImpl; + use traits::Into; use dojo::world::Context; - #[external(v0)] - fn execute(self: @ContractState, ctx: Context, name: felt252) { + fn execute(ctx: Context, name: felt252) { return (); } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: dojo::components::upgradeable::upgradeable::Event, + } +impl EventDrop of Drop::; } @@ -259,10 +378,14 @@ error: Unsupported attribute. use dojo::world; use dojo::world::IWorldDispatcher; use dojo::world::IWorldDispatcherTrait; + use dojo::world::IWorldProvider; + #[storage] struct Storage { world_dispatcher: IWorldDispatcher, + #[substorage(v0)] + upgradeable: dojo::components::upgradeable::upgradeable::Storage, } #[external(v0)] @@ -271,21 +394,25 @@ error: Unsupported attribute. } #[external(v0)] - impl Upgradeable of dojo::upgradable::IUpgradeable { - fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { - let caller = starknet::get_caller_address(); - assert( - self.world_dispatcher.read().contract_address == caller, 'only World can upgrade' - ); - dojo::upgradable::UpgradeableTrait::upgrade(new_class_hash); + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + self.world_dispatcher.read() } } - - #[external(v0)] - fn execute(self: @ContractState, value: felt252) -> felt252 { + #[abi(embed_v0)] + impl UpgradableImpl = dojo::components::upgradeable::upgradeable::UpgradableImpl; + + fn execute(value: felt252) -> felt252 { value } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: dojo::components::upgradeable::upgradeable::Event, + } +impl EventDrop of Drop::; } @@ -294,10 +421,14 @@ error: Unsupported attribute. use dojo::world; use dojo::world::IWorldDispatcher; use dojo::world::IWorldDispatcherTrait; + use dojo::world::IWorldProvider; + #[storage] struct Storage { world_dispatcher: IWorldDispatcher, + #[substorage(v0)] + upgradeable: dojo::components::upgradeable::upgradeable::Storage, } #[external(v0)] @@ -306,22 +437,79 @@ error: Unsupported attribute. } #[external(v0)] - impl Upgradeable of dojo::upgradable::IUpgradeable { - fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { - let caller = starknet::get_caller_address(); - assert( - self.world_dispatcher.read().contract_address == caller, 'only World can upgrade' - ); - dojo::upgradable::UpgradeableTrait::upgrade(new_class_hash); + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + self.world_dispatcher.read() } } + #[abi(embed_v0)] + impl UpgradableImpl = dojo::components::upgradeable::upgradeable::UpgradableImpl; + use traits::Into; use dojo::world::Context; - #[external(v0)] - fn execute(self: @ContractState, ctx2: Context, name: felt252) { + fn execute(ctx2: Context, name: felt252) { return (); } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: dojo::components::upgradeable::upgradeable::Event, + } +impl EventDrop of Drop::; } + + #[starknet::contract] + mod withevent { + use dojo::world; + use dojo::world::IWorldDispatcher; + use dojo::world::IWorldDispatcherTrait; + use dojo::world::IWorldProvider; + + + #[storage] + struct Storage { + world_dispatcher: IWorldDispatcher, + #[substorage(v0)] + upgradeable: dojo::components::upgradeable::upgradeable::Storage, + } + + #[external(v0)] + fn dojo_resource(self: @ContractState) -> felt252 { + 'withevent' + } + + #[external(v0)] + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + self.world_dispatcher.read() + } + } + + #[abi(embed_v0)] + impl UpgradableImpl = dojo::components::upgradeable::upgradeable::UpgradableImpl; + + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: dojo::components::upgradeable::upgradeable::Event, + TestEvent: TestEvent + } + + #[derive(Drop, starknet::Event)] + struct TestEvent { + address: ContractAddress, + } + + #[external(v0)] + fn test(value: felt252) -> value { + value + } +impl EventDrop of Drop::; +impl TestEventDrop of Drop::; + + } diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index cabad47c4f..2576c5227c 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -289,7 +289,7 @@ pub async fn spinup_types_test() -> Result { execute_strategy(&ws, &migration, &account, None).await.unwrap(); // Execute `create` and insert 10 records into storage - let records_contract = "0x58eaab9779694efb92b43ccad6dc9b40cd8fc69e3efa97137f1c83af69ca8a1"; + let records_contract = "0x27f701de7d71a2a6ee670bc1ff47a901fdc671cca26fe234ca1a42273aa7f7d"; let InvokeTransactionResult { transaction_hash } = account .execute(vec![Call { calldata: vec![FieldElement::from_str("0xa").unwrap()], From db2811d03ba7271fc78b6409dccff15f50ae0192 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Tue, 21 Nov 2023 15:27:30 -0500 Subject: [PATCH 2/4] Prepare v0.3.12 --- Cargo.lock | 30 +++++++++---------- Cargo.toml | 2 +- crates/dojo-core/Scarb.lock | 6 ++-- crates/dojo-core/Scarb.toml | 4 +-- crates/dojo-lang/Scarb.toml | 2 +- .../simple_crate/Scarb.toml | 2 +- examples/spawn-and-move/Scarb.lock | 8 ++--- examples/spawn-and-move/Scarb.toml | 2 +- scripts/prepare_release.sh | 3 +- 9 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30dc4f0f66..58a4fc18a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2265,7 +2265,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dojo-lang" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "cairo-lang-compiler", @@ -2312,7 +2312,7 @@ dependencies = [ [[package]] name = "dojo-languge-server" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "cairo-lang-compiler", @@ -2334,7 +2334,7 @@ dependencies = [ [[package]] name = "dojo-signers" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "starknet", @@ -2342,7 +2342,7 @@ dependencies = [ [[package]] name = "dojo-test-utils" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "assert_fs", @@ -2373,7 +2373,7 @@ dependencies = [ [[package]] name = "dojo-types" -version = "0.3.11" +version = "0.3.12" dependencies = [ "crypto-bigint", "hex", @@ -2388,7 +2388,7 @@ dependencies = [ [[package]] name = "dojo-world" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "assert_fs", @@ -4816,7 +4816,7 @@ dependencies = [ [[package]] name = "katana" -version = "0.3.11" +version = "0.3.12" dependencies = [ "assert_matches", "clap", @@ -4834,7 +4834,7 @@ dependencies = [ [[package]] name = "katana-core" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "assert_matches", @@ -4865,7 +4865,7 @@ dependencies = [ [[package]] name = "katana-rpc" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "assert_matches", @@ -7236,7 +7236,7 @@ dependencies = [ [[package]] name = "sozo" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "assert_fs", @@ -8267,7 +8267,7 @@ dependencies = [ [[package]] name = "torii-client" -version = "0.3.11" +version = "0.3.12" dependencies = [ "async-trait", "camino", @@ -8293,7 +8293,7 @@ dependencies = [ [[package]] name = "torii-core" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "async-trait", @@ -8328,7 +8328,7 @@ dependencies = [ [[package]] name = "torii-graphql" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "async-graphql", @@ -8366,7 +8366,7 @@ dependencies = [ [[package]] name = "torii-grpc" -version = "0.3.11" +version = "0.3.12" dependencies = [ "bytes", "dojo-types", @@ -8403,7 +8403,7 @@ dependencies = [ [[package]] name = "torii-server" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index b398a41b1f..957b0458b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ edition = "2021" license = "Apache-2.0" license-file = "LICENSE" repository = "https://github.com/dojoengine/dojo/" -version = "0.3.11" +version = "0.3.12" [profile.performance] codegen-units = 1 diff --git a/crates/dojo-core/Scarb.lock b/crates/dojo-core/Scarb.lock index a6f55141e4..13286a04f2 100644 --- a/crates/dojo-core/Scarb.lock +++ b/crates/dojo-core/Scarb.lock @@ -3,12 +3,12 @@ version = 1 [[package]] name = "dojo" -version = "0.3.11" +version = "0.3.12" dependencies = [ "dojo_plugin", ] [[package]] name = "dojo_plugin" -version = "0.3.11" -source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#adab82da604669393bf5391439ed4ab1825923d1" +version = "0.3.12" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.12#12d58f29ec53454317f1f6d265007a053d279288" diff --git a/crates/dojo-core/Scarb.toml b/crates/dojo-core/Scarb.toml index 076c4523b2..d1f9ec3818 100644 --- a/crates/dojo-core/Scarb.toml +++ b/crates/dojo-core/Scarb.toml @@ -2,8 +2,8 @@ cairo-version = "2.3.1" description = "The Dojo Core library for autonomous worlds." name = "dojo" -version = "0.3.11" +version = "0.3.12" [dependencies] -dojo_plugin = { git = "https://github.com/dojoengine/dojo", tag = "v0.3.11" } +dojo_plugin = { git = "https://github.com/dojoengine/dojo", tag = "v0.3.12" } starknet = "2.3.1" diff --git a/crates/dojo-lang/Scarb.toml b/crates/dojo-lang/Scarb.toml index d6108e0735..0f7e1b1532 100644 --- a/crates/dojo-lang/Scarb.toml +++ b/crates/dojo-lang/Scarb.toml @@ -1,5 +1,5 @@ [package] name = "dojo_plugin" -version = "0.3.11" +version = "0.3.12" [cairo-plugin] diff --git a/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml b/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml index 167b5875ab..3acc1969d9 100644 --- a/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml +++ b/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml @@ -1,7 +1,7 @@ [package] cairo-version = "2.3.1" name = "test_crate" -version = "0.3.11" +version = "0.3.12" [cairo] sierra-replace-ids = true diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index 8c1d889a10..40830134bd 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -3,19 +3,19 @@ version = 1 [[package]] name = "dojo" -version = "0.3.11" +version = "0.3.12" dependencies = [ "dojo_plugin", ] [[package]] name = "dojo_examples" -version = "0.3.11" +version = "0.3.12" dependencies = [ "dojo", ] [[package]] name = "dojo_plugin" -version = "0.3.11" -source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7d8aa29a92062fcb9e6659" +version = "0.3.12" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.12#12d58f29ec53454317f1f6d265007a053d279288" diff --git a/examples/spawn-and-move/Scarb.toml b/examples/spawn-and-move/Scarb.toml index 2e9d836793..01191e1edb 100644 --- a/examples/spawn-and-move/Scarb.toml +++ b/examples/spawn-and-move/Scarb.toml @@ -1,7 +1,7 @@ [package] cairo-version = "2.3.1" name = "dojo_examples" -version = "0.3.11" +version = "0.3.12" [cairo] sierra-replace-ids = true diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index 2659a79cca..def15d4949 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -9,7 +9,6 @@ find . -type f -name "*.toml" -exec sed -i "" "s/dojo_plugin = \"$prev_version\" scripts/clippy.sh -git checkout -b release/v$1 git commit -am "Prepare v$1" git tag -a "v$1" -m "Version $1" -git push origin release/v$1 --tags \ No newline at end of file +# git push origin --tags \ No newline at end of file From 9c9e1d686bf20cdedf62c631c4e286191d1506e0 Mon Sep 17 00:00:00 2001 From: Shramee Srivastav Date: Wed, 22 Nov 2023 23:24:30 +0800 Subject: [PATCH 3/4] chore: test on localhost (#1201) Don't listen on all IP addresses when testing --- crates/dojo-test-utils/src/sequencer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dojo-test-utils/src/sequencer.rs b/crates/dojo-test-utils/src/sequencer.rs index c44c7ee4f7..1ed8704c05 100644 --- a/crates/dojo-test-utils/src/sequencer.rs +++ b/crates/dojo-test-utils/src/sequencer.rs @@ -36,7 +36,7 @@ impl TestSequencer { Arc::clone(&sequencer), ServerConfig { port: 0, - host: "0.0.0.0".into(), + host: "127.0.0.1".into(), max_connections: 100, apis: vec![ApiKind::Starknet, ApiKind::Katana], }, From b3f49977fe921564725c30b912126aea57859bbd Mon Sep 17 00:00:00 2001 From: Yun Date: Wed, 22 Nov 2023 07:33:41 -0800 Subject: [PATCH 4/4] Torii grpc entities query (#1196) * Refactor torii grpc * error freeeee! * Refactor torii grpc * rebase changes * clean up * Move model to individual query clause * Refactor subscription argument to keys * Add Torii grpc retrieve entities endpoint * Add cache to store schema ty and query * map rows to proto model * add grpc limit offset * refactor query logic * clippy & fmt * remove unused * fix tests * move limit offset to query clause --------- Co-authored-by: Tarrence van As --- Cargo.lock | 1 + crates/dojo-types/src/primitive.rs | 172 ++++++-------- crates/dojo-types/src/schema.rs | 10 + crates/torii/core/Cargo.toml | 1 + crates/torii/core/src/cache.rs | 56 +++++ crates/torii/core/src/error.rs | 14 ++ crates/torii/core/src/lib.rs | 1 + crates/torii/core/src/model.rs | 200 +++++++++++++++- .../torii/graphql/src/tests/entities_test.rs | 2 +- crates/torii/graphql/src/tests/models_test.rs | 2 +- .../src/tests/types-test/src/contracts.cairo | 5 +- .../src/tests/types-test/src/models.cairo | 7 + crates/torii/grpc/proto/types.proto | 36 ++- crates/torii/grpc/proto/world.proto | 14 +- crates/torii/grpc/src/server/mod.rs | 219 ++++++++++++++++-- crates/torii/grpc/src/types.rs | 8 +- 16 files changed, 627 insertions(+), 121 deletions(-) create mode 100644 crates/torii/core/src/cache.rs diff --git a/Cargo.lock b/Cargo.lock index 58a4fc18a9..e5157272aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8300,6 +8300,7 @@ dependencies = [ "base64 0.21.5", "camino", "chrono", + "crypto-bigint", "dojo-test-utils", "dojo-types", "dojo-world", diff --git a/crates/dojo-types/src/primitive.rs b/crates/dojo-types/src/primitive.rs index d5a159bad9..2e91d0fdbb 100644 --- a/crates/dojo-types/src/primitive.rs +++ b/crates/dojo-types/src/primitive.rs @@ -33,6 +33,8 @@ pub enum PrimitiveError { NotEnoughFieldElements, #[error("Unsupported CairoType for SQL formatting")] UnsupportedType, + #[error("Set value type mismatch")] + TypeMismatch, #[error(transparent)] ValueOutOfRange(#[from] ValueOutOfRangeError), } @@ -44,97 +46,60 @@ pub enum SqlType { Text, } -impl Primitive { - /// If the `Primitive` is a u8, returns the associated [`u8`]. Returns `None` otherwise. - pub fn as_u8(&self) -> Option { - match self { - Primitive::U8(value) => *value, - _ => None, - } - } - - /// If the `Primitive` is a u16, returns the associated [`u16`]. Returns `None` otherwise. - pub fn as_u16(&self) -> Option { - match self { - Primitive::U16(value) => *value, - _ => None, - } - } - - /// If the `Primitive` is a u32, returns the associated [`u32`]. Returns `None` otherwise. - pub fn as_u32(&self) -> Option { - match self { - Primitive::U32(value) => *value, - _ => None, - } - } - - /// If the `Primitive` is a u64, returns the associated [`u64`]. Returns `None` otherwise. - pub fn as_u64(&self) -> Option { - match self { - Primitive::U64(value) => *value, - _ => None, - } - } - - /// If the `Primitive` is a u128, returns the associated [`u128`]. Returns `None` otherwise. - pub fn as_u128(&self) -> Option { - match self { - Primitive::U128(value) => *value, - _ => None, - } - } - - /// If the `Primitive` is a u256, returns the associated [`U256`]. Returns `None` otherwise. - pub fn as_u256(&self) -> Option { - match self { - Primitive::U256(value) => *value, - _ => None, - } - } - - /// If the `Primitive` is a felt252, returns the associated [`FieldElement`]. Returns `None` - /// otherwise. - pub fn as_felt252(&self) -> Option { - match self { - Primitive::Felt252(value) => *value, - _ => None, - } - } - - /// If the `Primitive` is a ClassHash, returns the associated [`FieldElement`]. Returns `None` - /// otherwise. - pub fn as_class_hash(&self) -> Option { - match self { - Primitive::ClassHash(value) => *value, - _ => None, +/// Macro to generate setter methods for Primitive enum variants. +macro_rules! set_primitive { + ($method_name:ident, $variant:ident, $type:ty) => { + /// Sets the inner value of the `Primitive` enum if variant matches. + pub fn $method_name(&mut self, value: Option<$type>) -> Result<(), PrimitiveError> { + match self { + Primitive::$variant(_) => { + *self = Primitive::$variant(value); + Ok(()) + } + _ => Err(PrimitiveError::TypeMismatch), + } } - } + }; +} - /// If the `Primitive` is a ContractAddress, returns the associated [`FieldElement`]. Returns - /// `None` otherwise. - pub fn as_contract_address(&self) -> Option { - match self { - Primitive::ContractAddress(value) => *value, - _ => None, +/// Macro to generate getter methods for Primitive enum variants. +macro_rules! as_primitive { + ($method_name:ident, $variant:ident, $type:ty) => { + /// If the `Primitive` is variant type, returns the associated vartiant value. Returns + /// `None` otherwise. + pub fn $method_name(&self) -> Option<$type> { + match self { + Primitive::$variant(value) => *value, + _ => None, + } } - } + }; +} - /// If the `Primitive` is a usize, returns the associated [`u32`]. Returns `None` otherwise. - pub fn as_usize(&self) -> Option { - match self { - Primitive::USize(value) => *value, - _ => None, - } - } +impl Primitive { + as_primitive!(as_u8, U8, u8); + as_primitive!(as_u16, U16, u16); + as_primitive!(as_u32, U32, u32); + as_primitive!(as_u64, U64, u64); + as_primitive!(as_u128, U128, u128); + as_primitive!(as_u256, U256, U256); + as_primitive!(as_bool, Bool, bool); + as_primitive!(as_usize, USize, u32); + as_primitive!(as_felt252, Felt252, FieldElement); + as_primitive!(as_class_hash, ClassHash, FieldElement); + as_primitive!(as_contract_address, ContractAddress, FieldElement); - /// If the `Primitive` is a bool, returns the associated [`bool`]. Returns `None` otherwise. - pub fn as_bool(&self) -> Option { - match self { - Primitive::Bool(value) => *value, - _ => None, - } - } + set_primitive!(set_u8, U8, u8); + set_primitive!(set_u16, U16, u16); + set_primitive!(set_u32, U32, u32); + set_primitive!(set_u64, U64, u64); + set_primitive!(set_u128, U128, u128); + set_primitive!(set_u256, U256, U256); + set_primitive!(set_bool, Bool, bool); + set_primitive!(set_usize, USize, u32); + set_primitive!(set_felt252, Felt252, FieldElement); + set_primitive!(set_class_hash, ClassHash, FieldElement); + set_primitive!(set_contract_address, ContractAddress, FieldElement); pub fn to_sql_type(&self) -> SqlType { match self { @@ -333,28 +298,39 @@ mod tests { } #[test] - fn as_inner_value() { - let primitive = Primitive::U8(Some(1u8)); + fn inner_value_getter_setter() { + let mut primitive = Primitive::U8(None); + primitive.set_u8(Some(1u8)).unwrap(); assert_eq!(primitive.as_u8(), Some(1u8)); - let primitive = Primitive::U16(Some(1u16)); + let mut primitive = Primitive::U16(None); + primitive.set_u16(Some(1u16)).unwrap(); assert_eq!(primitive.as_u16(), Some(1u16)); - let primitive = Primitive::U32(Some(1u32)); + let mut primitive = Primitive::U32(None); + primitive.set_u32(Some(1u32)).unwrap(); assert_eq!(primitive.as_u32(), Some(1u32)); - let primitive = Primitive::U64(Some(1u64)); + let mut primitive = Primitive::U64(None); + primitive.set_u64(Some(1u64)).unwrap(); assert_eq!(primitive.as_u64(), Some(1u64)); - let primitive = Primitive::U128(Some(1u128)); + let mut primitive = Primitive::U128(None); + primitive.set_u128(Some(1u128)).unwrap(); assert_eq!(primitive.as_u128(), Some(1u128)); - let primitive = Primitive::U256(Some(U256::from(1u128))); + let mut primitive = Primitive::U256(None); + primitive.set_u256(Some(U256::from(1u128))).unwrap(); assert_eq!(primitive.as_u256(), Some(U256::from(1u128))); - let primitive = Primitive::USize(Some(1u32)); + let mut primitive = Primitive::USize(None); + primitive.set_usize(Some(1u32)).unwrap(); assert_eq!(primitive.as_usize(), Some(1u32)); - let primitive = Primitive::Bool(Some(true)); + let mut primitive = Primitive::Bool(None); + primitive.set_bool(Some(true)).unwrap(); assert_eq!(primitive.as_bool(), Some(true)); - let primitive = Primitive::Felt252(Some(FieldElement::from(1u128))); + let mut primitive = Primitive::Felt252(None); + primitive.set_felt252(Some(FieldElement::from(1u128))).unwrap(); assert_eq!(primitive.as_felt252(), Some(FieldElement::from(1u128))); - let primitive = Primitive::ClassHash(Some(FieldElement::from(1u128))); + let mut primitive = Primitive::ClassHash(None); + primitive.set_class_hash(Some(FieldElement::from(1u128))).unwrap(); assert_eq!(primitive.as_class_hash(), Some(FieldElement::from(1u128))); - let primitive = Primitive::ContractAddress(Some(FieldElement::from(1u128))); + let mut primitive = Primitive::ContractAddress(None); + primitive.set_contract_address(Some(FieldElement::from(1u128))).unwrap(); assert_eq!(primitive.as_contract_address(), Some(FieldElement::from(1u128))); } } diff --git a/crates/dojo-types/src/schema.rs b/crates/dojo-types/src/schema.rs index ec86efd39c..f791c7bb96 100644 --- a/crates/dojo-types/src/schema.rs +++ b/crates/dojo-types/src/schema.rs @@ -269,6 +269,16 @@ impl Enum { Ok(self.options[option].name.clone()) } + pub fn set_option(&mut self, name: &str) -> Result<(), EnumError> { + match self.options.iter().position(|option| option.name == name) { + Some(index) => { + self.option = Some(index as u8); + Ok(()) + } + None => Err(EnumError::OptionInvalid), + } + } + pub fn to_sql_value(&self) -> Result { self.option() } diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index d6020e930d..9ad2757856 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -13,6 +13,7 @@ anyhow.workspace = true async-trait.workspace = true base64.workspace = true chrono.workspace = true +crypto-bigint = { version = "0.5.3", features = [ "serde" ] } dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world", features = [ "contracts", "manifest" ] } futures-channel = "0.3.0" diff --git a/crates/torii/core/src/cache.rs b/crates/torii/core/src/cache.rs new file mode 100644 index 0000000000..dc8d39bc63 --- /dev/null +++ b/crates/torii/core/src/cache.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; + +use dojo_types::schema::Ty; +use sqlx::SqlitePool; +use tokio::sync::RwLock; + +use crate::error::{Error, QueryError}; +use crate::model::{parse_sql_model_members, SqlModelMember}; + +type ModelName = String; + +pub struct ModelCache { + pool: SqlitePool, + schemas: RwLock>, +} + +impl ModelCache { + pub fn new(pool: SqlitePool) -> Self { + Self { pool, schemas: RwLock::new(HashMap::new()) } + } + + pub async fn schema(&self, model: &str) -> Result { + { + let schemas = self.schemas.read().await; + if let Some(schema) = schemas.get(model) { + return Ok(schema.clone()); + } + } + + self.update_schema(model).await + } + + async fn update_schema(&self, model: &str) -> Result { + let model_members: Vec = sqlx::query_as( + "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ + model_members WHERE model_id = ? ORDER BY model_idx ASC, member_idx ASC", + ) + .bind(model) + .fetch_all(&self.pool) + .await?; + + if model_members.is_empty() { + return Err(QueryError::ModelNotFound(model.into()).into()); + } + + let ty = parse_sql_model_members(model, &model_members); + let mut schemas = self.schemas.write().await; + schemas.insert(model.into(), ty.clone()); + + Ok(ty) + } + + pub async fn clear(&self) { + self.schemas.write().await.clear(); + } +} diff --git a/crates/torii/core/src/error.rs b/crates/torii/core/src/error.rs index cdcf6b9b95..0d73633076 100644 --- a/crates/torii/core/src/error.rs +++ b/crates/torii/core/src/error.rs @@ -1,3 +1,7 @@ +use std::num::ParseIntError; + +use dojo_types::primitive::PrimitiveError; +use dojo_types::schema::EnumError; use starknet::core::types::{FromByteSliceError, FromStrError}; use starknet::core::utils::CairoShortStringToFeltError; @@ -9,6 +13,10 @@ pub enum Error { Sql(#[from] sqlx::Error), #[error(transparent)] QueryError(#[from] QueryError), + #[error(transparent)] + PrimitiveError(#[from] PrimitiveError), + #[error(transparent)] + EnumError(#[from] EnumError), } #[derive(Debug, thiserror::Error)] @@ -19,10 +27,16 @@ pub enum ParseError { CairoShortStringToFelt(#[from] CairoShortStringToFeltError), #[error(transparent)] FromByteSliceError(#[from] FromByteSliceError), + #[error(transparent)] + ParseIntError(#[from] ParseIntError), } #[derive(Debug, thiserror::Error)] pub enum QueryError { #[error("unsupported query")] UnsupportedQuery, + #[error("model not found: {0}")] + ModelNotFound(String), + #[error("exceeds sqlite `JOIN` limit (64)")] + SqliteJoinLimit, } diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs index 877aab65a9..e36c3f2e3b 100644 --- a/crates/torii/core/src/lib.rs +++ b/crates/torii/core/src/lib.rs @@ -3,6 +3,7 @@ use sqlx::FromRow; use crate::types::SQLFieldElement; +pub mod cache; pub mod engine; pub mod error; pub mod model; diff --git a/crates/torii/core/src/model.rs b/crates/torii/core/src/model.rs index e701cf28e0..c05831c664 100644 --- a/crates/torii/core/src/model.rs +++ b/crates/torii/core/src/model.rs @@ -1,10 +1,16 @@ +use std::str::FromStr; + use async_trait::async_trait; +use crypto_bigint::U256; +use dojo_types::primitive::Primitive; use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; use dojo_world::contracts::model::ModelReader; -use sqlx::{Pool, Sqlite}; +use sqlx::sqlite::SqliteRow; +use sqlx::{Pool, Row, Sqlite}; use starknet::core::types::FieldElement; use super::error::{self, Error}; +use crate::error::{ParseError, QueryError}; pub struct ModelSQLReader { /// The name of the model @@ -144,11 +150,154 @@ pub fn parse_sql_model_members(model: &str, model_members_all: &[SqlModelMember] parse_sql_model_members_impl(model, model_members_all) } +/// Creates a query that fetches all models and their nested data. +pub fn build_sql_query(model_schemas: &Vec) -> Result { + fn parse_struct( + path: &str, + schema: &Struct, + selections: &mut Vec, + tables: &mut Vec, + ) { + for child in &schema.children { + match &child.ty { + Ty::Struct(s) => { + let table_name = format!("{}${}", path, s.name); + parse_struct(&table_name, s, selections, tables); + + tables.push(table_name); + } + _ => { + // alias selected columns to avoid conflicts in `JOIN` + selections.push(format!( + "{}.external_{} AS \"{}.{}\"", + path, child.name, path, child.name + )); + } + } + } + } + + let primary_table = model_schemas[0].name(); + let mut global_selections = Vec::new(); + let mut global_tables = model_schemas + .iter() + .enumerate() + .filter(|(index, _)| *index != 0) // primary_table don't `JOIN` itself + .map(|(_, schema)| schema.name()) + .collect::>(); + + for ty in model_schemas { + let schema = ty.as_struct().expect("schema should be struct"); + let model_table = &schema.name; + let mut selections = Vec::new(); + let mut tables = Vec::new(); + + parse_struct(model_table, schema, &mut selections, &mut tables); + + global_selections.push(selections.join(", ")); + global_tables.extend(tables); + } + + // TODO: Fallback to subqueries, SQLite has a max limit of 64 on 'table 'JOIN' + if global_tables.len() > 64 { + return Err(QueryError::SqliteJoinLimit.into()); + } + + let selections_clause = global_selections.join(", "); + let join_clause = global_tables + .into_iter() + .map(|table| format!(" LEFT JOIN {table} ON {primary_table}.entity_id = {table}.entity_id")) + .collect::>() + .join(" "); + + Ok(format!("SELECT {selections_clause} FROM {primary_table}{join_clause}")) +} + +/// Populate the values of a Ty (schema) from SQLite row. +pub fn map_row_to_ty(path: &str, struct_ty: &mut Struct, row: &SqliteRow) -> Result<(), Error> { + for member in struct_ty.children.iter_mut() { + let column_name = format!("{}.{}", path, member.name); + match &mut member.ty { + Ty::Primitive(primitive) => { + match &primitive { + Primitive::Bool(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_bool(Some(value))?; + } + Primitive::USize(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_usize(Some(value))?; + } + Primitive::U8(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_u8(Some(value))?; + } + Primitive::U16(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_u16(Some(value))?; + } + Primitive::U32(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_u32(Some(value))?; + } + Primitive::U64(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_u64(Some(value as u64))?; + } + Primitive::U128(_) => { + let value = row.try_get::(&column_name)?; + let hex_str = value.trim_start_matches("0x"); + primitive.set_u128(Some( + u128::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, + ))?; + } + Primitive::U256(_) => { + let value = row.try_get::(&column_name)?; + let hex_str = value.trim_start_matches("0x"); + primitive.set_u256(Some(U256::from_be_hex(hex_str)))?; + } + Primitive::Felt252(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_felt252(Some( + FieldElement::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + Primitive::ClassHash(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_class_hash(Some( + FieldElement::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + Primitive::ContractAddress(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_contract_address(Some( + FieldElement::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + }; + } + Ty::Enum(enum_ty) => { + let value = row.try_get::(&column_name)?; + enum_ty.set_option(&value)?; + } + Ty::Struct(struct_ty) => { + let path = [path, &struct_ty.name].join("$"); + map_row_to_ty(&path, struct_ty, row)?; + } + ty => { + unimplemented!("unimplemented type_enum: {ty}"); + } + }; + } + + Ok(()) +} + #[cfg(test)] mod tests { use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; - use super::SqlModelMember; + use super::{build_sql_query, SqlModelMember}; use crate::model::parse_sql_model_members; #[test] @@ -321,4 +470,51 @@ mod tests { assert_eq!(parse_sql_model_members("Moves", &model_members), expected_ty); } + + #[test] + fn struct_ty_to_query() { + let ty = Ty::Struct(Struct { + name: "Position".into(), + children: vec![ + dojo_types::schema::Member { + name: "name".into(), + key: false, + ty: Ty::Primitive("felt252".parse().unwrap()), + }, + dojo_types::schema::Member { + name: "age".into(), + key: false, + ty: Ty::Primitive("u8".parse().unwrap()), + }, + dojo_types::schema::Member { + name: "vec".into(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".into(), + children: vec![ + Member { + name: "x".into(), + key: false, + ty: Ty::Primitive("u256".parse().unwrap()), + }, + Member { + name: "y".into(), + key: false, + ty: Ty::Primitive("u256".parse().unwrap()), + }, + ], + }), + }, + ], + }); + + let query = build_sql_query(&vec![ty]).unwrap(); + assert_eq!( + query, + "SELECT Position.external_name AS \"Position.name\", Position.external_age AS \ + \"Position.age\", Position$Vec2.external_x AS \"Position$Vec2.x\", \ + Position$Vec2.external_y AS \"Position$Vec2.y\" FROM Position LEFT JOIN \ + Position$Vec2 ON Position.entity_id = Position$Vec2.entity_id" + ); + } } diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index cf2850268e..ac42333c8d 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -98,7 +98,7 @@ mod tests { assert_eq!(connection.edges.len(), 10); assert_eq!(connection.total_count, 20); assert_eq!(&first_entity.node.model_names, "Subrecord"); - assert_eq!(&last_entity.node.model_names, "Record"); + assert_eq!(&last_entity.node.model_names, "Record,RecordSibling"); // first key param - returns all entities with `0x0` as first key let entities = entities_query(&schema, "(keys: [\"0x0\"])").await; diff --git a/crates/torii/graphql/src/tests/models_test.rs b/crates/torii/graphql/src/tests/models_test.rs index a27b97e314..c0942937bc 100644 --- a/crates/torii/graphql/src/tests/models_test.rs +++ b/crates/torii/graphql/src/tests/models_test.rs @@ -92,7 +92,7 @@ mod tests { assert_eq!(connection.total_count, 10); assert_eq!(connection.edges.len(), 10); assert_eq!(&record.node.__typename, "Record"); - assert_eq!(&entity.model_names, "Record"); + assert_eq!(&entity.model_names, "Record,RecordSibling"); assert_eq!(entity.keys.clone().unwrap(), vec!["0x0"]); assert_eq!(record.node.depth, "Zero"); assert_eq!(nested.depth, "One"); diff --git a/crates/torii/graphql/src/tests/types-test/src/contracts.cairo b/crates/torii/graphql/src/tests/types-test/src/contracts.cairo index 1e5a0a3dca..638df904a6 100644 --- a/crates/torii/graphql/src/tests/types-test/src/contracts.cairo +++ b/crates/torii/graphql/src/tests/types-test/src/contracts.cairo @@ -8,7 +8,7 @@ trait IRecords { #[dojo::contract] mod records { use starknet::{ContractAddress, get_caller_address}; - use types_test::models::{Record, Subrecord, Nested, NestedMore, NestedMoreMore, Depth}; + use types_test::models::{Record, RecordSibling, Subrecord, Nested, NestedMore, NestedMoreMore, Depth}; use types_test::{seed, random}; use super::IRecords; @@ -90,6 +90,9 @@ mod records { random_u8, random_u128 }, + RecordSibling { + record_id, random_u8 + }, Subrecord { record_id, subrecord_id, type_u8: record_idx.into(), random_u8, } diff --git a/crates/torii/graphql/src/tests/types-test/src/models.cairo b/crates/torii/graphql/src/tests/types-test/src/models.cairo index af855773d4..a68b11ae8e 100644 --- a/crates/torii/graphql/src/tests/types-test/src/models.cairo +++ b/crates/torii/graphql/src/tests/types-test/src/models.cairo @@ -21,6 +21,13 @@ struct Record { random_u128: u128, } +#[derive(Model, Copy, Drop, Serde)] +struct RecordSibling { + #[key] + record_id: u32, + random_u8: u8 +} + #[derive(Copy, Drop, Serde, Introspect)] struct Nested { depth: Depth, diff --git a/crates/torii/grpc/proto/types.proto b/crates/torii/grpc/proto/types.proto index 1c853c005b..80d9ae8da6 100644 --- a/crates/torii/grpc/proto/types.proto +++ b/crates/torii/grpc/proto/types.proto @@ -29,6 +29,38 @@ message ModelMetadata { bytes schema = 6; } +message Enum { + // Variant + uint32 option = 1; + // Variants of the enum + repeated string options = 2; +} + +message Member { + // Name of the member + string name = 1; + // Type of member + oneof member_type { + Value value = 2; + Enum enum = 3; + Model struct = 4; + } +} + +message Model { + // Name of the model + string name = 1; + // Members of the model + repeated Member members = 2; +} + +message Entity { + // The entity key + bytes key = 1; + // Models of the entity + repeated Model models = 2; +} + message StorageEntry { // The key of the changed value string key = 1; @@ -55,6 +87,8 @@ message EntityUpdate { message EntityQuery { Clause clause = 1; + uint32 limit = 2; + uint32 offset = 3; } message Clause { @@ -72,7 +106,7 @@ message KeysClause { message AttributeClause { string model = 1; - string attribute = 2; + string member = 2; ComparisonOperator operator = 3; Value value = 4; } diff --git a/crates/torii/grpc/proto/world.proto b/crates/torii/grpc/proto/world.proto index 66f5fce09d..ec6dccc469 100644 --- a/crates/torii/grpc/proto/world.proto +++ b/crates/torii/grpc/proto/world.proto @@ -9,8 +9,11 @@ service World { rpc WorldMetadata (MetadataRequest) returns (MetadataResponse); - // Subscribes to entity updates. + // Subscribes to entities updates. rpc SubscribeEntities (SubscribeEntitiesRequest) returns (stream SubscribeEntitiesResponse); + + // Retrieve entity + rpc RetrieveEntities (RetrieveEntitiesRequest) returns (RetrieveEntitiesResponse); } @@ -33,3 +36,12 @@ message SubscribeEntitiesResponse { // List of entities that have been updated. types.EntityUpdate entity_update = 1; } + +message RetrieveEntitiesRequest { + // The entities to retrieve + types.EntityQuery query = 1; +} + +message RetrieveEntitiesResponse { + repeated types.Entity entities = 1; +} diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index d5ccac1680..8b7d484817 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -5,13 +5,18 @@ pub mod subscription; use std::future::Future; use std::net::SocketAddr; use std::pin::Pin; +use std::str::FromStr; use std::sync::Arc; +use dojo_types::primitive::Primitive; +use dojo_types::schema::{Struct, Ty}; use futures::Stream; use proto::world::{ - MetadataRequest, MetadataResponse, SubscribeEntitiesRequest, SubscribeEntitiesResponse, + MetadataRequest, MetadataResponse, RetrieveEntitiesRequest, RetrieveEntitiesResponse, + SubscribeEntitiesRequest, SubscribeEntitiesResponse, }; -use sqlx::{Pool, Sqlite}; +use sqlx::sqlite::SqliteRow; +use sqlx::{Pool, Row, Sqlite}; use starknet::core::utils::cairo_short_string_to_felt; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; @@ -21,10 +26,12 @@ use tokio::sync::mpsc::Receiver; use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; use tonic::transport::Server; use tonic::{Request, Response, Status}; -use torii_core::error::{Error, ParseError}; -use torii_core::model::{parse_sql_model_members, SqlModelMember}; +use torii_core::cache::ModelCache; +use torii_core::error::{Error, ParseError, QueryError}; +use torii_core::model::build_sql_query; use self::subscription::SubscribeRequest; +use crate::proto::types::clause::ClauseType; use crate::proto::world::world_server::WorldServer; use crate::proto::{self}; @@ -33,6 +40,7 @@ pub struct DojoWorld { world_address: FieldElement, pool: Pool, subscriber_manager: Arc, + model_cache: Arc, } impl DojoWorld { @@ -51,7 +59,9 @@ impl DojoWorld { Arc::clone(&subscriber_manager), )); - Self { pool, world_address, subscriber_manager } + let model_cache = Arc::new(ModelCache::new(pool.clone())); + + Self { pool, model_cache, world_address, subscriber_manager } } } @@ -78,7 +88,7 @@ impl DojoWorld { let mut models_metadata = Vec::with_capacity(models.len()); for model in models { - let schema = self.model_schema(&model.0).await?; + let schema = self.model_cache.schema(&model.0).await?; models_metadata.push(proto::types::ModelMetadata { name: model.0, class_hash: model.1, @@ -98,16 +108,79 @@ impl DojoWorld { }) } - async fn model_schema(&self, model: &str) -> Result { - let model_members: Vec = sqlx::query_as( - "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ - model_members WHERE model_id = ? ORDER BY model_idx ASC, member_idx ASC", + async fn entities_by_keys( + &self, + keys_clause: proto::types::KeysClause, + limit: u32, + offset: u32, + ) -> Result, Error> { + let keys = keys_clause + .keys + .iter() + .map(|bytes| { + if bytes.is_empty() { + return Ok("%".to_string()); + } + Ok(FieldElement::from_byte_slice_be(bytes) + .map(|felt| format!("{:#x}", felt)) + .map_err(ParseError::FromByteSliceError)?) + }) + .collect::, Error>>()?; + let keys_pattern = keys.join("/") + "/%"; + + let db_entities: Vec<(String, String)> = sqlx::query_as( + "SELECT id, model_names FROM entities WHERE keys LIKE ? ORDER BY event_id ASC LIMIT ? \ + OFFSET ?", ) - .bind(model) + .bind(&keys_pattern) + .bind(limit) + .bind(offset) .fetch_all(&self.pool) .await?; - Ok(parse_sql_model_members(model, &model_members)) + let mut entities = Vec::new(); + for (entity_id, models_str) in db_entities { + let model_names: Vec<&str> = models_str.split(',').collect(); + let mut schemas = Vec::new(); + for model in &model_names { + schemas.push(self.model_cache.schema(model).await?); + } + + let entity_query = + format!("{} WHERE {}.entity_id = ?", build_sql_query(&schemas)?, schemas[0].name()); + let row = sqlx::query(&entity_query).bind(&entity_id).fetch_one(&self.pool).await?; + + let mut models = Vec::new(); + for schema in schemas { + let struct_ty = schema.as_struct().expect("schema should be struct"); + models.push(Self::map_row_to_model(&schema.name(), struct_ty, &row)?); + } + + let key = FieldElement::from_str(&entity_id).map_err(ParseError::FromStr)?; + entities.push(proto::types::Entity { key: key.to_bytes_be().to_vec(), models }) + } + + Ok(entities) + } + + async fn entities_by_attribute( + &self, + _attribute: proto::types::AttributeClause, + _limit: u32, + _offset: u32, + ) -> Result, Error> { + // TODO: Implement + Err(QueryError::UnsupportedQuery.into()) + } + + async fn entities_by_composite( + &self, + _composite: proto::types::CompositeClause, + _limit: u32, + _offset: u32, + ) -> Result, Error> { + // TODO: Implement + Err(QueryError::UnsupportedQuery.into()) } pub async fn model_metadata(&self, model: &str) -> Result { @@ -124,7 +197,7 @@ impl DojoWorld { .fetch_one(&self.pool) .await?; - let schema = self.model_schema(model).await?; + let schema = self.model_cache.schema(model).await?; let layout = hex::decode(&layout).unwrap(); Ok(proto::types::ModelMetadata { @@ -161,6 +234,111 @@ impl DojoWorld { self.subscriber_manager.add_subscriber(subs).await } + + async fn retrieve_entities( + &self, + query: proto::types::EntityQuery, + ) -> Result { + let clause_type = query + .clause + .ok_or(QueryError::UnsupportedQuery)? + .clause_type + .ok_or(QueryError::UnsupportedQuery)?; + + let entities = match clause_type { + ClauseType::Keys(keys) => { + self.entities_by_keys(keys, query.limit, query.offset).await? + } + ClauseType::Attribute(attribute) => { + self.entities_by_attribute(attribute, query.limit, query.offset).await? + } + ClauseType::Composite(composite) => { + self.entities_by_composite(composite, query.limit, query.offset).await? + } + }; + + Ok(RetrieveEntitiesResponse { entities }) + } + + fn map_row_to_model( + path: &str, + struct_ty: &Struct, + row: &SqliteRow, + ) -> Result { + let members = struct_ty + .children + .iter() + .map(|member| { + let column_name = format!("{}.{}", path, member.name); + let name = member.name.clone(); + let member = match &member.ty { + Ty::Primitive(primitive) => { + let value_type = match primitive { + Primitive::Bool(_) => proto::types::value::ValueType::BoolValue( + row.try_get::(&column_name)?, + ), + Primitive::U8(_) + | Primitive::U16(_) + | Primitive::U32(_) + | Primitive::U64(_) + | Primitive::USize(_) => { + let value = row.try_get::(&column_name)?; + proto::types::value::ValueType::UintValue(value as u64) + } + Primitive::U128(_) + | Primitive::U256(_) + | Primitive::Felt252(_) + | Primitive::ClassHash(_) + | Primitive::ContractAddress(_) => { + let value = row.try_get::(&column_name)?; + proto::types::value::ValueType::StringValue(value) + } + }; + + proto::types::Member { + name, + member_type: Some(proto::types::member::MemberType::Value( + proto::types::Value { value_type: Some(value_type) }, + )), + } + } + Ty::Enum(enum_ty) => { + let value = row.try_get::(&column_name)?; + let options = enum_ty + .options + .iter() + .map(|e| e.name.to_string()) + .collect::>(); + let option = + options.iter().position(|o| o == &value).expect("wrong enum value") + as u32; + proto::types::Member { + name: enum_ty.name.clone(), + member_type: Some(proto::types::member::MemberType::Enum( + proto::types::Enum { option, options }, + )), + } + } + Ty::Struct(struct_ty) => { + let path = [path, &struct_ty.name].join("$"); + proto::types::Member { + name, + member_type: Some(proto::types::member::MemberType::Struct( + Self::map_row_to_model(&path, struct_ty, row)?, + )), + } + } + ty => { + unimplemented!("unimplemented type_enum: {ty}"); + } + }; + + Ok(member) + }) + .collect::, Error>>()?; + + Ok(proto::types::Model { name: struct_ty.name.clone(), members }) + } } type ServiceResult = Result, Status>; @@ -194,6 +372,21 @@ impl proto::world::world_server::World for DojoWorld { .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(Box::pin(ReceiverStream::new(rx)) as Self::SubscribeEntitiesStream)) } + + async fn retrieve_entities( + &self, + request: Request, + ) -> Result, Status> { + let query = request + .into_inner() + .query + .ok_or_else(|| Status::invalid_argument("Missing query argument"))?; + + let entities = + self.retrieve_entities(query).await.map_err(|e| Status::internal(e.to_string()))?; + + Ok(Response::new(entities)) + } } pub async fn new( diff --git a/crates/torii/grpc/src/types.rs b/crates/torii/grpc/src/types.rs index e62dc3b8a8..00efe50358 100644 --- a/crates/torii/grpc/src/types.rs +++ b/crates/torii/grpc/src/types.rs @@ -13,6 +13,8 @@ use crate::proto; #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] pub struct Query { pub clause: Clause, + pub limit: u32, + pub offset: u32, } #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] @@ -31,7 +33,7 @@ pub struct KeysClause { #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] pub struct AttributeClause { pub model: String, - pub attribute: String, + pub member: String, pub operator: ComparisonOperator, pub value: Value, } @@ -105,7 +107,7 @@ impl TryFrom for dojo_types::WorldMetadata { impl From for proto::types::EntityQuery { fn from(value: Query) -> Self { - Self { clause: Some(value.clause.into()) } + Self { clause: Some(value.clause.into()), limit: value.limit, offset: value.offset } } } @@ -152,7 +154,7 @@ impl From for proto::types::AttributeClause { fn from(value: AttributeClause) -> Self { Self { model: value.model, - attribute: value.attribute, + member: value.member, operator: value.operator as i32, value: Some(value.value.into()), }